---
title: "Data Handling Category"
description: "Data Handling"
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/data"
---

# Data Handling



---

## ORM Project

Models and Entities

# Models and Entities

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/orm/main)
![CI Workflow](https://github.com/derafu/orm/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/orm)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/orm)
![Total Downloads](https://poser.pugx.org/derafu/orm/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/orm/d/monthly)

&gt; Since 2006, [I participated in FLISoL](https://flisol.info/FLISOL2006/Chile/Valparaiso). In 2014, I presented [SowerPHP at UTEM (Santiago, Chile)](https://www.cnsl.cl/index.php/noticias/81-blog/isabel/1063-programa-flisol-santiago-2014). Years before, I presented MiPaGiNa at the University of Chile. On that occasion, someone in the audience said to me &quot;Oh, you built an ORM,&quot; and I said, &quot;What is that?&quot; That day, during the presentation I was giving, I learned what an ORM was. This project compiles the ideas that have appeared in my mind over the years. Always with the goal of making everything &quot;automagical.&quot;
&gt;
&gt; -- &lt;cite&gt;Esteban De La Fuente Rubio&lt;/cite&gt;

## Ideas

&gt; [!NOTE] WIP
&gt;
&gt; Work In Progress.




---

## Query Project

Derafu Query

# Derafu Query




---

### Introduction

Expressive Path-Based Query Builder for PHP

# Expressive Path-Based Query Builder for PHP

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/query/main)
![CI Workflow](https://github.com/derafu/query/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/query)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/query)
![Total Downloads](https://poser.pugx.org/derafu/query/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/query/d/monthly)

`derafu/query` is a PHP library for building and filtering SQL queries using a compact, URL-safe string syntax. A single expression like `customers[alias:c]__invoices[on:id=customer_id,alias:i]__total?&gt;1000` carries the full join path, join conditions, and filter in one string — no separate join calls required.

## Why Derafu\Query?

Traditional query builders require explicit join definitions, verbose relationship navigation, and different filter syntaxes across engines. `derafu/query` replaces all of that with:

- A **single expression format** that encodes path, joins, and filter in one string.
- **Automatic join generation** from path segments — no manual `join()` calls when using path syntax.
- **50+ operators** covering comparisons, patterns, lists, ranges, dates, NULL, regex, bitwise, and subqueries.
- **Database-specific SQL** generated from the same expression — PostgreSQL, MySQL, and SQLite without code changes.
- **Framework bridges** for Doctrine DBAL, Doctrine ORM, Laravel&#039;s Illuminate query builder, and API Platform.

| Feature                      | Derafu\Query | Traditional Query Builders |
|------------------------------|:---:|:---:|
| Path-Based Relationships     | ✅  | ❌  |
| Automatic Join Resolution    | ✅  | ❌  |
| Configurable Operators       | ✅  | ❌  |
| Framework Agnostic Core      | ✅  | ⚠️  |
| Unified Filter Syntax        | ✅  | ⚠️  |
| Multi-DB SQL Generation      | ✅  | ⚠️  |

---

## Installation

```bash
composer require derafu/query
```

---

## Quick Start

### With SqlQueryBuilder (standalone)

```php
use Derafu\Query\Builder\SqlQueryBuilder;
use Derafu\Query\Engine\PdoEngine;
use Derafu\Query\Filter\ExpressionParser;
use Derafu\Query\Filter\PathParser;
use Derafu\Query\Filter\FilterParser;
use Derafu\Query\Filter\CompositeExpressionParser;
use Derafu\Query\Operator\OperatorLoader;
use Derafu\Query\Operator\OperatorManager;

$pdo    = new PDO(&#039;sqlite:/path/to/db.sqlite&#039;);
$engine = new PdoEngine($pdo);

$loader  = new OperatorLoader();
$manager = new OperatorManager($loader-&gt;loadFromFile(&#039;vendor/derafu/query/resources/operators.yaml&#039;));
$parser  = new CompositeExpressionParser(
    new ExpressionParser(new PathParser(), new FilterParser($manager))
);

$qb = new SqlQueryBuilder($engine, $parser);

// Simple filter.
$rows = $qb-&gt;table(&#039;products&#039;)-&gt;where(&#039;price?&gt;1000&#039;)-&gt;execute();

// Multi-table path (auto-generates the JOIN).
$rows = $qb
    -&gt;select(&#039;c.name AS customer_name, i.number, i.total&#039;)
    -&gt;where(&#039;customers[alias:c]__invoices[on:id=customer_id,alias:i]__total?&gt;1000&#039;)
    -&gt;execute();
```

### With a Framework Bridge

```php
// Doctrine DBAL.
use Derafu\Query\Bridge\DoctrineDBALQueryBuilderConditionApplier;
use Derafu\Query\Filter\CompositeExpressionParser;

$applier = new DoctrineDBALQueryBuilderConditionApplier();
$condition = $parser-&gt;parse(&#039;status?=active&amp;&amp;total?&gt;1000&#039;);
$applier-&gt;apply($dbalQueryBuilder, $condition);
```

---

## The Expression Format

Every filter is a string of the form:

```
path?filter
```

The `?` separates the **path** (which column or relationship to target) from the **filter** (which operator and value to apply).

```
price?&gt;1000                         ← column &quot;price&quot;, operator &quot;&gt;&quot;, value &quot;1000&quot;
status?in:paid,issued               ← column &quot;status&quot;, operator &quot;in:&quot;, value &quot;paid,issued&quot;
created_at?date:20240301            ← column &quot;created_at&quot;, operator &quot;date:&quot;, value &quot;20240301&quot;
deleted_at?is:null                  ← column &quot;deleted_at&quot;, operator &quot;is:null&quot;, no value
```

Multi-segment paths navigate relationships and generate joins automatically:

```
customers[alias:c]__invoices[on:id=customer_id,alias:i]__total?&gt;1000
```

Subquery paths starting with `___` generate correlated EXISTS / aggregate subqueries:

```
___payments[on:id=invoice_id]?is:empty          ← NOT EXISTS (payments)
___payments[on:id=invoice_id]__status?=pending  ← EXISTS with column filter
___payments[on:id=invoice_id]__SUM(amount)?&gt;=500← aggregate scalar subquery
```

Multiple conditions can be combined with `&amp;&amp;` (AND), `||` (OR), and `()` (grouping):

```
status?=active&amp;&amp;total?&gt;1000
category?=electronics||(category?=software&amp;&amp;price?&lt;500)
```

---

## Operator Overview

`derafu/query` ships with over 50 operators across 10 types:

| Type         | Examples                                  | SQL produced                      |
|--------------|-------------------------------------------|-----------------------------------|
| Standard     | `=`, `!=`, `&gt;`, `&lt;`, `&gt;=`, `&lt;=`           | `col = :p`, `col &gt; :p`            |
| AutoLike     | `^`, `$`, `~~`, `~~*`, `!~~`              | `col LIKE :p` with auto `%`       |
| Like         | `like:`, `ilike:`, `notlike:`             | `col LIKE :p`, `col ILIKE :p`     |
| List         | `in:`, `notin:`                           | `col IN (:p1, :p2, …)`            |
| Range        | `between:`, `notbetween:`                 | `col BETWEEN :p1 AND :p2`         |
| Date         | `date:`, `month:`, `year:`, `period:`     | `DATE(col) = :p`, etc.            |
| NULL         | `is:null`, `isnot:null`, `&lt;=&gt;`            | `col IS NULL`                     |
| Subquery     | `is:empty`, `isnot:empty`                 | `NOT EXISTS (…)`, `EXISTS (…)`    |
| RegExp       | `~`, `~*`, `!~`, `similarto:`             | `col ~ :p`, `col SIMILAR TO :p`   |
| Binary       | `b&amp;`, `b\|`, `b^`, `b&lt;&lt;`, `b&gt;&gt;`           | `col &amp; :p`, etc.                  |

---

## What This Documentation Covers

| Page | Content |
|------|---------|
| [Architecture](./architecture) | Layer structure, class responsibilities, data flow |
| [Expression Syntax](./expression-syntax) | `path?filter` format, composite `&amp;&amp;`/`\|\|`/`()` |
| [Path Syntax](./path-syntax) | Segments, options, join paths, subquery paths |
| [Operators Reference](./operators) | All operators with SQL templates, validation, casting |
| [Query Builder](./query-builder) | Fluent API and declarative `QueryConfig` |
| [Framework Bridges](./bridges) | Doctrine DBAL/ORM, Illuminate, API Platform |
| [Security Guide](./security-guide) | SQL sanitization and safe usage patterns |




---

### Architecture

Architecture

# Architecture

`derafu/query` is organized into six cooperating layers. Understanding them helps you choose which classes to instantiate, which bridges to use, and where to add custom behavior.

{.w-75 .mx-auto}
![Architecture diagram showing the six layers of derafu/query: Filter (parsing), Operator (management), Builder (SQL generation), Engine (execution), Bridge (framework integration), and Config (declarative queries). Arrows show the dependency flow from top to bottom.](https://www.derafu.dev/img/diagrams/content/docs/data/query/architecture-layers.svg)

---

## Layers at a Glance

| Namespace | Responsibility |
|-----------|---------------|
| `Derafu\Query\Filter` | Parse string expressions into structured condition objects |
| `Derafu\Query\Operator` | Load, validate, and manage operator definitions from YAML |
| `Derafu\Query\Builder` | Build SQL strings and named-parameter arrays from conditions |
| `Derafu\Query\Engine` | Execute SQL against a real database connection |
| `Derafu\Query\Bridge` | Apply conditions to third-party query builders (Doctrine, Illuminate, etc.) |
| `Derafu\Query\Config` | Load declarative query definitions from arrays, YAML, or JSON |

---

## Filter Layer

The filter layer turns a raw string expression like `customers[alias:c]__invoices[on:id=customer_id,alias:i]__total?&gt;1000` into an object tree.

### Key Classes

| Class | Interface | Role |
|-------|-----------|------|
| `CompositeExpressionParser` | `CompositeExpressionParserInterface` | Entry point. Parses composite expressions with `&amp;&amp;`, `\|\|`, `()` into a tree of conditions. |
| `ExpressionParser` | `ExpressionParserInterface` | Parses a single `path?filter` string into a `Condition`. |
| `PathParser` | `PathParserInterface` | Splits the path part (`table__column`) into `Segment` objects. |
| `FilterParser` | `FilterParserInterface` | Matches the filter part (`&gt;1000`) against known operators. |
| `Path` | `PathInterface` | Immutable value object holding an ordered list of `Segment` objects. |
| `Segment` | `SegmentInterface` | Immutable value object for one path segment: name + options. |
| `Filter` | `FilterInterface` | Immutable value object: operator + raw value string. |
| `Condition` | `ConditionInterface` | Ties a `Path` to a `Filter`. Carries a `literal` flag. |
| `CompositeCondition` | `CompositeConditionInterface` | AND or OR container of `Condition`/`CompositeCondition` objects. |

### Parsing Pipeline

{.w-75 .mx-auto}
![Diagram showing the parsing pipeline: raw string → CompositeExpressionParser splits on &amp;&amp;/|| → ExpressionParser splits on ? → PathParser creates Path/Segments, FilterParser creates Filter → Condition object returned.](https://www.derafu.dev/img/diagrams/content/docs/data/query/parsing-pipeline.svg)

```
&quot;status?=active&amp;&amp;total?&gt;1000&quot;
       │
       ▼ CompositeExpressionParser
  CompositeCondition (AND)
  ├── Condition
  │   ├── Path [Segment(&quot;status&quot;)]
  │   └── Filter [Operator(&quot;=&quot;), value=&quot;active&quot;]
  └── Condition
      ├── Path [Segment(&quot;total&quot;)]
      └── Filter [Operator(&quot;&gt;&quot;), value=&quot;1000&quot;]
```

The `literal` flag on `Condition` distinguishes normal filter values from expression references (used with the `?E` marker for self-referencing conditions like `id?E!=other_alias.id`).

---

## Operator Layer

Operators are defined in `resources/operators.yaml` and loaded at startup. Each operator carries:

- A **symbol** (e.g. `&gt;=`, `like:`, `b&amp;`)
- A **type** classifying its behavior
- Engine-specific **SQL templates** with `{{column}}` / `{{value}}` / `{{values}}` / `{{value_1}}` / `{{value_2}}` placeholders
- An optional **validation pattern** (regex) for the value
- Optional **casting rules** that transform the value before binding
- An optional **alias** pointing to another operator&#039;s SQL template

### Key Classes

| Class | Interface | Role |
|-------|-----------|------|
| `Operator` | `OperatorInterface` | Immutable value object for one operator definition |
| `OperatorLoader` | `OperatorLoaderInterface` | Reads and validates `operators.yaml` |
| `OperatorManager` | `OperatorManagerInterface` | Registry; provides operators sorted by symbol length for greedy matching |
| `OperatorManagerFactory` | `OperatorManagerFactoryInterface` | Convenience factory: loader + manager in one call |

---

## Builder Layer

The builder layer converts condition objects into SQL strings with named parameters.

### Key Classes

| Class | Interface | Role |
|-------|-----------|------|
| `SqlBuilderWhere` | `QueryBuilderWhereInterface` | Converts a single `Condition` or `CompositeCondition` into a WHERE fragment + parameters |
| `SqlQueryBuilder` | `QueryBuilderInterface` | Full query builder: SELECT, FROM, JOIN, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET |
| `SqlQuery` | `QueryInterface` | Immutable value object holding SQL string and parameters. Implements `ArrayAccess` for `$q[&#039;sql&#039;]` and `$q[&#039;parameters&#039;]`. |
| `SqlSanitizerTrait` | — | Sanitizes identifiers and expressions; supports a custom quoting callback |

### SqlBuilderWhere Internals

`SqlBuilderWhere` is constructed with a database driver name (`pgsql`, `mysql`, `sqlite`). For each condition it:

1. Resolves the effective SQL template (operator&#039;s own `sql` key, or its base operator&#039;s).
2. Validates the raw value against the operator&#039;s pattern (if defined).
3. Normalizes the value: booleans → `0`/`1`, arrays → delimiter-joined string.
4. Applies casting rules (e.g. `like_start` appends `%`, `date` normalizes `YYYYMMDD` → `YYYY-MM-DD`).
5. Creates named parameters (`param_{column}_{uniqid}`).
6. Replaces `{{column}}`, `{{value}}`, `{{values}}`, `{{value_1}}`, `{{value_2}}` in the template.

For composite conditions it recursively builds each child and joins them with `AND` / `OR` inside parentheses.

For EXISTS paths (starting with `___`) it generates correlated `EXISTS (SELECT 1 FROM …)` or `NOT EXISTS (…)` subqueries. Aggregate column segments like `SUM(amount)` produce scalar subqueries: `(SELECT SUM(p.amount) FROM payments p WHERE …) &gt;= :param`.

### Column Resolution from Paths

When a path has more than one segment, `SqlBuilderWhere` qualifies the column with the previous segment&#039;s alias (or name):

```
authors__books__title  →  books.title
authors[alias:a]__books[alias:b]__title  →  b.title
```

For `SqlQueryBuilder`, intermediate segments automatically become JOIN clauses (INNER by default; override with `join:left` option).

---

## Engine Layer

The engine layer executes the SQL produced by the builder against a real connection.

| Class | Interface | Role |
|-------|-----------|------|
| `PdoEngine` | `SqlEngineInterface` | Wraps a `PDO` instance; prepares, executes, and fetches rows |
| `DoctrineEngine` | `SqlEngineInterface` | Wraps a Doctrine DBAL `Connection` |
| `AbstractSqlEngine` | — | Shared `getDriver()` detection logic |

---

## Bridge Layer

Bridges apply parsed conditions to existing third-party query builder instances, without requiring the full `SqlQueryBuilder`.

| Class | Target QB | Notes |
|-------|-----------|-------|
| `DoctrineDBALQueryBuilderConditionApplier` | Doctrine DBAL `QueryBuilder` | Resolves driver via reflection; handles FROM inference and JOIN deduplication |
| `DoctrineORMQueryBuilderConditionApplier` | Doctrine ORM `QueryBuilder` | Generates DQL; rewrites single-segment paths to qualify with root alias; throws `UnsupportedOperatorException` for DQL-incompatible operators |
| `IlluminateQueryBuilderConditionApplier` | Illuminate `Query\Builder` / `Eloquent\Builder` | Accepts both; resolves Eloquent to base via `toBase()` |
| `SmartFilter` (API Platform) | Doctrine ORM `QueryBuilder` | Wraps `DoctrineORMQueryBuilderConditionApplier` for use as an API Platform `#[QueryParameter]` filter |

All bridge appliers share `ConditionApplierTrait`, which provides:
- `buildConditionSql()` — compiles to SQL via `SqlBuilderWhere`
- `extractPaths()` — collects all paths from a condition tree
- `inferFromTable()` — detects the base table from multi-segment paths
- `buildJoinSpecsFromPaths()` — builds JOIN specs for SQL-level bridges
- `buildOrmJoinSpecsFromPaths()` — builds JOIN specs for Doctrine ORM DQL

---

## Config Layer

The config layer provides a declarative alternative to the fluent builder API.

| Class | Role |
|-------|------|
| `QueryConfig` | Wraps an array; calls builder methods when `applyTo($builder)` is invoked |
| `YamlConfigLoader` | Loads YAML files or strings into arrays |
| `JsonConfigLoader` | Loads JSON files or strings into arrays |

`QueryConfig::fromFile()` auto-detects YAML vs JSON by file extension.

---

## Data Flow Summary

{.w-75 .mx-auto}
![End-to-end data flow diagram: user string expression → CompositeExpressionParser → condition tree → SqlBuilderWhere (or bridge) → SQL + named parameters → PDO/Doctrine/Illuminate → result rows.](https://www.derafu.dev/img/diagrams/content/docs/data/query/data-flow.svg)

```
User provides string expression
        │
        ▼
CompositeExpressionParser
  ├── ExpressionParser
  │     ├── PathParser  → Path (Segments)
  │     └── FilterParser → Filter (Operator + value)
  └── returns Condition | CompositeCondition
        │
        ▼
SqlQueryBuilder  (or a Bridge applier)
  └── SqlBuilderWhere
        ├── Resolves SQL template (operator + engine)
        ├── Normalizes &amp; casts value
        ├── Creates named parameters
        └── Returns SqlQuery { sql, parameters }
              │
              ▼
        Engine::execute(sql, parameters)
              │
              ▼
        array of result rows
```




---

### Expression Syntax

Expression Syntax

# Expression Syntax

An expression is the fundamental unit passed to `where()`, `andWhere()`, `orWhere()`, `having()`, and the bridge appliers. It encodes **what** to filter, **how** to filter it, and (optionally) how to combine multiple filters into one string.

---

## Single Expression: `path?filter`

A single expression has the form:

```
path?filter
```

The `?` is the mandatory separator between the **path** (which column or relationship to target) and the **filter** (which operator and value to apply).

```
price?&gt;1000
^─────  ^────
path    filter
```

### Path

The path identifies a column. For a simple column in the driving table, it is just the column name:

```
price
status
created_at
```

For a column in a related table, segments are chained with double underscores `__`:

```
invoices__customers__name
```

For more details about the path syntax — including aliases, join conditions, and subquery paths — see [Path Syntax](./path-syntax).

### Filter

The filter starts with an operator symbol immediately followed by the value (if any):

```
&gt;1000         ← operator &quot;&gt;&quot;, value &quot;1000&quot;
=active       ← operator &quot;=&quot;, value &quot;active&quot;
in:a,b,c      ← operator &quot;in:&quot;, value &quot;a,b,c&quot;
is:null       ← operator &quot;is:null&quot;, no value (the value part is empty)
^^start       ← NOT valid — &quot;^^&quot; is not a registered operator
```

The `FilterParser` tries registered operators longest-first to avoid ambiguity between, for example, `!~*` and `!~`.

For the complete list of operators with their symbols, value formats, and SQL output, see [Operators Reference](./operators).

---

## Composite Expressions

Multiple conditions can be combined in a single string using:

| Operator | Meaning | Precedence |
|----------|---------|------------|
| `&amp;&amp;`     | AND     | Higher     |
| `\|\|`   | OR      | Lower      |
| `(` `)` | Grouping | Overrides default precedence |

The grammar follows standard boolean precedence: `&amp;&amp;` binds tighter than `||`.

```
A &amp;&amp; B || C        is parsed as   (A &amp;&amp; B) || C
A || B &amp;&amp; C        is parsed as   A || (B &amp;&amp; C)
(A || B) &amp;&amp; C      grouping overrides, result: (A || B) &amp;&amp; C
```

### Examples

Simple AND:

```
status?=active&amp;&amp;total?&gt;1000
```

Produces: `status = :p1 AND total &gt; :p2`

Simple OR:

```
category?=electronics||category?=hardware
```

Produces: `category = :p1 OR category = :p2`

AND with nested OR:

```
status?=active&amp;&amp;(type?=person||tax_id?^78)
```

Produces: `status = :p1 AND (type = :p2 OR tax_id LIKE :p3)`

OR with nested AND:

```
category?=software||(category?=hardware&amp;&amp;price?&gt;200)
```

Produces: `category = :p1 OR (category = :p2 AND price &gt; :p3)`

### Parser Rules

- `&amp;&amp;` and `||` are only split at **parenthesis depth 0**, so function calls like `SUM(amount)` inside paths are not split.
- Whitespace around `&amp;&amp;` and `||` is trimmed.
- A fully parenthesized expression like `(A&amp;&amp;B)` is unwrapped and parsed recursively.

---

## Multiple Expressions as an Array

Instead of combining with `&amp;&amp;`/`||` in a string, you can pass an array of expressions to `where()` or `andWhere()`. Each element is ANDed together:

```php
$qb-&gt;where([&#039;status?=active&#039;, &#039;total?&gt;1000&#039;]);
// Equivalent to: status?=active&amp;&amp;total?&gt;1000
```

This is useful when expressions are generated dynamically:

```php
$filters = [];
if ($status) {
    $filters[] = &#039;status?=&#039; . $status;
}
if ($minTotal) {
    $filters[] = &#039;total?&gt;=&#039; . $minTotal;
}
$qb-&gt;where($filters);
```

---

## The `?E` Literal-Expression Marker

By default the value part of a filter is treated as a **literal** — it becomes a bound parameter:

```
price?&gt;1000   →   price &gt; :param   with :param = &#039;1000&#039;
```

To reference another column or expression (not a literal value), replace `?` with `?E`:

```
id?E!=other_alias.id
```

This tells the parser that the value is an SQL identifier or expression, not a bound parameter. The value is sanitized as an identifier and inserted directly into the SQL — **no parameter binding**.

Use this sparingly and only for trusted, controlled inputs, since the sanitizer strips characters but does not provide the same guarantees as prepared statement binding.

Example from the test suite — simulating a self-join:

```php
$qb-&gt;where([
    &#039;products[alias:p1]__invoice_details[on:id=product_id,alias:id1]__invoice_id?isnot:null&#039;,
    &#039;products[alias:p1]__invoice_details[...alias:id2]__products[...alias:p2]__id?E!=p1.id&#039;,
]);
```

The second condition generates `p2.id != p1.id` (column reference, not a literal).

---

## How Expressions Are Parsed

The entry point is `CompositeExpressionParser::parse(string $expression)`.

1. Trim whitespace.
2. Split on `||` at depth 0 → if more than one part, build an OR composite.
3. For each part, split on `&amp;&amp;` at depth 0 → if more than one part, build an AND composite.
4. For each atom, if wrapped in `(…)` unwrap and recurse; otherwise, call `ExpressionParser::parse()`.

`ExpressionParser::parse()`:

1. Look for `?E` marker → set `literal = false`, replace `?E` with `?`.
2. Split on the first `?` → `pathExpression` and `filterExpression`.
3. Call `PathParser::parse(pathExpression)` → `Path`.
4. Call `FilterParser::parse(filterExpression)` → `Filter`.
5. Return `new Condition(path, filter, literal)`.

`FilterParser::parse()`:

1. Retrieve operators sorted longest-first from `OperatorManager`.
2. Try each symbol as a prefix of `filterExpression`.
3. On match: extract value, create `Filter`, call `validate()`, return.
4. No match: throw `InvalidArgumentException`.

---

## Validation at Parse Time

The `Filter::validate()` method checks the value against the operator&#039;s `pattern` field (if defined):

- Operators whose symbol ends in `:` (e.g. `like:`, `in:`, `between:`) always require a non-empty value.
- Specific patterns: dates must match `YYYYMMDD` or `YYMMDD`, bitwise values must be integers, list values must match the allowed character set.
- NULL operators (`is:null`, `isnot:null`, `is:empty`, `isnot:empty`) require an **empty** value — the operator symbol itself is the full expression.

Invalid expressions throw `InvalidArgumentException` immediately, before any SQL is generated.




---

### Path Syntax

Path Syntax

# Path Syntax

The **path** is the left-hand side of a `path?filter` expression. It identifies which column to apply the filter to, and optionally encodes the JOIN chain required to reach that column.

---

## Simple Paths (Single Segment)

A simple path is just a column name:

```
price
status
created_at
deleted_at
```

When used with `SqlQueryBuilder.where()`, the column is unqualified — it refers to a column in the driving table set via `table()` or `from()`.

When used with the Doctrine ORM bridge, single-segment paths are automatically qualified with the root entity alias (e.g. `status` → `c.status`).

---

## Multi-Segment Paths (Join Chains)

Segments are separated by double underscores `__`. The first segment is the **base table**, intermediate segments are **join targets**, and the last segment is the **column**:

```
customers__name
invoices__customers__name
products__invoice_details__invoices__customers__type
```

```
customers  __  name
^─ table      ^─ column

invoices  __  customers  __  name
^─ table     ^─ join target   ^─ column
```

When `SqlQueryBuilder` processes a multi-segment path, it automatically generates INNER JOIN clauses for all intermediate segments that carry `on:` options. The base table is inferred from the first segment when no explicit `table()` call has been made.

### Column Qualification

`SqlBuilderWhere` qualifies the column with the alias (or name) of the segment immediately before it:

```
invoices__customers__name          →  customers.name
invoices[alias:i]__customers[alias:c]__name  →  c.name
```

---

## Segment Options

Each segment can carry metadata inside square brackets `[key:value,key2:value2]`:

```
customers[alias:c]
invoices[on:id=customer_id,alias:i,join:left]
```

### `alias:value`

Sets an alias for the table in the generated SQL. The alias is used for column qualification, JOIN declarations, and correlation conditions.

```
customers[alias:c]__invoices[alias:i]__total
→  FROM customers AS c INNER JOIN invoices AS i …  →  i.total
```

### `on:left_col=right_col`

Defines the JOIN condition between the previous segment and this segment. `left_col` belongs to the previous table; `right_col` belongs to this table.

```
invoices__customers[on:customer_id=id]__name
→  … INNER JOIN customers ON invoices.customer_id = customers.id
```

Multiple `on:` pairs in the same brackets add multiple conditions (AND):

```
orders__items[on:order_id=id,on:branch_id=branch_id]__product
→  … INNER JOIN items ON orders.order_id = items.id AND orders.branch_id = items.branch_id
```

### `join:type`

Overrides the join type for this segment. Valid values: `inner` (default), `left`, `right`, `cross`.

```
customers__invoices[on:id=customer_id,join:left]__total
→  … LEFT JOIN invoices ON customers.id = invoices.customer_id
```

### Combining Options

All options can be combined in any order, separated by commas:

```
customers[alias:c]__invoices[on:id=customer_id,join:left,alias:i]__total
```

The segment above sets `alias=i`, `on.id=customer_id`, and `join=left` simultaneously.

---

## Exists/Subquery Paths (`___`)

A path that starts with **triple underscore** `___` generates a correlated subquery instead of a JOIN. This is the mechanism for filtering by the existence or properties of child records.

### Basic Existence Check

```
___payments[on:id=invoice_id]
```

No column segment → pure existence check. Combine with `is:empty` or `isnot:empty`:

```
___payments[on:id=invoice_id]?is:empty       →  NOT EXISTS (SELECT 1 FROM payments WHERE …)
___payments[on:id=invoice_id]?isnot:empty    →  EXISTS (SELECT 1 FROM payments WHERE …)
```

### Column Filter Inside the Subquery

Adding a column segment after `__` filters on that column inside the `EXISTS`:

```
___payments[on:id=invoice_id]__status?=pending
→  EXISTS (SELECT 1 FROM payments WHERE invoices.id = payments.invoice_id AND payments.status = :p)
```

Any operator can be used for the column filter inside the subquery:

```
___payments[on:id=invoice_id]__amount?&gt;500
___payments[on:id=invoice_id]__method?in:card,transfer
___payments[on:id=invoice_id]__created_at?date:20240101
```

### Aggregate Scalar Subqueries

When the column segment is an aggregate function call, a scalar subquery is generated:

```
___payments[on:id=invoice_id]__SUM(amount)?&gt;=1200
→  (SELECT SUM(p.amount) FROM payments p WHERE p.invoice_id = invoices.id) &gt;= :p
```

Supported aggregate functions: `SUM`, `AVG`, `COUNT`, `MIN`, `MAX`.

```
___payments[on:id=invoice_id]__COUNT(*)?&gt;1
___invoices[on:id=customer_id]__AVG(total)?&lt;1000
___invoices[on:id=customer_id]__MIN(total)?&gt;=100
```

**`COUNT(*)` optimizations**: when the comparison is a pure existence check, it is automatically rewritten to `EXISTS` / `NOT EXISTS`:

| Expression | Rewritten as |
|------------|-------------|
| `COUNT(*)?=0` | `NOT EXISTS (…)` |
| `COUNT(*)?&gt;0` | `EXISTS (…)` |
| `COUNT(*)?!=0` | `EXISTS (…)` |
| `COUNT(*)?&gt;=1` | `EXISTS (…)` |
| `COUNT(*)?&lt;1` | `NOT EXISTS (…)` |
| `COUNT(*)?&lt;=0` | `NOT EXISTS (…)` |
| `COUNT(*)?&gt;1` | scalar subquery (kept as-is) |

### Aliases on Exists Segments

You can add `alias:` to a subquery path segment:

```
___payments[alias:p,on:id=invoice_id]__status?=pending
→  EXISTS (SELECT 1 FROM payments AS p WHERE invoices.id = p.invoice_id AND p.status = :p)
```

### Nested Exists (Multi-Level)

Chain multiple `___` separators to traverse deeper relationships:

```
___payments[on:id=invoice_id]___items[on:id=payment_id]__amount?&gt;100
```

This generates an EXISTS subquery with an inner JOIN to `items`, filtering on `items.amount`.

---

## Function Notation in Paths

The **last segment** may be an aggregate or SQL function call. This is used for HAVING conditions and similar:

```
AVG(price)?&gt;500     ← in a having() call
SUM(i.total)?&gt;1000  ← qualified with a prior alias
```

When a parent segment exists, `SqlBuilderWhere` qualifies the function arguments automatically:

```
products[alias:p]__AVG(price)?&gt;500   →  AVG(p.price) &gt; :p
```

---

## Path Validation Rules

`PathParser` enforces these rules:

- A path cannot be empty.
- Segment names cannot be empty — `author__` (trailing `__`) and `__books` (leading `__`) are invalid.
- Segment names must start and end with `[a-zA-Z0-9_]` (closing `)` also allowed at the end, for function calls).
- Option keys and values must both be non-empty.
- An `on:` option value must contain `=` with non-empty parts on both sides.
- Exists paths (`___`) must have a non-empty body after the triple underscore.

---

## Path Examples Reference

| Path | Result |
|------|--------|
| `status` | `status` (no qualification) |
| `invoices__status` | `invoices.status` |
| `invoices[alias:i]__status` | `i.status` |
| `customers[alias:c]__invoices[on:id=customer_id,alias:i]__total` | `i.total` with JOIN |
| `products[alias:p]__invoice_details[on:id=product_id,alias:id]__invoices[on:invoice_id=id,alias:i]__customers[on:customer_id=id,alias:c]__name` | 3-level deep join |
| `___payments[on:id=invoice_id]` | Subquery path, no column |
| `___payments[on:id=invoice_id]__status` | Subquery path with column |
| `___payments[on:id=invoice_id]__SUM(amount)` | Aggregate subquery |
| `___payments[on:id=invoice_id]___items[on:id=payment_id]__amount` | Nested subquery |

---

## Paths in Doctrine ORM

The Doctrine ORM bridge handles paths differently from SQL bridges:

- **Single-segment paths** are automatically qualified with the root entity alias (e.g. `status` → `c.status`).
- **Multi-segment paths** use the association name from Doctrine entity mappings, not SQL column names. The `on:` option is **ignored** — Doctrine resolves join conditions from the mapping.
- **EXISTS paths** use DQL `SIZE(alias.assoc) = 0` for simple emptiness checks, and correlated `EXISTS(SELECT sub.id FROM EntityClass sub WHERE …)` for column filters.




---

### Operators Reference

Operators Reference

# Operators Reference

Every operator has a **symbol** — the prefix that appears immediately after `?` in an expression. The filter parser matches operators longest-first, so `!~~*` is matched before `!~~` and `!~*` before `!~`.

This page documents all built-in operators grouped by type.

---

## Value Format Conventions

| Placeholder | Meaning |
|-------------|---------|
| `VALUE`     | Any string value |
| `PATTERN`   | A LIKE pattern (may contain `%` and `_`) |
| `DATE`      | `YYYYMMDD` or `YYMMDD` |
| `MONTH`     | `MM` or `M` (1–12) |
| `YEAR`      | `YYYY` or `YY` |
| `PERIOD`    | `YYYYMM` or `YYMM` |
| `INT`       | Non-negative integer (no decimals) |
| `V1,V2`     | Two comma-separated values |
| `V1,V2,…`   | One or more comma-separated values |
| _(empty)_   | No value; the symbol alone is the full filter |

---

## Standard Comparison Operators

Map directly to SQL comparison operators. Accept any string or numeric value.

| Symbol | Name | Value | SQL Template |
|--------|------|-------|-------------|
| `=` | Equals | `VALUE` | `{{column}} = {{value}}` |
| `!=` | Not Equal | `VALUE` | `{{column}} != {{value}}` |
| `!` | Not Equal (shorthand) | `VALUE` | alias of `!=` |
| `&lt;&gt;` | Not Equal (SQL standard) | `VALUE` | alias of `!=` |
| `&gt;=` | Greater Than or Equal | `VALUE` | `{{column}} &gt;= {{value}}` |
| `&lt;=` | Less Than or Equal | `VALUE` | `{{column}} &lt;= {{value}}` |
| `&gt;` | Greater Than | `VALUE` | `{{column}} &gt; {{value}}` |
| `&lt;` | Less Than | `VALUE` | `{{column}} &lt; {{value}}` |

### Examples

```
price?=1000            →  price = :p             (:p = &#039;1000&#039;)
status?!=archived      →  status != :p           (:p = &#039;archived&#039;)
total?&gt;=500            →  total &gt;= :p            (:p = &#039;500&#039;)
created_at?&lt;2025-01-01 →  created_at &lt; :p        (:p = &#039;2025-01-01&#039;)
```

**Tip**: Standard operators work with any data type — numbers, strings, ISO dates. The database engine applies its own type coercion.

---

## AutoLike Operators (Automatic Pattern Generation)

These operators accept a plain text value and wrap it in `%` wildcards automatically before binding. They delegate to the `like:` / `ilike:` family for the actual SQL.

### Starts With

| Symbol | Case | Value | Bound value | SQL |
|--------|------|-------|-------------|-----|
| `^`    | Sensitive | `VALUE` | `VALUE%` | `col LIKE :p` (pgsql/sqlite), `col LIKE BINARY :p` (mysql) |
| `^*`   | Insensitive | `VALUE` | `VALUE%` | `col ILIKE :p` (pgsql), `col LIKE :p` (mysql/sqlite) |
| `!^`   | Sensitive | `VALUE` | `VALUE%` | `col NOT LIKE :p` |
| `!^*`  | Insensitive | `VALUE` | `VALUE%` | `col NOT ILIKE :p` / `col NOT LIKE :p` |

### Contains

| Symbol | Case | Value | Bound value | SQL |
|--------|------|-------|-------------|-----|
| `~~`   | Sensitive | `VALUE` | `%VALUE%` | `col LIKE :p` |
| `~~*`  | Insensitive | `VALUE` | `%VALUE%` | `col ILIKE :p` / `col LIKE :p` |
| `!~~`  | Sensitive | `VALUE` | `%VALUE%` | `col NOT LIKE :p` |
| `!~~*` | Insensitive | `VALUE` | `%VALUE%` | `col NOT ILIKE :p` / `col NOT LIKE :p` |

### Ends With

| Symbol | Case | Value | Bound value | SQL |
|--------|------|-------|-------------|-----|
| `$`    | Sensitive | `VALUE` | `%VALUE` | `col LIKE :p` |
| `$*`   | Insensitive | `VALUE` | `%VALUE` | `col ILIKE :p` / `col LIKE :p` |
| `!$`   | Sensitive | `VALUE` | `%VALUE` | `col NOT LIKE :p` |
| `!$*`  | Insensitive | `VALUE` | `%VALUE` | `col NOT ILIKE :p` / `col NOT LIKE :p` |

### Examples

```
name?^John      →  name LIKE :p         (:p = &#039;John%&#039;)
name?$*LLC      →  name ILIKE :p        (:p = &#039;%LLC&#039;)    (pgsql)
description?~~keyword  →  description LIKE :p  (:p = &#039;%keyword%&#039;)
title?!~~*draft →  title NOT ILIKE :p  (:p = &#039;%draft%&#039;)  (pgsql)
```

**Note on case sensitivity**: PostgreSQL natively supports `ILIKE`; MySQL uses `LIKE BINARY` for case-sensitive and plain `LIKE` for case-insensitive; SQLite uses plain `LIKE` for both (SQLite&#039;s `LIKE` is case-insensitive by default for ASCII).

---

## Pattern (LIKE) Operators

Explicit LIKE operators where you supply the full pattern including `%` and `_` wildcards.

| Symbol | Case | Value | SQL |
|--------|------|-------|-----|
| `like:` | Sensitive | `PATTERN` | pgsql: `col LIKE :p` · mysql: `col LIKE BINARY :p` · sqlite: `col LIKE :p` |
| `notlike:` | Sensitive | `PATTERN` | pgsql: `col NOT LIKE :p` · mysql: `col NOT LIKE BINARY :p` |
| `ilike:` | Insensitive | `PATTERN` | pgsql: `col ILIKE :p` · mysql: `col LIKE :p` · sqlite: `col LIKE :p` |
| `notilike:` | Insensitive | `PATTERN` | pgsql: `col NOT ILIKE :p` · mysql/sqlite: `col NOT LIKE :p` |

### Examples

```
email?like:%.example.com  →  email LIKE :p   (:p = &#039;%.example.com&#039;)
name?ilike:jo_n%          →  name ILIKE :p   (:p = &#039;jo_n%&#039;)   (pgsql)
code?notlike:TEST_%       →  code NOT LIKE BINARY :p  (mysql)
```

**Note**: `ilike:` and `notilike:` are **not supported** in Doctrine ORM DQL (only usable via SQL/DBAL/Illuminate bridges).

---

## List Operators

Work with comma-separated lists of values. The list is split and each item becomes a separate named parameter.

| Symbol | Value | SQL |
|--------|-------|-----|
| `in:` | `V1,V2,…` | `{{column}} IN (:p1, :p2, …)` |
| `notin:` | `V1,V2,…` | `{{column}} NOT IN (:p1, :p2, …)` |

### Value Format

Values are separated by commas. To include a literal comma in a value, escape it with `\,`:

```
status?in:active,pending,review   →  status IN (:p1, :p2, :p3)
tags?in:a\,b,c\,d                 →  tags IN (:p1, :p2)   (:p1=&#039;a,b&#039;, :p2=&#039;c,d&#039;)
```

### Validation

The value must contain at least one item. Each item may contain word characters, dots, hyphens, and Unicode letters/marks. Whitespace inside values is not supported.

### Examples

```
status?in:paid,issued           →  status IN (:p1, :p2)
category?notin:archived,draft   →  category NOT IN (:p1, :p2)
id?in:1,2,3,4,5                 →  id IN (:p1, :p2, :p3, :p4, :p5)
```

---

## Range Operators

Filter values between two bounds (inclusive) or outside them.

| Symbol | Value | SQL |
|--------|-------|-----|
| `between:` | `V1,V2` | `{{column}} BETWEEN {{value_1}} AND {{value_2}}` |
| `notbetween:` | `V1,V2` | `{{column}} NOT BETWEEN {{value_1}} AND {{value_2}}` |

### Value Format

Exactly two comma-separated values. Each value may contain word characters, dots, and hyphens (no spaces).

### Examples

```
price?between:100,500      →  price BETWEEN :p1 AND :p2
price?notbetween:0,10      →  price NOT BETWEEN :p1 AND :p2
created_at?between:2024-01-01,2024-12-31  →  created_at BETWEEN :p1 AND :p2
```

**Note**: `BETWEEN` is inclusive on both ends. For exclusive ranges, use `&gt;` and `&lt;` separately.

---

## Date Operators

Specialized operators for date and time filtering. They apply SQL date functions and normalize the value.

### `date:` — Specific Date

```
date:YYYYMMDD   or   date:YYMMDD
```

Matches rows where the date portion of a datetime column equals a specific day.

| Engine | SQL |
|--------|-----|
| All | `DATE({{column}}) = {{value}}` |

**Value normalization**:
- `YYYYMMDD` (8 digits) → `YYYY-MM-DD`
- `YYMMDD` (6 digits) → `20YY-MM-DD`

```
created_at?date:20240823   →  DATE(created_at) = :p   (:p = &#039;2024-08-23&#039;)
created_at?date:240101     →  DATE(created_at) = :p   (:p = &#039;2024-01-01&#039;)
```

### `month:` — Month Number

```
month:MM   or   month:M
```

Matches rows in a specific calendar month, regardless of year.

| Engine | SQL |
|--------|-----|
| All | `MONTH({{column}}) = {{value}}` |

**Value normalization**: zero-padded to 2 digits (`8` → `08`).

```
created_at?month:08    →  MONTH(created_at) = :p   (:p = &#039;08&#039;)
created_at?month:3     →  MONTH(created_at) = :p   (:p = &#039;03&#039;)
```

### `year:` — Year

```
year:YYYY   or   year:YY
```

Matches rows in a specific year.

| Engine | SQL |
|--------|-----|
| All | `YEAR({{column}}) = {{value}}` |

**Value normalization**: 2-digit years are expanded to `20YY`.

```
created_at?year:2024   →  YEAR(created_at) = :p   (:p = &#039;2024&#039;)
created_at?year:24     →  YEAR(created_at) = :p   (:p = &#039;2024&#039;)
```

### `period:` — Year and Month

```
period:YYYYMM   or   period:YYMM
```

Matches rows in a specific year+month.

| Engine | SQL |
|--------|-----|
| pgsql  | `TO_CHAR({{column}}, &quot;YYYYMM&quot;) = {{value}}` |
| mysql  | `DATE_FORMAT({{column}}, &quot;%Y%m&quot;) = {{value}}` |
| sqlite | `strftime(&quot;%Y%m&quot;, {{column}}) = {{value}}` |

**Value normalization**: 4-digit `YYMM` is expanded to `20YYMM`.

```
created_at?period:202408   →  strftime(&quot;%Y%m&quot;, created_at) = :p   (:p = &#039;202408&#039;)   (sqlite)
created_at?period:2408     →  …                                   (:p = &#039;202408&#039;)
```

**Note**: Date operators use SQL functions (`DATE()`, `MONTH()`, etc.) and are **not compatible with Doctrine ORM DQL**. Use the Doctrine DBAL or Illuminate bridges for date filtering.

---

## NULL Operators

| Symbol | Alias of | SQL |
|--------|----------|-----|
| `is:null` | — | `{{column}} IS NULL` |
| `&lt;=&gt;` | `is:null` | `{{column}} IS NULL` |
| `isnot:null` | — | `{{column}} IS NOT NULL` |

### Value Format

The symbol is the complete filter — no value is expected after the symbol. `is:null` means the expression is literally `column?is:null`.

### Examples

```
deleted_at?is:null      →  deleted_at IS NULL
deleted_at?&lt;=&gt;          →  deleted_at IS NULL    (alternative syntax)
email?isnot:null        →  email IS NOT NULL
```

**Note**: NULL operators produce no bound parameters.

---

## Subquery Operators

Used exclusively with **exists paths** (`___`). They determine whether the correlated subquery checks for existence or absence.

| Symbol | SQL |
|--------|-----|
| `is:empty` | `NOT EXISTS (SELECT 1 FROM … WHERE …)` |
| `isnot:empty` | `EXISTS (SELECT 1 FROM … WHERE …)` |

```
___payments[on:id=invoice_id]?is:empty
→  NOT EXISTS (SELECT 1 FROM payments WHERE invoices.id = payments.invoice_id)

___payments[on:id=invoice_id]?isnot:empty
→  EXISTS (SELECT 1 FROM payments WHERE invoices.id = payments.invoice_id)
```

In Doctrine ORM DQL, these generate `SIZE(alias.assoc) = 0` and `SIZE(alias.assoc) &gt; 0` respectively.

---

## Regular Expression Operators

These operators match POSIX extended regular expressions. Support varies by engine.

### Core Operators

| Symbol | Case | SQL (pgsql) | SQL (mysql) |
|--------|------|-------------|-------------|
| `~` | Sensitive | `{{column}} ~ {{value}}` | `{{column}} REGEXP BINARY {{value}}` |
| `~*` | Insensitive | `{{column}} ~* {{value}}` | `{{column}} REGEXP {{value}}` |
| `!~` | Sensitive | `{{column}} !~ {{value}}` | `{{column}} NOT REGEXP BINARY {{value}}` |
| `!~*` | Insensitive | `{{column}} !~* {{value}}` | `{{column}} NOT REGEXP {{value}}` |

**SQLite** does not have built-in regex support — these operators have no SQLite template and will fail if used against a SQLite connection.

### MySQL Aliases

| Symbol | Alias of |
|--------|----------|
| `regexp:` | `~` |
| `notregexp:` | `!~` |
| `rlike:` | `~` |
| `notrlike:` | `!~` |

These are provided for familiarity with MySQL&#039;s SQL syntax:

```
name?regexp:^John   →  name REGEXP BINARY :p   (mysql)
name?rlike:John.*   →  name REGEXP BINARY :p   (mysql)
```

### PostgreSQL SIMILAR TO

| Symbol | SQL |
|--------|-----|
| `similarto:` | `{{column}} SIMILAR TO {{value}}` |
| `notsimilarto:` | `{{column}} NOT SIMILAR TO {{value}}` |

`SIMILAR TO` uses SQL-standard regex patterns (a hybrid of LIKE wildcards and regex). It is PostgreSQL-specific.

Pattern rules: may contain `%`, `_`, `[a-z]`, `(a\|b)`, `?`, `*`, `+`. Unbalanced `(`, `)`, `[`, `]` are rejected at parse time.

```
name?similarto:John%           →  name SIMILAR TO :p
code?similarto:[0-9]+          →  code SIMILAR TO :p
status?notsimilarto:(a|b)%     →  status NOT SIMILAR TO :p
```

### Examples

```
email?~@example\.com    →  email ~ :p   (pgsql)
name?~*john             →  name ~* :p   (pgsql) / name REGEXP :p (mysql)
phone?!~^\+             →  phone !~ :p  (pgsql)
```

**Validation**: Regex patterns must contain at least one non-metacharacter character to prevent trivially-matching expressions.

**Note**: Regex operators are **not compatible with Doctrine ORM DQL**.

---

## Bitwise Operators

Operate on integer values at the bit level. All values must be non-negative integers (no decimals).

| Symbol | Name | Value | SQL |
|--------|------|-------|-----|
| `b&amp;` | Bitwise AND | `INT` | pgsql/sqlite: `col &amp; :p` · mysql: `BIT_AND(col, :p)` |
| `b\|` | Bitwise OR | `INT` | all: `col \| :p` |
| `b^` | Bitwise XOR | `INT` | pgsql: `col # :p` · mysql: `BIT_XOR(col, :p)` · sqlite: `(col \| :p) &amp; ~(col &amp; :p)` |
| `b&lt;&lt;` | Left Shift | `INT` | pgsql/sqlite: `col &lt;&lt; :p` · mysql: `&lt;&lt; col, :p` |
| `b&gt;&gt;` | Right Shift | `INT` | pgsql/sqlite: `col &gt;&gt; :p` · mysql: `&gt;&gt; col, :p` |
| `b&amp;~` | AND NOT | `INT` | all: `col &amp; ~( :p )` |

### Common Use Case: Feature Flags

Bitwise AND is commonly used to test whether a flag bit is set in a bitmask column:

```
flags?b&amp;1    →  flags &amp; :p   (:p = &#039;1&#039;)   — tests bit 0 (value 1)
flags?b&amp;2    →  flags &amp; :p   (:p = &#039;2&#039;)   — tests bit 1 (value 2)
flags?b&amp;4    →  flags &amp; :p   (:p = &#039;4&#039;)   — tests bit 2 (value 4)
```

### Examples

```
permissions?b&amp;4          →  permissions &amp; :p   (:p = &#039;4&#039;)   (pgsql)
permissions?b|3          →  permissions | :p   (:p = &#039;3&#039;)
version?b^255            →  (version | :p) &amp; ~(version &amp; :p)   (sqlite)
offset?b&lt;&lt;2              →  offset &lt;&lt; :p   (:p = &#039;2&#039;)
mask?b&amp;~8                →  mask &amp; ~( :p )   (:p = &#039;8&#039;)
```

**Validation**: Non-integer values (letters, decimals like `1.5`) are rejected at parse time.

**Note**: Bitwise operators are **not compatible with Doctrine ORM DQL**.

---

## Operator Aliases

Several operators are aliases that reuse another operator&#039;s SQL template:

| Symbol | Alias of | Reason |
|--------|----------|--------|
| `!` | `!=` | Shorthand not-equal |
| `&lt;&gt;` | `!=` | SQL-standard not-equal |
| `&lt;=&gt;` | `is:null` | MySQL NULL-safe equality syntax |
| `^` | `like:` | auto-casts with `like_start` |
| `^*` | `ilike:` | auto-casts with `like_start` |
| `!^` | `notlike:` | auto-casts with `like_start` |
| `!^*` | `notilike:` | auto-casts with `like_start` |
| `~~` | `like:` | auto-casts with `like` |
| `~~*` | `ilike:` | auto-casts with `like` |
| `!~~` | `notlike:` | auto-casts with `like` |
| `!~~*` | `notilike:` | auto-casts with `like` |
| `$` | `like:` | auto-casts with `like_end` |
| `$*` | `ilike:` | auto-casts with `like_end` |
| `!$` | `notlike:` | auto-casts with `like_end` |
| `!$*` | `notilike:` | auto-casts with `like_end` |
| `regexp:` | `~` | MySQL familiarity |
| `notregexp:` | `!~` | MySQL familiarity |
| `rlike:` | `~` | MySQL familiarity |
| `notrlike:` | `!~` | MySQL familiarity |

---

## Casting Rules

Casting rules transform the raw value string before it is bound as a parameter:

| Rule | Input | Output |
|------|-------|--------|
| `like_start` | `John` | `John%` |
| `like` | `John` | `%John%` |
| `like_end` | `Smith` | `%Smith` |
| `list` | `a,b,c` | `[&#039;a&#039;, &#039;b&#039;, &#039;c&#039;]` (split) |
| `date` | `20240823` | `2024-08-23` |
| `date` | `240823` | `2024-08-23` |
| `month` | `8` | `08` |
| `year` | `24` | `2024` |
| `period` | `202408` | `202408` (unchanged) |
| `period` | `2408` | `202408` |

---

## DQL Compatibility Matrix

The Doctrine ORM bridge generates DQL, which does not support all SQL features:

| Operator type | DQL compatible? |
|---------------|:--------------:|
| Standard (`=`, `!=`, `&gt;`, etc.) | ✅ |
| AutoLike (`^`, `~~`, `$`, etc.) | ✅ |
| `like:`, `notlike:` | ✅ |
| `ilike:`, `notilike:` | ❌ |
| `in:`, `notin:` | ✅ |
| `between:`, `notbetween:` | ✅ |
| Date (`date:`, `month:`, `year:`, `period:`) | ❌ |
| NULL (`is:null`, `isnot:null`) | ✅ |
| Subquery (`is:empty`, `isnot:empty`) | ✅ (→ SIZE()) |
| RegExp (`~`, `~*`, `similarto:`, etc.) | ❌ |
| Binary (`b&amp;`, `b\|`, etc.) | ❌ |

For incompatible operators, use the Doctrine DBAL bridge or `IlluminateQueryBuilderConditionApplier` instead. In API Platform&#039;s `SmartFilter`, incompatible operators are silently skipped.

---

## Custom Operators

Operators are defined in `resources/operators.yaml`. You can load a custom YAML file to add or override operators:

```yaml
# my-operators.yaml
version: &#039;1.0.0&#039;
description: &#039;Custom operators&#039;

types:
    standard:
        name: &#039;Standard Operators&#039;
        description: &#039;SQL comparison operators&#039;

operators:
    &#039;=&#039;:
        type: standard
        name: Equals
        description: Exact match
        sql: &#039;{{column}} = {{value}}&#039;

    &#039;startswith:&#039;:
        type: like
        name: Starts With (verbose)
        description: Explicit starts-with LIKE operator
        sql: &#039;{{column}} LIKE {{value}}&#039;
        cast: [&#039;like_start&#039;]
```

```php
use Derafu\Query\Operator\OperatorLoader;
use Derafu\Query\Operator\OperatorManagerFactory;

$factory = new OperatorManagerFactory(new OperatorLoader());
$manager = $factory-&gt;create(&#039;/path/to/my-operators.yaml&#039;);
```

Required fields per operator: `type`, `name`, `description`. The `type` key must reference an entry in the `types` section.




---

### Query Builder

Query Builder

# Query Builder

`derafu/query` provides two complementary ways to build queries:

1. **Fluent API** (`SqlQueryBuilder`) — chainable method calls, useful in application code.
2. **Declarative Config** (`QueryConfig`) — array, YAML, or JSON definitions, useful for reusable templates and API-driven queries.

Both produce the same output: a `SqlQuery` with an SQL string and named-parameter array.

---

## Setting Up SqlQueryBuilder

`SqlQueryBuilder` requires an engine and an expression parser:

```php
use Derafu\Query\Builder\SqlQueryBuilder;
use Derafu\Query\Engine\PdoEngine;
use Derafu\Query\Filter\CompositeExpressionParser;
use Derafu\Query\Filter\ExpressionParser;
use Derafu\Query\Filter\FilterParser;
use Derafu\Query\Filter\PathParser;
use Derafu\Query\Operator\OperatorLoader;
use Derafu\Query\Operator\OperatorManager;

// 1. Set up the operator registry.
$loader  = new OperatorLoader();
$manager = new OperatorManager(
    $loader-&gt;loadFromFile(&#039;vendor/derafu/query/resources/operators.yaml&#039;)
);

// 2. Build the expression parser.
$parser = new CompositeExpressionParser(
    new ExpressionParser(new PathParser(), new FilterParser($manager))
);

// 3. Create a database engine.
$pdo    = new PDO(&#039;pgsql:host=localhost;dbname=mydb&#039;, $user, $pass);
$engine = new PdoEngine($pdo);

// 4. Instantiate the builder.
$qb = new SqlQueryBuilder($engine, $parser);
```

For Doctrine DBAL use `DoctrineEngine` instead of `PdoEngine`. For framework-integrated setups, consider the [bridges](./bridges) instead of `SqlQueryBuilder`.

---

## Fluent API Reference

All mutating methods return `$this` (or a clone via `new()`) so they can be chained.

### `table(string $table, ?string $alias = null): self`

Sets the `FROM` clause and resets the builder state via an internal `new()` clone. Call this first when starting a new query from a builder instance that may have prior state.

```php
$qb-&gt;table(&#039;products&#039;)-&gt;where(&#039;price?&gt;1000&#039;)-&gt;execute();
$qb-&gt;table(&#039;invoices&#039;, &#039;i&#039;)-&gt;select(&#039;i.id, i.total&#039;)-&gt;execute();
```

### `from(string $table, ?string $alias = null): self`

Sets the `FROM` clause without resetting. Use `table()` for new queries; use `from()` internally or when you already have a fresh builder.

### `select(string|array $columns, bool $sanitize = true): self`

Sets the `SELECT` columns. Accepts a comma-separated string or an array.

```php
$qb-&gt;select(&#039;id, name, price&#039;);
$qb-&gt;select([&#039;id&#039;, &#039;name&#039;, &#039;price&#039;]);
$qb-&gt;select(&#039;c.name AS customer_name, i.total&#039;);
$qb-&gt;select(&#039;COUNT(*) AS total&#039;);
$qb-&gt;select(&#039;DISTINCT type&#039;);  // Use distinct() instead for the DISTINCT keyword
```

Pass `$sanitize = false` to skip identifier sanitization (use only for trusted, pre-validated expressions).

### `distinct(bool $distinct = true): self`

Adds `DISTINCT` to the `SELECT` clause.

```php
$qb-&gt;table(&#039;customers&#039;)-&gt;select(&#039;type&#039;)-&gt;distinct()-&gt;execute();
// → SELECT DISTINCT type FROM customers
```

### `where(string|array|Condition|CompositeCondition $condition): self`

Sets the `WHERE` clause. Resets any previous `WHERE` conditions. All elements are AND-combined.

```php
$qb-&gt;where(&#039;status?=active&#039;);
$qb-&gt;where([&#039;status?=active&#039;, &#039;total?&gt;1000&#039;]);
$qb-&gt;where(&#039;status?=active&amp;&amp;total?&gt;1000&#039;);  // composite string
```

### `andWhere(string|array|Condition|CompositeCondition $condition): self`

Appends conditions to the existing `WHERE` with AND. If no `WHERE` exists yet, behaves like `where()`.

```php
$qb-&gt;where(&#039;status?=active&#039;)-&gt;andWhere(&#039;total?&gt;1000&#039;);
```

### `orWhere(string|array|Condition|CompositeCondition $condition): self`

Wraps the existing `WHERE` and the new condition in an OR composite.

```php
// WHERE (status = &#039;electronics&#039; OR category = &#039;hardware&#039;)
$qb-&gt;where(&#039;category?=electronics&#039;)-&gt;orWhere(&#039;category?=hardware&#039;);

// WHERE (category = &#039;software&#039; OR (category = &#039;hardware&#039; AND price &gt; 200))
$qb-&gt;where(&#039;category?=software&#039;)-&gt;orWhere([&#039;category?=hardware&#039;, &#039;price?&gt;200&#039;]);

// Multiple OR groups:
// WHERE (status = &#039;cancelled&#039; OR status = &#039;draft&#039; OR (status = &#039;issued&#039; AND total &gt; 1000))
$qb-&gt;where(&#039;status?=cancelled&#039;)
   -&gt;orWhere([&#039;status?=draft&#039;, [&#039;status?=issued&#039;, &#039;total?&gt;1000&#039;]]);
```

### `andWhereOr(array|Condition|CompositeCondition $conditions): self`

Appends an OR composite to the existing AND WHERE. Each element of the array is one OR branch; arrays within the array become AND groups.

```php
// WHERE status = &#039;active&#039; AND (type = &#039;person&#039; OR tax_id LIKE &#039;78%&#039;)
$qb-&gt;where(&#039;status?=active&#039;)
   -&gt;andWhereOr([&#039;type?=person&#039;, &#039;tax_id?^78&#039;]);

// WHERE status = &#039;active&#039; AND ((status = &#039;paid&#039; AND total &gt; 1000) OR (status = &#039;issued&#039; AND date &gt;= &#039;2024-03-01&#039;))
$qb-&gt;where(&#039;customer_id?in:1,2&#039;)
   -&gt;andWhereOr([
       [&#039;status?=paid&#039;, &#039;total?&gt;1000&#039;],
       [&#039;status?=issued&#039;, &#039;date?&gt;=2024-03-01&#039;],
   ]);
```

### `join(string $table, string $condition, string $type = &#039;INNER&#039;, ?string $alias = null): self`

Adds an explicit JOIN clause. Valid types: `INNER`, `LEFT`, `RIGHT`, `CROSS`. Duplicates (same table+alias combination) are ignored.

```php
$qb-&gt;join(&#039;customers&#039;, &#039;i.customer_id = c.id&#039;, &#039;INNER&#039;, &#039;c&#039;);
```

Convenience wrappers:

```php
$qb-&gt;innerJoin(&#039;customers&#039;, &#039;i.customer_id = c.id&#039;, &#039;c&#039;);
$qb-&gt;leftJoin(&#039;payments&#039;, &#039;i.id = p.invoice_id&#039;, &#039;p&#039;);
$qb-&gt;rightJoin(&#039;invoices&#039;, &#039;c.id = i.customer_id&#039;, &#039;i&#039;);
$qb-&gt;crossJoin(&#039;currencies&#039;);
```

**Note**: When using path expressions in `where()`, joins are generated automatically from the path segments. Manual `join()` calls are only needed when you want explicit control, or when building the query without path expressions.

### `groupBy(string|array $columns): self`

Adds `GROUP BY` columns. Multiple calls accumulate.

```php
$qb-&gt;groupBy(&#039;category&#039;);
$qb-&gt;groupBy([&#039;c.id&#039;, &#039;c.name&#039;]);
```

### `having(string|array|Condition|CompositeCondition $condition): self`

Sets the `HAVING` clause. Accepts the same expression formats as `where()`.

```php
$qb-&gt;groupBy(&#039;category&#039;)-&gt;having(&#039;AVG(price)?&gt;500&#039;);
$qb-&gt;groupBy(&#039;status&#039;)-&gt;having(&#039;COUNT(*)?&gt;1&#039;);
```

### `orderBy(string|array $columns, string $direction = &#039;ASC&#039;): self`

Sets `ORDER BY`. Accepts a column string, or an associative array of `column =&gt; direction`.

```php
$qb-&gt;orderBy(&#039;price&#039;, &#039;DESC&#039;);
$qb-&gt;orderBy([&#039;category&#039; =&gt; &#039;ASC&#039;, &#039;price&#039; =&gt; &#039;DESC&#039;]);
$qb-&gt;orderBy([&#039;created_at&#039;, &#039;id&#039;]);  // defaults to ASC
```

### `limit(int $limit): self` / `offset(int $offset): self`

Set pagination. `OFFSET` is only appended when both `LIMIT` and `OFFSET` are set.

```php
$qb-&gt;limit(20)-&gt;offset(40);
// → LIMIT 20 OFFSET 40
```

### `getQuery(): QueryInterface`

Builds and returns the SQL without executing. Returns a `SqlQuery` that implements both `QueryInterface` and `ArrayAccess`:

```php
$query = $qb-&gt;table(&#039;products&#039;)-&gt;where(&#039;price?&gt;1000&#039;)-&gt;getQuery();

echo $query[&#039;sql&#039;];         // SELECT * FROM products WHERE (price &gt; :param_price_...)
echo $query[&#039;parameters&#039;];  // [&#039;param_price_...&#039; =&gt; &#039;1000&#039;]
```

### `execute(): array`

Builds and executes the query, returning rows as an array of associative arrays.

```php
$rows = $qb-&gt;table(&#039;products&#039;)-&gt;where(&#039;price?&gt;1000&#039;)-&gt;execute();
```

---

## Complete Query Examples

### Simple Filters

```php
// Active customers.
$qb-&gt;table(&#039;customers&#039;)-&gt;where(&#039;status?=active&#039;)-&gt;execute();

// Products in a price range.
$qb-&gt;table(&#039;products&#039;)-&gt;where(&#039;price?between:100,1000&#039;)-&gt;execute();

// Soft-deleted records.
$qb-&gt;table(&#039;users&#039;)-&gt;where(&#039;deleted_at?isnot:null&#039;)-&gt;execute();

// March 2024 invoices (SQLite).
$qb-&gt;table(&#039;invoices&#039;)-&gt;where(&#039;date?period:202403&#039;)-&gt;execute();
```

### Composite Conditions

```php
// AND: software products over $200.
$qb-&gt;table(&#039;products&#039;)
   -&gt;where([&#039;category?=software&#039;, &#039;price?&gt;200&#039;])
   -&gt;execute();

// OR: electronics or hardware.
$qb-&gt;table(&#039;products&#039;)
   -&gt;where(&#039;category?=electronics&#039;)
   -&gt;orWhere(&#039;category?=hardware&#039;)
   -&gt;execute();

// AND + OR: active customers who are persons OR whose tax_id starts with &quot;78&quot;.
$qb-&gt;table(&#039;customers&#039;)
   -&gt;where(&#039;status?=active&#039;)
   -&gt;andWhereOr([&#039;type?=person&#039;, &#039;tax_id?^78&#039;])
   -&gt;execute();
```

### Path-Based Joins

```php
// Auto-generated JOIN from path.
$qb-&gt;select(&#039;c.name AS customer_name, i.number, i.total&#039;)
   -&gt;where(&#039;customers[alias:c]__invoices[on:id=customer_id,alias:i]__total?&gt;1000&#039;)
   -&gt;execute();
// → SELECT … FROM customers AS c INNER JOIN invoices AS i ON c.id = i.customer_id WHERE i.total &gt; :p

// Multi-level join.
$qb-&gt;select(&#039;p.name, i.number, c.name AS customer&#039;)
   -&gt;where([
       &#039;products[alias:p]__category?=electronics&#039;,
       &#039;products[alias:p]__invoice_details[on:id=product_id,alias:id]__invoices[on:invoice_id=id,alias:i]__status?=paid&#039;,
       &#039;products[alias:p]__invoice_details[on:id=product_id,alias:id]__invoices[on:invoice_id=id,alias:i]__customers[on:customer_id=id,alias:c]__type?=company&#039;,
   ])
   -&gt;execute();
```

### Subquery / EXISTS

```php
// Invoices with no payments.
$qb-&gt;table(&#039;invoices&#039;)
   -&gt;where(&#039;___payments[on:id=invoice_id]?is:empty&#039;)
   -&gt;execute();
// → SELECT * FROM invoices WHERE NOT EXISTS (SELECT 1 FROM payments WHERE invoices.id = payments.invoice_id)

// Invoices with at least one pending payment.
$qb-&gt;table(&#039;invoices&#039;)
   -&gt;where(&#039;___payments[on:id=invoice_id]__status?=pending&#039;)
   -&gt;execute();

// Invoices where total payments &gt;= 1200.
$qb-&gt;table(&#039;invoices&#039;)
   -&gt;where(&#039;___payments[on:id=invoice_id]__SUM(amount)?&gt;=1200&#039;)
   -&gt;execute();
```

### Grouping, Having, Ordering, Pagination

```php
// Category stats.
$qb-&gt;table(&#039;products&#039;)
   -&gt;select(&#039;category, AVG(price) AS avg_price&#039;)
   -&gt;groupBy(&#039;category&#039;)
   -&gt;having(&#039;AVG(price)?&gt;500&#039;)
   -&gt;orderBy(&#039;avg_price&#039;, &#039;DESC&#039;)
   -&gt;limit(10)
   -&gt;execute();

// Paginated customer list.
$qb-&gt;table(&#039;customers&#039;)
   -&gt;orderBy([&#039;name&#039; =&gt; &#039;ASC&#039;])
   -&gt;limit(25)
   -&gt;offset(50)
   -&gt;execute();
```

### Explicit Joins

```php
// Manual JOIN for a complex ON clause.
$qb-&gt;table(&#039;invoices&#039;, &#039;i&#039;)
   -&gt;select(&#039;i.id, i.number, i.total, c.name AS customer_name&#039;)
   -&gt;innerJoin(&#039;customers&#039;, &#039;i.customer_id = c.id&#039;, &#039;c&#039;)
   -&gt;where(&#039;i.status?=paid&#039;)
   -&gt;execute();

// LEFT JOIN with GROUP BY.
$qb-&gt;table(&#039;customers&#039;, &#039;c&#039;)
   -&gt;select(&#039;c.name, COUNT(i.id) AS invoice_count&#039;)
   -&gt;leftJoin(&#039;invoices&#039;, &#039;c.id = i.customer_id&#039;, &#039;i&#039;)
   -&gt;groupBy([&#039;c.id&#039;, &#039;c.name&#039;])
   -&gt;execute();
```

---

## Declarative Query Configuration

`QueryConfig` provides an array-based way to describe a query. It is particularly useful for storing query templates in files and for API-driven queries.

### Basic Usage

```php
use Derafu\Query\Config\QueryConfig;

$config = new QueryConfig([
    &#039;table&#039;   =&gt; &#039;products&#039;,
    &#039;select&#039;  =&gt; &#039;id, name, price&#039;,
    &#039;where&#039;   =&gt; &#039;category?=electronics&#039;,
    &#039;orderBy&#039; =&gt; [&#039;price&#039; =&gt; &#039;DESC&#039;],
    &#039;limit&#039;   =&gt; 10,
]);

$result = $config-&gt;applyTo($qb)-&gt;execute();
```

`applyTo()` calls the corresponding builder methods in order and returns the modified builder, so you can chain additional calls after:

```php
$builder = $config-&gt;applyTo($qb);
if ($extraFilter) {
    $builder-&gt;andWhere(&#039;status?=active&#039;);
}
$rows = $builder-&gt;execute();
```

### Loading from Files

```php
// YAML file.
$config = QueryConfig::fromYamlFile(&#039;/path/to/queries/product_report.yaml&#039;);

// JSON file.
$config = QueryConfig::fromJsonFile(&#039;/path/to/queries/sales.json&#039;);

// Auto-detect by extension (.yml, .yaml, .json).
$config = QueryConfig::fromFile(&#039;/path/to/query.yaml&#039;);

// From strings.
$config = QueryConfig::fromYamlString($yamlString);
$config = QueryConfig::fromJsonString($jsonString);
```

### YAML Template Example

```yaml
# recent_products.yaml
table: products
select: id, name, price, created_at
where: deleted_at?is:null
orderBy:
    created_at: DESC
limit: 20
```

```php
$config  = QueryConfig::fromYamlFile(&#039;queries/recent_products.yaml&#039;);
$builder = $config-&gt;applyTo($qb);

// Add extra filter at runtime.
if ($category) {
    $builder-&gt;andWhere(&#039;category?=&#039; . $category);
}

$rows = $builder-&gt;execute();
```

### Complete Configuration Reference

All `QueryConfig` keys and their equivalent builder calls:

| Key | Builder Method | Value Type |
|-----|---------------|------------|
| `table` | `table()` | string |
| `alias` | `table($t, $alias)` | string |
| `select` | `select()` | string or array |
| `distinct` | `distinct(true)` | boolean |
| `where` | `where()` | string, array, or nested |
| `andWhere` | `andWhere()` | string or array |
| `orWhere` | `orWhere()` | string or array |
| `andWhereOr` | `andWhereOr()` | array of arrays |
| `innerJoin` | `innerJoin()` | `{table, condition, alias?}` |
| `leftJoin` | `leftJoin()` | `{table, condition, alias?}` |
| `rightJoin` | `rightJoin()` | `{table, condition, alias?}` |
| `crossJoin` | `crossJoin()` | `{table, alias?}` |
| `groupBy` | `groupBy()` | string or array |
| `having` | `having()` | string or array |
| `orderBy` | `orderBy()` | `{column: direction}` |
| `limit` | `limit()` | integer |
| `offset` | `offset()` | integer |

```php
$config = new QueryConfig([
    &#039;table&#039;    =&gt; &#039;invoices&#039;,
    &#039;alias&#039;    =&gt; &#039;i&#039;,
    &#039;select&#039;   =&gt; &#039;i.id, i.number, c.name AS customer_name&#039;,
    &#039;distinct&#039; =&gt; true,

    &#039;where&#039;      =&gt; &#039;i.status?=paid&#039;,
    &#039;andWhere&#039;   =&gt; &#039;i.total?&gt;1000&#039;,
    &#039;orWhere&#039;    =&gt; &#039;i.date?period:202403&#039;,
    &#039;andWhereOr&#039; =&gt; [
        [&#039;i.category?=service&#039;, &#039;i.total?&gt;500&#039;],
        [&#039;i.category?=product&#039;, &#039;i.total?&gt;1000&#039;],
    ],

    &#039;innerJoin&#039; =&gt; [&#039;table&#039; =&gt; &#039;customers&#039;, &#039;alias&#039; =&gt; &#039;c&#039;, &#039;condition&#039; =&gt; &#039;i.customer_id = c.id&#039;],

    &#039;groupBy&#039; =&gt; [&#039;i.status&#039;],
    &#039;having&#039;  =&gt; &#039;COUNT(*)?&gt;1&#039;,
    &#039;orderBy&#039; =&gt; [&#039;i.created_at&#039; =&gt; &#039;DESC&#039;],
    &#039;limit&#039;   =&gt; 20,
    &#039;offset&#039;  =&gt; 40,
]);
```

### API-Driven Queries

`QueryConfig` is well-suited for accepting query parameters from an API:

```php
$requestData = $request-&gt;getJsonBody();

$config = new QueryConfig([
    &#039;table&#039;   =&gt; &#039;products&#039;,
    &#039;where&#039;   =&gt; $requestData[&#039;filters&#039;] ?? [],
    &#039;orderBy&#039; =&gt; $requestData[&#039;sort&#039;] ?? [&#039;id&#039; =&gt; &#039;ASC&#039;],
    &#039;limit&#039;   =&gt; min($requestData[&#039;limit&#039;] ?? 20, 100),
    &#039;offset&#039;  =&gt; max($requestData[&#039;offset&#039;] ?? 0, 0),
]);

$rows = $config-&gt;applyTo($qb)-&gt;execute();
```

Ensure the `where` filters are either pre-validated strings that only allow known column names, or use the full path syntax so that only correctly-structured expressions reach the parser.




---

### Framework Bridges

Framework Bridges

# Framework Bridges

Bridges allow you to apply `derafu/query` expressions to existing third-party query builder instances — without replacing them. You keep your existing query builder and add Derafu filters on top.

All bridges accept a parsed `ConditionInterface | CompositeConditionInterface` and apply it to the query builder. You parse expressions with `CompositeExpressionParser` and pass the result to the bridge.

---

## Shared Setup: The Expression Parser

All bridges share the same expression parser setup:

```php
use Derafu\Query\Filter\CompositeExpressionParser;
use Derafu\Query\Filter\ExpressionParser;
use Derafu\Query\Filter\FilterParser;
use Derafu\Query\Filter\PathParser;
use Derafu\Query\Operator\OperatorLoader;
use Derafu\Query\Operator\OperatorManager;

$loader  = new OperatorLoader();
$manager = new OperatorManager(
    $loader-&gt;loadFromFile(&#039;vendor/derafu/query/resources/operators.yaml&#039;)
);
$parser = new CompositeExpressionParser(
    new ExpressionParser(new PathParser(), new FilterParser($manager))
);
```

---

## Doctrine DBAL Bridge

**Class**: `Derafu\Query\Bridge\DoctrineDBALQueryBuilderConditionApplier`

Applies conditions to a Doctrine DBAL `QueryBuilder`. Supports all operators including date, regex, and bitwise operators.

### Methods

```php
public function apply(object $queryBuilder, ConditionInterface|CompositeConditionInterface $condition): void
public function applyHaving(object $queryBuilder, ConditionInterface|CompositeConditionInterface $condition): void
```

### Usage

```php
use Derafu\Query\Bridge\DoctrineDBALQueryBuilderConditionApplier;

$applier = new DoctrineDBALQueryBuilderConditionApplier();

// Parse the expression.
$condition = $parser-&gt;parse(&#039;status?=active&amp;&amp;total?&gt;1000&#039;);

// Apply to an existing DBAL QueryBuilder.
$applier-&gt;apply($dbalQb, $condition);

// The QB now has: WHERE (status = :param_status_... AND total &gt; :param_total_...)
$rows = $dbalQb-&gt;executeQuery()-&gt;fetchAllAssociative();
```

### FROM and JOIN Inference

When a condition uses multi-segment paths, the bridge:

1. **Infers `FROM`** from the first segment of the first multi-segment path, if no `FROM` has been set yet.
2. **Generates JOINs** from intermediate segments, with deduplication (same alias → skip).

```php
// No from() call needed — it&#039;s inferred from the path.
$condition = $parser-&gt;parse(
    &#039;customers[alias:c]__invoices[on:id=customer_id,alias:i]__total?&gt;1000&#039;
);
$applier-&gt;apply($dbalQb, $condition);
// Sets FROM customers AS c, adds INNER JOIN invoices AS i ON c.id = i.customer_id
// WHERE i.total &gt; :p
```

### Driver Detection

The bridge reads the database platform from the DBAL connection (via reflection on the internal `connection` property) and maps it to the driver name used by `SqlBuilderWhere`:

| Platform | Driver |
|----------|--------|
| `PostgreSQLPlatform` | `pgsql` |
| `MySQLPlatform` | `mysql` |
| `SQLitePlatform` | `sqlite` |
| `SQLServerPlatform` | `sqlsrv` |
| `OraclePlatform` | `oci` |
| Other | `pgsql` (fallback) |

### HAVING

```php
$dbalQb-&gt;groupBy(&#039;status&#039;);
$condition = $parser-&gt;parse(&#039;COUNT(*)?&gt;1&#039;);
$applier-&gt;applyHaving($dbalQb, $condition);
```

---

## Doctrine ORM Bridge

**Class**: `Derafu\Query\Bridge\DoctrineORMQueryBuilderConditionApplier`

Applies conditions to a Doctrine ORM `QueryBuilder` as DQL. This bridge generates DQL-compatible fragments — it does not produce raw SQL.

### Methods

```php
public function apply(object $queryBuilder, ConditionInterface|CompositeConditionInterface $condition): void
public function applyHaving(object $queryBuilder, ConditionInterface|CompositeConditionInterface $condition): void
```

### Usage

```php
use Derafu\Query\Bridge\DoctrineORMQueryBuilderConditionApplier;

$applier = new DoctrineORMQueryBuilderConditionApplier();

$condition = $parser-&gt;parse(&#039;status?=active&amp;&amp;total?&gt;1000&#039;);

$ormQb = $em-&gt;createQueryBuilder()
    -&gt;select(&#039;i&#039;)
    -&gt;from(Invoice::class, &#039;i&#039;);

$applier-&gt;apply($ormQb, $condition);

$invoices = $ormQb-&gt;getQuery()-&gt;getResult();
```

### Single-Segment Path Qualification

Single-segment paths like `status` are automatically qualified with the root entity alias. The root alias is read from the first `FROM` clause:

```
status?=active   →   i.status = :param_status_...   (when root alias is &#039;i&#039;)
```

Multi-segment paths are left as-is, allowing explicit alias control.

### JOIN Generation

The ORM bridge adds JOINs to the QueryBuilder using Doctrine&#039;s association names — the `on:` option in path segments is **ignored**. Doctrine derives join conditions from entity mappings.

```
invoices[alias:i]__payments[alias:p]__status
```

If `Invoice` has a `payments` association, the bridge calls:
```
$ormQb-&gt;innerJoin(&#039;i.payments&#039;, &#039;p&#039;);
```

### EXISTS Paths in DQL

Subquery paths (`___`) are translated to DQL:

| Expression | DQL |
|------------|-----|
| `___payments?is:empty` | `SIZE(i.payments) = 0` |
| `___payments?isnot:empty` | `SIZE(i.payments) &gt; 0` |
| `___payments__status?=pending` | `EXISTS(SELECT _payments0.id FROM App\Entity\Payment _payments0 WHERE _payments0.invoice = i AND _payments0.status = :p)` |
| `___invoices__AVG(total)?&lt;1000` | `(SELECT AVG(_i0.total) FROM App\Entity\Invoice _i0 WHERE _i0.customer = i) &lt; :p` |
| `___invoices__COUNT(*)?=0` | `SIZE(i.invoices) = 0` (optimized) |

### DQL-Incompatible Operators

The following operator types throw `UnsupportedOperatorException` when used with this bridge:

- **`date` type**: `date:`, `month:`, `year:`, `period:` — require SQL functions not available in DQL.
- **`binary` type**: `b&amp;`, `b|`, `b^`, etc. — bitwise SQL not supported in DQL.
- **`regexp` type**: `~`, `~*`, `similarto:`, etc. — database-specific regex not in DQL.
- **`ilike:` and `notilike:`** — ILIKE is not DQL.

In the API Platform `SmartFilter`, these exceptions are caught and the filter is silently skipped.

---

## Illuminate (Laravel) Bridge

**Class**: `Derafu\Query\Bridge\IlluminateQueryBuilderConditionApplier`

Applies conditions to Illuminate&#039;s `Query\Builder` or Eloquent&#039;s `Builder`. Both are accepted — Eloquent builders are resolved to their underlying `Query\Builder` via `toBase()`.

### Methods

```php
public function apply(object $queryBuilder, ConditionInterface|CompositeConditionInterface $condition): void
public function applyHaving(object $queryBuilder, ConditionInterface|CompositeConditionInterface $condition): void
```

### Usage

```php
use Derafu\Query\Bridge\IlluminateQueryBuilderConditionApplier;

$applier = new IlluminateQueryBuilderConditionApplier();

$condition = $parser-&gt;parse(&#039;status?=active&amp;&amp;total?&gt;1000&#039;);

// With a plain Query\Builder.
$qb = DB::table(&#039;invoices&#039;);
$applier-&gt;apply($qb, $condition);
$rows = $qb-&gt;get();

// With an Eloquent Builder.
$builder = Invoice::query();
$applier-&gt;apply($builder, $condition);
$invoices = $builder-&gt;get();
```

### FROM and JOIN Inference

Same behavior as the DBAL bridge: infers the `FROM` table from multi-segment paths and adds `join()`, `leftJoin()`, or `rightJoin()` calls as needed. Duplicate joins (same table reference) are skipped.

### Driver Detection

The bridge reads the driver name directly from `Connection::getDriverName()`:

| Driver name | SQL generated |
|-------------|--------------|
| `pgsql` | PostgreSQL SQL |
| `mysql` | MySQL SQL |
| `sqlite` | SQLite SQL |

### HAVING

```php
$condition = $parser-&gt;parse(&#039;AVG(price)?&gt;500&#039;);
$applier-&gt;applyHaving($qb, $condition);
// Calls: $qb-&gt;havingRaw($sql, $params)
```

---

## API Platform Bridge

**Class**: `Derafu\Query\Bridge\ApiPlatform\SmartFilter`

Integrates `derafu/query` as an API Platform filter via the `QueryParameter` attribute. When a request comes in, the filter builds a Derafu expression from the parameter&#039;s property name and value, parses it, and applies it to the ORM `QueryBuilder`.

### Registration

```php
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\QueryParameter;
use Derafu\Query\Bridge\ApiPlatform\SmartFilter;

#[ApiResource]
#[QueryParameter(key: &#039;price&#039;,  property: &#039;price&#039;,  filter: SmartFilter::class)]
#[QueryParameter(key: &#039;status&#039;, property: &#039;status&#039;, filter: SmartFilter::class)]
class Product
{
    // …
}
```

### URL Filter Syntax

The URL parameter value is the filter part (operator + value); the property name provides the path segment:

```
GET /api/products?price=&gt;1000&amp;status=in:paid,issued
```

The `SmartFilter` builds expressions internally:
- `price` property + `&gt;1000` value → expression `price?&gt;1000`
- `status` property + `in:paid,issued` value → expression `status?in:paid,issued`

### Generic SmartFilter Property

For a single parameter that accepts a full composite expression, use the special `__derafu_smart_filter` property name:

```php
#[QueryParameter(key: &#039;filter&#039;, property: &#039;__derafu_smart_filter&#039;, filter: SmartFilter::class)]
```

```
GET /api/products?filter=status?=active&amp;&amp;price?&gt;1000
```

### Dependency Injection (Symfony)

Register `CompositeExpressionParser` and `DoctrineORMQueryBuilderConditionApplier` as services and inject them into `SmartFilter`:

```yaml
# services.yaml
services:
    Derafu\Query\Bridge\ApiPlatform\SmartFilter:
        arguments:
            $compositeParser: &#039;@Derafu\Query\Filter\Contract\CompositeExpressionParserInterface&#039;
            $applier:         &#039;@Derafu\Query\Bridge\DoctrineORMQueryBuilderConditionApplier&#039;
```

### Error Handling

`SmartFilter` silently catches two exceptions:

- **`UnsupportedOperatorException`**: DQL-incompatible operators (`date:`, `b&amp;`, `ilike:`, regex) — the filter is skipped.
- **Any `Throwable`**: Malformed expressions — the filter is skipped.

This prevents a bad filter parameter from crashing the entire collection endpoint.

---

## Bridge Comparison

| Feature | Doctrine DBAL | Doctrine ORM | Illuminate |
|---------|:---:|:---:|:---:|
| SQL operators (all) | ✅ | ✅† | ✅ |
| Date operators | ✅ | ❌ | ✅ |
| Regex operators | ✅ | ❌ | ✅ |
| Bitwise operators | ✅ | ❌ | ✅ |
| `ilike:` / `notilike:` | ✅ | ❌ | ✅ |
| FROM inference from paths | ✅ | ❌‡ | ✅ |
| JOIN from path segments | ✅ | ✅ (ORM assoc) | ✅ |
| EXISTS paths | ✅ | ✅ (SIZE/EXISTS DQL) | ✅ |
| Aggregate subqueries | ✅ | ✅ | ✅ |

† Compatible operators only — DQL-incompatible operators throw `UnsupportedOperatorException`.  
‡ ORM bridge requires an explicit `FROM` / `from()` call before applying conditions.




---

### Security Guide

Security Guide

# Security Guide

`derafu/query` uses prepared statements for all filter values. This page explains the security model, what is and is not protected, and how to use the library safely.

---

## How Values Are Protected

All filter values — the part after the operator symbol in `column?operatorVALUE` — become **named parameters** in the generated SQL:

```
price?&gt;1000
→  price &gt; :param_price_5f3a...
   with parameter binding: :param_price_5f3a... = &#039;1000&#039;
```

The SQL string itself never contains the user-supplied value. It goes through PDO/Doctrine/Illuminate&#039;s prepared statement mechanism, which prevents SQL injection in values.

This holds for every operator type: standard comparisons, LIKE patterns, lists, ranges, dates, regex values, and bitwise operands are all bound as parameters.

---

## What SqlSanitizerTrait Protects

`SqlSanitizerTrait` is used for **SQL identifiers** — column names, table names, aliases, and aggregate function names. It strips all characters except `[a-zA-Z0-9_]` from simple identifiers.

Safe uses (identifiers derived from path segments or select expressions):

```php
$qb-&gt;select(&#039;column_name&#039;);         // simple identifier
$qb-&gt;select(&#039;table.column&#039;);        // qualified identifier
$qb-&gt;select(&#039;column AS alias&#039;);     // with alias
$qb-&gt;select(&#039;COUNT(*) AS total&#039;);   // aggregate function
$qb-&gt;select(&#039;SUM(price) AS total&#039;); // aggregate with column
```

Cases requiring attention:

```php
// Complex expressions — arithmetic operators are detected and left as-is.
$qb-&gt;select(&#039;price * quantity AS total&#039;);

// Multi-argument functions — outer function is sanitized; inner args are handled separately.
$qb-&gt;select(&#039;COALESCE(column1, column2, 0) AS result&#039;);
```

**The sanitizer is a defense-in-depth measure for identifier injection, not a substitute for input validation.** It strips unexpected characters but cannot reason about business logic (e.g. it does not know which columns a user is allowed to access).

---

## What Is NOT Automatically Protected

### Explicit JOIN Conditions

The `join()` / `innerJoin()` / `leftJoin()` / `rightJoin()` methods accept a raw `$condition` string that is inserted verbatim into the SQL:

```php
// This condition is NOT sanitized.
$qb-&gt;innerJoin(&#039;customers&#039;, &#039;i.customer_id = c.id&#039;, &#039;c&#039;);
```

**Never pass user-supplied input directly as a join condition.** Use path-based syntax in `where()` instead — those are parsed and sanitized:

```php
// Safe: join condition comes from the path parser.
$qb-&gt;where(&#039;invoices[alias:i]__customers[on:customer_id=id,alias:c]__name?isnot:null&#039;);
```

### The `?E` Expression Marker

When a filter expression uses `?E` (instead of `?`), the value is treated as a column reference and sanitized as an identifier — **not** bound as a parameter:

```
id?E!=other_alias.id
→  id != other_alias.id   (identifier, no parameter binding)
```

Only use `?E` for trusted, controlled values (e.g. comparing two known column aliases). Never pass user input as a `?E` value.

### `select()` with `$sanitize = false`

```php
$qb-&gt;select($trustedExpression, sanitize: false);
```

When `$sanitize` is `false`, the expression is inserted without processing. Only use this for pre-validated, application-controlled strings.

---

## Parameters vs. Identifiers

The key distinction in SQL security:

| Type | Example | How it&#039;s handled |
|------|---------|-----------------|
| **Parameter** (value) | `price?&gt;1000` — the `1000` | Bound via prepared statement |
| **Identifier** (column/table) | Path segment names, `select()` columns | Sanitized by `SqlSanitizerTrait` |

Never try to pass a value as an identifier or vice versa.

---

## Operator Value Validation

Before SQL generation, `FilterParser` validates the raw value against each operator&#039;s `pattern` (if defined). Invalid values throw `InvalidArgumentException` immediately:

```php
$parser-&gt;parse(&#039;price?date:not-a-date&#039;);   // throws: pattern mismatch
$parser-&gt;parse(&#039;flags?b&amp;1.5&#039;);             // throws: decimal not allowed for binary op
$parser-&gt;parse(&#039;status?in:&#039;);             // throws: empty list
```

This prevents malformed inputs from reaching the SQL layer, even though the actual injection risk is eliminated by parameter binding.

---

## Input Validation Best Practices

1. **Whitelist columns**: If you accept filter column names from user input (e.g. in an API), validate them against a known-safe list before passing to the expression parser.

2. **Validate operator intent**: Consider whether a user should be allowed to use all operators. For example, regex operators can cause high-CPU queries on large tables; restrict them if necessary.

3. **Limit list sizes**: The `in:` and `notin:` operators accept arbitrarily long lists. Consider validating or capping list length before parsing.

4. **Principle of least privilege**: The database user used by your application should only have `SELECT` (and only `INSERT`/`UPDATE`/`DELETE` where needed). A read-only connection cannot be abused into DDL statements even if SQL injection were somehow possible.

5. **Audit HAVING and JOIN conditions**: `having()` accepts the same expression format as `where()` and is equally safe. Explicit `join()` conditions are not sanitized — see above.

6. **Log and monitor**: Log queries that raise `InvalidArgumentException` — they may indicate probing attempts.

---

## Composite Expression Safety

Composite expressions (`&amp;&amp;`, `||`, `()`) are parsed structurally, not evaluated as SQL. The parser splits on `&amp;&amp;` and `||` at parenthesis depth 0 and recurses — it does not execute or interpolate anything. There is no risk of injection through the composite syntax itself.

```
A?=1&amp;&amp;B?=2      →  two separate bound parameters
(A?=1||B?=2)    →  same, with OR grouping
```

The value inside each leaf expression is always bound as a parameter (unless `?E` is used — see above).

---

## Summary

| Area | Protection |
|------|-----------|
| Filter values | ✅ Prepared statement parameters |
| Column/table identifiers | ✅ `SqlSanitizerTrait` stripping |
| Operator validation patterns | ✅ Regex validation at parse time |
| Composite expression parsing | ✅ Structural parsing, no SQL eval |
| Explicit `join()` conditions | ⚠️ Not sanitized — keep application-controlled |
| `?E` expression references | ⚠️ Sanitized as identifier — keep application-controlled |
| `select($expr, sanitize: false)` | ⚠️ Raw insert — keep application-controlled |
| Column name whitelisting | 🔲 Application responsibility |
| Operator allowlisting | 🔲 Application responsibility |




---

### Query Config

Declarative Query Configuration

# Declarative Query Configuration

The `QueryConfig` class provides a configuration-based alternative to the fluent builder API. It is covered in detail in the [Query Builder](./query-builder) page, under the **Declarative Query Configuration** section.

## Quick Reference

```php
use Derafu\Query\Config\QueryConfig;

// From an array.
$config = new QueryConfig([
    &#039;table&#039;   =&gt; &#039;products&#039;,
    &#039;select&#039;  =&gt; &#039;id, name, price&#039;,
    &#039;where&#039;   =&gt; &#039;category?=electronics&#039;,
    &#039;orderBy&#039; =&gt; [&#039;price&#039; =&gt; &#039;DESC&#039;],
    &#039;limit&#039;   =&gt; 10,
]);
$result = $config-&gt;applyTo($queryBuilder)-&gt;execute();

// From a YAML file.
$config = QueryConfig::fromYamlFile(&#039;queries/product_report.yaml&#039;);

// From a JSON file.
$config = QueryConfig::fromJsonFile(&#039;queries/sales.json&#039;);

// Auto-detect format by extension.
$config = QueryConfig::fromFile(&#039;queries/report.yaml&#039;);
```

### Supported Configuration Keys

| Key | Description |
|-----|-------------|
| `table` | Table name (FROM clause) |
| `alias` | Table alias |
| `select` | Columns to select |
| `distinct` | `true` to add DISTINCT |
| `where` | WHERE condition(s) |
| `andWhere` | Additional AND condition(s) |
| `orWhere` | OR condition(s) |
| `andWhereOr` | AND with nested OR groups |
| `innerJoin` | `{table, condition, alias?}` |
| `leftJoin` | `{table, condition, alias?}` |
| `rightJoin` | `{table, condition, alias?}` |
| `crossJoin` | `{table, alias?}` |
| `groupBy` | GROUP BY column(s) |
| `having` | HAVING condition(s) |
| `orderBy` | `{column: direction}` pairs |
| `limit` | Max rows to return |
| `offset` | Rows to skip |

For full documentation, examples, and API-driven query patterns, see [Query Builder → Declarative Query Configuration](./query-builder#declarative-query-configuration).




---

## ETL Project

Derafu ETL

# Derafu ETL




---

### Introduction

From Spreadsheets to Databases Seamlessly

# From Spreadsheets to Databases Seamlessly

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/etl/main)
![CI Workflow](https://github.com/derafu/etl/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/etl)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/etl)
![Total Downloads](https://poser.pugx.org/derafu/etl/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/etl/d/monthly)

A PHP package that transforms spreadsheet data into database structures and content with minimal effort.

## Overview

Derafu ETL provides a streamlined solution for converting data between spreadsheets and databases. With a clean, fluent API, it simplifies complex data integration tasks through a pipeline architecture.

```php
$pipeline = new Pipeline();
$result = $pipeline
    -&gt;extract(&#039;data.xlsx&#039;)      // Extract data from a spreadsheet.
    -&gt;transform($rules)         // Apply transformations rules (optional).
    -&gt;load(&#039;database.sqlite&#039;)   // Load into a database.
    -&gt;execute()
;
```

## Key Features

{.list-unstyled}
- 📤 **Extract** data from various sources (XLSX, ODS, CSV, databases).
- 🔄 **Transform** data with customizable rules.
- 📥 **Load** data into different target systems.
- 🔁 **Bidirectional** conversion between spreadsheets and databases.
- 🏗️ **Schema management** with automatic table creation and structure updates.
- 📊 **Data visualization** capabilities with schema export to Markdown, D2, and more.
- 🧩 **Extensible** architecture for custom source and target systems.

## Installation

Install via Composer:

```bash
composer require derafu/etl
```

## Quick Start

### Command Line

The quickest way to use Derafu ETL is through the command line:

```bash
php app/console.php derafu:etl data.xlsx database.sqlite
```

This extracts data from `data.xlsx` and loads it into a new SQLite database on `database.sqlite`.

#### Example

Run the example used in tests with:

```shell
php app/console.php derafu:etl tests/fixtures/spreadsheet-data.xlsx
```

This will create a `spreadsheet-data.sqlite` in the current directory.


### PHP Code

```php
use Derafu\ETL\Pipeline\Pipeline;

$pipeline = new Pipeline();
$result = $pipeline
    -&gt;extract(&#039;data.xlsx&#039;)  // Load data from a XLSX.
    -&gt;transform()           // This will use default transformations.
    -&gt;load([                // You can specify the configuration for Doctrine.
        &#039;doctrine&#039; =&gt; [
            &#039;driver&#039; =&gt; &#039;pdo_sqlite&#039;,
            &#039;path&#039; =&gt; &#039;database.sqlite&#039;,
        ]
    ])
    -&gt;execute();            // This is will run the process.

echo &quot;Rows loaded: &quot; . $result-&gt;rowsLoaded();
```

## Understanding ETL Pipelines

An ETL pipeline consists of three main steps:

1. **Extract**: Read data from a source (e.g., spreadsheet).
2. **Transform**: Apply rules and transformations to the data.
3. **Load**: Write the transformed data to a target (e.g., database).

Derafu ETL provides a clean interface for each step while handling the complex details behind the scenes.

## More than just move data to a target

### Export Database Schema to Markdown

```php
use Derafu\ETL\Database\DatabaseManager;
use Derafu\ETL\Schema\Target\MarkdownSchemaTarget;

$manager = new DatabaseManager();
$database = $manager-&gt;connect(&#039;database.sqlite&#039;);

$target = new MarkdownSchemaTarget();
$markdown = $target-&gt;applySchema($database-&gt;schema());

file_put_contents(&#039;schema.md&#039;, $markdown);
```

### Generate Database Diagram

```php
use Derafu\ETL\Database\DatabaseManager;
use Derafu\ETL\Schema\Target\D2SchemaTarget;

$manager = new DatabaseManager();
$database = $manager-&gt;connect(&#039;database.sqlite&#039;);

$target = new D2SchemaTarget();
$d2 = $target-&gt;applySchema($database-&gt;schema());

file_put_contents(&#039;schema.d2&#039;, $d2);
```




---

### ETL Architecture

Architecture

# Architecture

This document explains the architecture and design principles behind Derafu ETL.

## ETL Pattern

The Extract-Transform-Load (ETL) pattern is a data integration process used to collect data from various sources, transform it to fit operational needs, and load it into a target database for analysis and storage.

### The Three Phases

1. **Extract**: Gathering data from source systems.
2. **Transform**: Converting the extracted data to satisfy operational requirements.
3. **Load**: Writing the transformed data to the target system.

## Derafu ETL Implementation

Derafu ETL implements this pattern with a clean, object-oriented approach centered around the Pipeline concept.

### Core Components

{.w-75 .mx-auto}
![ETL Pipeline](https://www.derafu.dev/img/diagrams/content/docs/data/etl/derafu-etl-core-components.svg)

#### Extract Phase

- `DataSource`: Encapsulates the data source (spreadsheet, database).
- `DataExtractor`: Handles extraction logic.
- `SchemaSource`: Extracts schema information from the source.

#### Transform Phase

- `DataRules`: Defines transformation rules.
- `DataTransformer`: Applies transformations to extracted data.

#### Load Phase

- `DataTarget`: Represents the destination system.
- `DataLoader`: Manages loading data into the target.
- `SchemaTarget`: Applies schema to the target system.

### Pipeline Orchestration

The `Pipeline` class orchestrates the entire ETL process, providing a fluent interface:

```php
$pipeline
    -&gt;extract($source)    // Configure extraction.
    -&gt;transform($rules)   // Configure transformation.
    -&gt;load($target)       // Configure loading.
    -&gt;execute()           // Run the pipeline.
;
```

When `execute()` is called, the pipeline:

1. Validates the configuration.
2. Extracts data from the source.
3. Transforms the data according to rules.
4. Synchronizes the target schema with the source.
5. Loads the transformed data into the target.
6. Returns a result object with statistics.

## Key Abstractions

### Database

The `Database` abstraction provides a unified interface for different database types:

- `SpreadsheetDatabase`: Treats spreadsheets as databases using [Derafu Spreadsheet](https://www.derafu.dev/docs/data/spreadsheet).
- `DoctrineDatabase`: Works with any database supported by Doctrine DBAL.

### Schema

The `Schema` system represents database structure:

- Tables, columns, indexes, foreign keys.
- Import/export to various formats (Spreadsheet, Doctrine, Markdown, D2, etc.).

## Extension Points

Derafu ETL is designed for extensibility:

1. **New Data Sources**: Implement `DataSourceInterface`.
2. **Custom Transformations**: Extend `DataRules`.
3. **New Data Targets**: Implement `DataTargetInterface`.
4. **Schema Visualization**: Implement `SchemaTargetInterface`.

## Design Principles

1. **Separation of Concerns**: Each component has a clear responsibility.
2. **Fluent Interface**: Expressive, chainable API.
3. **Flexibility**: Support for various formats and systems.
4. **Extensibility**: Easy to extend with custom components.




---

### Schema Visualization

Schema Visualization

# Schema Visualization

Derafu ETL includes powerful tools for visualizing database schemas through various output formats. These visualizations are useful for documentation, planning, and understanding the structure of your data.

## Available Visualization Formats

Derafu ETL supports multiple visualization formats through schema targets:

- **Markdown**: Human-readable documentation.
- **D2**: Interactive entity-relationship diagrams.
- **Text**: Simple text representation.
- **SQL**: Database creation scripts.
- **Doctrine Schema**: Programmatic schema representation.

## Using Schema Targets

Schema targets implement the `SchemaTargetInterface` and convert a schema to a specific format.

### Basic Usage

All schema targets follow a similar pattern:

```php
use Derafu\ETL\Database\DatabaseManager;
use Derafu\ETL\Schema\Target\CustomSchemaTarget; // Just an example.

// Connect to the database.
$manager = new DatabaseManager();
$database = $manager-&gt;connect(&#039;database.sqlite&#039;);

// Get the schema.
$schema = $database-&gt;schema();

// Create the target.
$target = new CustomSchemaTarget();

// Apply the schema to the target.
$output = $target-&gt;applySchema($schema);

// Use the output.
file_put_contents(&#039;output-file.custom&#039;, $output);
```

## Markdown Schema

The `MarkdownSchemaTarget` generates comprehensive Markdown documentation for your database schema.

```php
use Derafu\ETL\Schema\Target\MarkdownSchemaTarget;

$target = new MarkdownSchemaTarget();
$markdown = $target-&gt;applySchema($schema);

file_put_contents(&#039;schema.md&#039;, $markdown);
```

The generated Markdown includes:

- Table of contents.
- Detailed table definitions.
- Column information with types and constraints.
- Primary keys, indexes, and foreign key relationships.

Example output:

```markdown
# Database Schema

## Table of Contents

- [Table: users](#table-users)
- [Table: posts](#table-posts)

## Table: users {#table-users}

### Columns

| Column     | Type        | Attributes              | Description |
|------------|-------------|-------------------------|-------------|
| id         | integer     | PRIMARY KEY / NOT NULL  |             |
| username   | string(100) | NOT NULL                |             |
| email      | string(255) | NOT NULL                |             |
| created_at | datetime    | NOT NULL                |             |

### Primary Key

- Columns: `id`

### Indexes

| Name         | Columns  | Type   | Flags |
|--------------|----------|--------|-------|
| idx_username | username | INDEX  |       |
| idx_email    | email    | UNIQUE |       |
```

## D2 Diagrams

The `D2SchemaTarget` generates diagrams in [D2 format](https://d2lang.com/), a modern diagram scripting language.

```php
use Derafu\ETL\Schema\Target\D2SchemaTarget;

$target = new D2SchemaTarget(
    detailLevel: D2SchemaTarget::DETAIL_FULL,
    direction: D2SchemaTarget::DIRECTION_RIGHT,
    layout: D2SchemaTarget::LAYOUT_DEFAULT,
    includeIndexes: true
);
$d2 = $target-&gt;applySchema($schema);

file_put_contents(&#039;schema.d2&#039;, $d2);
```

Options include:

- **Detail Level**: `DETAIL_FULL`, `DETAIL_KEYS_ONLY`, `DETAIL_MINIMAL`.
- **Direction**: `DIRECTION_UP`, `DIRECTION_DOWN`, `DIRECTION_LEFT`, `DIRECTION_RIGHT`.
- **Layout**: `LAYOUT_DEFAULT`, `LAYOUT_CLUSTERED`, `LAYOUT_HIERARCHICAL`.
- **Include Indexes**: Whether to include indexes in the diagram.

Example D2 output:

```
# Database Schema

direction: right

# Tables
users: {
  shape: sql_table
  id: integer NOT NULL pk
  username: string(100) NOT NULL column
  email: string(255) NOT NULL column
  created_at: datetime NOT NULL column
}

posts: {
  shape: sql_table
  id: integer NOT NULL pk
  user_id: integer NOT NULL fk
  title: string(255) NOT NULL column
  content: text column
  created_at: datetime NOT NULL column
}

# Relationships
posts -&gt; users
```

## Practical Applications

### Documentation

Generate comprehensive documentation for your database:

```php
use Derafu\ETL\Database\DatabaseManager;
use Derafu\ETL\Schema\Target\MarkdownSchemaTarget;

$manager = new DatabaseManager();
$database = $manager-&gt;connect(&#039;production.sqlite&#039;);

$target = new MarkdownSchemaTarget();
$docs = $target-&gt;applySchema($database-&gt;schema());

file_put_contents(&#039;database-schema.md&#039;, $docs);
```

### Visual Modeling

Create visual diagrams for presentations or analysis:

```php
use Derafu\ETL\Schema\Target\D2SchemaTarget;

// Filter to only show specific tables.
$target = new D2SchemaTarget(
    tableFilter: [&#039;users&#039;, &#039;posts&#039;, &#039;comments&#039;]
);

$diagram = $target-&gt;applySchema($schema);
file_put_contents(&#039;entity-relationship.d2&#039;, $diagram);
```

### Schema Migration

Export a schema definition to create a new database:

```php
use Derafu\ETL\Schema\Target\SqliteSchemaTarget;

$target = new SqliteSchemaTarget();
$sql = $target-&gt;applySchema($schema);

file_put_contents(&#039;schema.sql&#039;, $sql);
```

## Integration with ETL Pipeline

Schema visualization can be integrated with the ETL pipeline for comprehensive documentation:

```php
use Derafu\ETL\Pipeline\Pipeline;
use Derafu\ETL\Schema\Target\MarkdownSchemaTarget;

// Run ETL pipeline.
$pipeline = new Pipeline();
$result = $pipeline
    -&gt;extract(&#039;data.xlsx&#039;)
    -&gt;transform()
    -&gt;load(&#039;database.sqlite&#039;)
    -&gt;execute();

// Document the resulting database.
$database = $result-&gt;target()-&gt;database();
$target = new MarkdownSchemaTarget();
$docs = $target-&gt;applySchema($database-&gt;schema());

file_put_contents(&#039;database-schema.md&#039;, $docs);
```




---

## Spreadsheet Project

Derafu Spreadsheet

# Derafu Spreadsheet




---

### Introduction

Unified Spreadsheet Processing for PHP

# Unified Spreadsheet Processing for PHP

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/spreadsheet/main)
![CI Workflow](https://github.com/derafu/spreadsheet/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/spreadsheet)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/spreadsheet)
![Total Downloads](https://poser.pugx.org/derafu/spreadsheet/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/spreadsheet/d/monthly)

Derafu Spreadsheet is a modern PHP library that provides a **single, consistent API** for working with spreadsheet files in multiple formats (XLSX, CSV, ODS, JSON, XML, YAML, and more).

## 🌟 Features

- **Unified API** across all file formats.
- **Multiple format support**: XLSX, XLS, CSV, ODS, JSON, XML, YAML, HTML, PDF.
- **Smart type casting**: Automatically detects and converts data types (dates, numbers, booleans, JSON).
- **Format-agnostic data manipulation**: Work with your data consistently regardless of source format.
- **Minimal dependencies**: Use only what you need.
- **PSR-7 compatible**: Generate HTTP responses with downloadable spreadsheets.
- **Modern PHP**: Written for PHP 8 with strict typing.

## 🚀 Why Derafu Spreadsheet?

While powerful libraries like PhpSpreadsheet exist, and this library use it under de hood, they often require different approaches for different formats and have complex APIs. Derafu Spreadsheet offers several key advantages:

- **Simplified API**: Work with all spreadsheet formats through a consistent interface.
- **Intelligent type handling**: Focus on your data, not type conversion.
- **Format abstraction**: Write your code once, and it works with any format.
- **Flexible format handlers**: Easily switch between formats or implement custom handlers.
- **Minimal learning curve**: Clean, intuitive API with sensible defaults.

## 📦 Installation

```bash
composer require derafu/spreadsheet
```

For specific format support, you may need additional dependencies:

```bash
# For CSV support with League CSV (recommended).
composer require league/csv

# For XLSX, XLS, ODS, HTML support.
composer require phpoffice/phpspreadsheet

# For YAML support.
composer require symfony/yaml

# For HTTP response generation.
composer require nyholm/psr7
```

## 📝 Basic Usage

```php
&lt;?php

use Derafu\Spreadsheet\SpreadsheetLoader;
use Derafu\Spreadsheet\SpreadsheetDumper;

// Load a spreadsheet (format auto-detected from extension).
// Under the hood it will create default Factory and Caster instances. If you
// don&#039;t want that, inject your implementation.
$loader = new Loader();
$spreadsheet = $loader-&gt;loadFromFile(&#039;data.xlsx&#039;);

// Access data from sheets.
$sheet = $spreadsheet-&gt;getSheet(&#039;Sheet1&#039;);
$rows = $sheet-&gt;getRows();

// Modify data.
$sheet-&gt;setCell(0, 0, &#039;Updated value&#039;);
$spreadsheet-&gt;createSheet(&#039;NewSheet&#039;, [[&#039;Header1&#039;, &#039;Header2&#039;], [1, 2]]);

// Save in different format.
// Under the hood it will create default Factory and Caster instances. If you
// don&#039;t want that, inject your implementation.
$dumper = new Dumper();
$dumper-&gt;dumpToFile($spreadsheet, &#039;output.csv&#039;);

// Or convert to string
$jsonString = $dumper-&gt;dumpToString($spreadsheet, &#039;json&#039;);
```

## 🔄 Working with Different Formats

Derafu Spreadsheet handles format conversion automatically:

```php
// Load Excel file.
$spreadsheet = $loader-&gt;loadFromFile(&#039;data.xlsx&#039;);

// Save as CSV.
$dumper-&gt;dumpToFile($spreadsheet, &#039;data.csv&#039;);

// Save as JSON.
$dumper-&gt;dumpToFile($spreadsheet, &#039;data.json&#039;);

// Save as YAML.
$dumper-&gt;dumpToFile($spreadsheet, &#039;data.yaml&#039;);
```

## 📋 Intelligent Type Casting

One of Derafu Spreadsheet&#039;s key features is automatic type casting for both reading and writing:

```php
$spreadsheet = $loader-&gt;loadFromFile(&#039;data.csv&#039;);

// String &#039;123&#039; is automatically cast to integer 123.
// &#039;true&#039; is cast to boolean true.
// &#039;2025-03-12&#039; is cast to DateTimeImmutable object.
// JSON strings are parsed to indexed arrays or associative arrays (&quot;objects&quot;).

$cell = $sheet-&gt;getCell(0, 0); // Typed data, not just strings.

// When writing, types are automatically converted to appropriate formats.
$dumper-&gt;dumpToFile($spreadsheet, &#039;output.xlsx&#039;);
```




---

### Architecture

Architecture

# Architecture

This document provides an overview of the architecture and design principles behind Derafu Spreadsheet.

## Core Components

Derafu Spreadsheet is built around several key components that work together to provide a unified approach to spreadsheet processing:

![Architecture Diagram](https://www.derafu.dev/img/diagrams/content/docs/data/spreadsheet/derafu-spreadsheet-architecture-diagram.svg)

### Key Components

1. **Loader**
   - Handles loading spreadsheet data from files or strings.
   - Uses Factory to create appropriate Format Handlers.
   - Uses Caster to convert raw data to appropriate PHP types.

2. **Dumper**
   - Handles saving spreadsheet data to files or strings.
   - Uses Factory to create appropriate Format Handlers.
   - Uses Caster to convert PHP types to format-appropriate representations.

3. **Factory**
   - Creates and manages Format Handlers based on file extension or specified format.
   - Allows registering custom Format Handlers.
   - Provides format detection capabilities.

4. **Caster**
   - Converts raw data values to appropriate PHP types when reading.
   - Converts PHP types to appropriate string representations when writing.
   - Handles intelligent date, boolean, numeric, and JSON detection.

5. **Format Handlers**
   - Format-specific implementations that know how to read/write a particular format.
   - All implement a common FormatHandlerInterface.
   - Includes handlers for XLSX, XLS, CSV, ODS, JSON, XML, YAML, HTML, PDF.

6. **Data Model**
   - **Spreadsheet**: Top-level container for all data.
   - **Sheet**: Container for rows and cells with a name.
   - Both implement interfaces for consistent interaction.

## Design Principles

Derafu Spreadsheet was designed with several key principles in mind:

1. **Unified API**
   - Consistent interface across all supported formats.
   - Same code works regardless of source or target format.

2. **Type Intelligence**
   - Automatic conversion between string values and appropriate PHP types.
   - No manual type casting required in application code.

3. **Separation of Concerns**
   - Format handling is separated from data model.
   - Type conversion is separated from data loading/saving.
   - Each component has a single, clear responsibility.

4. **Interface-Based Design**
   - All components define and implement interfaces.
   - Allows for custom implementations and extensions.

5. **Minimal Dependencies**
   - Core functionality has minimal dependencies.
   - Format-specific dependencies only required for formats you use.

## Data Flow

When working with Derafu Spreadsheet, data flows through the components as follows:

### Loading Process:
1. **Loader** receives a file or string.
2. **Factory** creates appropriate Format Handler based on format.
3. **Format Handler** reads raw data into internal Spreadsheet structure.
4. **Caster** converts raw values to appropriate PHP types.
5. Typed **Spreadsheet** object is returned to application.

### Saving Process:
1. **Dumper** receives a Spreadsheet object and target format.
2. **Caster** converts PHP values to appropriate string representations.
3. **Factory** creates appropriate Format Handler for target format.
4. **Format Handler** writes structured data to file or string.
5. File path or string content is returned to application.

## Extensibility

Derafu Spreadsheet is designed to be extensible:

- **Custom Format Handlers**: Create handlers for proprietary or custom formats.
- **Custom Casters**: Implement specialized type conversion logic.
- **HTTP Integration**: Generate responses with downloadable spreadsheets.
- **Framework Integration**: Easy to integrate with popular PHP frameworks.

## Directory Structure

```
src/
├── Abstract/
│   └── AbstractPhpSpreadsheetFormatHandler.php
├── Contract/
│   ├── SpreadsheetCasterInterface.php
│   ├── SpreadsheetDumperInterface.php
│   ├── FactoryInterface.php
│   ├── FormatHandlerInterface.php
│   ├── SpreadsheetLoaderInterface.php
│   ├── SheetInterface.php
│   ├── SpreadsheetInterface.php
│   └── Http/
│       └── SpreadsheetHttpResponseGeneratorInterface.php
├── Exception/
│   ├── SpreadsheetDumpException.php
│   ├── SpreadsheetFileNotFoundException.php
│   ├── SpreadsheetFormatNotSupportedException.php
│   └── SpreadsheetLoadException.php
├── Format/
│   ├── CsvLeagueHandler.php
│   ├── CsvPhpSpreadsheetHandler.php
│   ├── HtmlHandler.php
│   ├── JsonHandler.php
│   ├── OdsHandler.php
│   ├── PdfHandler.php
│   ├── XlsHandler.php
│   ├── XlsxHandler.php
│   ├── XmlHandler.php
│   └── YamlHandler.php
├── Http/
│   └── NyholmSpreadsheetHttpResponseGenerator.php
├── Caster.php
├── Dumper.php
├── Factory.php
├── Loader.php
├── Sheet.php
└── Spreadsheet.php
```




---

### Install

Install the library

# Install the library

This guide will walk you through installing Derafu Spreadsheet and its dependencies.

## Basic Installation

The simplest way to install Derafu Spreadsheet is through Composer:

```bash
composer require derafu/spreadsheet
```

This installs the core package. Out of the box, only works with JSON and XML. Depending on which file formats you want to work with, you may need to install additional dependencies.

## Format-Specific Dependencies

Derafu Spreadsheet supports multiple formats through different handlers. Each format may require additional dependencies:

### CSV Support

For CSV support, you can choose between two handlers:

```bash
# Recommended: League CSV (faster, more memory efficient).
composer require league/csv

# Alternative: PhpSpreadsheet CSV handling.
composer require phpoffice/phpspreadsheet
```

### Excel and OpenDocument Support

For XLSX, XLS, and ODS support:

```bash
composer require phpoffice/phpspreadsheet
```

### YAML Support

For YAML support:

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

### PDF Support

For PDF export:

```bash
# Base requirement.
composer require phpoffice/phpspreadsheet

# Choose one PDF library:
composer require mpdf/mpdf        # For MpdfWriter (recommended).
# OR
composer require dompdf/dompdf    # For DompdfWriter.
# OR
composer require tecnickcom/tcpdf # For TcpdfWriter.
```

### HTTP Response Support

For PSR-7 HTTP response integration:

```bash
composer require nyholm/psr7
```

If you don&#039;t want to use nyholm/psr7, you can use any PSR-7 compatible library implementing by your own `SpreadsheetHttpResponseGeneratorInterface`.

## Complete Installation (All Formats)

If you want to support all formats, with the default handlers, you can install all dependencies at once:

```bash
composer require derafu/spreadsheet league/csv phpoffice/phpspreadsheet \
    symfony/yaml mpdf/mpdf nyholm/psr7
```

## Installation in Frameworks

### Symfony

To use Derafu Spreadsheet in Symfony, install the package:

```bash
composer require derafu/spreadsheet
```

Then define services in your `services.yaml`:

```yaml
services:
    Derafu\Spreadsheet\Contract\SpreadsheetFactoryInterface:
        class: Derafu\Spreadsheet\Factory

    Derafu\Spreadsheet\Contract\SpreadsheetCasterInterface:
        class: Derafu\Spreadsheet\Caster

    Derafu\Spreadsheet\Contract\SpreadsheetLoaderInterface:
        class: Derafu\Spreadsheet\Loader
        arguments:
            - &#039;@Derafu\Spreadsheet\Factory&#039;
            - &#039;@Derafu\Spreadsheet\Caster&#039;

    Derafu\Spreadsheet\Contract\SpreadsheetDumperInterface:
        class: Derafu\Spreadsheet\Dumper
        arguments:
            - &#039;@Derafu\Spreadsheet\Factory&#039;
            - &#039;@Derafu\Spreadsheet\Caster&#039;
```

Check the constructors of the classes to see what arguments they expect. You can configure them as you want, for example change the delimiter for CSV.




---

### Basic Usage

Basic Usage

# Basic Usage

This guide covers the fundamental operations with Derafu Spreadsheet: loading, manipulating, and saving spreadsheet data.

## Loading a Spreadsheet

You can load a spreadsheet from a file or from a string:

```php
&lt;?php

use Derafu\Spreadsheet\SpreadsheetLoader;

// Create a loader.
$loader = new Loader();

// From a file (format auto-detected from extension).
$spreadsheet = $loader-&gt;loadFromFile(&#039;data.xlsx&#039;);

// From a string (must specify format).
$csvString = &quot;header1,header2\nvalue1,value2&quot;;
$spreadsheet = $loader-&gt;loadFromString($csvString, &#039;csv&#039;);
```

## Working with Sheets

A spreadsheet contains one or more sheets, which you can access and manipulate:

```php
// Get all sheet names.
$sheetNames = $spreadsheet-&gt;getSheetNames();

// Get a specific sheet.
$sheet = $spreadsheet-&gt;getSheet(&#039;Sheet1&#039;);

// Create a new sheet with data.
$spreadsheet-&gt;createSheet(&#039;NewSheet&#039;, [
    [&#039;Header1&#039;, &#039;Header2&#039;, &#039;Header3&#039;],
    [&#039;Value1&#039;, &#039;Value2&#039;, &#039;Value3&#039;],
    [&#039;Value4&#039;, &#039;Value5&#039;, &#039;Value6&#039;]
]);

// Set active sheet.
$spreadsheet-&gt;setActiveSheet(&#039;NewSheet&#039;);

// Get active sheet.
$activeSheet = $spreadsheet-&gt;getActiveSheet();

// Check if a sheet exists.
if ($spreadsheet-&gt;hasSheet(&#039;SomeSheet&#039;)) {
    // ...
}

// Remove a sheet.
$spreadsheet-&gt;removeSheet(&#039;SomeSheet&#039;);
```

## Working with Rows and Cells

Once you have a sheet, you can access and modify its data:

```php
// Get all rows.
$rows = $sheet-&gt;getRows();

// Get a specific row.
$firstRow = $sheet-&gt;getRow(0);

// Add a new row.
$sheet-&gt;addRow([&#039;New&#039;, &#039;Row&#039;, &#039;Data&#039;]);

// Get a specific cell.
$value = $sheet-&gt;getCell(0, 0); // Row 0, Column 0.
// or with named columns (for associative sheets).
$value = $sheet-&gt;getCell(0, &#039;column_name&#039;);

// Update a cell.
$sheet-&gt;setCell(0, 0, &#039;New Value&#039;);

// Get header row.
$headers = $sheet-&gt;getHeaderRow();

// Get data rows (excludes header for indexed sheets).
$dataRows = $sheet-&gt;getDataRows();
```

## Data Types

Thanks to the automatic type casting, you can work with native PHP types:

```php
// Numbers are already cast to int/float.
$number = $sheet-&gt;getCell(0, 0); // e.g., 123 (int).

// Dates are cast to DateTimeImmutable.
$date = $sheet-&gt;getCell(0, 1); // e.g., DateTimeImmutable object.
echo $date-&gt;format(&#039;Y-m-d&#039;); // &quot;2025-03-12&quot;.

// Booleans are properly typed.
$bool = $sheet-&gt;getCell(0, 2); // e.g., true (bool).

// You can set any type and it will be properly stored.
$sheet-&gt;setCell(0, 3, new DateTimeImmutable());
$sheet-&gt;setCell(0, 4, [&#039;array&#039;, &#039;values&#039;]);
$sheet-&gt;setCell(0, 5, [&#039;nested&#039; =&gt; [&#039;object&#039; =&gt; &#039;structure&#039;]]);
```

## Associative vs. Indexed Sheets

Derafu Spreadsheet supports both associative (column names as keys) and indexed (numeric keys) sheets:

```php
// Check if a sheet is associative.
if ($sheet-&gt;isAssociative()) {
    // Work with associative data.
    $rowData = $sheet-&gt;getRow(0);
    echo $rowData[&#039;column_name&#039;];
} else {
    // Work with indexed data.
    $rowData = $sheet-&gt;getRow(0);
    echo $rowData[0]; // First column.
}

    // Convert between formats.
$associativeSheet = $sheet-&gt;toAssociative(); // First row becomes header.
$indexedSheet = $sheet-&gt;toIndexed(); // Keys become first row.
```

## Saving a Spreadsheet

Once you&#039;re done modifying the data, you can save it to a file or get it as a string:

```php
use Derafu\Spreadsheet\SpreadsheetDumper;

// Create a dumper.
$dumper = new Dumper();

// Save to a file (format detected from extension).
$dumper-&gt;dumpToFile($spreadsheet, &#039;output.xlsx&#039;);

// Convert to a different format.
$dumper-&gt;dumpToFile($spreadsheet, &#039;output.csv&#039;);

// Get as a string.
$jsonString = $dumper-&gt;dumpToString($spreadsheet, &#039;json&#039;);
```

## Creating a Spreadsheet from Scratch

You can also create a spreadsheet from scratch:

```php
use Derafu\Spreadsheet\Spreadsheet;

// Create an empty spreadsheet.
$spreadsheet = new Spreadsheet();

// Create a sheet with data.
$spreadsheet-&gt;createSheet(&#039;Sheet1&#039;, [
    [&#039;Name&#039;, &#039;Age&#039;, &#039;Email&#039;],
    [&#039;John Doe&#039;, 30, &#039;john@example.com&#039;],
    [&#039;Jane Smith&#039;, 25, &#039;jane@example.com&#039;]
]);

// Create from array.
$data = [
    &#039;Users&#039; =&gt; [
        [&#039;Name&#039;, &#039;Age&#039;, &#039;Email&#039;],
        [&#039;John Doe&#039;, 30, &#039;john@example.com&#039;],
        [&#039;Jane Smith&#039;, 25, &#039;jane@example.com&#039;]
    ],
    &#039;Products&#039; =&gt; [
        [&#039;ID&#039;, &#039;Name&#039;, &#039;Price&#039;],
        [1, &#039;Product A&#039;, 29.99],
        [2, &#039;Product B&#039;, 49.99]
    ]
];

$spreadsheet = Spreadsheet::fromArray($data);

// Save it.
$dumper = new Dumper();
$dumper-&gt;dumpToFile($spreadsheet, &#039;new_spreadsheet.xlsx&#039;);
```




---

### Format Handlers

Format Handlers

# Format Handlers

Format handlers are a key component of Derafu Spreadsheet that enable the library to work with different file formats. Each format handler implements the `FormatHandlerInterface` and knows how to read and write a specific format.

## Supported Formats

![Format conversion diagram](https://www.derafu.dev/img/diagrams/content/docs/data/spreadsheet/derafu-spreadsheet-format-conversion-diagram.svg)

Derafu Spreadsheet supports the following formats in the core package:

| Format               | Extension | Handler Class              | Dependencies                  |
|----------------------|-----------|----------------------------|-------------------------------|
| Excel XLSX           | .xlsx     | `XlsxHandler`              | PhpSpreadsheet                |
| Excel XLS            | .xls      | `XlsHandler`               | PhpSpreadsheet                |
| OpenDocument         | .ods      | `OdsHandler`               | PhpSpreadsheet                |
| CSV (League)         | .csv      | `CsvLeagueHandler`         | League/CSV                    |
| CSV (PhpSpreadsheet) | .csv      | `CsvPhpSpreadsheetHandler` | PhpSpreadsheet                |
| JSON                 | .json     | `JsonHandler`              | PHP built-in                  |
| XML                  | .xml      | `XmlHandler`               | PHP built-in                  |
| YAML                 | .yaml     | `YamlHandler`              | Symfony/Yaml                  |
| HTML                 | .html     | `HtmlHandler`              | PhpSpreadsheet                |
| PDF                  | .pdf      | `PdfHandler`               | PhpSpreadsheet + PDF renderer |

## How Format Handlers Work

Each format handler is responsible for:

1. Loading data from a file into a `SpreadsheetInterface` object.
2. Dumping data from a `SpreadsheetInterface` object to a file or string (memory).
3. Providing metadata like the file extension and MIME type.

The `Factory` class manages the format handlers and automatically selects the appropriate handler based on the file extension.

## Using Format Handlers

Most of the time, you don&#039;t need to interact with format handlers directly. The `Loader` and `Dumper` classes handle this for you:

```php
$loader = new Loader();
$spreadsheet = $loader-&gt;loadFromFile(&#039;data.xlsx&#039;);

$dumper = new Dumper();
$dumper-&gt;dumpToFile($spreadsheet, &#039;output.csv&#039;);
```

However, if needed, you can access format handlers directly through the `Factory`:

```php
$factory = new Factory();
$xlsxHandler = $factory-&gt;createFormatHandler(&#039;filepath.xlsx&#039;);

// or with explicit format
$csvHandler = $factory-&gt;createFormatHandler(&#039;filepath.file&#039;, &#039;csv&#039;);
```

## Format-Specific Options

Some format handlers support additional options in the constructor.

### CSV Options (CsvLeagueHandler)

```php
$csvHandler = new CsvLeagueHandler(
    delimiter: &#039;,&#039;,      // Column delimiter.
    enclosure: &#039;&quot;&#039;,      // Field enclosure character.
    escape: &#039;\\&#039;,        // Escape character.
    sheetName: &#039;Sheet&#039;   // Default sheet name when loading CSV.
);
```

### OpenDocument/Excel Options (OdsHandler, XlsHandler, XlsxHandler)

```php
$xlsxHandler = new XlsxHandler(
    readDataOnly: true   // Read values only, ignore formatting.
);
```

### PDF Options (PdfHandler)

```php
$pdfHandler = new PdfHandler(
    writerType: &#039;Mpdf&#039;   // Can be &#039;Mpdf&#039;, &#039;Dompdf&#039;, or &#039;Tcpdf&#039;.
);
```

### JSON Options (JsonHandler)

```php
$jsonHandler = new JsonHandler(
    encodeOptions: JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE,
    decodeOptions: JSON_OBJECT_AS_ARRAY,
    depth: 512,
    sheetName: &#039;Sheet&#039;
);
```

### YAML Options (YamlHandler)

```php
use Symfony\Component\Yaml\Yaml;

$yamlHandler = new YamlHandler(
    parseFlags: Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE,
    dumpFlags: Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK,
    sheetName: &#039;Sheet&#039;
);
```

## Custom Format Handlers

You can create your own format handlers by implementing the `FormatHandlerInterface`:

```php
use Derafu\Spreadsheet\Contract\SpreadsheetFormatHandlerInterface;

class MyCustomHandler implements FormatHandlerInterface
{
    // Implement required methods.
}

// Register with the factory.
$factory = new Factory();
$factory-&gt;registerFormatHandler(&#039;custom&#039;, MyCustomHandler::class);

// Now you can use it.
$loader = new Loader($factory);
$spreadsheet = $loader-&gt;loadFromFile(&#039;filepath.custom&#039;);
```

## Format Detection

The `Factory` class detects the format based on the file extension. If the file doesn&#039;t have an extension or if you want to override it, you can explicitly specify the format:

```php
$loader = new Loader();
$spreadsheet = $loader-&gt;loadFromFile(&#039;filepath&#039;, &#039;xlsx&#039;); // Treat as XLSX.
```

You can also check which formats are supported:

```php
$factory = new Factory();
$supportedFormats = $factory-&gt;getSupportedFormats();
// [&#039;csv&#039;, &#039;xlsx&#039;, &#039;xls&#039;, &#039;ods&#039;, &#039;xml&#039;, &#039;json&#039;, &#039;yaml&#039;, &#039;html&#039;, &#039;pdf&#039;]
```

## Alternative Format Handlers

For some formats like CSV, Derafu Spreadsheet provides multiple handlers. For example:

- `CsvLeagueHandler` - Uses League/CSV (recommended for most cases).
- `CsvPhpSpreadsheetHandler` - Uses PhpSpreadsheet.

You can choose which handler to use by registering it for the format:

```php
$factory = new Factory();
$factory-&gt;registerFormatHandler(&#039;csv&#039;, CsvPhpSpreadsheetHandler::class);

$loader = new Loader($factory);
// Now CSV files will use PhpSpreadsheet handler.
```

## Format Handling and Type Casting

When loading a file, the process works like this:

1. Format handler reads the raw data from the file.
2. Data is converted to a `SpreadsheetInterface` object.
3. `Caster` processes all values to convert them to the appropriate PHP types.

When saving a file:

1. `Caster` converts PHP types to appropriate string representations.
2. Format handler writes the data to the file in the correct format.




---

### Type Casting

Type Casting

# Type Casting

One of the most powerful features of Derafu Spreadsheet is its intelligent type casting system. This enables seamless conversion between spreadsheet file data and native PHP data types.

## The Type Casting Problem

Spreadsheet files typically store everything as text, but your application needs properly typed data to work effectively. Other libraries often leave the type conversion to you, resulting in code like:

```php
// Without automatic type casting.
$value = $spreadsheet-&gt;getCell(0, 0);
if (is_numeric($value)) {
    $value = (int)$value;
} elseif ($value === &#039;true&#039; || $value === &#039;false&#039;) {
    $value = $value === &#039;true&#039;;
} elseif (preg_match(&#039;/^\d{4}-\d{2}-\d{2}$/&#039;, $value)) {
    $value = new \DateTime($value);
}
// ... and so on for every cell.
```

## Automatic Type Casting with Derafu Spreadsheet

Under the hood, the `Caster` class automatically handles all of these conversions for you:

```php
// With Derafu Spreadsheet - all values are properly typed.
$spreadsheet = $loader-&gt;loadFromFile(&#039;data.csv&#039;);
$value = $spreadsheet-&gt;getSheet(&#039;Sheet1&#039;)-&gt;getCell(0, 0); // Already properly typed!
```

{.w-25 .mx-auto}
![Type casting flow diagram](https://www.derafu.dev/img/diagrams/content/docs/data/spreadsheet/derafu-spreadsheet-type-casting-flow-diagram.svg)

## Supported Type Conversions

### After doing a load (reading from file/data to PHP)

| File Value    | PHP Type              | Example                                       |
|---------------|-----------------------|-----------------------------------------------|
| Empty string  | `null`                | `&quot;&quot;` → `null`                                 |
| Integer       | `int`                 | `&quot;123&quot;` → `123`                               |
| Decimal       | `float`               | `&quot;123.45&quot;` → `123.45`                         |
| Boolean text  | `bool`                | `&quot;true&quot;` → `true`                             |
| Date string   | `DateTimeImmutable`   | `&quot;2025-03-12&quot;` → `DateTimeImmutable`          |
| ISO 8601 date | `DateTimeImmutable`   | `&quot;2025-03-12T14:30:00&quot;` → `DateTimeImmutable` |
| JSON array    | `array`               | `&quot;[1,2,3]&quot;` → `[1, 2, 3]`                     |
| JSON object   | `array` (associative) | `&quot;{&quot;key&quot;:&quot;value&quot;}&quot;` → `[&quot;key&quot; =&gt; &quot;value&quot;]`    |

### Before doing a dump (writing from PHP to file/data)

| PHP Type             | File Value            | Example                                       |
|----------------------|-----------------------|-----------------------------------------------|
| `null`               | Empty string          | `null` → `&quot;&quot;`                                 |
| `int`/`float`        | Preserved as is       | `123.45` → `123.45`                           |
| `bool`               | String &quot;true&quot;/&quot;false&quot; | `true` → `&quot;true&quot;`                             |
| `DateTimeInterface`  | ISO 8601 / Date       | `new DateTime()` → `&quot;2025-03-12T14:30:00&quot;`    |
| `array`/`object`     | JSON string           | `[&quot;a&quot;, &quot;b&quot;]` → `&quot;[&quot;a&quot;,&quot;b&quot;]&quot;`                  |
| `Stringable` objects | String representation | `$stringable` → `(string)$stringable`         |

## Date Format Detection

The library automatically detects various date formats:

- `Y-m-d` (2025-03-12).
- `d/m/Y` (12/03/2025).
- `Y-m-d H:i:s` (2025-03-12 14:30:45).
- `d/m/Y H:i:s` (12/03/2025 14:30:45).
- `Y-m-d\TH:i:s` (2025-03-12T14:30:45).
- `Y-m-d\TH:i:sP` (2025-03-12T14:30:45+00:00).

All dates are automatically converted to UTC for consistency.

## JSON Detection and Parsing

With strings that appear to be JSON (starting with `{` or `[`), the caster automatically will try to parse them into PHP arrays or objects:

```php
// Cell contains: {&quot;name&quot;:&quot;John&quot;,&quot;age&quot;:30}
$person = $sheet-&gt;getCell(0, 0);
// Returns associative array: [&quot;name&quot; =&gt; &quot;John&quot;, &quot;age&quot; =&gt; 30]
```

## Custom Type Casting

If you need different casting behavior, you can implement your own `SpreadsheetCasterInterface`:

```php
use Derafu\Spreadsheet\Contract\SpreadsheetCasterInterface;

class MyCaster implements SpreadsheetCasterInterface
{
    // Your custom implementation.
}

// Then use it with your loader/dumper.
$loader = new Loader(new Factory(), new MyCaster());
```

## How Type Casting Works Internally

1. When loading a file with `Loader`, after the raw data is read, `Caster::castAfterLoad()` is called.
2. When saving a file with `Dumper`, before writing data, `Caster::castBeforeDump()` is called.
3. The type casting is performed on every cell in every sheet, ensuring consistent typing.

## Performance Considerations

Type casting is performed in-memory and typically adds minimal overhead. However, for extremely large spreadsheets with millions of cells, you might consider implementing a more selective casting strategy through a custom `SpreadsheetCasterInterface` implementation.

If you don&#039;t want to use the Caster, you can use the Factory with the Format Handlers directly. If you bypass the Loader and Dumper, no type casting will be done.




---

### HTTP Responses

Generating HTTP Responses

# Generating HTTP Responses

Derafu Spreadsheet makes it easy to generate downloadable spreadsheet files directly from your web application. This guide shows how to use the library to create HTTP responses for spreadsheet downloads.

## PSR-7 Response Integration

Derafu Spreadsheet includes a PSR-7 compatible response generator for easy integration with frameworks that support PSR-7 standards.

### Requirements

To use the HTTP response features, you need to install Nyholm&#039;s PSR-7 implementation:

```bash
composer require nyholm/psr7
```

If you don’t want to use nyholm/psr7, you can use any PSR-7 compatible library implementing by your own `SpreadsheetHttpResponseGeneratorInterface`.

### Basic Usage

```php
&lt;?php

use Derafu\Spreadsheet\Http\NyholmSpreadsheetHttpResponseGenerator;
use Derafu\Spreadsheet\Spreadsheet;

// Create or load a spreadsheet.
$spreadsheet = new Spreadsheet();
$spreadsheet-&gt;createSheet(&#039;Sheet1&#039;, [
    [&#039;Name&#039;, &#039;Email&#039;, &#039;Age&#039;],
    [&#039;John Doe&#039;, &#039;john@example.com&#039;, 30],
    [&#039;Jane Smith&#039;, &#039;jane@example.com&#039;, 25]
]);

// Create response generator.
$responseGenerator = new NyholmSpreadsheetHttpResponseGenerator();

// Generate PSR-7 response with auto-detected format (xlsx).
$response = $responseGenerator-&gt;createResponse(
    $spreadsheet,
    &#039;users-export.xlsx&#039;
);

// The response can now be sent by any PSR-7 compatible framework.
```

### Specifying Format

You can explicitly specify the format for the response:

```php
$response = $responseGenerator-&gt;createResponse(
    $spreadsheet,
    &#039;users-export.csv&#039;,
    &#039;csv&#039;  // Explicitly specify format.
);
```

### PSR-7 Compatible Frameworks (Slim, Mezzio, etc.)

With PSR-7 compatible frameworks, you can directly use the `NyholmSpreadsheetHttpResponseGenerator`:

```php
use Derafu\Spreadsheet\Http\NyholmSpreadsheetHttpResponseGenerator;
use Derafu\Spreadsheet\SpreadsheetLoader;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class ExportAction
{
    public function __invoke(ServerRequestInterface $request): ResponseInterface
    {
        // Create or load your spreadsheet
        $loader = new Loader();
        $spreadsheet = $loader-&gt;loadFromFile(&#039;data.xlsx&#039;);

        // Modify it if needed
        // ...

        // Generate response
        $responseGenerator = new NyholmSpreadsheetHttpResponseGenerator();
        return $responseGenerator-&gt;createResponse(
            $spreadsheet,
            &#039;exported-data.xlsx&#039;
        );
    }
}
```

## Custom Response Generation

If you need to customize the response generation or use a different PSR-7 implementation, you can implement your own `SpreadsheetHttpResponseGeneratorInterface`:

```php
&lt;?php

namespace App\Spreadsheet;

use Derafu\Spreadsheet\Contract\Http\SpreadsheetHttpResponseGeneratorInterface;
use Derafu\Spreadsheet\Contract\SpreadsheetInterface;
use Psr\Http\Message\ResponseInterface;

class CustomResponseGenerator implements SpreadsheetHttpResponseGeneratorInterface
{
    public function createResponse(
        SpreadsheetInterface $spreadsheet,
        ?string $filename = null,
        ?string $format = null
    ): ResponseInterface {
        // Your custom implementation
    }
}
```

## MIME Types for Different Formats

When generating HTTP responses, it&#039;s important to use the correct MIME type. Derafu Spreadsheet handles this for you, but here&#039;s a reference of the MIME types used for each format:

| Format | MIME Type                                                         |
|--------|-------------------------------------------------------------------|
| XLSX   | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
| XLS    | application/vnd.ms-excel                                          |
| CSV    | text/csv                                                          |
| ODS    | application/vnd.oasis.opendocument.spreadsheet                    |
| JSON   | application/json                                                  |
| XML    | application/xml                                                   |
| YAML   | application/yaml                                                  |
| HTML   | text/html                                                         |
| PDF    | application/pdf                                                   |

## Security Considerations

When generating downloadable files from user data, be sure to:

1. **Sanitize data**: Ensure user-provided data doesn&#039;t contain malicious content.
2. **Validate filenames**: Clean and validate user-provided filenames.
3. **Set appropriate headers**: Ensure you&#039;re using the correct content type and disposition.
4. **Clean up temporary files**: Remove any temporary files after sending the response.

## Performance Tips

For large spreadsheets, generating the response can be resource-intensive. Consider:

1. **Queuing exports**: For large exports, process them in a background job and notify the user when ready.
2. **Streaming responses**: Some frameworks support streaming responses to reduce memory usage.
3. **Pagination**: Consider exporting data in smaller batches if possible.




---

### Comparison

Comparison with Alternative Libraries

# Comparison with Alternative Libraries

When choosing a spreadsheet library for PHP, it&#039;s important to understand the differences between available options. This guide compares Derafu Spreadsheet with some popular alternatives.

## Derafu Spreadsheet vs PhpSpreadsheet

[PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) is the most widely used PHP spreadsheet library and is actually used internally by Derafu Spreadsheet for some format handlers.

### Advantages of Derafu Spreadsheet

- **Simplified API**: The API is more streamlined and consistent across formats.
- **Customizable type casting**: Has a custom and flexible type casting system.
- **Format-agnostic code**: Write code once that works with any spreadsheet format.
- **Easier data access**: Direct methods for working with rows and cells.
- **Native handling of JSON/YAML**: Built-in support for these data exchange formats.
- **JSON inside**: Allows you to easily store and retrieve JSON data inside spreadsheets.
- **Modular approach**: Use only the format handlers you need.
- **Less overhead**: Simpler in-memory structure for data representation.

### When to use PhpSpreadsheet instead

- **Advanced Excel features**: If you need complex Excel features like conditional formatting, charts, etc.
- **Cell styling**: If you need detailed control over cell appearance.
- **Formula calculation**: If you need to evaluate Excel formulas.
- **Mature ecosystem**: PhpSpreadsheet has been around longer and has more examples and community code.

## Derafu Spreadsheet vs league/csv

[league/csv](https://csv.thephpleague.com/) is a focused library for working with CSV files. Internally, Derafu Spreadsheet uses it, as default, for CSV format handling.

### Advantages of Derafu Spreadsheet

- **Multiple format support**: Work with many formats using the same code.
- **Automatic type casting**: league/csv returns everything as strings.
- **Higher-level abstractions**: Work with the concepts of sheets, rows, and cells.
- **Easy format conversion**: Convert between CSV and other formats seamlessly.

### When to use league/csv instead

- **CSV-only projects**: If you only need to work with CSV files.
- **Memory efficiency for large files**: league/csv has specialized streaming capabilities.
- **CSV-specific features**: If you need advanced CSV features like RFC compliance options.
- **Simplicity**: If you want a more focused, single-purpose library.

## Code Comparison Examples

### Basic Reading Example

**Derafu Spreadsheet**:
```php
use Derafu\Spreadsheet\SpreadsheetLoader;

$loader = new Loader();
$sheet = $loader-&gt;loadFromFile(&#039;data.xlsx&#039;)-&gt;getActiveSheet();

foreach ($sheet-&gt;getDataRows() as $row) {
    $id = $row[0]; // Already cast to proper type (int).
    $date = $row[1]; // Already a DateTimeImmutable.
    // Process...
}
```

**PhpSpreadsheet**:
```php
use DateTimeImmutable;
use PhpOffice\PhpSpreadsheet\IOFactory;

$spreadsheet = IOFactory::load(&#039;data.xlsx&#039;);
$sheet = $spreadsheet-&gt;getActiveSheet();

foreach ($sheet-&gt;getRowIterator(2) as $row) {
    $cellIterator = $row-&gt;getCellIterator();
    $rowData = [];
    foreach ($cellIterator as $cell) {
        $rowData[] = $cell-&gt;getValue();
    }
    $id = (int)$rowData[0]; // Depending on your app and code, manual casting needed.
    $date = new DateTimeImmutable($rowData[1]); // Manual conversion.
    // Process...
}
```

**league/csv**:
```php
use DateTimeImmutable;
use League\Csv\Reader;

$reader = Reader::createFromPath(&#039;data.csv&#039;);
$reader-&gt;setHeaderOffset(0);

foreach ($reader-&gt;getRecords() as $record) {
    $id = (int)$record[&#039;id&#039;]; // Depending on your app and code, manual casting needed.
    $date = new DateTimeImmutable($record[&#039;date&#039;]); // Manual conversion.
    // Process...
}
```

### Format Conversion Example

**Derafu Spreadsheet**:
```php
use Derafu\Spreadsheet\SpreadsheetLoader;
use Derafu\Spreadsheet\SpreadsheetDumper;

$loader = new Loader();
$dumper = new Dumper();

// Load XLSX and save as CSV.
$spreadsheet = $loader-&gt;loadFromFile(&#039;data.xlsx&#039;);
$dumper-&gt;dumpToFile($spreadsheet, &#039;data.csv&#039;);

// Load CSV and save as JSON.
$spreadsheet = $loader-&gt;loadFromFile(&#039;data.csv&#039;);
$dumper-&gt;dumpToFile($spreadsheet, &#039;data.json&#039;);
```

**With alternative libraries**:
```php
use League\Csv\Reader as CsvReader;
use PhpOffice\PhpSpreadsheet\IOFactory as PhpSpreadsheetIOFactory;
use PhpOffice\PhpSpreadsheet\Writer\Csv as PhpSpreadsheetCsvWriter;

// Load with PhpSpreadsheet, save as CSV.
$spreadsheet = PhpSpreadsheetIOFactory::load(&#039;data.xlsx&#039;);
$writer = new PhpSpreadsheetCsvWriter($spreadsheet);
$writer-&gt;save(&#039;data.csv&#039;);

// To convert CSV to JSON, would require:
$csv = CsvReader::createFromPath(&#039;data.csv&#039;);
$csv-&gt;setHeaderOffset(0);
$records = iterator_to_array($csv-&gt;getRecords());
file_put_contents(&#039;data.json&#039;, json_encode($records, JSON_PRETTY_PRINT));
```

## Feature Comparison Table

| Feature                    | Derafu Spreadsheet                              | PhpSpreadsheet                 | league/csv |
|----------------------------|-------------------------------------------------|--------------------------------|------------|
| **Formats Supported**      | XLSX, XLS, CSV, ODS, JSON, XML, YAML, HTML, PDF | XLSX, XLS, CSV, ODS, HTML, PDF | CSV only   |
| **API Consistency**        | Excellent                                       | Complex                        | Simple     |
| **Memory Efficiency**      | Can be better with streaming support            | Poor for large files           | Excellent  |
| **Streaming Support**      | Soon                                            | Yes                            | Excellent  |
| **Customizable Casting**   | ✅                                              | ❌                             | ❌         |
| **PSR-7 Integration**      | ✅                                              | ❌                             | ❌         |
| **JSON/YAML Support**      | ✅                                              | ❌                             | ❌         |
| **Formula Support**        | ❌                                              | ✅                             | ❌         |
| **Cell Styling**           | ❌                                              | ✅                             | ❌         |

## Conclusion

Derafu Spreadsheet is focused on simplicity and ease of use for handling the data inside spreadsheets, not the styling. It excels when you need:

1. **Unified handling** of multiple file formats.
2. **Clean, intuitive API** for spreadsheet operations.
3. **Automatic type handling** to reduce boilerplate code.
4. **Format conversion** capabilities.
5. **Modern PHP architecture** with a focus on developer experience.

Other libraries may be better suited for specific use cases:

- **PhpSpreadsheet**: For complex Excel features and formatting.
- **league/csv**: For CSV-specific operations and streaming large files.

Choose the tool that best matches your specific requirements and constraints.




---

## Data Processor Project

Derafu Data Processor

# Derafu Data Processor




---

### Introduction

Four-Phase Data Processing Library

# Four-Phase Data Processing Library

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/data-processor/main)
![CI Workflow](https://github.com/derafu/data-processor/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/data-processor)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/data-processor)
![Total Downloads](https://poser.pugx.org/derafu/data-processor/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/data-processor/d/monthly)

A PHP library designed to process data through four distinct phases: casting, transformation, sanitization and validation.

## What Makes it Different?

Unlike traditional validation libraries that focus solely on validation, Derafu Data Processor offers:

- **Clear Phase Separation**: Each processing phase (cast → transform → sanitize → validate) has its own purpose and runs in a specific order.
- **Independent Transformations**: A dedicated transformation layer for complex data conversions, separate from basic type casting.
- **Type-Safe Operations**: Strong typing and clear error handling in each phase.
- **Extensible Rule System**: Each type of rule (caster, transformer, sanitizer, validator) follows its own interface.
- **Minimal Dependencies**: `intl` extension for specific format rules, `Carbon` for dates, and `derafu/translation` for translatable error messages (see [Translations](translations)).

## Features

- 🎯 **Type-Safe Casting**: Convert between data types safely.
  - Basic types (string, int, float, bool).
  - Date and time handling.

- 🔄 **Data Transformations**: Complex data conversions.
  - Base64 encoding/decoding.
  - JSON processing.
  - Slug generation.
  - Character transliteration.

- 🧹 **Rich Sanitization Rules**: Clean and normalize input data.
  - String sanitization (trim, strip_tags, substring, etc.).
  - Regex-based cleaning (regex_remove, regex_keep).
  - HTML entity handling (htmlspecialchars, htmlentities, etc.).
  - Character set manipulation (remove_non_printable, remove_chars, etc.).

- ✅ **Comprehensive Validation**: Various validation rules.
  - String validations (required, min_length, max_length, etc.).
  - Number validations (range, gte, lte, etc.).
  - Date validations (after, before, between, etc.).
  - Format validations (email, URL, UUID, etc.).
  - File validations (file, image, mime_types).
  - Array validations (min_items, max_items, not_empty, etc.).

## Installation

```bash
composer require derafu/data-processor
```

## Basic Usage

```php
use Derafu\DataProcessor\ProcessorFactory;

$processor = ProcessorFactory::create();

// Simple validation.
$result = $processor-&gt;process(&#039;test@example.com&#039;, [
    &#039;validate&#039; =&gt; [&#039;email&#039;],
]);

// Multi-phase processing.
$result = $processor-&gt;process(&#039; TEST@EXAMPLE.COM &#039;, [
    &#039;transform&#039; =&gt; [&#039;lowercase&#039;],
    &#039;sanitize&#039; =&gt; [&#039;trim&#039;],
    &#039;validate&#039; =&gt; [&#039;email&#039;, &#039;max_length:255&#039;],
]);

// Array validation.
$result = $processor-&gt;process([1, 2, 2, 3], [
    &#039;validate&#039; =&gt; [&#039;unique&#039;, &#039;max_items:5&#039;],
]);

// File validation.
$result = $processor-&gt;process($_FILES[&#039;upload&#039;], [
    &#039;validate&#039; =&gt; [&#039;file:2M&#039;, &#039;mimetype:application/pdf,image/jpeg&#039;]
]);
```




---

### Rules Reference

Rules Reference

# Rules Reference

## Cast Rules

| Rule | Description | Example |
|------|-------------|---------|
| `boolean`, `bool` | Convert value to boolean | `&#039;cast&#039; =&gt; &#039;boolean&#039;` |
| `date` | Convert value to Carbon date | `&#039;cast&#039; =&gt; &#039;date&#039;` |
| `datetime` | Convert value to Carbon datetime | `&#039;cast&#039; =&gt; &#039;datetime&#039;` |
| `float`, `decimal` | Convert value to float | `&#039;cast&#039; =&gt; &#039;float&#039;` |
| `integer`, `int` | Convert value to integer | `&#039;cast&#039; =&gt; &#039;integer&#039;` |
| `string`, `str` | Convert value to string | `&#039;cast&#039; =&gt; &#039;string&#039;` |
| `timestamp` | Convert value to Unix timestamp | `&#039;cast&#039; =&gt; &#039;timestamp&#039;` |

## Transform Rules

| Rule | Description | Example |
|------|-------------|---------|
| `base64_decode` | Decode base64 string | `&#039;transform&#039; =&gt; [&#039;base64_decode&#039;]` |
| `base64_encode` | Encode value to base64 | `&#039;transform&#039; =&gt; [&#039;base64_encode&#039;]` |
| `json_decode` | Decode JSON string | `&#039;transform&#039; =&gt; [&#039;json_decode&#039;]`, `&#039;transform&#039; =&gt; [&#039;json_decode:array&#039;]` |
| `json_encode` | Encode value to JSON | `&#039;transform&#039; =&gt; [&#039;json_encode&#039;]`, `&#039;transform&#039; =&gt; [&#039;json_encode:pretty&#039;]` |
| `json_to_array` | Convert JSON string to array | `&#039;transform&#039; =&gt; [&#039;json_to_array&#039;]` |
| `json_to_object` | Convert JSON string to object | `&#039;transform&#039; =&gt; [&#039;json_to_object&#039;]` |
| `round` | Round numeric value | `&#039;transform&#039; =&gt; [&#039;round:2&#039;]` |
| `lowercase`, `lower` | Convert string to lowercase | `&#039;transform&#039; =&gt; [&#039;lowercase&#039;]` |
| `slug` | Convert string to URL friendly format | `&#039;transform&#039; =&gt; [&#039;slug&#039;]` |
| `transliterate` | Convert characters to ASCII | `&#039;transform&#039; =&gt; [&#039;transliterate&#039;]` |
| `uppercase`, `upper` | Convert string to uppercase | `&#039;transform&#039; =&gt; [&#039;uppercase&#039;]` |

## Sanitize Rules

| Rule | Description | Example |
|------|-------------|---------|
| `addslashes` | Add slashes before quotes | `&#039;sanitize&#039; =&gt; [&#039;addslashes&#039;]` |
| `htmlentities` | Convert characters to HTML entities | `&#039;sanitize&#039; =&gt; [&#039;htmlentities&#039;]` |
| `htmlspecialchars` | Convert special characters to HTML entities | `&#039;sanitize&#039; =&gt; [&#039;htmlspecialchars&#039;]` |
| `regex_keep` | Keep only characters matching pattern | `&#039;sanitize&#039; =&gt; [&#039;regex_keep:/[0-9]/&#039;]` |
| `regex_remove` | Remove characters matching pattern | `&#039;sanitize&#039; =&gt; [&#039;regex_remove:/[^a-z]/&#039;]` |
| `remove_chars` | Remove specific characters | `&#039;sanitize&#039; =&gt; [&#039;remove_chars:@#$&#039;]` |
| `remove_non_printable` | Remove non-printable characters | `&#039;sanitize&#039; =&gt; [&#039;remove_non_printable&#039;]` |
| `remove_prefix` | Remove string prefix | `&#039;sanitize&#039; =&gt; [&#039;remove_prefix:http&#039;]` |
| `remove_suffix` | Remove string suffix | `&#039;sanitize&#039; =&gt; [&#039;remove_suffix:.txt&#039;]` |
| `spaces` | Normalize multiple spaces to single | `&#039;sanitize&#039; =&gt; [&#039;spaces&#039;]` |
| `strip_tags` | Remove HTML and PHP tags | `&#039;sanitize&#039; =&gt; [&#039;strip_tags&#039;]`, `&#039;sanitize&#039; =&gt; [&#039;strip_tags:&lt;p&gt;&lt;br&gt;&#039;]` |
| `substring` | Extract part of string | `&#039;sanitize&#039; =&gt; [&#039;substring:5&#039;]`, `&#039;sanitize&#039; =&gt; [&#039;substring:0,10&#039;]` |
| `trim` | Remove whitespace from ends | `&#039;sanitize&#039; =&gt; [&#039;trim&#039;]` |

## Validate Rules

### Array Validation

| Rule | Description | Example |
|------|-------------|---------|
| `in`, `choices` | Check if value is in list | `&#039;validate&#039; =&gt; [&#039;in:a,b,c&#039;]` |
| `max_items` | Check array maximum length | `&#039;validate&#039; =&gt; [&#039;max_items:5&#039;]` |
| `min_items` | Check array minimum length | `&#039;validate&#039; =&gt; [&#039;min_items:1&#039;]` |
| `notempty` | Check if array is not empty | `&#039;validate&#039; =&gt; [&#039;notempty&#039;]` |
| `unique` | Check if array values are unique | `&#039;validate&#039; =&gt; [&#039;unique&#039;]` |

### Date Validation

| Rule | Description | Example |
|------|-------------|---------|
| `after` | Check if date is after another | `&#039;validate&#039; =&gt; [&#039;after:2024-01-01&#039;]` |
| `after_or_equal` | Check if date is after or equal | `&#039;validate&#039; =&gt; [&#039;after_or_equal:2024-01-01&#039;]` |
| `before` | Check if date is before another | `&#039;validate&#039; =&gt; [&#039;before:2024-12-31&#039;]` |
| `before_or_equal` | Check if date is before or equal | `&#039;validate&#039; =&gt; [&#039;before_or_equal:2024-12-31&#039;]` |
| `between` | Check if date is between two dates | `&#039;validate&#039; =&gt; [&#039;between:2024-01-01,2024-12-31&#039;]` |
| `date_equals` | Check if date equals another | `&#039;validate&#039; =&gt; [&#039;date_equals:2024-01-01&#039;]` |
| `date_format` | Check if date matches format | `&#039;validate&#039; =&gt; [&#039;date_format:Y-m-d&#039;]` |
| `weekday` | Check if date is weekday | `&#039;validate&#039; =&gt; [&#039;weekday&#039;]` |
| `weekend` | Check if date is weekend | `&#039;validate&#039; =&gt; [&#039;weekend&#039;]` |

### File Validation

| Rule | Description | Example |
|------|-------------|---------|
| `file` | Validate file with size limit | `&#039;validate&#039; =&gt; [&#039;file:2M&#039;]` |
| `image` | Validate image file with size limit | `&#039;validate&#039; =&gt; [&#039;image:2M&#039;]` |
| `mimetype` | Validate file mime type | `&#039;validate&#039; =&gt; [&#039;mimetype:application/pdf,image/jpeg&#039;]` |

### Financial Validation

| Rule | Description | Example |
|------|-------------|---------|
| `bic` | Validate BIC/SWIFT code | `&#039;validate&#039; =&gt; [&#039;bic&#039;]` |
| `card_number` | Validate credit card number | `&#039;validate&#039; =&gt; [&#039;card_number&#039;]` |
| `iban` | Validate IBAN | `&#039;validate&#039; =&gt; [&#039;iban&#039;]` |

### I18n Validation

| Rule | Description | Example |
|------|-------------|---------|
| `country` | Validate country code | `&#039;validate&#039; =&gt; [&#039;country&#039;]` |
| `currency` | Validate currency code | `&#039;validate&#039; =&gt; [&#039;currency&#039;]` |
| `language` | Validate language code | `&#039;validate&#039; =&gt; [&#039;language&#039;]` |
| `locale` | Validate locale code | `&#039;validate&#039; =&gt; [&#039;locale&#039;]` |
| `timezone` | Validate timezone identifier | `&#039;validate&#039; =&gt; [&#039;timezone&#039;]` |

### ID Validation

| Rule | Description | Example |
|------|-------------|---------|
| `uuid` | Validate UUID string | `&#039;validate&#039; =&gt; [&#039;uuid&#039;]` |

### Internet Validation

| Rule | Description | Example |
|------|-------------|---------|
| `email` | Validate email address | `&#039;validate&#039; =&gt; [&#039;email&#039;]` |
| `hostname` | Validate hostname | `&#039;validate&#039; =&gt; [&#039;hostname&#039;]` |
| `ip` | Validate IP address | `&#039;validate&#039; =&gt; [&#039;ip&#039;]`, `&#039;validate&#039; =&gt; [&#039;ip:v4&#039;]`, `&#039;validate&#039; =&gt; [&#039;ip:v6&#039;]` |
| `url` | Validate URL | `&#039;validate&#039; =&gt; [&#039;url&#039;]` |

### Numeric Validation

| Rule | Description | Example |
|------|-------------|---------|
| `decimal`, `float`, `real` | Validate decimal number | `&#039;validate&#039; =&gt; [&#039;decimal:2&#039;]` |
| `digits_range` | Validate number of digits range | `&#039;validate&#039; =&gt; [&#039;digits_range:3,5&#039;]` |
| `digits` | Validate exact number of digits | `&#039;validate&#039; =&gt; [&#039;digits:4&#039;]` |
| `gt` | Greater than | `&#039;validate&#039; =&gt; [&#039;gt:10&#039;]` |
| `gte`, `min` | Greater than or equal | `&#039;validate&#039; =&gt; [&#039;gte:10&#039;]` |
| `integer`, `int` | Validate integer | `&#039;validate&#039; =&gt; [&#039;integer&#039;]` |
| `lt` | Less than | `&#039;validate&#039; =&gt; [&#039;lt:10&#039;]` |
| `lte`, `max` | Less than or equal | `&#039;validate&#039; =&gt; [&#039;lte:10&#039;]` |
| `multiple_of` | Check if number is multiple | `&#039;validate&#039; =&gt; [&#039;multiple_of:5&#039;]` |
| `numeric` | Validate numeric value | `&#039;validate&#039; =&gt; [&#039;numeric&#039;]` |
| `range` | Validate number in range | `&#039;validate&#039; =&gt; [&#039;range:1,100&#039;]` |

### String Validation

| Rule | Description | Example |
|------|-------------|---------|
| `alpha` | Only alphabetic characters | `&#039;validate&#039; =&gt; [&#039;alpha&#039;]` |
| `alpha_dash` | Alphanumeric with dashes/underscores | `&#039;validate&#039; =&gt; [&#039;alpha_dash&#039;]` |
| `alpha_num` | Only alphanumeric characters | `&#039;validate&#039; =&gt; [&#039;alpha_num&#039;]` |
| `base64` | Validate base64 string | `&#039;validate&#039; =&gt; [&#039;base64&#039;]` |
| `ends_with` | String ends with value | `&#039;validate&#039; =&gt; [&#039;ends_with:Test&#039;]` |
| `json` | Validate JSON string | `&#039;validate&#039; =&gt; [&#039;json&#039;]` |
| `length` | Exact string length | `&#039;validate&#039; =&gt; [&#039;length:10&#039;]` |
| `max_length` | Maximum string length | `&#039;validate&#039; =&gt; [&#039;max_length:255&#039;]` |
| `min_length` | Minimum string length | `&#039;validate&#039; =&gt; [&#039;min_length:3&#039;]` |
| `not_regex` | Not match regular expression | `&#039;validate&#039; =&gt; [&#039;not_regex:/[0-9]/&#039;]` |
| `no_whitespace` | No whitespace in string | `&#039;validate&#039; =&gt; [&#039;no_whitespace&#039;]` |
| `regex` | Match regular expression | `&#039;validate&#039; =&gt; [&#039;regex:/^[A-Z]+$/&#039;]` |
| `required` | Value is required | `&#039;validate&#039; =&gt; [&#039;required&#039;]` |
| `slug` | Validate URL friendly string | `&#039;validate&#039; =&gt; [&#039;slug&#039;]` |
| `starts_with` | String starts with value | `&#039;validate&#039; =&gt; [&#039;starts_with:Test&#039;]` |




---

### Custom Registries

Creating Custom Registries

# Creating Custom Registries

You can create your own rule registry to customize which rules are available:

```php
use Derafu\DataProcessor\RuleRegistry;
use Derafu\DataProcessor\Processor;
use Derafu\DataProcessor\RuleResolver;

// Create a custom registry.
$registry = new RuleRegistry();

// Register only the rules you need.
$registry
    -&gt;addTransformerRule(&#039;base64_encode&#039;, Base64EncodeRule::class)
    -&gt;addSanitizerRule(&#039;trim&#039;, TrimRule::class)
    -&gt;addCasterRule(&#039;integer&#039;, IntegerRule::class)
    -&gt;addValidatorRule(&#039;email&#039;, EmailRule::class);

// Create resolver, parser and processor with your registry.
$resolver = new RuleResolver($registry);
$parser = new RuleParser();
$processor = new Processor($resolver, $parser);
```




---

### Custom Rules

Creating Custom Rules

# Creating Custom Rules

## Custom Cast Rule

```php
use Derafu\DataProcessor\Contract\CasterRuleInterface;

final class CustomCastRule implements CasterRuleInterface
{
    public function cast(mixed $value, array $parameters = []): mixed
    {
        // Your casting logic here.
    }
}

// Register the rule.
$registry-&gt;addCasterRule(&#039;custom_cast&#039;, CustomCastRule::class);
```

## Custom Transform Rule

```php
use Derafu\DataProcessor\Contract\TransformerRuleInterface;

final class CustomTransformRule implements TransformerRuleInterface
{
    public function transform(mixed $value, array $parameters = []): mixed
    {
        // Your transformation logic here.
    }
}

// Register the rule.
$registry-&gt;addTransformerRule(&#039;custom_transform&#039;, CustomTransformRule::class);
```

## Custom Sanitizer Rule

```php
use Derafu\DataProcessor\Contract\SanitizerRuleInterface;

final class CustomSanitizerRule implements SanitizerRuleInterface
{
    public function sanitize(mixed $value, array $parameters = []): mixed
    {
        // Your sanitization logic here.
    }
}

// Register the rule.
$registry-&gt;addSanitizerRule(&#039;custom_sanitize&#039;, CustomSanitizerRule::class);
```

## Custom Validator Rule

```php
use Derafu\DataProcessor\Contract\ValidatorRuleInterface;

final class CustomValidatorRule implements ValidatorRuleInterface
{
    public function validate(mixed $value, array $parameters = []): void
    {
        // Your validation logic here.
    }
}

// Register the rule.
$registry-&gt;addValidatorRule(&#039;custom_validate&#039;, CustomValidatorRule::class);
```

## Using Custom Rules

Once registered, you can use your custom rules just like built-in ones:

```php
$processor-&gt;process(&#039;field&#039;, $value, [
    &#039;cast&#039; =&gt; &#039;custom_cast&#039;,
    &#039;transform&#039; =&gt; [&#039;custom_transform&#039;],
    &#039;sanitize&#039; =&gt; [&#039;custom_sanitize&#039;],
    &#039;validate&#039; =&gt; [&#039;custom_validate&#039;],
]);
```




---

### Custom Registrar

Creating Custom Rule Registrar

# Creating Custom Rule Registrar

For better organization, you can create a custom rule registrar:

```php
use Derafu\DataProcessor\Contract\RuleRegistrarInterface;
use Derafu\DataProcessor\Contract\RuleRegistryInterface;

final class CustomRuleRegistrar implements RuleRegistrarInterface
{
    public function register(RuleRegistryInterface $registry): void
    {
        // Register caster rules.
        $registry
            -&gt;addCasterRule(&#039;custom_type_1&#039;, CustomType1Rule::class)
            -&gt;addCasterRule(&#039;custom_type_2&#039;, CustomType2Rule::class);

        // Register transform rules.
        $registry
            -&gt;addTransformerRule(&#039;custom_transform_1&#039;, CustomTransform1Rule::class)
            -&gt;addTransformerRule(&#039;custom_transform_2&#039;, CustomTransform2Rule::class);

        // Register sanitizer rules.
        $registry
            -&gt;addSanitizerRule(&#039;custom_sanitize_1&#039;, CustomSanitize1Rule::class)
            -&gt;addSanitizerRule(&#039;custom_sanitize_2&#039;, CustomSanitize2Rule::class);

        // Register validator rules.
        $registry
            -&gt;addValidatorRule(&#039;custom_validate_1&#039;, CustomValidate1Rule::class)
            -&gt;addValidatorRule(&#039;custom_validate_2&#039;, CustomValidate2Rule::class);
    }
}

// Usage.
$processor = ProcessorFactory::create(
    new CustomRuleRegistrar(),
    withDefaultRules: false // Create without default rules using the factory.
);
```

This allows you to:

- Create domain-specific rule sets.
- Override default rules with custom implementations.
- Group related rules together.
- Control which rules are available in your application.




---

### Custom Parser

Creating Custom Rule Parser

# Creating Custom Rule Parser

You can create your own rule parser to customize how rules are parsed from strings or arrays:

```php
use Derafu\DataProcessor\Contract\RuleParserInterface;

final class CustomRuleParser implements RuleParserInterface
{
    public function parse(string|array $rules): array
    {
        if (is_array($rules)) {
            return $this-&gt;parseArray($rules);
        }
        return $this-&gt;parseString($rules);
    }

    private function parseArray(array $rules): array
    {
        // Your array parsing logic here.
        $parsed = [];
        foreach ($rules as $type =&gt; $typeRules) {
            // Handle different formats for rules.
            $parsed[$type] = $this-&gt;parseTypeRules($typeRules);
        }
        return $parsed;
    }

    private function parseString(string $rules): array
    {
        // Your string parsing logic here.
        // Example: parse rules like &quot;t(rule1|rule2) s(rule3) required|email&quot;
        return [
            &#039;transform&#039; =&gt; [&#039;rule1&#039;, &#039;rule2&#039;],
            &#039;sanitize&#039; =&gt; [&#039;rule3&#039;],
            &#039;validate&#039; =&gt; [&#039;required&#039;, &#039;email&#039;],
        ];
    }

    private function parseTypeRules(string|array $rules): array|string
    {
        // Handle parsing of individual rule types.
        if (is_string($rules)) {
            return explode(&#039;|&#039;, $rules);
        }
        return $rules;
    }
}

// Usage with processor factory.
$processor = ProcessorFactory::create(
    parser: new CustomRuleParser()
);

// Or manual instantiation.
$registry = new RuleRegistry(); // Then register the rules you need.
$resolver = new RuleResolver($registry);
$parser = new CustomRuleParser();
$processor = new Processor($resolver, $parser);

// Using your custom parser.
$result = $processor-&gt;process(&#039;test@example.com&#039;, &#039;t(lowercase) s(trim) required|email&#039;);

// Or.
$result = $processor-&gt;process(&#039;test@example.com&#039;, [
    &#039;transform&#039; =&gt; &#039;lowercase&#039;,
    &#039;sanitize&#039; =&gt; &#039;trim&#039;,
    &#039;validate&#039; =&gt; &#039;required|email&#039;,
]);
```

This allows you to:

- Create custom rule parsing formats.
- Support different string formats for rules.
- Add custom shorthand notations.
- Implement domain-specific rule syntax.
- Handle complex rule configurations.




---

### Translations

Translations

# Translations

`derafu/data-processor`&#039;s exceptions can be translated. This page explains
what&#039;s translatable, what ships out of the box, and how to activate it in
your application.

## What&#039;s Translatable

Four exception classes implement
`Derafu\Translation\Contract\TranslatableInterface` (via
`TranslatableExceptionTrait`, from
[`derafu/translation`](https://www.derafu.dev/docs/core/translation)):

- `Derafu\DataProcessor\Exception\ValidationException`
- `Derafu\DataProcessor\Exception\SanitizationException`
- `Derafu\DataProcessor\Exception\TransformationException`
- `Derafu\DataProcessor\Exception\CastingException`

`Derafu\DataProcessor\Exception\RuleNotFoundException` is **not**
translatable on purpose — it signals a misconfigured rule name (a
programming/configuration error), not something an end user should ever
see.

All four use the `errors` domain (`TranslatableExceptionTrait`&#039;s default).
Every message thrown by a built-in rule is written in ICU MessageFormat
syntax and uses the literal English text as its translation id — for
example, `&#039;Value must be greater than {value}.&#039;` — following the same
convention documented in
[derafu/translation&#039;s Exceptions guide](https://www.derafu.dev/docs/core/translation).

Without any translator configured, `getMessage()` already returns the
fully formatted English text — this library works exactly as before,
whether or not you set up translation at all:

```php
use Derafu\DataProcessor\Exception\ValidationException;

$e = new ValidationException([&#039;Array must not contain more than {max} items.&#039;, &#039;max&#039; =&gt; 5]);

echo $e-&gt;getMessage();
// &quot;Array must not contain more than 5 items.&quot;
```

## What Ships

`derafu/data-processor` includes:

- `resources/translations/errors+intl-icu.es.php` — a Spanish translation
  for every distinct message used by the four exception classes above (the
  `+intl-icu` domain suffix is required for `derafu/translation`&#039;s ICU
  placeholders like `{max}` to be substituted — see
  [ICU Formatting](https://www.derafu.dev/docs/core/translation)).
- `Derafu\DataProcessor\Translation\DataProcessorTranslationResourceProvider`
  — a `TranslationResourceProviderInterface` implementation pointing at
  that directory, ready to hand to a `TranslationResourceRegistrar` or a
  dependency-injection container.

Neither of these does anything by itself — `derafu/data-processor` is a
library, it doesn&#039;t build or own a `Translator`. Activating translation is
entirely up to the application that uses it.

## Activating It (Plain PHP)

```php
use Derafu\DataProcessor\Exception\ValidationException;
use Derafu\DataProcessor\Translation\DataProcessorTranslationResourceProvider;
use Derafu\Translation\TranslatorFactory;

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

$e = new ValidationException([&#039;Array must not contain more than {max} items.&#039;, &#039;max&#039; =&gt; 5]);

echo $e-&gt;trans($translator, &#039;es&#039;);
// &quot;El arreglo no debe contener más de 5 elementos.&quot;
```

If you&#039;d rather point at the directory directly (for example, to combine
it with your own translation files) without going through the provider
class, use `TranslationResourceRegistrar` instead:

```php
use Derafu\Translation\TranslationResourceRegistrar;

$registrar = new TranslationResourceRegistrar($translator);
$registrar-&gt;registerDirectory(__DIR__ . &#039;/vendor/derafu/data-processor/resources/translations&#039;);
$registrar-&gt;registerDirectory(__DIR__ . &#039;/translations&#039;); // Your own, registered last: it can override.
```

## Activating It (Dependency Injection)

If your application uses `symfony/dependency-injection`, import both
recipe files and feed the tagged providers into your own `Translator`
registration:

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

services:
    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
```

`data-processor-services.yaml` already tags
`DataProcessorTranslationResourceProvider` with
`derafu_translation.resource_provider`, so it&#039;s picked up automatically —
nothing else to configure. Anything in your own application that
type-hints `Symfony\Contracts\Translation\TranslatorInterface` (as
`derafu/translation`&#039;s own exceptions do) resolves to this `Translator` via
the alias `translation-services.yaml` provides.

## A Note on Rule Names vs. Messages

Rule names (`&#039;email&#039;`, `&#039;max_length&#039;`, the strings you pass to `process()`)
are never translated — they&#039;re an internal API, not user-facing text.
Only the **exception messages** thrown when a rule fails go through
translation.




---

## Repository Project

Lightweight Data Source Management for PHP

# Lightweight Data Source Management for PHP

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/repository/main)
![CI Workflow](https://github.com/derafu/repository/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/repository)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/repository)
![Total Downloads](https://poser.pugx.org/derafu/repository/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/repository/d/monthly)

A lightweight, flexible PHP library for managing data repositories with multiple data sources and seamless integration with PHP frameworks.

## Why Derafu\Repository?

### 🚀 **Simplified Data Management**

Traditional PHP data repositories often:

- Are tightly coupled to specific storage mechanisms.
- Require complex configuration.
- Lack flexibility across different data sources.

### 🔥 **What Makes Derafu\Repository Unique?**

| Feature                     | Derafu\Repository | Traditional Repositories |
|-----------------------------|-------------------|--------------------------|
| **File-Based Data Sources** | ✅ Yes            | ❌ No                    |
| **Lightweight Design**      | ✅ Yes            | ❌ No                    |
| **Simple Configuration**    | ✅ Yes            | ❌ No                    |
| **Framework Agnostic**      | ✅ Yes            | ⚠️ Varies                |

---

## Features

- ✅ **Multiple Data Source Support** – Load data from PHP arrays, JSON, YAML files.
- ✅ **Generic Entity Handling** – Work with any type of data structure.
- ✅ **Flexible Querying** – Find, filter, and order data with ease.
- ✅ **Doctrine Collections Compatible** – Use Doctrine Criteria for advanced filtering.
- ✅ **Zero Heavy Dependencies** – Lightweight and performance-focused.
- ✅ **PHP 8+ Ready** – Leverages modern PHP features.

---

## Installation

Install via Composer:

```bash
composer require derafu/repository
```

## Basic Usage

```php
use Derafu\Repository\Repository;

// Load data from a PHP file or array.
$data = [
    &#039;products&#039; =&gt; [
        &#039;prod-001&#039; =&gt; [
            &#039;name&#039; =&gt; &#039;Laptop XPS&#039;,
            &#039;category&#039; =&gt; &#039;computers&#039;,
            &#039;price&#039; =&gt; 1299.99,
        ],
        // More products...
    ],
];

// Create a repository.
$repository = new Repository($data, idAttribute: &#039;id&#039;);

// Find all products.
$allProducts = $repository-&gt;findAll();

// Find products by criteria.
$computerProducts = $repository-&gt;findBy([
    &#039;category&#039; =&gt; &#039;computers&#039;
]);

// Find a single product.
$laptop = $repository-&gt;findOneBy([
    &#039;name&#039; =&gt; &#039;Laptop XPS&#039;,
]);
```

## Advanced Usage with Doctrine Criteria

```php
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;

// Advanced filtering with Doctrine Criteria.
$expensiveProducts = $repository-&gt;findByCriteria(
    Criteria::create()
        -&gt;where(Criteria::expr()-&gt;gt(&#039;price&#039;, 1000))
        -&gt;orderBy([&#039;price&#039; =&gt; Order::Descending])
);
```

## Supported Data Sources

- PHP Arrays.
- PHP Files returning arrays.
- JSON Files.
- YAML Files.

## Performance Considerations

- Optimized for small to medium-sized datasets.
- In-memory data management.
- Recommended for configuration, lookup tables, and static data.




---

## Config Project

Yet Another Config Lib

# Yet Another Config Lib

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/config/main)
![CI Workflow](https://github.com/derafu/config/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/config)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/config)
![Total Downloads](https://poser.pugx.org/derafu/config/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/config/d/monthly)

## Installation

Install via Composer:

```bash
composer require derafu/config
```

## Basic Usage

```php
use Derafu\Config\Configuration; // Or `Options`.

$configuration = new Configuration([
    &#039;app&#039; =&gt; [
        &#039;name&#039; =&gt; &#039;Derafu Config&#039;,
    ],
]);

echo $configuration[&#039;app&#039;][&#039;name&#039;];           // &quot;Derafu Config&quot;
echo $configuration[&#039;app.name&#039;];              // &quot;Derafu Config&quot;
echo $configuration-&gt;get(&#039;app.name&#039;);         // &quot;Derafu Config&quot;
echo $configuration-&gt;get(&#039;app&#039;)-&gt;get(&#039;name&#039;); // &quot;Derafu Config&quot;
```




---

## Container Project

Flexible Data Containers for PHP

# Flexible Data Containers for PHP

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/container/main)
![CI Workflow](https://github.com/derafu/container/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/container)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/container)
![Total Downloads](https://poser.pugx.org/derafu/container/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/container/d/monthly)

A collection of specialized PHP data containers that provide different levels of structure and validation, from simple bags to schema-validated stores.

## Features

- 🎯 Purpose-built containers for different needs.
- 🔍 JSON Schema validation support.
- 📦 Simple to complex data structures.
- 🔄 Sequential data handling.
- 🛡️ Type-safe operations.
- 🧩 Common interface across containers.
- 🪶 Minimal dependencies.
- 🧪 Comprehensive test coverage.

## Why Derafu\Container?

Unlike generic data structures, Derafu\Container provides specialized containers each designed for specific use cases:

- **Bag**: Simple, flexible data storage without restrictions.
- **Vault**: Basic structured data with validation.
- **Store**: Advanced data validation using JSON Schema.
- **Journal**: Sequential data storage with LIFO access.

## Installation

Install via Composer:

```bash
composer require derafu/container
```

## Basic Usage

### Bag - Flexible Container

```php
use Derafu\Container\Bag;

$bag = new Bag();
$bag-&gt;set(&#039;user.name&#039;, &#039;John&#039;);
$bag-&gt;set(&#039;user.email&#039;, &#039;john@example.com&#039;);

echo $bag-&gt;get(&#039;user.name&#039;); // &quot;John&quot;
```

### Vault - Structured Container

```php
use Derafu\Container\Vault;

$vault = new Vault([], [
    &#039;user&#039; =&gt; [
        &#039;type&#039; =&gt; &#039;array&#039;,
        &#039;required&#039; =&gt; true,
        &#039;schema&#039; =&gt; [
            &#039;name&#039; =&gt; [&#039;type&#039; =&gt; &#039;string&#039;],
            &#039;age&#039; =&gt; [&#039;type&#039; =&gt; &#039;integer&#039;],
        ],
    ],
]);

$vault-&gt;set(&#039;user&#039;, [
    &#039;name&#039; =&gt; &#039;John&#039;,
    &#039;age&#039; =&gt; 30,
]);
```

### Store - JSON Schema Container

```php
use Derafu\Container\Store;

$schema = [
    &#039;type&#039; =&gt; &#039;object&#039;,
    &#039;properties&#039; =&gt; [
        &#039;user&#039; =&gt; [
            &#039;type&#039; =&gt; &#039;object&#039;,
            &#039;required&#039; =&gt; [&#039;name&#039;, &#039;email&#039;],
            &#039;properties&#039; =&gt; [
                &#039;name&#039; =&gt; [&#039;type&#039; =&gt; &#039;string&#039;],
                &#039;email&#039; =&gt; [&#039;type&#039; =&gt; &#039;string&#039;, &#039;format&#039; =&gt; &#039;email&#039;],
            ],
        ],
    ],
];

$store = new Store([], $schema);
$store-&gt;set(&#039;user&#039;, [
    &#039;name&#039; =&gt; &#039;John&#039;,
    &#039;email&#039; =&gt; &#039;john@example.com&#039;,
]);
```

### Journal - Sequential Container

```php
use Derafu\Container\Journal;

$journal = new Journal();
$journal-&gt;add(&#039;First Entry&#039;);
$journal-&gt;add(&#039;Second Entry&#039;);

print_r($journal-&gt;reverse()); // Shows entries newest first.
```

## Container Comparison

| Feature           | Bag | Vault | Store | Journal |
|------------------|-----|--------|--------|----------|
| Schema           | No  | Simple | JSON   | No       |
| Validation       | No  | Basic  | Full   | No       |
| Nested Data      | Yes | Yes    | Yes    | No       |
| Sequential       | No  | No     | No     | Yes      |
| Dot Notation     | Yes | Yes    | Yes    | No       |

## Common Interface

All containers implement `ContainerInterface`:

```php
interface ContainerInterface extends ArrayAccess
{
    public function set(string $key, mixed $value): static;
    public function get(string $key, mixed $default = null): mixed;
    public function has(string $key): bool;
    public function clear(?string $key = null): void;
}
```

## Advanced Usage

### Custom Validation in Vault

```php
use Derafu\Container\Vault;

$vault = new Vault([], [
    &#039;age&#039; =&gt; [
        &#039;type&#039; =&gt; &#039;integer&#039;,
        &#039;validator&#039; =&gt; fn ($value) =&gt; $value &gt;= 18
    ]
]);
```

### Complex JSON Schema in Store

```php
use Derafu\Container\Store;

$store = new Store([], [
    &#039;type&#039; =&gt; &#039;object&#039;,
    &#039;properties&#039; =&gt; [
        &#039;users&#039; =&gt; [
            &#039;type&#039; =&gt; &#039;array&#039;,
            &#039;items&#039; =&gt; [
                &#039;type&#039; =&gt; &#039;object&#039;,
                &#039;required&#039; =&gt; [&#039;id&#039;, &#039;name&#039;],
                &#039;properties&#039; =&gt; [
                    &#039;id&#039; =&gt; [&#039;type&#039; =&gt; &#039;integer&#039;],
                    &#039;name&#039; =&gt; [&#039;type&#039; =&gt; &#039;string&#039;],
                    &#039;email&#039; =&gt; [&#039;type&#039; =&gt; &#039;string&#039;, &#039;format&#039; =&gt; &#039;email&#039;],
                ],
            ],
        ],
    ],
]);
```

### Journal with Filtering

```php
use Derafu\Container\Journal;

$journal = new Journal();
$journal-&gt;add([&#039;level&#039; =&gt; &#039;info&#039;, &#039;message&#039; =&gt; &#039;System started&#039;]);
$journal-&gt;add([&#039;level&#039; =&gt; &#039;error&#039;, &#039;message&#039; =&gt; &#039;Connection failed&#039;]);

$errors = array_filter($journal-&gt;reverse(), fn($entry) =&gt;
    $entry[&#039;level&#039;] === &#039;error&#039;
);
```




---

## Selector Project

Derafu Selector

# Derafu Selector




---

### Introduction

Elegant Data Structure Navigation for PHP

# Elegant Data Structure Navigation for PHP

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/selector/main)
![CI Workflow](https://github.com/derafu/selector/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/selector)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/selector)
![Total Downloads](https://poser.pugx.org/derafu/selector/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/selector/d/monthly)

A powerful, flexible, and efficient PHP library for querying, extracting, and modifying complex data structures using a **declarative selector syntax**.

## Why Derafu\Selector?

### 🚀 **A Smarter Way to Work with Data Structures**

Traditional PHP methods for accessing and modifying nested data structures (`$array[&#039;key&#039;][&#039;subkey&#039;]`) are:

- **Error-prone**: Risk of `Undefined index` or `Trying to access array offset on null` errors.
- **Verbose &amp; Hard to Read**: Requires multiple `isset()` checks and deep nesting.
- **Limited**: No built-in support for **filters, conditional selectors, or transformations**.

### 🔥 **What Makes Derafu\Selector Unique?**

| Feature                              | Derafu\Selector | PHP Native (`isset()` &amp; loops) | Other Libraries |
|--------------------------------------|-----------------|--------------------------------|-----------------|
| **Dot-Notation Access**              | ✅ Yes          | ❌ No                          | ⚠️ Limited      |
| **Nested Selection with Conditions** | ✅ Yes          | ❌ No                          | ⚠️ Limited      |
| **Dynamic Path Resolution**          | ✅ Yes          | ❌ No                          | ⚠️ Varies       |
| **Auto-Handles Undefined Keys**      | ✅ Yes          | ❌ No                          | ⚠️ Partial      |
| **Filters &amp; Expressions**            | ✅ Yes          | ❌ No                          | ⚠️ Partial      |

---

## 🔍 Overview

Derafu Selector gives you a clean, robust way to work with nested arrays and objects in PHP. It eliminates the need for repetitive null checks, nested loops, and error-prone array access, replacing them with a powerful, declarative syntax.

```php
// Instead of this:
$userName = isset($data[&#039;user&#039;]) &amp;&amp; isset($data[&#039;user&#039;][&#039;profile&#039;]) &amp;&amp; isset($data[&#039;user&#039;][&#039;profile&#039;][&#039;name&#039;])
    ? $data[&#039;user&#039;][&#039;profile&#039;][&#039;name&#039;]
    : null;

// Simply write this:
$userName = Selector::get($data, &#039;user.profile.name&#039;);

// Or even filter arrays with conditions:
$adminEmail = Selector::get($data, &#039;users[role=admin:email]&#039;);
```

## ✨ Features

{.list-unstyled}
- ✅ **Powerful Dot Notation** - Access deeply nested data with simple paths (`user.profile.name`).
- ✅ **Array Access** - Use numeric indices to access array elements (`items[0].name`).
- ✅ **Filtering** - Find array elements by key/value conditions (`users[id=123:email]`).
- ✅ **Conditional Logic** - Use ternary-like expressions (`((type) = &quot;admin&quot; ? (admin_role) : (user_role))`).
- ✅ **Multiple Query Languages** - Support for native syntax, JSONPath, and JMESPath.
- ✅ **Error Handling** - No more &quot;undefined index&quot; or &quot;trying to access array offset on null&quot; errors.
- ✅ **Write Support** - Modify data structures with the same powerful syntax.
- ✅ **Zero Dependencies** - Lightweight core with optional support for JSONPath and JMESPath.

---

## 📦 Installation

```bash
composer require derafu/selector
```

## 🚀 Quick Start

### Reading Data

```php
use Derafu\Selector\Selector;

$data = [
    &#039;user&#039; =&gt; [
        &#039;name&#039; =&gt; &#039;John Doe&#039;,
        &#039;email&#039; =&gt; &#039;john@example.com&#039;,
        &#039;profile&#039; =&gt; [
            &#039;avatar&#039; =&gt; &#039;john.jpg&#039;,
            &#039;settings&#039; =&gt; [&#039;theme&#039; =&gt; &#039;dark&#039;, &#039;notifications&#039; =&gt; true],
        ],
        &#039;roles&#039; =&gt; [&#039;editor&#039;, &#039;contributor&#039;],
    ],
    &#039;products&#039; =&gt; [
        [&#039;id&#039; =&gt; 101, &#039;name&#039; =&gt; &#039;Laptop&#039;, &#039;price&#039; =&gt; 999],
        [&#039;id&#039; =&gt; 102, &#039;name&#039; =&gt; &#039;Phone&#039;, &#039;price&#039; =&gt; 699],
        [&#039;id&#039; =&gt; 103, &#039;name&#039; =&gt; &#039;Headphones&#039;, &#039;price&#039; =&gt; 149],
    ],
];

// Simple property access.
$name = Selector::get($data, &#039;user.name&#039;);  // &quot;John Doe&quot;

// Nested properties.
$theme = Selector::get($data, &#039;user.profile.settings.theme&#039;);  // &quot;dark&quot;

// Array elements by index.
$firstRole = Selector::get($data, &#039;user.roles[0]&#039;);  // &quot;editor&quot;

// Array filtering.
$laptop = Selector::get($data, &#039;products[id=101:name]&#039;);  // &quot;Laptop&quot;

// With default values.
$address = Selector::get($data, &#039;user.address&#039;, &#039;Not specified&#039;);  // &quot;Not specified&quot;

// Check if a path exists.
$hasAvatar = Selector::has($data, &#039;user.profile.avatar&#039;);  // true
```

### Writing Data

```php
// Set a simple value.
Selector::set($data, &#039;user.address&#039;, &#039;123 Main St&#039;);

// Create nested structures automatically.
Selector::set($data, &#039;user.profile.settings.language&#039;, &#039;en&#039;);

// Update array elements.
Selector::set($data, &#039;products[id=101:price]&#039;, 899);

// Remove a value.
Selector::clear($data, &#039;user.profile.settings.notifications&#039;);
```

### Advanced Selector Syntax

```php
// Conditional selectors.
$role = Selector::get($data, &#039;((user.type) = &quot;admin&quot; ? (admin_role) : (user_role))&#039;);

// String concatenation.
$greeting = Selector::get($data, &#039;&quot;Hello, &quot;(user.name)&quot;!&quot;&#039;);  // &quot;Hello, John Doe!&quot;

// OR operator for fallbacks.
$contactInfo = Selector::get($data, &#039;user.phone||user.email&#039;);  // Uses email if phone is null

// JSONPath integration.
$expensiveProducts = Selector::get($data, &#039;$.products[?(@.price &gt; 500)].name&#039;);  // [&quot;Laptop&quot;, &quot;Phone&quot;]

// JMESPath integration.
$productNames = Selector::get($data, &#039;jmespath:products[*].name&#039;);  // [&quot;Laptop&quot;, &quot;Phone&quot;, &quot;Headphones&quot;]
```




---

### Basic Usage

Basic Usage

# Basic Usage

This guide covers the fundamentals of working with Derafu Selector for navigating and manipulating data structures.

## Reading Data

The most common operation with Selector is to read values from complex data structures. The `get()` method provides a clean, safe way to extract values without worry about undefined indices or null values.

### Basic Dot Notation

```php
use Derafu\Selector\Selector;

$data = [
    &#039;user&#039; =&gt; [
        &#039;name&#039; =&gt; &#039;John Doe&#039;,
        &#039;email&#039; =&gt; &#039;john@example.com&#039;,
        &#039;profile&#039; =&gt; [
            &#039;avatar&#039; =&gt; &#039;john.jpg&#039;,
        ],
    ],
];

// Simple property access.
$name = Selector::get($data, &#039;user.name&#039;);  // &quot;John Doe&quot;

// Nested properties.
$avatar = Selector::get($data, &#039;user.profile.avatar&#039;);  // &quot;john.jpg&quot;

// Non-existent path returns null.
$age = Selector::get($data, &#039;user.age&#039;);  // null
```

### Default Values

You can provide a default value that will be returned if the selector path doesn&#039;t exist:

```php
// With default value
$address = Selector::get($data, &#039;user.address&#039;, &#039;No address provided&#039;);  // &quot;No address provided&quot;
```

### Array Access

Access array elements by index:

```php
$data = [
    &#039;items&#039; =&gt; [&#039;apple&#039;, &#039;banana&#039;, &#039;cherry&#039;],
];

$firstItem = Selector::get($data, &#039;items[0]&#039;);  // &quot;apple&quot;
$lastItem = Selector::get($data, &#039;items[2]&#039;);   // &quot;cherry&quot;
```

### Combining Dot Notation and Array Access

```php
$data = [
    &#039;categories&#039; =&gt; [
        &#039;fruits&#039; =&gt; [&#039;apple&#039;, &#039;banana&#039;, &#039;cherry&#039;],
        &#039;vegetables&#039; =&gt; [&#039;carrot&#039;, &#039;broccoli&#039;, &#039;spinach&#039;],
    ],
];

$firstFruit = Selector::get($data, &#039;categories.fruits[0]&#039;);  // &quot;apple&quot;
$secondVegetable = Selector::get($data, &#039;categories.vegetables[1]&#039;);  // &quot;broccoli&quot;
```

## Writing Data

Writing data is just as easy as reading it. The `set()` method handles creating intermediate structures as needed.

### Setting Simple Values

```php
$data = [&#039;user&#039; =&gt; [&#039;name&#039; =&gt; &#039;John&#039;]];

// Update an existing value.
Selector::set($data, &#039;user.name&#039;, &#039;Jane&#039;);
// $data is now [&#039;user&#039; =&gt; [&#039;name&#039; =&gt; &#039;Jane&#039;]]

// Create a new property.
Selector::set($data, &#039;user.email&#039;, &#039;jane@example.com&#039;);
// $data is now [&#039;user&#039; =&gt; [&#039;name&#039; =&gt; &#039;Jane&#039;, &#039;email&#039; =&gt; &#039;jane@example.com&#039;]]
```

### Creating Nested Structures

Selector will automatically create intermediate arrays:

```php
$data = [];

// Create a deep structure.
Selector::set($data, &#039;settings.theme.colors.primary&#039;, &#039;#3366FF&#039;);
// $data is now [&#039;settings&#039; =&gt; [&#039;theme&#039; =&gt; [&#039;colors&#039; =&gt; [&#039;primary&#039; =&gt; &#039;#3366FF&#039;]]]]
```

### Working with Arrays

```php
$data = [&#039;items&#039; =&gt; [&#039;apple&#039;, &#039;banana&#039;]];

// Add or update an array element.
Selector::set($data, &#039;items[2]&#039;, &#039;cherry&#039;);
// $data is now [&#039;items&#039; =&gt; [&#039;apple&#039;, &#039;banana&#039;, &#039;cherry&#039;]]

// Update a specific element.
Selector::set($data, &#039;items[0]&#039;, &#039;red apple&#039;);
// $data is now [&#039;items&#039; =&gt; [&#039;red apple&#039;, &#039;banana&#039;, &#039;cherry&#039;]]
```

### Merging Arrays

When setting an array value to an existing array path, the arrays are merged:

```php
$data = [
    &#039;settings&#039; =&gt; [
        &#039;display&#039; =&gt; [&#039;theme&#039; =&gt; &#039;light&#039;],
    ],
];

Selector::set($data, &#039;settings.display&#039;, [&#039;fontSize&#039; =&gt; &#039;large&#039;]);
// $data is now [&#039;settings&#039; =&gt; [&#039;display&#039; =&gt; [&#039;theme&#039; =&gt; &#039;light&#039;, &#039;fontSize&#039; =&gt; &#039;large&#039;]]]
```

## Checking if Paths Exist

The `has()` method checks if a value exists at the specified path:

```php
$data = [
    &#039;user&#039; =&gt; [
        &#039;name&#039; =&gt; &#039;John&#039;,
        &#039;email&#039; =&gt; null,
    ],
];

Selector::has($data, &#039;user.name&#039;);      // true
Selector::has($data, &#039;user.email&#039;);     // false (because the value is null)
Selector::has($data, &#039;user.address&#039;);   // false (path doesn&#039;t exist)
```

## Clearing Values

Remove values from the data structure with the `clear()` method:

```php
$data = [
    &#039;user&#039; =&gt; [
        &#039;name&#039; =&gt; &#039;John&#039;,
        &#039;email&#039; =&gt; &#039;john@example.com&#039;,
        &#039;settings&#039; =&gt; [&#039;theme&#039; =&gt; &#039;dark&#039;],
    ],
];

// Remove a simple property.
Selector::clear($data, &#039;user.email&#039;);
// $data is now [&#039;user&#039; =&gt; [&#039;name&#039; =&gt; &#039;John&#039;, &#039;settings&#039; =&gt; [&#039;theme&#039; =&gt; &#039;dark&#039;]]]

// Remove a nested property.
Selector::clear($data, &#039;user.settings.theme&#039;);
// $data is now [&#039;user&#039; =&gt; [&#039;name&#039; =&gt; &#039;John&#039;, &#039;settings&#039; =&gt; []]]

// Parent arrays are cleaned up if they become empty.
Selector::clear($data, &#039;user.name&#039;);
// $data is now [&#039;user&#039; =&gt; [&#039;settings&#039; =&gt; []]]
```

## Working with Different Data Types

Selector handles various data types properly:

```php
$data = [
    &#039;values&#039; =&gt; [
        &#039;string&#039; =&gt; &#039;text&#039;,
        &#039;number&#039; =&gt; 42,
        &#039;boolean&#039; =&gt; true,
        &#039;null_value&#039; =&gt; null,
        &#039;array&#039; =&gt; [1, 2, 3],
        &#039;object&#039; =&gt; [&#039;key&#039; =&gt; &#039;value&#039;],
    ],
];

Selector::get($data, &#039;values.string&#039;);     // &quot;text&quot;
Selector::get($data, &#039;values.number&#039;);     // 42
Selector::get($data, &#039;values.boolean&#039;);    // true
Selector::get($data, &#039;values.null_value&#039;); // null
Selector::get($data, &#039;values.array&#039;);      // [1, 2, 3]
Selector::get($data, &#039;values.object&#039;);     // [&#039;key&#039; =&gt; &#039;value&#039;]
```

## Error Handling

Selector is designed to be forgiving and avoid common PHP errors:

```php
// No &quot;undefined index&quot; errors.
Selector::get($data, &#039;non.existent.path&#039;);  // returns null

// No &quot;trying to access array offset on null&quot; errors.
$data = [&#039;user&#039; =&gt; null];
Selector::get($data, &#039;user.name&#039;);  // returns null

// However, if you have syntax errors in your selector, you&#039;ll get a SelectorException.
try {
    Selector::get($data, &#039;user..name&#039;);  // Invalid selector with double dot
} catch (\Derafu\Selector\Exception\SelectorException $e) {
    echo $e-&gt;getMessage();  // &quot;Failed to read using selector &#039;user..name&#039;...&quot;
}
```

---

For more advanced usage, check out the other guides in the documentation.




---

### Working with Arrays and Filters

Working with Arrays and Filters

# Working with Arrays and Filters

This guide focuses on techniques for working with arrays and using filters in Derafu Selector.

## Basic Array Access

Arrays can be accessed using both dot notation and square bracket syntax:

```php
use Derafu\Selector\Selector;

$data = [
    &#039;items&#039; =&gt; [&#039;apple&#039;, &#039;banana&#039;, &#039;cherry&#039;],
    &#039;counts&#039; =&gt; [5, 10, 15],
];

// Access entire array.
$allItems = Selector::get($data, &#039;items&#039;);
// [&#039;apple&#039;, &#039;banana&#039;, &#039;cherry&#039;]

// First element.
$firstItem = Selector::get($data, &#039;items[0]&#039;);
// &#039;apple&#039;

// Last element.
$lastItem = Selector::get($data, &#039;items[2]&#039;);
// &#039;cherry&#039;

// Non-existent index.
$nonExistent = Selector::get($data, &#039;items[5]&#039;);
// null
```

## Numeric Indexing

```php
$data = [
    &#039;matrix&#039; =&gt; [
        [1, 2, 3],
        [4, 5, 6],
        [7, 8, 9],
    ],
];

// Accessing multi-dimensional arrays.
$center = Selector::get($data, &#039;matrix[1][1]&#039;);
// 5

// Alternative dot notation.
$center = Selector::get($data, &#039;matrix.1.1&#039;);
// 5

// Mixed notation.
$firstRow = Selector::get($data, &#039;matrix[0]&#039;);
// [1, 2, 3]
$firstRowThirdElement = Selector::get($data, &#039;matrix[0][2]&#039;);
// 3
```

## Filtering Arrays by Condition

The most powerful feature for array handling is filtering by key/value conditions:

```php
$data = [
    &#039;users&#039; =&gt; [
        [&#039;id&#039; =&gt; 1, &#039;name&#039; =&gt; &#039;John&#039;, &#039;role&#039; =&gt; &#039;admin&#039;],
        [&#039;id&#039; =&gt; 2, &#039;name&#039; =&gt; &#039;Jane&#039;, &#039;role&#039; =&gt; &#039;user&#039;],
        [&#039;id&#039; =&gt; 3, &#039;name&#039; =&gt; &#039;Bob&#039;, &#039;role&#039; =&gt; &#039;user&#039;],
    ]
];

// Find user by ID.
$userName = Selector::get($data, &#039;users[id=2:name]&#039;);
// &#039;Jane&#039;

// Find user by role.
$adminName = Selector::get($data, &#039;users[role=admin:name]&#039;);
// &#039;John&#039;

// Non-matching filter.
$nonExistent = Selector::get($data, &#039;users[id=99:name]&#039;);
// null
```

### Syntax for Dependent Selectors

The syntax for array filtering with dependent selectors is:

```
arrayKey[requiredKey=requiredValue:dependentKey]
```

Where:

- `arrayKey` is the key containing the array to search in.
- `requiredKey` is the key to match in each array element.
- `requiredValue` is the value that `requiredKey` should equal.
- `dependentKey` is the key to extract from the matching element.

## Complex Filtering

You can combine filters with further navigation:

```php
$data = [
    &#039;orders&#039; =&gt; [
        [
            &#039;id&#039; =&gt; 1001,
            &#039;customer&#039; =&gt; &#039;John&#039;,
            &#039;items&#039; =&gt; [
                [&#039;product&#039; =&gt; &#039;Laptop&#039;, &#039;price&#039; =&gt; 999],
                [&#039;product&#039; =&gt; &#039;Mouse&#039;, &#039;price&#039; =&gt; 25],
            ],
        ],
        [
            &#039;id&#039; =&gt; 1002,
            &#039;customer&#039; =&gt; &#039;Jane&#039;,
            &#039;items&#039; =&gt; [
                [&#039;product&#039; =&gt; &#039;Phone&#039;, &#039;price&#039; =&gt; 699],
                [&#039;product&#039; =&gt; &#039;Headphones&#039;, &#039;price&#039; =&gt; 149],
            ],
        ],
    ],
];

// Get the first item from order 1002.
$product = Selector::get($data, &#039;orders[id=1002:items][0].product&#039;);
// &#039;Phone&#039;

// Get the price of the second item from order 1001.
$price = Selector::get($data, &#039;orders[id=1001:items][1].price&#039;);
// 25

// Find an item by product name within an order.
$data = [
    &#039;orders&#039; =&gt; [
        [
            &#039;id&#039; =&gt; 1001,
            &#039;items&#039; =&gt; [
                [&#039;product&#039; =&gt; &#039;Laptop&#039;, &#039;price&#039; =&gt; 999],
                [&#039;product&#039; =&gt; &#039;Mouse&#039;, &#039;price&#039; =&gt; 25],
            ],
        ],
    ],
];
$mousePrice = Selector::get($data, &#039;orders[id=1001:items][product=Mouse:price]&#039;);
// 25
```

## Nested Arrays

For deeply nested structures:

```php
$data = [
    &#039;departments&#039; =&gt; [
        [
            &#039;name&#039; =&gt; &#039;Engineering&#039;,
            &#039;teams&#039; =&gt; [
                [
                    &#039;name&#039; =&gt; &#039;Frontend&#039;,
                    &#039;members&#039; =&gt; [
                        [&#039;id&#039; =&gt; 101, &#039;name&#039; =&gt; &#039;Alice&#039;],
                        [&#039;id&#039; =&gt; 102, &#039;name&#039; =&gt; &#039;Bob&#039;],
                    ]
                ],
                [
                    &#039;name&#039; =&gt; &#039;Backend&#039;,
                    &#039;members&#039; =&gt; [
                        [&#039;id&#039; =&gt; 201, &#039;name&#039; =&gt; &#039;Charlie&#039;],
                        [&#039;id&#039; =&gt; 202, &#039;name&#039; =&gt; &#039;Diana&#039;],
                    ],
                ],
            ],
        ],
    ],
];

// Find Charlie&#039;s ID through the department and team.
$charlieId = Selector::get($data,
    &#039;departments[name=Engineering:teams][name=Backend:members][name=Charlie:id]&#039;
); // 201
```

## Array Operations

### Writing to Arrays

```php
$data = [
    &#039;users&#039; =&gt; [
        [&#039;id&#039; =&gt; 1, &#039;name&#039; =&gt; &#039;John&#039;],
        [&#039;id&#039; =&gt; 2, &#039;name&#039; =&gt; &#039;Jane&#039;],
    ],
];

// Update a value.
Selector::set($data, &#039;users[id=1:name]&#039;, &#039;Johnny&#039;);
// $data[&#039;users&#039;][0][&#039;name&#039;] is now &#039;Johnny&#039;

// Add a new property.
Selector::set($data, &#039;users[id=2:email]&#039;, &#039;jane@example.com&#039;);
// $data[&#039;users&#039;][1][&#039;email&#039;] is now &#039;jane@example.com&#039;

// Add a new element.
Selector::set($data, &#039;users[3]&#039;, [&#039;id&#039; =&gt; 3, &#039;name&#039; =&gt; &#039;Bob&#039;]);
// $data[&#039;users&#039;][3] is now [&#039;id&#039; =&gt; 3, &#039;name&#039; =&gt; &#039;Bob&#039;]

// If no matching element exists, a new one is created.
Selector::set($data, &#039;users[id=4:name]&#039;, &#039;Alice&#039;);
// Adds a new element to $data[&#039;users&#039;] with id=4 and name=&#039;Alice&#039;
```

### Clearing Array Elements

```php
$data = [
    &#039;users&#039; =&gt; [
        [&#039;id&#039; =&gt; 1, &#039;name&#039; =&gt; &#039;John&#039;, &#039;email&#039; =&gt; &#039;john@example.com&#039;],
        [&#039;id&#039; =&gt; 2, &#039;name&#039; =&gt; &#039;Jane&#039;, &#039;email&#039; =&gt; &#039;jane@example.com&#039;],
    ],
];

// Remove a property.
Selector::clear($data, &#039;users[id=1:email]&#039;);
// John&#039;s email is now removed.

// Remove an entire array element.
Selector::clear($data, &#039;users[id=2]&#039;);
// Jane&#039;s record is now completely removed.

// Clean up empty array elements.
$data = [
    &#039;teams&#039; =&gt; [
        [&#039;id&#039; =&gt; 1, &#039;members&#039; =&gt; [&#039;Alice&#039;, &#039;Bob&#039;]],
        [&#039;id&#039; =&gt; 2, &#039;members&#039; =&gt; [&#039;Charlie&#039;]],
    ],
];

// Remove the last member from team 2.
Selector::clear($data, &#039;teams[id=2:members][0]&#039;);
// Now teams[id=2:members] is an empty array.

// Derafu Selector automatically cleans up empty arrays:
// $data[&#039;teams&#039;][1][&#039;members&#039;] is now an empty array.
// If we clear team 1&#039;s members too, it would clean up further.
Selector::clear($data, &#039;teams[id=1:members]&#039;);
// Now both teams have empty members arrays.
```

### Working with Mixed Array Types

Selector handles both associative and indexed arrays seamlessly:

```php
$data = [
    &#039;products&#039; =&gt; [
        &#039;electronics&#039; =&gt; [
            [&#039;id&#039; =&gt; &#039;e1&#039;, &#039;name&#039; =&gt; &#039;Laptop&#039;, &#039;price&#039; =&gt; 999],
            [&#039;id&#039; =&gt; &#039;e2&#039;, &#039;name&#039; =&gt; &#039;Phone&#039;, &#039;price&#039; =&gt; 699],
        ],
        &#039;books&#039; =&gt; [
            [&#039;id&#039; =&gt; &#039;b1&#039;, &#039;name&#039; =&gt; &#039;Novel&#039;, &#039;price&#039; =&gt; 15],
            [&#039;id&#039; =&gt; &#039;b2&#039;, &#039;name&#039; =&gt; &#039;Textbook&#039;, &#039;price&#039; =&gt; 50],
        ],
    ],
];

// Access by category and index.
$firstElectronic = Selector::get($data, &#039;products.electronics[0].name&#039;);
// &#039;Laptop&#039;

// Access by category and ID.
$textbookPrice = Selector::get($data, &#039;products.books[id=b2:price]&#039;);
// 50

// Numeric indices still work with filtering.
$secondBookName = Selector::get($data, &#039;products.books[1].name&#039;);
// &#039;Textbook&#039;
```

### Handling Empty or Non-existent Arrays

Selector provides safe handling for empty or non-existent arrays:

```php
$data = [
    &#039;categories&#039; =&gt; [
        &#039;active&#039; =&gt; [
            [&#039;id&#039; =&gt; 1, &#039;name&#039; =&gt; &#039;Electronics&#039;],
            [&#039;id&#039; =&gt; 2, &#039;name&#039; =&gt; &#039;Books&#039;],
        ],
        &#039;inactive&#039; =&gt; [],
    ],
];

// Access on empty array.
$inactiveCategory = Selector::get($data, &#039;categories.inactive[0]&#039;);
// null

// Filter on empty array.
$result = Selector::get($data, &#039;categories.inactive[id=1:name]&#039;);
// null

// Non-existent array path.
$result = Selector::get($data, &#039;categories.archived[0].name&#039;);
// null

// Providing defaults.
$result = Selector::get($data, &#039;categories.archived[0].name&#039;, &#039;Not found&#039;);
// &#039;Not found&#039;
```

### Modifying Array Elements in Place

You can modify array elements directly:

```php
$data = [
    &#039;cart&#039; =&gt; [
        [&#039;id&#039; =&gt; &#039;p1&#039;, &#039;name&#039; =&gt; &#039;Laptop&#039;, &#039;quantity&#039; =&gt; 1],
        [&#039;id&#039; =&gt; &#039;p2&#039;, &#039;name&#039; =&gt; &#039;Mouse&#039;, &#039;quantity&#039; =&gt; 2],
    ],
];

// Increase quantity.
$currentQuantity = Selector::get($data, &#039;cart[id=p1:quantity]&#039;);
Selector::set($data, &#039;cart[id=p1:quantity]&#039;, $currentQuantity + 1);
// Now p1&#039;s quantity is 2

// Add a new property to an array element.
Selector::set($data, &#039;cart[id=p2:price]&#039;, 25);
// Mouse now has a price property

// Modify a nested property.
$data = [
    &#039;orders&#039; =&gt; [
        [
            &#039;id&#039; =&gt; 1,
            &#039;items&#039; =&gt; [
                [&#039;product&#039; =&gt; &#039;Laptop&#039;, &#039;price&#039; =&gt; 999],
            ],
        ],
    ],
];

// Apply discount.
Selector::set($data, &#039;orders[id=1:items][product=Laptop:price]&#039;, 899);
// Price is now 899
```

---

By leveraging these array handling capabilities, you can work with complex nested data structures in a clean, readable way without worrying about undefined indices or complex nesting logic.




---

### Advanced Syntax

Advanced Selector Syntax

# Advanced Selector Syntax

This guide covers the advanced selector syntax features that make Derafu Selector powerful for complex data manipulation.

## String Literals and Concatenation

You can include literal strings in your selectors and concatenate them with values from your data:

```php
$data = [&#039;user&#039; =&gt; [&#039;name&#039; =&gt; &#039;John&#039;]];

// Simple string concatenation.
$greeting = Selector::get($data, &#039;&quot;Hello, &quot;(user.name)&quot;!&quot;&#039;);
// &quot;Hello, John!&quot;

// Multiple concatenations.
$message = Selector::get($data, &#039;&quot;User &quot;(user.name)&quot; logged in at &quot;(timestamp)&#039;);
// &quot;User John logged in at 2023-04-15 10:30:45&quot;
```

## OR Operator for Fallbacks

The `||` operator provides fallback values when a selector path doesn&#039;t exist or returns null:

```php
$data = [
    &#039;user&#039; =&gt; [
        &#039;name&#039; =&gt; &#039;John&#039;,
        &#039;email&#039; =&gt; &#039;john@example.com&#039;,
        // phone is not set
    ],
];

// Use email if phone doesn&#039;t exist.
$contact = Selector::get($data, &#039;user.phone||user.email&#039;);
// &quot;john@example.com&quot;

// Chains of fallbacks.
$identifier = Selector::get($data, &#039;user.id||user.username||user.email&#039;);
// &quot;john@example.com&quot;

// Fallback to a literal string.
$phone = Selector::get($data, &#039;user.phone||&quot;Not provided&quot;&#039;);
// &quot;Not provided&quot;

// Combine with string concatenation.
$phoneDisplay = Selector::get($data, &#039;&quot;Phone: &quot;(user.phone||&quot;N/A&quot;)&#039;);
// &quot;Phone: N/A&quot;
```

## Conditional (Ternary) Selectors

Conditionals let you choose between different selector paths based on conditions:

```php
$data = [
    &#039;user&#039; =&gt; [
        &#039;type&#039; =&gt; &#039;admin&#039;,
        &#039;admin_role&#039; =&gt; &#039;super_admin&#039;,
        &#039;user_role&#039; =&gt; &#039;regular&#039;,
    ],
];

// Simple condition using equality.
$role = Selector::get($data,
    &#039;((user.type) = &quot;admin&quot; ? (user.admin_role) : (user.user_role))&#039;
); // &quot;super_admin&quot;

// Using not equal.
$accessLevel = Selector::get($data,
    &#039;((user.type) != &quot;guest&quot; ? (&quot;authenticated&quot;) : (&quot;anonymous&quot;))&#039;
); // &quot;authenticated&quot;

// Numeric comparisons.
$data = [&#039;score&#039; =&gt; 85];
$grade = Selector::get($data,
    &#039;((score) &gt;= &quot;90&quot; ? (&quot;A&quot;) : ((score) &gt;= &quot;80&quot; ? (&quot;B&quot;) : (&quot;C&quot;)))&#039;
); // &quot;B&quot;
```

## Dependent Selectors

Find and access specific elements in arrays based on key/value matching:

```php
$data = [
    &#039;users&#039; =&gt; [
        [&#039;id&#039; =&gt; 101, &#039;name&#039; =&gt; &#039;John&#039;, &#039;email&#039; =&gt; &#039;john@example.com&#039;],
        [&#039;id&#039; =&gt; 102, &#039;name&#039; =&gt; &#039;Jane&#039;, &#039;email&#039; =&gt; &#039;jane@example.com&#039;],
        [&#039;id&#039; =&gt; 103, &#039;name&#039; =&gt; &#039;Bob&#039;, &#039;email&#039; =&gt; &#039;bob@example.com&#039;],
    ],
];

// Get the name of user with id 102.
$name = Selector::get($data, &#039;users[id=102:name]&#039;);
// &quot;Jane&quot;

// Get the email of user with id 101.
$email = Selector::get($data, &#039;users[id=101:email]&#039;);
// &quot;john@example.com&quot;

// Nested properties.
$data = [
    &#039;orders&#039; =&gt; [
        [
            &#039;id&#039; =&gt; 1001,
            &#039;customer&#039; =&gt; [&#039;id&#039; =&gt; 101, &#039;name&#039; =&gt; &#039;John&#039;],
            &#039;items&#039; =&gt; [[&#039;product&#039; =&gt; &#039;Laptop&#039;, &#039;price&#039; =&gt; 999]],
        ],
        [
            &#039;id&#039; =&gt; 1002,
            &#039;customer&#039; =&gt; [&#039;id&#039; =&gt; 102, &#039;name&#039; =&gt; &#039;Jane&#039;],
            &#039;items&#039; =&gt; [[&#039;product&#039; =&gt; &#039;Phone&#039;, &#039;price&#039; =&gt; 699]],
        ],
    ],
];

// Get Jane&#039;s first order item.
$janeProduct = Selector::get($data, &#039;orders[id=1002:items][0].product&#039;);
// &quot;Phone&quot;

// Access by string values with special characters.
$data = [
    &#039;categories&#039; =&gt; [
        [&#039;id&#039; =&gt; &#039;cat-1&#039;, &#039;name&#039; =&gt; &#039;Electronics&#039;],
        [&#039;id&#039; =&gt; &#039;cat-2&#039;, &#039;name&#039; =&gt; &#039;Books&#039;]
    ]
];

$catName = Selector::get($data, &#039;categories[id=cat-1:name]&#039;);
// &quot;Electronics&quot;
```

## Comparison Operators

Conditional selectors support several comparison operators:

```php
$data = [
    &#039;product&#039; =&gt; [
        &#039;price&#039; =&gt; 50,
        &#039;stock&#039; =&gt; 10,
        &#039;name&#039; =&gt; &#039;Gadget&#039;,
        &#039;tags&#039; =&gt; [&#039;electronics&#039;, &#039;new&#039;],
    ],
];

// Equality.
$isGadget = Selector::get($data,
    &#039;((product.name) = &quot;Gadget&quot; ? (&quot;Yes&quot;) : (&quot;No&quot;))&#039;
); // &quot;Yes&quot;

// Not equal.
$isExpensive = Selector::get($data,
    &#039;((product.price) != &quot;100&quot; ? (&quot;Affordable&quot;) : (&quot;Expensive&quot;))&#039;
); // &quot;Affordable&quot;

// Greater than.
$priceTier = Selector::get($data,
    &#039;((product.price) &gt; &quot;75&quot; ? (&quot;Premium&quot;) : (&quot;Standard&quot;))&#039;
); // &quot;Standard&quot;

// Less than or equal.
$stockStatus = Selector::get($data,
    &#039;((product.stock) &lt;= &quot;5&quot; ? (&quot;Low Stock&quot;) : (&quot;In Stock&quot;))&#039;
); // &quot;In Stock&quot;

// Contains (for arrays and strings).
$isNew = Selector::get($data,
    &#039;((product.tags) contains &quot;new&quot; ? (&quot;New Arrival&quot;) : (&quot;Regular Item&quot;))&#039;
); // &quot;New Arrival&quot;

// Length check.
$nameLength = Selector::get($data,
    &#039;((product.name) length &quot;6&quot; ? (&quot;Six Letters&quot;) : (&quot;Other Length&quot;))&#039;
); // &quot;Six Letters&quot;

// Null check.
$data[&#039;product&#039;][&#039;description&#039;] = null;
$hasDescription = Selector::get($data,
    &#039;((product.description) is &quot;null&quot; ? (&quot;No Description&quot;) : (&quot;Has Description&quot;))&#039;
); // &quot;No Description&quot;
```

## Special Functions

The selector syntax includes several special functions:

```php
$data = [
    &#039;product&#039; =&gt; [
        &#039;name&#039; =&gt; &#039;Gadget&#039;,
        &#039;price&#039; =&gt; 49.99,
        &#039;tags&#039; =&gt; [&#039;electronics&#039;, &#039;gadget&#039;, &#039;new&#039;],
    ],
];

// contains - check if an array or string contains a value.
$hasTag = Selector::get($data,
    &#039;((product.tags) contains &quot;gadget&quot; ? (&quot;Tagged as gadget&quot;) : (&quot;Not tagged&quot;))&#039;
); // &quot;Tagged as gadget&quot;

// length - check the length of a string or array.
$tagCount = Selector::get($data,
    &#039;((product.tags) length &quot;3&quot; ? (&quot;Has 3 tags&quot;) : (&quot;Has another tag count&quot;))&#039;
); // &quot;Has 3 tags&quot;

$nameLength = Selector::get($data,
    &#039;((product.name) length &quot;6&quot; ? (&quot;Name has 6 chars&quot;) : (&quot;Name has different length&quot;))&#039;
); // &quot;Name has 6 chars&quot;

// is - check the type of a value (currently supports null checks).
$hasDescription = Selector::get($data,
    &#039;((product.description) is &quot;null&quot; ? (&quot;No description&quot;) : (&quot;Has description&quot;))&#039;
); // &quot;No description&quot;
```

## Parentheses and Escaping

The selector syntax uses parentheses and quotes extensively:

```php
// (selector) extracts a value from the data
// &quot;string&quot; is a literal string

$data = [&#039;message&#039; =&gt; &#039;Hello World&#039;];

// Simple extraction.
$msg = Selector::get($data, &#039;(message)&#039;); // &quot;Hello World&quot;

// Literal string.
$literal = Selector::get($data, &#039;&quot;Static text&quot;&#039;); // &quot;Static text&quot;

// Concatenating extractions and literals.
$full = Selector::get($data, &#039;(message)&quot; - welcome!&quot;&#039;); // &quot;Hello World - welcome!&quot;

// Escaping quotes and parentheses.
$escaped = Selector::get($data, &#039;&quot;This is a \\&quot;quoted\\&quot; string&quot;&#039;); // &#039;This is a &quot;quoted&quot; string&#039;
$parentheses = Selector::get($data, &#039;&quot;Formula: (x + y)&quot;&#039;); // &quot;Formula: (x + y)&quot;

// Nested parentheses for complex expressions.
$nested = Selector::get($data, &#039;((message) = &quot;Hello World&quot; ? (&quot;Greeting&quot;) : (&quot;Other message&quot;))&#039;);
// &quot;Greeting&quot;
```

---

For even more advanced capabilities, explore the integration with JSONPath and JMESPath in their respective guides.




---

### JSONPath Integration

JSONPath Integration

# JSONPath Integration

This guide explains how to use JSONPath with Derafu Selector for powerful data structure querying.

## Introduction to JSONPath

JSONPath is a query language for JSON, similar to XPath for XML. It provides a powerful way to extract data from complex JSON structures. Derafu Selector integrates JSONPath to give you even more flexibility in navigating your data.

All JSONPath expressions in Derafu Selector start with `$.` to distinguish them from regular selectors.

## Basic JSONPath Queries

Here are some basic JSONPath examples:

```php
use Derafu\Selector\Selector;

$data = [
    &#039;store&#039; =&gt; [
        &#039;books&#039; =&gt; [
            [&#039;title&#039; =&gt; &#039;Book 1&#039;, &#039;price&#039; =&gt; 10],
            [&#039;title&#039; =&gt; &#039;Book 2&#039;, &#039;price&#039; =&gt; 20],
            [&#039;title&#039; =&gt; &#039;Book 3&#039;, &#039;price&#039; =&gt; 30],
        ],
        &#039;bicycles&#039; =&gt; [
            [&#039;model&#039; =&gt; &#039;Model A&#039;, &#039;price&#039; =&gt; 100],
            [&#039;model&#039; =&gt; &#039;Model B&#039;, &#039;price&#039; =&gt; 200],
        ],
    ],
    &#039;user&#039; =&gt; [
        &#039;name&#039; =&gt; &#039;John&#039;,
        &#039;email&#039; =&gt; &#039;john@example.com&#039;,
    ],
];

// Access a simple property.
$userName = Selector::get($data, &#039;$.user.name&#039;);
// &quot;John&quot;

// Access an array element by index.
$firstBook = Selector::get($data, &#039;$.store.books[0].title&#039;);
// &quot;Book 1&quot;

// Get all book titles.
$allTitles = Selector::get($data, &#039;$.store.books[*].title&#039;);
// [&quot;Book 1&quot;, &quot;Book 2&quot;, &quot;Book 3&quot;]

// Get the entire books array.
$allBooks = Selector::get($data, &#039;$.store.books&#039;);
// The complete books array
```

## Advanced JSONPath Features

JSONPath offers powerful filtering capabilities:

```php
// Filter books by price (more than 15).
$expensiveBooks = Selector::get($data, &#039;$.store.books[?(@.price &gt; 15)].title&#039;);
// [&quot;Book 2&quot;, &quot;Book 3&quot;]

// Filter books by title (exact match).
$specificBook = Selector::get($data, &#039;$.store.books[?(@.title == &quot;Book 2&quot;)]&#039;);
// Returns the complete book object with title &quot;Book 2&quot;

// Multiple conditions with AND.
$filtered = Selector::get($data,
    &#039;$.store.books[?(@.price &gt; 15 &amp;&amp; @.price &lt; 25)].title&#039;
);
// [&quot;Book 2&quot;]

// Regular expression matching (if supported by the JSONPath implementation).
$booksWithPattern = Selector::get($data, &#039;$.store.books[?(@.title =~ /Book [1-2]/)].title&#039;);
// [&quot;Book 1&quot;, &quot;Book 2&quot;]

// Array slicing.
$firstTwoBooks = Selector::get($data, &#039;$.store.books[0:2].title&#039;);
// [&quot;Book 1&quot;, &quot;Book 2&quot;]
```

### Common JSONPath Syntax Elements

| Symbol            | Description                                              |
|-------------------|----------------------------------------------------------|
| `$`               | The root object/element                                  |
| `@`               | The current object/element                               |
| `.`               | Child operator                                           |
| `..`              | Recursive descent (find all matches at any level)        |
| `*`               | Wildcard (all objects/elements)                          |
| `[n]`             | Array index (0-based)                                    |
| `[n:m]`           | Array slice from n to m                                  |
| `[?()]`           | Filter expression                                        |
| `==` `!=`         | Equality operators                                       |
| `&gt;` `&lt;` `&gt;=` `&lt;=` | Comparison operators                                     |
| `&amp;&amp;` `\|\|`       | Logical AND and OR                                       |

## Combining JSONPath with Derafu Selectors

You can mix JSONPath with regular Derafu selectors for maximum flexibility:

```php
// Use JSONPath to get a value and then concatenate with text.
$message = Selector::get($data, &#039;&quot;User: &quot;($.user.name)&#039;);
// &quot;User: John&quot;

// Use JSONPath as part of an OR selector.
$contact = Selector::get($data, &#039;$.user.phone||$.user.email&#039;);
// Falls back to &quot;john@example.com&quot; if phone doesn&#039;t exist

// Use JSONPath result in a conditional expression.
$userType = Selector::get($data,
    &#039;(($.user.email) contains &quot;example.com&quot; ? (&quot;Standard&quot;) : (&quot;Premium&quot;))&#039;
);
// &quot;Standard&quot;

// Format array results.
$bookList = Selector::get($data,
    &#039;&quot;Available books: &quot;($.store.books[*].title)&#039;
);
// &quot;Available books: [\&quot;Book 1\&quot;, \&quot;Book 2\&quot;, \&quot;Book 3\&quot;]&quot;

// Mix JSONPath with regular selectors.
$data = [
    &#039;config&#039; =&gt; [&#039;theme&#039; =&gt; &#039;dark&#039;],
    &#039;preferences&#039; =&gt; [
        &#039;admin&#039; =&gt; [&#039;theme&#039; =&gt; &#039;light&#039;],
        &#039;user&#039; =&gt; [&#039;theme&#039; =&gt; &#039;system&#039;]
    ]
];

$theme = Selector::get($data,
    &#039;(($.user.role) = &quot;admin&quot; ? (preferences.admin.theme) : (config.theme))&#039;
);
// Falls back to &quot;dark&quot; if user.role doesn&#039;t exist
```

## Performance Considerations

While JSONPath provides powerful querying capabilities, it may have different performance characteristics compared to native Derafu selectors:

1. **Initialization Cost**: JSONPath queries require initializing the JSONPath processor, which adds some overhead.

2. **Complex Queries**: For very complex queries with multiple conditions, JSONPath may be more efficient than chaining multiple native selectors.

3. **Large Data Sets**: For very large data structures, JSONPath&#039;s optimized filtering can be more efficient than iterating through the data manually.

4. **Writing Operations**: Note that JSONPath in Derafu Selector supports read operations only. For writing operations, you need to use native selectors.

**Best Practices**:

- Use native Derafu selectors for simple path access (`user.profile.name`).
- Use JSONPath for complex filtering and array operations.
- Consider caching results of expensive JSONPath queries if they&#039;re used repeatedly.
- Avoid using `..` (recursive descent) on very large data structures if performance is critical.

```php
// Less efficient for simple access.
$name = Selector::get($data, &#039;$.user.name&#039;);

// More efficient native syntax for simple access.
$name = Selector::get($data, &#039;user.name&#039;);

// JSONPath is more efficient for complex filtering.
$expensiveItems = Selector::get($data,
    &#039;$.items[?(@.price &gt; 100 &amp;&amp; @.category == &quot;electronics&quot;)].name&#039;
);
```

---

By understanding when to use JSONPath versus native selectors, you can optimize your code for both readability and performance.




---

### JMESPath Integration

JMESPath Integration

# JMESPath Integration

This guide explains how to use JMESPath with Derafu Selector for powerful data querying.

## Introduction to JMESPath

JMESPath is a query language for JSON that allows you to extract and transform elements from complex data structures. It provides a more structured and feature-rich alternative to JSONPath with a clear specification.

All JMESPath expressions in Derafu Selector start with `jmespath:` to distinguish them from regular selectors.

## Basic JMESPath Queries

Here are some basic JMESPath examples:

```php
use Derafu\Selector\Selector;

$data = [
    &#039;people&#039; =&gt; [
        [
            &#039;name&#039; =&gt; &#039;John&#039;,
            &#039;age&#039; =&gt; 30,
            &#039;phones&#039; =&gt; [&#039;home&#039; =&gt; &#039;555-1234&#039;, &#039;mobile&#039; =&gt; &#039;555-5678&#039;],
        ],
        [
            &#039;name&#039; =&gt; &#039;Jane&#039;,
            &#039;age&#039; =&gt; 25,
            &#039;phones&#039; =&gt; [&#039;home&#039; =&gt; &#039;555-4321&#039;, &#039;mobile&#039; =&gt; &#039;555-8765&#039;],
        ],
    ],
    &#039;locations&#039; =&gt; [
        &#039;Seattle&#039; =&gt; [&#039;state&#039; =&gt; &#039;WA&#039;, &#039;population&#039; =&gt; 724305],
        &#039;New York&#039; =&gt; [&#039;state&#039; =&gt; &#039;NY&#039;, &#039;population&#039; =&gt; 8804190],
    ],
];

// Access a simple path.
$firstPerson = Selector::get($data, &#039;jmespath:people[0]&#039;);
// Complete first person object.

// Access a specific property.
$firstName = Selector::get($data, &#039;jmespath:people[0].name&#039;);
// &quot;John&quot;

// Access all names.
$allNames = Selector::get($data, &#039;jmespath:people[*].name&#039;);
// [&quot;John&quot;, &quot;Jane&quot;]

// Access a nested property.
$firstMobile = Selector::get($data, &#039;jmespath:people[0].phones.mobile&#039;);
// &quot;555-5678&quot;

// Use bracket notation for keys with special characters.
$nyState = Selector::get($data, &#039;jmespath:locations.&quot;New York&quot;.state&#039;);
// &quot;NY&quot;
```

## Advanced JMESPath Features

JMESPath offers powerful filtering, projection, and transformation capabilities:

### Filtering

```php
// Filter people over age 25.
$over25 = Selector::get($data, &#039;jmespath:people[?age &gt; `25`]&#039;);
// Returns the complete person object for John.

// Filter with multiple conditions.
$filtered = Selector::get($data,
    &#039;jmespath:people[?age &gt; `25` &amp;&amp; contains(name, `Jo`)]&#039;
);
// Returns John&#039;s complete object.

// Get just the names of filtered results.
$names = Selector::get($data, &#039;jmespath:people[?age &gt; `25`].name&#039;);
// [&quot;John&quot;]

// Filter on nested properties.
$withMobile = Selector::get($data,
    &#039;jmespath:people[?phones.mobile != null].name&#039;
);
// [&quot;John&quot;, &quot;Jane&quot;]
```

### Multi-select and Projections

```php
// Select specific fields (projection).
$simplified = Selector::get($data, &#039;jmespath:people[*].{n: name, a: age}&#039;);
// [{&quot;n&quot;:&quot;John&quot;,&quot;a&quot;:30}, {&quot;n&quot;:&quot;Jane&quot;,&quot;a&quot;:25}]

// Multi-select on a single object.
$details = Selector::get($data, &#039;jmespath:people[0].{name: name, mobile: phones.mobile}&#039;);
// {&quot;name&quot;:&quot;John&quot;,&quot;mobile&quot;:&quot;555-5678&quot;}

// Flatten nested structures.
$phones = Selector::get($data, &#039;jmespath:people[*].phones.*&#039;);
// [&quot;555-1234&quot;, &quot;555-5678&quot;, &quot;555-4321&quot;, &quot;555-8765&quot;]
```

### Functions

JMESPath supports a variety of built-in functions:

```php
// String functions.
$upperNames = Selector::get($data, &#039;jmespath:people[*].name | [*].to_upper(@)&#039;);
// [&quot;JOHN&quot;, &quot;JANE&quot;]

// Length/count.
$peopleCount = Selector::get($data, &#039;jmespath:length(people)&#039;);
// 2

// Min/max of values.
$data = [&#039;values&#039; =&gt; [5, 3, 8, 1, 7]];
$maxValue = Selector::get($data, &#039;jmespath:max(values)&#039;);
// 8

// Sort function.
$sortedNames = Selector::get($data, &#039;jmespath:sort(people[*].name)&#039;);
// [&quot;Jane&quot;, &quot;John&quot;]

// Map function for transformations.
$ages = Selector::get($data, &#039;jmespath:people[*].age | map(&amp;to_string(@), @)&#039;);
// [&quot;30&quot;, &quot;25&quot;]
```

### Slicing and Indexing

```php
// Array slicing.
$firstPerson = Selector::get($data, &#039;jmespath:people[:1]&#039;);
// [Complete first person object]

// Negative indices.
$lastPerson = Selector::get($data, &#039;jmespath:people[-1]&#039;);
// Complete last person object (Jane).

// Step values.
$everyOtherPerson = Selector::get($data, &#039;jmespath:people[::2]&#039;);
// Every other person in the array.
```

## Combining JMESPath with Derafu Selectors

You can mix JMESPath with regular Derafu selectors:

```php
// Use JMESPath to get a value and then concatenate with text.
$greeting = Selector::get($data, &#039;&quot;Hello, &quot;(jmespath:people[0].name)&quot;!&quot;&#039;);
// &quot;Hello, John!&quot;

// Use JMESPath as part of an OR selector.
$location = Selector::get($data,
    &#039;jmespath:current_location||jmespath:locations.&quot;Seattle&quot;.state&#039;
);
// Falls back to &quot;WA&quot; if current_location doesn&#039;t exist.

// Use JMESPath within a conditional.
$message = Selector::get($data,
    &#039;((jmespath:people | length(@)) &gt; &quot;1&quot; ? (&quot;Multiple people&quot;) : (&quot;One person&quot;))&#039;
);
// &quot;Multiple people&quot;

// Format array results.
$nameList = Selector::get($data,
    &#039;&quot;People: &quot;(jmespath:people[*].name)&#039;
);
// &quot;People: [\&quot;John\&quot;, \&quot;Jane\&quot;]&quot;
```

## Choosing Between JMESPath and JSONPath

Derafu Selector supports both JMESPath and JSONPath. Here are some considerations for choosing between them:

### JMESPath Advantages

- **Formal Specification**: JMESPath has a formal, well-defined specification.
- **Functions**: Built-in functions for transformation and manipulation.
- **Multi-select Projection**: Create new structures with the data you extract.
- **Expressions**: More powerful expression language.
- **Pipe Operator**: Chain operations together.

### JSONPath Advantages

- **Simplicity**: More straightforward syntax for basic queries.
- **Familiarity**: If you&#039;re already using JSONPath elsewhere.
- **Recursive Descent**: The `..` operator for finding elements at any level.

### Examples of the Same Query in Both

```php
$data = [
    &#039;products&#039; =&gt; [
        [&#039;name&#039; =&gt; &#039;Laptop&#039;, &#039;price&#039; =&gt; 999, &#039;category&#039; =&gt; &#039;electronics&#039;],
        [&#039;name&#039; =&gt; &#039;Phone&#039;, &#039;price&#039; =&gt; 699, &#039;category&#039; =&gt; &#039;electronics&#039;],
        [&#039;name&#039; =&gt; &#039;Book&#039;, &#039;price&#039; =&gt; 15, &#039;category&#039; =&gt; &#039;media&#039;],
    ],
];

// Get electronics products with JSONPath.
$electronics = Selector::get($data,
    &#039;$.products[?(@.category == &quot;electronics&quot;)].name&#039;
);
// [&quot;Laptop&quot;, &quot;Phone&quot;]

// Same query with JMESPath.
$electronics = Selector::get($data,
    &#039;jmespath:products[?category == `electronics`].name&#039;
);
// [&quot;Laptop&quot;, &quot;Phone&quot;]

// Filter by price and sort with JSONPath (sorting may not be available in all implementations).
// May require multiple selector calls.

// With JMESPath, this is built-in.
$sortedProducts = Selector::get($data,
    &#039;jmespath:products[?price &gt; `500`].name | sort(@)&#039;
);
// [&quot;Laptop&quot;, &quot;Phone&quot;] (sorted)
```

---

By understanding the strengths of each query language, you can choose the right tool for your specific data navigation needs.




---

## XML Project

Derafu XML

# Derafu XML




---

### Introduction

Library for XML manipulation

# Library for XML manipulation

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/xml/main)
![CI Workflow](https://github.com/derafu/xml/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/xml)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/xml)
![Total Downloads](https://poser.pugx.org/derafu/xml/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/xml/d/monthly)

A comprehensive PHP library for XML manipulation, providing robust tools for encoding, decoding, validating, and querying XML documents.

## Features

- **Conversion**: Transform between XML and PHP arrays in both directions.
- **Validation**: Validate XML documents against XSD schemas.
- **Querying**: Powerful XPath querying with parameter support.
- **Canonicalization**: Support for C14N and ISO-8859-1 encoding.
- **Encoding**: Proper handling of character encoding between UTF-8 and ISO-8859-1.
- **Special Characters**: Automatic handling of XML special characters and entities.

## Installation

```bash
composer require derafu/xml
```

## Basic Usage

### Creating XML from an Array

```php
use Derafu\Xml\Service\XmlEncoder;

$encoder = new XmlEncoder();

// Create an array to convert to XML.
$data = [
    &#039;root&#039; =&gt; [
        &#039;element1&#039; =&gt; &#039;value1&#039;,
        &#039;element2&#039; =&gt; &#039;value2&#039;,
        &#039;element3&#039; =&gt; [
            &#039;@attributes&#039; =&gt; [
                &#039;attr1&#039; =&gt; &#039;attrValue&#039;,
            ],
            &#039;@value&#039; =&gt; &#039;value3&#039;,
        ],
        &#039;repeatedElement&#039; =&gt; [&#039;value4&#039;, &#039;value5&#039;, &#039;value6&#039;],
    ],
];

// Convert the array to XML.
$xmlDocument = $encoder-&gt;encode($data);

// Save as XML string.
$xmlString = $xmlDocument-&gt;saveXml();

// Get the XML without the XML declaration.
$xmlContent = $xmlDocument-&gt;getXml();

// Get a canonicalized version of the XML.
$c14nXml = $xmlDocument-&gt;C14N();

// Get a canonicalized version with ISO-8859-1 encoding.
$isoXml = $xmlDocument-&gt;C14NWithIso88591Encoding();
```

### Converting XML to an Array

```php
use Derafu\Xml\Service\XmlDecoder;
use Derafu\Xml\XmlDocument;

$decoder = new XmlDecoder();

// Load an existing XML string.
$xmlContent = &#039;&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;root&gt;&lt;element&gt;value&lt;/element&gt;&lt;/root&gt;&#039;;
$document = new XmlDocument();
$document-&gt;loadXml($xmlContent);

// Convert to an array.
$array = $decoder-&gt;decode($document);

// Now $array contains [&#039;root&#039; =&gt; [&#039;element&#039; =&gt; &#039;value&#039;]]
```

### Working with XPath

```php
use Derafu\Xml\XPathQuery;

$xmlContent = &#039;&lt;?xml version=&quot;1.0&quot;?&gt;&lt;root&gt;&lt;item id=&quot;1&quot;&gt;First&lt;/item&gt;&lt;item id=&quot;2&quot;&gt;Second&lt;/item&gt;&lt;/root&gt;&#039;;

// Create XPath query instance.
$query = new XPathQuery($xmlContent);

// Get a specific value.
$value = $query-&gt;getValue(&#039;/root/item[@id=&quot;2&quot;]&#039;); // &quot;Second&quot;

// Get multiple values.
$values = $query-&gt;getValues(&#039;/root/item&#039;); // [&quot;First&quot;, &quot;Second&quot;]

// Get structured array.
$array = $query-&gt;get(&#039;/root&#039;);
// Result: [&#039;item&#039; =&gt; [&#039;First&#039;, &#039;Second&#039;]]
```

### XML Validation

```php
use Derafu\Xml\Exception\XmlException;
use Derafu\Xml\Service\XmlValidator;

$validator = new XmlValidator();

// Validate an XML document against a schema.
try {
    $validator-&gt;validate($xmlDocument, &#039;/path/to/schema.xsd&#039;);
    echo &quot;XML is valid!&quot;;
} catch (XmlException $e) {
    echo &quot;Validation failed: &quot; . $e-&gt;getMessage();
    $errors = $e-&gt;getErrors(); // Get detailed error information.
}
```

## Advanced Usage

### Working with Namespaces

```php
// Create XPath query with namespace support.
$namespaces = [&#039;ns&#039; =&gt; &#039;http://example.com/namespace&#039;];
$query = new XPathQuery($xmlContent, $namespaces);

// Query with namespace.
$result = $query-&gt;get(&#039;//ns:Element&#039;);
```

### Array Structure for XML Creation

When creating XML from arrays, the structure follows these conventions:

- Simple key-value pairs become element nodes.
- Use `@attributes` for node attributes.
- Use `@value` for node text content when attributes are present.
- Arrays of values create repeated nodes with the same name.

Example:
```php
$data = [
    &#039;root&#039; =&gt; [
        &#039;element&#039; =&gt; [
            &#039;@attributes&#039; =&gt; [&#039;id&#039; =&gt; &#039;123&#039;],
            &#039;@value&#039; =&gt; &#039;content&#039;,
        ],
        &#039;items&#039; =&gt; [
            &#039;item&#039; =&gt; [&#039;value1&#039;, &#039;value2&#039;, &#039;value3&#039;],
        ],
    ],
];
```

Produces:
```xml
&lt;root&gt;
  &lt;element id=&quot;123&quot;&gt;content&lt;/element&gt;
  &lt;items&gt;
    &lt;item&gt;value1&lt;/item&gt;
    &lt;item&gt;value2&lt;/item&gt;
    &lt;item&gt;value3&lt;/item&gt;
  &lt;/items&gt;
&lt;/root&gt;
```




---

### Array Structure

XML-Array Conversion Structure

# XML-Array Conversion Structure

The Derafu XML library uses a specific convention for representing the structure of XML documents as PHP arrays and vice versa. Understanding this structure is essential for effectively using the encoding and decoding features.

## Basic Structure

At its most basic level, an XML element with a value is represented as a key-value pair in the array:

XML:
```xml
&lt;element&gt;value&lt;/element&gt;
```

Array:
```php
[&#039;element&#039; =&gt; &#039;value&#039;]
```

## Nested Elements

Nested elements are represented as nested arrays:

XML:
```xml
&lt;parent&gt;
  &lt;child&gt;value&lt;/child&gt;
&lt;/parent&gt;
```

Array:
```php
[
    &#039;parent&#039; =&gt; [
        &#039;child&#039; =&gt; &#039;value&#039;
    ]
]
```

## Attributes

XML attributes are handled using the special `@attributes` key:

XML:
```xml
&lt;element id=&quot;123&quot; type=&quot;example&quot;&gt;value&lt;/element&gt;
```

Array:
```php
[
    &#039;element&#039; =&gt; [
        &#039;@attributes&#039; =&gt; [
            &#039;id&#039; =&gt; &#039;123&#039;,
            &#039;type&#039; =&gt; &#039;example&#039;
        ],
        &#039;@value&#039; =&gt; &#039;value&#039;
    ]
]
```

Note the use of `@value` to hold the element&#039;s text content when attributes are present.

## Repeated Elements

Elements with the same name are collected into arrays:

XML:
```xml
&lt;parent&gt;
  &lt;child&gt;value1&lt;/child&gt;
  &lt;child&gt;value2&lt;/child&gt;
  &lt;child&gt;value3&lt;/child&gt;
&lt;/parent&gt;
```

Array:
```php
[
    &#039;parent&#039; =&gt; [
        &#039;child&#039; =&gt; [
            &#039;value1&#039;,
            &#039;value2&#039;,
            &#039;value3&#039;
        ]
    ]
]
```

## Complex Structures

The conventions can be combined to represent complex XML structures:

XML:
```xml
&lt;root&gt;
  &lt;items&gt;
    &lt;item id=&quot;1&quot;&gt;
      &lt;name&gt;Item 1&lt;/name&gt;
      &lt;price currency=&quot;USD&quot;&gt;19.99&lt;/price&gt;
    &lt;/item&gt;
    &lt;item id=&quot;2&quot;&gt;
      &lt;name&gt;Item 2&lt;/name&gt;
      &lt;price currency=&quot;EUR&quot;&gt;29.99&lt;/price&gt;
    &lt;/item&gt;
  &lt;/items&gt;
  &lt;summary total=&quot;2&quot;&gt;Multiple items&lt;/summary&gt;
&lt;/root&gt;
```

Array:
```php
[
    &#039;root&#039; =&gt; [
        &#039;items&#039; =&gt; [
            &#039;item&#039; =&gt; [
                [
                    &#039;@attributes&#039; =&gt; [&#039;id&#039; =&gt; &#039;1&#039;],
                    &#039;name&#039; =&gt; &#039;Item 1&#039;,
                    &#039;price&#039; =&gt; [
                        &#039;@attributes&#039; =&gt; [&#039;currency&#039; =&gt; &#039;USD&#039;],
                        &#039;@value&#039; =&gt; &#039;19.99&#039;
                    ]
                ],
                [
                    &#039;@attributes&#039; =&gt; [&#039;id&#039; =&gt; &#039;2&#039;],
                    &#039;name&#039; =&gt; &#039;Item 2&#039;,
                    &#039;price&#039; =&gt; [
                        &#039;@attributes&#039; =&gt; [&#039;currency&#039; =&gt; &#039;EUR&#039;],
                        &#039;@value&#039; =&gt; &#039;29.99&#039;
                    ]
                ]
            ]
        ],
        &#039;summary&#039; =&gt; [
            &#039;@attributes&#039; =&gt; [&#039;total&#039; =&gt; &#039;2&#039;],
            &#039;@value&#039; =&gt; &#039;Multiple items&#039;
        ]
    ]
]
```

## Special Cases

### Empty Elements

Empty elements are represented as `null` or empty strings:

XML:
```xml
&lt;element&gt;&lt;/element&gt;
```

Array:
```php
[&#039;element&#039; =&gt; null]
```

or when creating XML:

```php
[&#039;element&#039; =&gt; &#039;&#039;]
```

### Skipping Elements

When creating XML, certain values cause an element to be skipped:

```php
[
    &#039;included&#039; =&gt; &#039;This will be in the XML&#039;,
    &#039;excluded&#039; =&gt; null,       // This will be skipped.
    &#039;alsoExcluded&#039; =&gt; false,  // This will be skipped.
    &#039;emptyArraySkipped&#039; =&gt; [] // This will be skipped.
]
```

## Array to XML Encoding Rules

When using `XmlEncoder` to create XML from arrays, these rules apply:

1. Simple key-value pairs become elements with text content.
2. Nested arrays become nested elements.
3. The special key `@attributes` defines attributes for the parent element.
4. The special key `@value` defines the text content when attributes are present.
5. Arrays of values create multiple elements with the same name.
6. Null, false, and empty arrays are skipped (not included in the XML).
7. Empty strings (&#039;&#039;) create empty elements.

## XML to Array Decoding Rules

When using `XmlDecoder` to convert XML to arrays, these rules apply:

1. Elements become keys in the array.
2. Text content becomes the value of the key.
3. Attributes are collected under the `@attributes` key.
4. When attributes are present, text content is stored under the `@value` key.
5. Multiple elements with the same name are collected into arrays.
6. Empty elements become null values.
7. Complex nested structures are preserved in the array hierarchy.

## Usage Examples

### Creating Complex XML

```php
use Derafu\Xml\Service\XmlEncoder;

$data = [
    &#039;invoice&#039; =&gt; [
        &#039;@attributes&#039; =&gt; [
            &#039;id&#039; =&gt; &#039;INV-2025-001&#039;,
            &#039;date&#039; =&gt; &#039;2025-03-05&#039;
        ],
        &#039;customer&#039; =&gt; [
            &#039;name&#039; =&gt; &#039;Acme Inc.&#039;,
            &#039;address&#039; =&gt; [
                &#039;street&#039; =&gt; &#039;123 Main St&#039;,
                &#039;city&#039; =&gt; &#039;Anytown&#039;,
                &#039;zipcode&#039; =&gt; &#039;12345&#039;
            ]
        ],
        &#039;items&#039; =&gt; [
            &#039;item&#039; =&gt; [
                [
                    &#039;@attributes&#039; =&gt; [&#039;sku&#039; =&gt; &#039;PROD1&#039;],
                    &#039;description&#039; =&gt; &#039;Product One&#039;,
                    &#039;quantity&#039; =&gt; &#039;2&#039;,
                    &#039;price&#039; =&gt; &#039;10.99&#039;
                ],
                [
                    &#039;@attributes&#039; =&gt; [&#039;sku&#039; =&gt; &#039;PROD2&#039;],
                    &#039;description&#039; =&gt; &#039;Product Two&#039;,
                    &#039;quantity&#039; =&gt; &#039;1&#039;,
                    &#039;price&#039; =&gt; &#039;24.99&#039;
                ]
            ]
        ],
        &#039;total&#039; =&gt; &#039;46.97&#039;
    ]
];

$encoder = new XmlEncoder();

$xmlDocument = $encoder-&gt;encode($data);
echo $xmlDocument-&gt;saveXml();
```

### Processing XML into Arrays

```php
use Derafu\Xml\Service\XmlDecoder;

$xmlString = &#039;
&lt;catalog&gt;
    &lt;book id=&quot;bk101&quot;&gt;
        &lt;author&gt;Gambardella, Matthew&lt;/author&gt;
        &lt;title&gt;XML Developer\&#039;s Guide&lt;/title&gt;
        &lt;genre&gt;Computer&lt;/genre&gt;
        &lt;price&gt;44.95&lt;/price&gt;
        &lt;publish_date&gt;2025-01-15&lt;/publish_date&gt;
    &lt;/book&gt;
    &lt;book id=&quot;bk102&quot;&gt;
        &lt;author&gt;Ralls, Kim&lt;/author&gt;
        &lt;title&gt;Midnight Rain&lt;/title&gt;
        &lt;genre&gt;Fantasy&lt;/genre&gt;
        &lt;price&gt;5.95&lt;/price&gt;
        &lt;publish_date&gt;2025-02-20&lt;/publish_date&gt;
    &lt;/book&gt;
&lt;/catalog&gt;&#039;;

$document = new \Derafu\Xml\XmlDocument();
$document-&gt;loadXml($xmlString);

$decoder = new XmlDecoder();

$array = $decoder-&gt;decode($document);

// Access data directly.
$firstBookTitle = $array[&#039;catalog&#039;][&#039;book&#039;][0][&#039;title&#039;]; // &quot;XML Developer&#039;s Guide&quot;
$secondBookPrice = $array[&#039;catalog&#039;][&#039;book&#039;][1][&#039;price&#039;]; // &quot;5.95&quot;
```

## Tips for Working with the Array Structure

1. **Plan Your Structure**: When creating XML, plan your array structure carefully to match the desired XML output.
2. **Handle Repeated Elements**: Always use numeric arrays for repeated elements with the same name.
3. **Use `@attributes` and `@value`**: Remember to use these special keys when working with elements that have both attributes and text content.
4. **Validate Your Output**: Always validate the generated XML against your schema if one exists.
5. **Check for Empty Values**: When processing arrays from XML, check for null values to handle empty elements properly.




---

### XML Document

XmlDocument Class

# XmlDocument Class

The `XmlDocument` class extends PHP&#039;s native `DOMDocument` with additional functionality for XML manipulation, transformation, and querying. This class is the core component for working with XML documents in the Derafu XML library.

## Key Features

- Extended XML loading with character encoding handling.
- Simplified access to XML metadata (namespace, schema).
- Canonicalization support with encoding options.
- XPath query integration.
- Array conversion capabilities.
- Signature node handling for XML digital signatures.

## Basic Usage

### Creating a New Document

```php
use Derafu\Xml\XmlDocument;

// Create with default version (1.0) and encoding (ISO-8859-1).
$document = new XmlDocument();

// Or specify version and encoding.
$document = new XmlDocument(&#039;1.0&#039;, &#039;UTF-8&#039;);
```

### Loading XML Content

```php
// Load from a string.
$xmlString = &#039;&lt;root&gt;&lt;element&gt;value&lt;/element&gt;&lt;/root&gt;&#039;;
$document-&gt;loadXml($xmlString);

// The method handles encoding conversion automatically.
$utf8Xml = &#039;&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;root&gt;&lt;element&gt;Árbol&lt;/element&gt;&lt;/root&gt;&#039;;
$document-&gt;loadXml($utf8Xml); // Will convert to ISO-8859-1 if needed.
```

### Accessing Document Information

```php
// Get the root element name.
$rootName = $document-&gt;getName(); // e.g., &quot;root&quot;

// Get the XML namespace (if any).
$namespace = $document-&gt;getNamespace(); // e.g., &quot;http://example.com&quot;

// Get the schema location (if any).
$schema = $document-&gt;getSchema(); // e.g., &quot;schema.xsd&quot;
```

### Saving and Serializing

```php
// Get the complete XML document with declaration.
$xmlString = $document-&gt;saveXml();

// Get only the XML content without the declaration.
$xmlContent = $document-&gt;getXml();

// Get the canonicalized (C14N) version.
$canonXml = $document-&gt;C14N();

// Get canonicalized version with ISO-8859-1 encoding.
$isoCanonXml = $document-&gt;C14NWithIso88591Encoding();

// Get flattened canonicalized version (whitespace removed between tags).
$flatXml = $document-&gt;C14NWithIso88591EncodingFlattened();
```

## XPath Querying

The XmlDocument class has integrated XPath querying capabilities:

```php
// Execute an XPath query and get result as string or array.
$result = $document-&gt;query(&#039;/root/element&#039;);

// Get DOMNodeList from an XPath query.
$nodes = $document-&gt;getNodes(&#039;/root/element&#039;);

// Use parameters in XPath queries.
$params = [&#039;id&#039; =&gt; &#039;123&#039;];
$result = $document-&gt;query(&#039;/root/element[@id=:id]&#039;, $params);
```

## Array Conversion

```php
// Convert the entire document to an array.
$array = $document-&gt;toArray();

// Access a specific part using dot notation.
$value = $document-&gt;get(&#039;root.element&#039;);

// With a default value if not found.
$value = $document-&gt;get(&#039;root.missing&#039;, &#039;default value&#039;);
```

## Working with XML Digital Signatures

If the document contains an XML digital signature, you can extract it:

```php
// Get the signature node as XML.
$signatureXml = $document-&gt;getSignatureNodeXml();

// Returns null if no signature is present.
if ($signatureXml !== null) {
    // Process signature...
}
```

## Handling Special Characters and Entities

The `XmlDocument` class works with the `XmlHelper` to properly handle special characters and entities:

```php
// When saving XML, entities are handled correctly.
$document-&gt;loadXml(&#039;&lt;root&gt;&lt;element&gt;Text with &amp; &lt; &gt; &quot; \&#039;&lt;/element&gt;&lt;/root&gt;&#039;);
$xml = $document-&gt;saveXml();
// Produces: &lt;root&gt;&lt;element&gt;Text with &amp;amp; &amp;lt; &amp;gt; &amp;quot; &amp;apos;&lt;/element&gt;&lt;/root&gt;
```

## Advanced Canonicalization

### Working with Specific Nodes

You can apply canonicalization to specific parts of the document:

```php
// Canonicalize only a portion of the document.
$canonXml = $document-&gt;C14NWithIso88591Encoding(&#039;/root/section&#039;);
```

### Differences Between Canonicalization Methods

The class offers several canonicalization methods:

1. **C14N()**: Standard canonicalization, always outputs UTF-8.
2. **C14NWithIso88591Encoding()**: Canonicalization with conversion to ISO-8859-1.
3. **C14NWithIso88591EncodingFlattened()**: ISO-8859-1 canonicalization with whitespace removal.

These methods are particularly useful when working with digital signatures or when XML needs to be processed by systems with specific encoding requirements.

## Error Handling

The `loadXml()` method throws descriptive exceptions when it encounters errors:

```php
use Derafu\Xml\Exception\XmlException;

try {
    $document-&gt;loadXml($potentiallyInvalidXml);
} catch (XmlException $e) {
    echo &quot;Error loading XML: &quot; . $e-&gt;getMessage();
    $errors = $e-&gt;getErrors(); // Get detailed error information if available.
}
```

## Performance Considerations

- When working with large XML documents, prefer using XPath queries to extract specific sections rather than converting the entire document to an array.
- The canonicalization methods perform more processing, so use them only when needed.
- The `getXml()` method is more efficient than `saveXml()` when you only need the XML content without the declaration.




---

### XPath Query

XPathQuery Class

# XPathQuery Class

The `XPathQuery` class is a powerful tool for extracting data from XML documents using XPath expressions. It provides a convenient interface to query XML with support for namespaces, parameterized queries, and complex data structures.

## Basic Usage

```php
use Derafu\Xml\XPathQuery;

// Initialize with XML string or DOMDocument.
$query = new XPathQuery($xmlString);

// Get a single value.
$value = $query-&gt;getValue(&#039;/root/element&#039;);

// Get multiple values.
$values = $query-&gt;getValues(&#039;/root/items/item&#039;);

// Get DOM nodes.
$nodes = $query-&gt;getNodes(&#039;/root/items/item&#039;);

// Get structured data.
$data = $query-&gt;get(&#039;/root&#039;);
```

## Working with Namespaces

```php
// Initialize with namespace support.
$namespaces = [
    &#039;ns&#039; =&gt; &#039;http://example.com/ns&#039;,
    &#039;xs&#039; =&gt; &#039;http://www.w3.org/2001/XMLSchema&#039;
];

$query = new XPathQuery($xmlString, $namespaces);

// Use namespaces in queries.
$result = $query-&gt;get(&#039;//ns:Element/xs:Type&#039;);
```

## Parameterized Queries

The `XPathQuery` class supports parameterized queries, making it safer and easier to build dynamic XPath expressions:

```php
// Query with parameters.
$params = [
    &#039;id&#039; =&gt; &#039;123&#039;,
    &#039;type&#039; =&gt; &#039;product&#039;
];

$result = $query-&gt;get(&#039;//item[@id=:id and @type=:type]&#039;, $params);
```

This automatically handles escaping of values, including proper handling of values containing quotes.

## Context Nodes

You can limit the scope of your query by providing a context node:

```php
// Get a context node.
$contextNode = $query-&gt;getNodes(&#039;//section&#039;)-&gt;item(0);

// Query within that context.
$result = $query-&gt;get(&#039;./item&#039;, [], $contextNode);
```

## Return Value Handling

The `get()` method is intelligent about what it returns:

1. `null` if no matching nodes are found.
2. A string if a single node with no children is found.
3. An array if multiple nodes are found.
4. A structured array if nodes with child elements are found.

### Example of Structured Results

For XML like:
```xml
&lt;root&gt;
  &lt;person&gt;
    &lt;name&gt;John&lt;/name&gt;
    &lt;age&gt;30&lt;/age&gt;
  &lt;/person&gt;
  &lt;person&gt;
    &lt;name&gt;Jane&lt;/name&gt;
    &lt;age&gt;25&lt;/age&gt;
  &lt;/person&gt;
&lt;/root&gt;
```

The query `$query-&gt;get(&#039;/root/person&#039;)` would return:
```php
[
    [
        &#039;name&#039; =&gt; &#039;John&#039;,
        &#039;age&#039; =&gt; &#039;30&#039;
    ],
    [
        &#039;name&#039; =&gt; &#039;Jane&#039;,
        &#039;age&#039; =&gt; &#039;25&#039;
    ]
]
```

## Special Features

### Using Without Namespaces

If you need to query XML with namespaces but don&#039;t want to specify them all, you can use the class without registering namespaces:

```php
$query = new XPathQuery($xmlString); // No namespaces registered.

// This will match any element named &#039;Element&#039; regardless of its namespace.
$result = $query-&gt;get(&#039;//Element&#039;);
```

### Value Quoting

The class handles the quoting of values in XPath expressions automatically, even when values contain both single and double quotes:

```php
$params = [&#039;complex&#039; =&gt; &#039;Value with &quot;double&quot; and \&#039;single\&#039; quotes&#039;];
$result = $query-&gt;get(&#039;//element[text()=:complex]&#039;, $params);
```

### Error Handling

The class provides clear error messages when there are issues with the XML document or XPath expressions:

```php
try {
    $result = $query-&gt;get(&#039;//invalid[xpath&#039;);
} catch (InvalidArgumentException $e) {
    // Handle the error.
}
```




---

### XML Service

XmlService Class

# XmlService Class

The `XmlService` class is the central component of the Derafu XML library, providing a unified interface for encoding, decoding, and validating XML documents. It follows a service-oriented architecture by delegating the actual work to specialized components.

## Service Structure

The `XmlService` relies on three key components:

1. **XmlEncoder**: Converts PHP arrays to XML documents.
2. **XmlDecoder**: Converts XML documents to PHP arrays.
3. **XmlValidator**: Validates XML documents against XSD schemas.

## Basic Usage

```php
use Derafu\Xml\Service\XmlEncoder;
use Derafu\Xml\Service\XmlDecoder;
use Derafu\Xml\Service\XmlValidator;
use Derafu\Xml\Service\XmlService;

// Initialize the components.
$encoder = new XmlEncoder();
$decoder = new XmlDecoder();
$validator = new XmlValidator();

// Create the service.
$xmlService = new XmlService($encoder, $decoder, $validator);
```

## Encoding (Array to XML)

The `encode()` method converts a PHP array to an XML document:

```php
$data = [
    &#039;invoice&#039; =&gt; [
        &#039;@attributes&#039; =&gt; [
            &#039;id&#039; =&gt; &#039;12345&#039;,
            &#039;date&#039; =&gt; &#039;2025-03-05&#039;
        ],
        &#039;client&#039; =&gt; [
            &#039;name&#039; =&gt; &#039;Acme Corporation&#039;,
            &#039;tax_id&#039; =&gt; &#039;123456789&#039;
        ],
        &#039;items&#039; =&gt; [
            &#039;item&#039; =&gt; [
                [
                    &#039;@attributes&#039; =&gt; [&#039;id&#039; =&gt; &#039;1&#039;],
                    &#039;description&#039; =&gt; &#039;Product A&#039;,
                    &#039;quantity&#039; =&gt; &#039;2&#039;,
                    &#039;price&#039; =&gt; &#039;19.99&#039;
                ],
                [
                    &#039;@attributes&#039; =&gt; [&#039;id&#039; =&gt; &#039;2&#039;],
                    &#039;description&#039; =&gt; &#039;Product B&#039;,
                    &#039;quantity&#039; =&gt; &#039;1&#039;,
                    &#039;price&#039; =&gt; &#039;29.99&#039;
                ]
            ]
        ],
        &#039;total&#039; =&gt; &#039;69.97&#039;
    ]
];

$xmlDocument = $xmlService-&gt;encode($data);
```

This produces:

```xml
&lt;?xml version=&quot;1.0&quot; encoding=&quot;ISO-8859-1&quot;?&gt;
&lt;invoice id=&quot;12345&quot; date=&quot;2025-03-05&quot;&gt;
  &lt;client&gt;
    &lt;name&gt;Acme Corporation&lt;/name&gt;
    &lt;tax_id&gt;123456789&lt;/tax_id&gt;
  &lt;/client&gt;
  &lt;items&gt;
    &lt;item id=&quot;1&quot;&gt;
      &lt;description&gt;Product A&lt;/description&gt;
      &lt;quantity&gt;2&lt;/quantity&gt;
      &lt;price&gt;19.99&lt;/price&gt;
    &lt;/item&gt;
    &lt;item id=&quot;2&quot;&gt;
      &lt;description&gt;Product B&lt;/description&gt;
      &lt;quantity&gt;1&lt;/quantity&gt;
      &lt;price&gt;29.99&lt;/price&gt;
    &lt;/item&gt;
  &lt;/items&gt;
  &lt;total&gt;69.97&lt;/total&gt;
&lt;/invoice&gt;
```

### Using Namespaces

You can specify XML namespaces during encoding:

```php
$namespace = [&#039;http://example.com/invoice&#039;, &#039;inv&#039;];
$xmlDocument = $xmlService-&gt;encode($data, $namespace);
```

This generates:

```xml
&lt;?xml version=&quot;1.0&quot; encoding=&quot;ISO-8859-1&quot;?&gt;
&lt;inv:invoice id=&quot;12345&quot; date=&quot;2025-03-05&quot; xmlns:inv=&quot;http://example.com/invoice&quot;&gt;
  &lt;!-- Content with namespace prefixes --&gt;
&lt;/inv:invoice&gt;
```

## Decoding (XML to Array)

The `decode()` method converts an XML document to a PHP array:

```php
// Assuming $xmlDocument is a Derafu\Xml\XmlDocument instance.
$array = $xmlService-&gt;decode($xmlDocument);

// Or starting from a DOMElement.
$element = $xmlDocument-&gt;getDocumentElement();
$array = $xmlService-&gt;decode($element);
```

### Handling Repeated Elements

By default, elements with the same name are collected into arrays:

```xml
&lt;root&gt;
  &lt;item&gt;Value 1&lt;/item&gt;
  &lt;item&gt;Value 2&lt;/item&gt;
  &lt;item&gt;Value 3&lt;/item&gt;
&lt;/root&gt;
```

Becomes:

```php
[
    &#039;root&#039; =&gt; [
        &#039;item&#039; =&gt; [
            &#039;Value 1&#039;,
            &#039;Value 2&#039;,
            &#039;Value 3&#039;
        ]
    ]
]
```

You can control this behavior with the `$twinsAsArray` parameter:

```php
$array = $xmlService-&gt;decode($xmlDocument, null, twinsAsArray: true);
```

## Validation

The `validate()` method checks an XML document against an XSD schema:

```php
use Derafu\Xml\Exception\XmlException;

try {
    $xmlService-&gt;validate($xmlDocument, &#039;/path/to/schema.xsd&#039;);
    echo &quot;XML is valid!&quot;;
} catch (XmlException $e) {
    echo &quot;Validation failed: &quot; . $e-&gt;getMessage();

    // Get detailed error information.
    $errors = $e-&gt;getErrors();
    foreach ($errors as $error) {
        echo &quot;- $error\n&quot;;
    }
}
```

### Schema Auto-Detection

If the XML document includes a `schemaLocation` attribute, you can omit the schema path:

```php
// XML with xsi:schemaLocation=&quot;http://example.com/ns schema.xsd&quot;
try {
    $xmlService-&gt;validate($xmlDocument); // Will use schema.xsd
} catch (XmlException $e) {
    // Handle validation error.
}
```

### Error Translations

The validator can translate technical libxml error messages into more user-friendly messages:

```php
$translations = [
    &#039;element1&#039; =&gt; &#039;Customer Info&#039;,
    &#039;element2&#039; =&gt; &#039;Product Details&#039;
];

try {
    $xmlService-&gt;validate($xmlDocument, &#039;/path/to/schema.xsd&#039;, $translations);
} catch (XmlException $e) {
    // Error messages will use the translated element names.
}
```

## Integration Example

This complete example shows how to use the XML service for a typical workflow:

```php
// Initialize the service.
$encoder = new XmlEncoder();
$decoder = new XmlDecoder();
$validator = new XmlValidator();
$xmlService = new XmlService($encoder, $decoder, $validator);

// Create XML from array.
$data = [&#039;root&#039; =&gt; [&#039;element&#039; =&gt; &#039;value&#039;]];
$xmlDocument = $xmlService-&gt;encode($data);

// Save to a file.
file_put_contents(&#039;document.xml&#039;, $xmlDocument-&gt;saveXml());

// Load from a file.
$loadedXml = new \Derafu\Xml\XmlDocument();
$loadedXml-&gt;loadXml(file_get_contents(&#039;document.xml&#039;));

// Validate.
try {
    $xmlService-&gt;validate($loadedXml, &#039;schema.xsd&#039;);

    // Convert back to array.
    $array = $xmlService-&gt;decode($loadedXml);

    // Process the data.
    // ...

} catch (\Derafu\Xml\Exception\XmlException $e) {
    // Handle validation errors.
}
```




---

## Certificate Project

Derafu Certificate

# Derafu Certificate




---

### Introduction

Library for digital certificates

# Library for digital certificates

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/certificate/main)
![CI Workflow](https://github.com/derafu/certificate/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/certificate)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/certificate)
![Total Downloads](https://poser.pugx.org/derafu/certificate/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/certificate/d/monthly)

A comprehensive PHP library for working with digital certificates, providing tools for loading, validating and generating certificates.

## Features

- **Certificate Loading**: Load certificates from files, data, arrays or keys.
- **Certificate Validation**: Validate certificates against specific requirements.
- **Certificate Generation**: Create self-signed certificates for testing.
- **Certificate Information**: Extract key information from certificates (ID, name, email, etc.).
- **Key Management**: Work with public and private keys, modulus, and exponent.

## Installation

```bash
composer require derafu/certificate
```

## Basic Usage

### Loading a Certificate

```php
use Derafu\Certificate\Service\CertificateLoader;

// Create a loader.
$loader = new CertificateLoader();

// Load from a file.
$certificate = $loader-&gt;loadFromFile(&#039;/path/to/certificate.p12&#039;, &#039;password&#039;);

// Load from data.
$certificate = $loader-&gt;loadFromData($certificateData, &#039;password&#039;);

// Load from array.
$certificate = $loader-&gt;loadFromArray([
    &#039;cert&#039; =&gt; $publicKey,
    &#039;pkey&#039; =&gt; $privateKey
]);

// Load from keys.
$certificate = $loader-&gt;loadFromKeys($publicKey, $privateKey);
```

### Accessing Certificate Information

```php
// Get basic certificate information.
$id = $certificate-&gt;getId(); // e.g., &quot;12345678-9&quot;
$name = $certificate-&gt;getName(); // e.g., &quot;John Doe&quot;
$email = $certificate-&gt;getEmail(); // e.g., &quot;john.doe@example.com&quot;

// Check certificate validity.
$isActive = $certificate-&gt;isActive(); // true if certificate is valid.
$expirationDays = $certificate-&gt;getExpirationDays(); // days until expiration.

// Get validity dates.
$validFrom = $certificate-&gt;getFrom(); // e.g., &quot;2025-01-01T00:00:00&quot;
$validTo = $certificate-&gt;getTo(); // e.g., &quot;2026-01-01T00:00:00&quot;

// Get certificate issuer.
$issuer = $certificate-&gt;getIssuer(); // e.g., &quot;Example CA&quot;

// Get key components.
$modulus = $certificate-&gt;getModulus();
$exponent = $certificate-&gt;getExponent();

// Get raw keys.
$publicKey = $certificate-&gt;getPublicKey(); // with headers.
$privateKey = $certificate-&gt;getPrivateKey(); // with headers.
$cleanPublicKey = $certificate-&gt;getPublicKey(true); // without headers.
$cleanPrivateKey = $certificate-&gt;getPrivateKey(true); // without headers.
```

### Validating a Certificate

```php
use Derafu\Certificate\Exception\CertificateException;
use Derafu\Certificate\Service\CertificateValidator;

$validator = new CertificateValidator();

try {
    $validator-&gt;validate($certificate);
    echo &quot;Certificate is valid&quot;;
} catch (CertificateException $e) {
    echo &quot;Certificate validation failed: &quot; . $e-&gt;getMessage();
}
```

### Creating a Fake Certificate for Testing

```php
use Derafu\Certificate\Service\CertificateLoader;
use Derafu\Certificate\Service\CertificateFaker;

$loader = new CertificateLoader();
$faker = new CertificateFaker($loader);

// Create a fake certificate with default values.
$certificate = $faker-&gt;createFake();

// Create a fake certificate with custom values.
$certificate = $faker-&gt;createFake(
    id: &#039;12345678-9&#039;,
    name: &#039;John Doe&#039;,
    email: &#039;john.doe@example.com&#039;,
    password: &#039;secure_password&#039;
);

// Export to PKCS#12 format.
$pkcs12Data = $certificate-&gt;getPkcs12(&#039;password&#039;);
file_put_contents(&#039;certificate.p12&#039;, $pkcs12Data);
```

### Using the Service

The `CertificateService` provides a unified interface to all library functionality:

```php
use Derafu\Certificate\Service\CertificateLoader;
use Derafu\Certificate\Service\CertificateFaker;
use Derafu\Certificate\Service\CertificateValidator;
use Derafu\Certificate\Service\CertificateService;

// Create the service with its dependencies.
$loader = new CertificateLoader();
$faker = new CertificateFaker($loader);
$validator = new CertificateValidator();
$service = new CertificateService($faker, $loader, $validator);

// Use the service for certificate operations.
$certificate = $service-&gt;loadFromFile(&#039;/path/to/certificate.p12&#039;, &#039;password&#039;);
$service-&gt;validate($certificate);

// Create a fake certificate for testing.
$fakeCertificate = $service-&gt;createFake(
    &#039;12345678-9&#039;,
    &#039;John Doe&#039;,
    &#039;john.doe@example.com&#039;
);
```

## Advanced Usage

### Creating a Self-Signed Certificate

For more control over certificate generation:

```php
use Derafu\Certificate\SelfSignedCertificate;
use Derafu\Certificate\Service\CertificateLoader;

// Create a self-signed certificate with custom values.
$selfSigned = new SelfSignedCertificate();
$selfSigned-&gt;setSubject(
    C: &#039;US&#039;,
    ST: &#039;California&#039;,
    L: &#039;San Francisco&#039;,
    O: &#039;Example Organization&#039;,
    OU: &#039;IT Department&#039;,
    CN: &#039;John Doe&#039;,
    emailAddress: &#039;john.doe@example.com&#039;,
    serialNumber: &#039;12345678-9&#039;
);

$selfSigned-&gt;setIssuer(
    CN: &#039;Example CA&#039;
);

$selfSigned-&gt;setValidity(365); // Valid for 1 year.
$selfSigned-&gt;setPassword(&#039;secure_password&#039;);

// Get the certificate array.
$certArray = $selfSigned-&gt;toArray();

// Load as a Certificate object.
$loader = new CertificateLoader();
$certificate = $loader-&gt;loadFromArray($certArray);
```

### Working with Asymmetric Keys

```php
use Derafu\Certificate\AsymmetricKeyHelper;

// Normalize a public key (add headers if missing).
$normalizedPublicKey = AsymmetricKeyHelper::normalizePublicKey($rawPublicKey);

// Normalize a private key (add headers if missing).
$normalizedPrivateKey = AsymmetricKeyHelper::normalizePrivateKey($rawPrivateKey);

// Generate a public key from modulus and exponent.
// Requirements: composer require phpseclib/phpseclib
$publicKey = AsymmetricKeyHelper::generatePublicKeyFromModulusExponent(
    $modulus,
    $exponent
);
```




---

### Certificate Class

Certificate Class

# Certificate Class

The `Certificate` class is the core component of the Derafu Certificate library, representing a digital certificate with its public and private keys and providing methods to access certificate details and properties.

## Overview

The `Certificate` class implements the `CertificateInterface` and provides functionality to:

- Access public and private keys.
- Extract certificate metadata (ID, name, email, issuer, etc.).
- Check certificate validity.
- Get certificate validity dates.
- Retrieve cryptographic components (modulus, exponent).
- Generate PKCS#12 data.

## Usage

### Creating a Certificate

To create a `Certificate` instance, you need a public key (certificate) and a private key:

```php
use Derafu\Certificate\Certificate;

$certificate = new Certificate($publicKey, $privateKey);
```

The constructor automatically normalizes the keys using `AsymmetricKeyHelper`.

### Getting Keys

```php
// Get both keys as an array.
$keys = $certificate-&gt;getKeys();
// Result: [&#039;cert&#039; =&gt; &#039;...public key...&#039;, &#039;pkey&#039; =&gt; &#039;...private key...&#039;]

// Get both keys without headers/footers.
$cleanKeys = $certificate-&gt;getKeys(clean: true);

// Get just the public key.
$publicKey = $certificate-&gt;getPublicKey();
// Alternatively.
$publicKey = $certificate-&gt;getCertificate();

// Get just the private key.
$privateKey = $certificate-&gt;getPrivateKey();

// Get clean keys (without headers/footers).
$cleanPublicKey = $certificate-&gt;getPublicKey(clean: true);
$cleanPrivateKey = $certificate-&gt;getPrivateKey(clean: true);
```

### Certificate Metadata

```php
// Get certificate ID (usually a document/tax number).
$id = $certificate-&gt;getId(); // e.g., &quot;12345678-9&quot;
$id = $certificate-&gt;getId(forceUpper:false); // with original case, e.g., &quot;12345678-k&quot;

// Get certificate owner&#039;s name.
$name = $certificate-&gt;getName();

// Get certificate owner&#039;s email.
$email = $certificate-&gt;getEmail();

// Get certificate issuer.
$issuer = $certificate-&gt;getIssuer();
```

### Certificate Validity

```php
// Check if certificate is valid (not expired).
$isActive = $certificate-&gt;isActive();

// Check if certificate is valid at a specific date.
$isActiveOn = $certificate-&gt;isActive(&#039;2025-06-01&#039;);

// Get certificate validity period.
$validFrom = $certificate-&gt;getFrom(); // e.g., &quot;2025-01-01T00:00:00&quot;
$validTo = $certificate-&gt;getTo(); // e.g., &quot;2026-01-01T00:00:00&quot;

// Get total validity period in days.
$totalDays = $certificate-&gt;getTotalDays();

// Get days remaining until expiration.
$daysRemaining = $certificate-&gt;getExpirationDays();

// Get days remaining from a specific date.
$daysRemainingFrom = $certificate-&gt;getExpirationDays(&#039;2025-06-01&#039;);
```

### Cryptographic Components

```php
// Get certificate raw data.
$data = $certificate-&gt;getData();

// Get private key details.
$privateKeyDetails = $certificate-&gt;getPrivateKeyDetails();

// Get modulus and exponent.
$modulus = $certificate-&gt;getModulus();
$exponent = $certificate-&gt;getExponent();

// With custom word wrapping.
$modulus = $certificate-&gt;getModulus(wordwrap: 75);
$exponent = $certificate-&gt;getExponent(wordwrap: 75);
```

### Exporting a Certificate

```php
// Export certificate as PKCS#12 data.
$pkcs12 = $certificate-&gt;getPkcs12(&#039;password&#039;);

// Save to a file.
file_put_contents(&#039;certificate.p12&#039;, $pkcs12);
```

## Error Handling

The `Certificate` class throws `CertificateException` when it encounters problems such as:

- Unable to extract the certificate ID.
- Unable to find the certificate name.
- Unable to find the certificate email.
- Unable to access the modulus or exponent.

Example:

```php
use Derafu\Certificate\Exception\CertificateException;

try {
    $id = $certificate-&gt;getId();
    $name = $certificate-&gt;getName();
    $email = $certificate-&gt;getEmail();
} catch (CertificateException $e) {
    echo &quot;Certificate error: &quot; . $e-&gt;getMessage();
}
```

## Implementation Details

### ID Extraction

The `getId()` method attempts to extract the certificate ID through multiple methods:

1. First, it checks the `serialNumber` field in the subject.
2. If not found, it looks for the ID in the Subject Alternative Name (SAN) extension.
3. If still not found, it throws a `CertificateException`.

### Certificate Validation

The `isActive()` method checks if the certificate is valid at the current date (or a specified date) by comparing it against the certificate&#039;s validity period.

### Cryptographic Components

The `getModulus()` and `getExponent()` methods extract these values from the private key details and return them as base64-encoded strings, which can be used for various cryptographic operations.




---

### Self-Signed Certificate

SelfSignedCertificate Class

# SelfSignedCertificate Class

The `SelfSignedCertificate` class provides functionality to generate self-signed certificates for testing and development purposes. It allows customization of certificate properties such as subject, issuer, validity period, and password protection.

## Overview

This class creates a digital certificate that includes:

1. A Certificate Authority (CA) certificate.
2. A user certificate signed by the CA.
3. Both packaged together in PKCS#12 format.

The generated certificate is suitable for testing electronic signature functionality without requiring a certificate from a commercial Certificate Authority.

## Basic Usage

```php
use Derafu\Certificate\SelfSignedCertificate;
use Derafu\Certificate\Service\CertificateLoader;

// Create with default settings.
$selfSigned = new SelfSignedCertificate();
$certificateArray = $selfSigned-&gt;toArray();

// Load as a Certificate object.
$loader = new CertificateLoader();
$certificate = $loader-&gt;loadFromArray($certificateArray);
```

## Configuration Methods

### Setting the Subject

The subject represents the owner of the certificate:

```php
$selfSigned-&gt;setSubject(
    C: &#039;US&#039;,                              // Country code (2 letters).
    ST: &#039;California&#039;,                     // State/Province.
    L: &#039;San Francisco&#039;,                   // Locality/City.
    O: &#039;Example Corporation&#039;,             // Organization.
    OU: &#039;IT Department&#039;,                  // Organizational Unit.
    CN: &#039;John Doe&#039;,                       // Common Name (full name).
    emailAddress: &#039;john.doe@example.com&#039;, // Email address.
    serialNumber: &#039;12345678-9&#039;,           // ID/Serial number (tax ID, etc.).
    title: &#039;System Administrator&#039;         // Title.
);
```

Only `CN`, `emailAddress`, and `serialNumber` are strictly required. The method will throw a `CertificateException` if any of these are empty.

### Setting the Issuer

The issuer represents the Certificate Authority that issues the certificate:

```php
$selfSigned-&gt;setIssuer(
    C: &#039;US&#039;,                           // Country code
    ST: &#039;Washington&#039;,                  // State/Province
    L: &#039;Seattle&#039;,                      // Locality/City
    O: &#039;Example CA&#039;,                   // Organization
    OU: &#039;Certificate Authority&#039;,       // Organizational Unit
    CN: &#039;Example Root CA&#039;,             // CA Name
    emailAddress: &#039;ca@example.com&#039;,    // CA Email
    serialNumber: &#039;87654321-0&#039;         // CA ID
);
```

### Setting the Validity Period

```php
// Set validity to 730 days (2 years).
$selfSigned-&gt;setValidity(730);
```

The default validity period is 365 days (1 year).

### Setting the Password

```php
// Set password for the private key.
$selfSigned-&gt;setPassword(&#039;secure_password&#039;);
```

The default password is `&#039;i_love_derafu&#039;`.

## Generating the Certificate

### Getting the Certificate Array

```php
// Get the certificate as an array with &#039;cert&#039; and &#039;pkey&#039; elements.
$certificateArray = $selfSigned-&gt;toArray();

// The array contains:
// [
//     &#039;cert&#039; =&gt; &#039;...public key certificate...&#039;,
//     &#039;pkey&#039; =&gt; &#039;...private key...&#039;
// ]
```

### Getting the Raw PKCS#12 Data

```php
// Get the toPkcs12() method result (PKCS#12 binary data).
$pkcs12Data = $selfSigned-&gt;toPkcs12();

// Save to a file.
file_put_contents(&#039;certificate.p12&#039;, $pkcs12Data);
```

## Certificate Structure

The generated certificate consists of:

1. An issuer (CA) certificate that is self-signed.
2. A subject certificate that is signed by the issuer.
3. Both certificates&#039; private keys.

The PKCS#12 container includes both certificates and is password-protected using the specified password.

## Default Values

If you create a `SelfSignedCertificate` without any customization, it will use the following default values:

### Default Subject

- Country: `CL` (Chile).
- State: `Colchagua`.
- Locality: `Santa Cruz`.
- Organization: `Intergalactic Robots Organization`.
- Organizational Unit: `Technology`.
- Common Name: `Daniel Bot`.
- Email: `daniel.bot@example.com`.
- Serial Number: `11222333-9`.
- Title: `Bot`.

### Default Issuer

- Country: `CL` (Chile).
- State: `Colchagua`.
- Locality: `Santa Cruz`.
- Organization: `Derafu`.
- Organizational Unit: `Technology`.
- Common Name: `Derafu Test Certificate Authority`.
- Email: `fakes-certificates@derafu.org`.
- Serial Number: `76192083-9`.

### Other Defaults

- Validity: 365 days.
- Password: `i_love_derafu`.

## Implementation Details

The `SelfSignedCertificate` class uses the PHP OpenSSL extension to:

1. Generate a key pair for the issuer.
2. Create a Certificate Signing Request (CSR) for the issuer.
3. Self-sign the issuer&#039;s CSR to create the issuer certificate.
4. Generate a key pair for the subject.
5. Create a CSR for the subject.
6. Sign the subject&#039;s CSR with the issuer&#039;s certificate.
7. Package everything into a PKCS#12 container.

The process mimics a real CA-issued certificate but is entirely self-contained and suitable for testing purposes.




---

### Asymmetric Key Helper

AsymmetricKeyHelper Class

# AsymmetricKeyHelper Class

The `AsymmetricKeyHelper` class provides utilities for working with RSA certificates and keys. It offers methods to normalize public and private keys and to generate public keys from modulus and exponent values.

## Overview

When working with digital certificates and electronic signatures, you often need to handle RSA key components in different formats. The `AsymmetricKeyHelper` class solves common problems related to key formatting and generation:

1. Adding standard PEM headers and footers to raw key data.
2. Converting between key components (modulus, exponent) and full key format.

## Key Normalization

### Normalizing a Public Key

```php
use Derafu\Certificate\AsymmetricKeyHelper;

// Raw public key without headers.
$rawPublicKey = &quot;MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvTLIKu... (more base64 data)&quot;;

// Normalize the public key (add headers and footers).
$normalizedPublicKey = AsymmetricKeyHelper::normalizePublicKey($rawPublicKey);

// Result:
// -----BEGIN CERTIFICATE-----
// MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvTLIKu...
// (more base64 data, wrapped, by default, at 64 characters per line)
// -----END CERTIFICATE-----
```

### Normalizing a Private Key

```php
// Raw private key without headers.
$rawPrivateKey = &quot;MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkA... (more base64 data)&quot;;

// Normalize the private key (add headers and footers).
$normalizedPrivateKey = AsymmetricKeyHelper::normalizePrivateKey($rawPrivateKey);

// Result:
// -----BEGIN PRIVATE KEY-----
// MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkA...
// (more base64 data, wrapped, by default, at 64 characters per line)
// -----END PRIVATE KEY-----
```

### Custom Line Length

Both normalization methods support a custom line length for wrapping the key data:

```php
// Wrap lines at 75 characters instead of the default 64.
$normalizedPublicKey = AsymmetricKeyHelper::normalizePublicKey($rawPublicKey, 75);
$normalizedPrivateKey = AsymmetricKeyHelper::normalizePrivateKey($rawPrivateKey, 75);
```

## Key Generation

### Generating a Public Key from Modulus and Exponent

You can generate a complete public key if you have the modulus and exponent components:

```php
// Base64-encoded modulus and exponent.
$modulus = &quot;wKhpaf5AYomI+0/tLxJtvjHVCveRYYZ9j0yDlL...&quot;;
$exponent = &quot;AQAB&quot;;

// Generate public key.
$publicKey = AsymmetricKeyHelper::generatePublicKeyFromModulusExponent(
    $modulus,
    $exponent
);

// Result is a complete public key in PKCS1 format:
// -----BEGIN RSA PUBLIC KEY-----
// MIIBCgKCAQEAwKhpaf5AYomI+0/tLxJtvjHVCveRYYZ9j0yDlL...
// -----END RSA PUBLIC KEY-----
```

This method requires the `phpseclib3/phpseclib` library. If the library is not installed, it will throw a `LogicException` with instructions to install it.

## Common Use Cases

### Preparing Keys for Use with OpenSSL Functions

Many OpenSSL functions require properly formatted keys with headers and footers:

```php
// Normalize keys before using with OpenSSL.
$normalizedPublicKey = AsymmetricKeyHelper::normalizePublicKey($rawPublicKey);
$normalizedPrivateKey = AsymmetricKeyHelper::normalizePrivateKey($rawPrivateKey);

// Now use with OpenSSL functions.
$signature = openssl_sign($data, $signature, $normalizedPrivateKey, OPENSSL_ALGO_SHA256);
$result = openssl_verify($data, $signature, $normalizedPublicKey, OPENSSL_ALGO_SHA256);
```

### Reconstructing Public Keys

In some systems, especially with hardware security modules (HSMs) or smart cards, you might only have access to the modulus and exponent components:

```php
// Reconstruct the public key from components.
$publicKey = AsymmetricKeyHelper::generatePublicKeyFromModulusExponent(
    $modulus,
    $exponent
);

// Use for verification.
$result = openssl_verify($data, $signature, $publicKey, OPENSSL_ALGO_SHA256);
```

## Implementation Details

### Public Key Normalization

The `normalizePublicKey()` method:

1. Checks if the key already contains the &quot;BEGIN CERTIFICATE&quot; header.
2. If not, adds the appropriate BEGIN/END headers.
3. Wraps the content to the specified line length.
4. Returns the normalized key.

### Private Key Normalization

The `normalizePrivateKey()` method:

1. Checks if the key already contains the &quot;BEGIN PRIVATE KEY&quot; header.
2. If not, adds the appropriate BEGIN/END headers.
3. Wraps the content to the specified line length.
4. Returns the normalized key.

### Public Key Generation

The `generatePublicKeyFromModulusExponent()` method:

1. Decodes the base64-encoded modulus and exponent.
2. Creates BigInteger instances from the binary data.
3. Loads these values into a RSA key object using phpseclib.
4. Exports the key in PKCS1 format.
5. Returns the complete public key string.




---

### Certificate Service

CertificateService Class

# CertificateService Class

The `CertificateService` is the main entry point for working with digital certificates in the Derafu Certificate library. It provides a unified interface to all the key functionality like loading, validating, and creating certificates.

## Overview

The `CertificateService` follows the service pattern, acting as a facade for:

- `CertificateFaker`: For creating test certificates.
- `CertificateLoader`: For loading certificates from various sources.
- `CertificateValidator`: For validating certificates.

By using the service, you can access all the library&#039;s functionality through a single interface.

## Basic Usage

### Setting Up the Service

```php
use Derafu\Certificate\Service\CertificateLoader;
use Derafu\Certificate\Service\CertificateFaker;
use Derafu\Certificate\Service\CertificateValidator;
use Derafu\Certificate\Service\CertificateService;

// Create dependencies.
$loader = new CertificateLoader();
$faker = new CertificateFaker($loader);
$validator = new CertificateValidator();

// Create the service.
$service = new CertificateService($faker, $loader, $validator);
```

### Loading Certificates

The service provides methods to load certificates from various sources:

```php
// Load from a PKCS#12 file.
$certificate = $service-&gt;loadFromFile(&#039;/path/to/certificate.p12&#039;, &#039;password&#039;);

// Load from PKCS#12 data.
$certificate = $service-&gt;loadFromData($pkcs12Data, &#039;password&#039;);

// Load from a certificate array.
$certificate = $service-&gt;loadFromArray([
    &#039;cert&#039; =&gt; $publicKey,
    &#039;pkey&#039; =&gt; $privateKey
]);

// Load directly from key strings.
$certificate = $service-&gt;loadFromKeys($publicKey, $privateKey);
```

### Validating Certificates

```php
use Derafu\Certificate\Exception\CertificateException;

try {
    $service-&gt;validate($certificate);
    echo &quot;Certificate is valid!&quot;;
} catch (CertificateException $e) {
    echo &quot;Validation failed: &quot; . $e-&gt;getMessage();
}
```

### Creating Test Certificates

```php
// Create with default values.
$certificate = $service-&gt;createFake();

// Create with custom values.
$certificate = $service-&gt;createFake(
    id: &#039;12345678-9&#039;,
    name: &#039;John Doe&#039;,
    email: &#039;john.doe@example.com&#039;
);

// Use the certificate.
$id = $certificate-&gt;getId();
$name = $certificate-&gt;getName();
$isValid = $certificate-&gt;isActive();
```

## Complete Example

```php
// Initialize the service.
$loader = new CertificateLoader();
$faker = new CertificateFaker($loader);
$validator = new CertificateValidator();
$service = new CertificateService($faker, $loader, $validator);

// Create a test certificate.
$certificate = $service-&gt;createFake(
    id: &#039;12345678-9&#039;,
    name: &#039;John Doe&#039;,
    email: &#039;john.doe@example.com&#039;
);

// Extract certificate data.
echo &quot;Certificate ID: &quot; . $certificate-&gt;getId() . &quot;\n&quot;;
echo &quot;Certificate Name: &quot; . $certificate-&gt;getName() . &quot;\n&quot;;
echo &quot;Valid until: &quot; . $certificate-&gt;getTo() . &quot;\n&quot;;
echo &quot;Days remaining: &quot; . $certificate-&gt;getExpirationDays() . &quot;\n&quot;;

// Validate the certificate.
try {
    $service-&gt;validate($certificate);
    echo &quot;Certificate is valid\n&quot;;
} catch (CertificateException $e) {
    echo &quot;Invalid certificate: &quot; . $e-&gt;getMessage() . &quot;\n&quot;;
}

// Export the certificate.
$pkcs12Data = $certificate-&gt;getPkcs12(&#039;password&#039;);
file_put_contents(&#039;certificate.p12&#039;, $pkcs12Data);

// Later, load the certificate back.
$loadedCertificate = $service-&gt;loadFromFile(&#039;certificate.p12&#039;, &#039;password&#039;);
```

## Customizing the Service

### Custom Validator

You can create a custom validator by implementing the `CertificateValidatorInterface`:

```php
use Derafu\Certificate\Contract\CertificateInterface;
use Derafu\Certificate\Contract\CertificateValidatorInterface;
use Derafu\Certificate\Exception\CertificateException;

class MyCustomValidator implements CertificateValidatorInterface
{
    public function validate(CertificateInterface $certificate): void
    {
        // Implement your validation logic.
        if (!$certificate-&gt;isActive()) {
            throw new CertificateException(
                &#039;Certificate is expired.&#039;
            );
        }

        // Add specific validation for your use case.
        if ($certificate-&gt;getExpirationDays() &lt; 30) {
            throw new CertificateException(
                &#039;Certificate will expire in less than 30 days.&#039;
            );
        }
    }
}

// Use your custom validator.
$customValidator = new MyCustomValidator();
$service = new CertificateService($faker, $loader, $customValidator);
```

## Service Components

### CertificateLoader

The `CertificateLoader` component handles loading certificates from various sources:

- PKCS#12 files.
- PKCS#12 binary data.
- Array of keys.
- Individual public and private keys.

### CertificateFaker

The `CertificateFaker` component provides a way to create self-signed certificates for testing purposes:

- Create certificates with custom subject information.
- Create certificates with custom issuer information.
- Create certificates with custom validity periods.

Under the hood, it uses the `SelfSignedCertificate` class.

### CertificateValidator

The `CertificateValidator` component validates certificates according to specific requirements:

- Checks if the certificate ID (RUN) is properly formatted.
- Ensures that &quot;K&quot; in the ID is uppercase.
- Verifies that the certificate is not expired.

The default validator is designed for Chilean certificates used with the SII (Servicio de Impuestos Internos), but you can implement your own validator for different requirements.




---

## Signature Project

Derafu Signature

# Derafu Signature




---

### Introduction

Library for digital signatures

# Library for digital signatures

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/signature/main)
![CI Workflow](https://github.com/derafu/signature/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/signature)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/signature)
![Total Downloads](https://poser.pugx.org/derafu/signature/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/signature/d/monthly)

A comprehensive PHP library for creating and validating digital signatures, with special focus on XML digital signatures (XML-DSIG).

## Features

- **Digital Signatures**: Sign and validate any data with RSA key pairs.
- **XML Signatures**: Full support for XML Digital Signatures (XML-DSIG).
- **Signature Verification**: Validate signatures against public keys.
- **Reference Support**: Sign specific sections of XML documents using ID references.
- **Integration**: Works seamlessly with Derafu Certificate and Derafu XML libraries.

## Installation

```bash
composer require derafu/signature
```

## Basic Usage

### Signing Data

```php
use Derafu\Signature\Service\SignatureGenerator;
use Derafu\Signature\Service\SignatureService;
use Derafu\Signature\Service\SignatureValidator;
use Derafu\Xml\Service\XmlDecoder;
use Derafu\Xml\Service\XmlEncoder;
use Derafu\Xml\Service\XmlService;
use Derafu\Xml\Service\XmlValidator;

// Set up the signature service.
$xmlEncoder = new XmlEncoder();
$xmlDecoder = new XmlDecoder();
$xmlValidator = new XmlValidator();
$xmlService = new XmlService($xmlEncoder, $xmlDecoder, $xmlValidator);

$signatureGenerator = new SignatureGenerator($xmlService);
$signatureValidator = new SignatureValidator($signatureGenerator, $xmlService);
$signatureService = new SignatureService($signatureGenerator, $signatureValidator);

// Sign simple data.
$privateKey = &#039;...&#039;;
$data = &#039;Hello, world!&#039;;
$signature = $signatureService-&gt;sign($data, $privateKey);

// Validate the signature.
$isValid = $signatureService-&gt;validate($data, $signature, $publicKey);
```

### Signing XML Documents

```php
use Derafu\Certificate\Service\CertificateLoader;

// Load a certificate.
$certificateLoader = new CertificateLoader();
$certificate = $certificateLoader-&gt;loadFromFile(
    &#039;/path/to/certificate.p12&#039;,
    &#039;password&#039;
);

// Load XML to sign.
$xml = file_get_contents(&#039;document.xml&#039;);

// Sign the entire XML document.
$signedXml = $signatureService-&gt;signXml($xml, $certificate);

// Sign a specific element in the XML document (identified by ID).
$signedXml = $signatureService-&gt;signXml($xml, $certificate, &#039;elementId&#039;);

// Save the signed XML.
file_put_contents(&#039;signed_document.xml&#039;, $signedXml);
```

### Validating XML Signatures

```php
use Derafu\Signature\Exception\SignatureException;

// Load signed XML.
$signedXml = file_get_contents(&#039;signed_document.xml&#039;);

try {
    // Validate the XML signature.
    $signatureService-&gt;validateXml($signedXml);
    echo &quot;Signature is valid!&quot;;
} catch (SignatureException $e) {
    echo &quot;Signature validation failed: &quot; . $e-&gt;getMessage();
}
```

## Advanced Usage

### Detailed XML Signature Validation

For more detailed control over the validation process:

```php
// Create a signature node from the signed XML.
$signatureNode = $signatureService-&gt;createSignatureNode($signatureXml);

// Validate the digest value (integrity of the signed content).
$signatureService-&gt;validateXmlDigestValue($xmlDocument, $signatureNode);

// Validate the signature value (authenticity of the signer).
$signatureService-&gt;validateXmlSignatureValue($signatureNode);
```

### Calculating Digest Values

```php
use Derafu\Xml\XmlDocument;

// Load XML document.
$xmlDoc = new XmlDocument();
$xmlDoc-&gt;loadXml($xml);

// Calculate digest value for the entire document.
$digestValue = $signatureService-&gt;generateXmlDigestValue($xmlDoc);

// Calculate digest value for a specific element.
$digestValue = $signatureService-&gt;generateXmlDigestValue($xmlDoc, &#039;elementId&#039;);
```

## XML-DSIG Implementation Details

The library implements XML Digital Signatures according to the [W3C XML Signature Syntax and Processing](https://www.w3.org/TR/xmldsig-core/) specification:

1. The `Signature` element is created with the following components:
   - `SignedInfo`: Contains information about what was signed.
   - `SignatureValue`: Contains the actual signature value.
   - `KeyInfo`: Contains information about the key used to validate the signature.

2. Canonicalization is performed using the C14N algorithm (http://www.w3.org/TR/2001/REC-xml-c14n-20010315).

3. Signatures are created using RSA-SHA1 (http://www.w3.org/2000/09/xmldsig#rsa-sha1).

4. Digests are created using SHA1 (http://www.w3.org/2000/09/xmldsig#sha1).

### XML-DSIG Structure

When signing an XML document, the resulting signature will have the following structure:

```xml
&lt;Signature xmlns=&quot;http://www.w3.org/2000/09/xmldsig#&quot;&gt;
  &lt;SignedInfo xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;&gt;
    &lt;CanonicalizationMethod Algorithm=&quot;http://www.w3.org/TR/2001/REC-xml-c14n-20010315&quot;/&gt;
    &lt;SignatureMethod Algorithm=&quot;http://www.w3.org/2000/09/xmldsig#rsa-sha1&quot;/&gt;
    &lt;Reference URI=&quot;#elementId&quot;&gt;
      &lt;Transforms&gt;
        &lt;Transform Algorithm=&quot;http://www.w3.org/2000/09/xmldsig#enveloped-signature&quot;/&gt;
      &lt;/Transforms&gt;
      &lt;DigestMethod Algorithm=&quot;http://www.w3.org/2000/09/xmldsig#sha1&quot;/&gt;
      &lt;DigestValue&gt;...&lt;/DigestValue&gt;
    &lt;/Reference&gt;
  &lt;/SignedInfo&gt;
  &lt;SignatureValue&gt;...&lt;/SignatureValue&gt;
  &lt;KeyInfo&gt;
    &lt;KeyValue&gt;
      &lt;RSAKeyValue&gt;
        &lt;Modulus&gt;...&lt;/Modulus&gt;
        &lt;Exponent&gt;...&lt;/Exponent&gt;
      &lt;/RSAKeyValue&gt;
    &lt;/KeyValue&gt;
    &lt;X509Data&gt;
      &lt;X509Certificate&gt;...&lt;/X509Certificate&gt;
    &lt;/X509Data&gt;
  &lt;/KeyInfo&gt;
&lt;/Signature&gt;
```

## Integration with Other Derafu Libraries

This library is designed to work seamlessly with other Derafu libraries:

- **Derafu Certificate**: For handling digital certificates and key pairs.
- **Derafu XML**: For handling XML documents and operations.




---

### Signature Class

Signature Class

# Signature Class

The `Signature` class is the core component of the Derafu Signature library, representing an XML digital signature node according to the XML-DSIG standard.

## Overview

The `Signature` class implements the `SignatureInterface` and represents the XML `&lt;Signature&gt;` element that contains all the information related to a digital signature in an XML document:

- The digest value of the signed content.
- The signature value.
- The public key information for signature verification.

This class is used both when creating new signatures and when validating existing ones.

## Usage

### Creating a New Signature Node

```php
use Derafu\Signature\Signature;
use Derafu\Certificate\Service\CertificateLoader;

// Load a certificate.
$certificateLoader = new CertificateLoader();
$certificate = $certificateLoader-&gt;loadFromFile(
    &#039;/path/to/certificate.p12&#039;,
    &#039;password&#039;
);

// Create a signature node.
$signatureNode = new Signature();

// Configure with digest value, certificate, and optional reference.
$signatureNode-&gt;configureSignatureData(
    digestValue: &#039;bWFpbkRpZ2VzdFZhbHVlQmFzZTY0&#039;,
    certificate: $certificate,
    reference: &#039;documentId&#039;  // Optional, specify to sign a specific element.
);
```

### Working with an Existing Signature Node

```php
// Typically you&#039;d use the SignatureValidator to create a signature node from XML.
$signatureNode = $signatureService-&gt;createSignatureNode($signatureXml);

// Access signature properties.
$reference = $signatureNode-&gt;getReference();
$digestValue = $signatureNode-&gt;getDigestValue();
$signatureValue = $signatureNode-&gt;getSignatureValue();
$x509Certificate = $signatureNode-&gt;getX509Certificate();
```

## API Reference

### Setting and Getting Data

```php
// Set raw data structure.
$signatureNode-&gt;setData($dataArray);

// Get the current data structure.
$dataArray = $signatureNode-&gt;getData();
```

### Configuring the Signature

```php
// Configure all signature components at once.
$signatureNode-&gt;configureSignatureData(
    digestValue: &#039;base64DigestValue&#039;,
    certificate: $certificate,
    reference: &#039;elementId&#039;
);
```

### Working with XML

```php
// Set the XML representation of the signature.
$signatureNode-&gt;setXml($xmlDocument);

// Get the XML representation of the signature.
$xmlDocument = $signatureNode-&gt;getXml();
```

### Setting Signature Value

```php
// Set the calculated signature value (after signing the SignedInfo element).
$signatureNode-&gt;setSignatureValue(&#039;base64SignatureValue&#039;);
```

### Getting Signature Components

```php
// Get the reference URI (without # prefix).
$reference = $signatureNode-&gt;getReference();

// Get the digest value.
$digestValue = $signatureNode-&gt;getDigestValue();

// Get the X.509 certificate (without headers/footers).
$certificate = $signatureNode-&gt;getX509Certificate();

// Get the signature value.
$signatureValue = $signatureNode-&gt;getSignatureValue();
```

## Data Structure

The `Signature` class maintains an internal data array that represents the XML structure of the signature. This structure follows the XML-DSIG standard:

```php
[
    &#039;Signature&#039; =&gt; [
        &#039;@attributes&#039; =&gt; [
            &#039;xmlns&#039; =&gt; &#039;http://www.w3.org/2000/09/xmldsig#&#039;,
        ],
        &#039;SignedInfo&#039; =&gt; [
            &#039;@attributes&#039; =&gt; [
                &#039;xmlns:xsi&#039; =&gt; &#039;http://www.w3.org/2001/XMLSchema-instance&#039;,
            ],
            &#039;CanonicalizationMethod&#039; =&gt; [
                &#039;@attributes&#039; =&gt; [
                    &#039;Algorithm&#039; =&gt; &#039;http://www.w3.org/TR/2001/REC-xml-c14n-20010315&#039;,
                ],
            ],
            &#039;SignatureMethod&#039; =&gt; [
                &#039;@attributes&#039; =&gt; [
                    &#039;Algorithm&#039; =&gt; &#039;http://www.w3.org/2000/09/xmldsig#rsa-sha1&#039;,
                ],
            ],
            &#039;Reference&#039; =&gt; [
                &#039;@attributes&#039; =&gt; [
                    &#039;URI&#039; =&gt; &#039;&#039;, // Reference URI, empty for entire document.
                ],
                &#039;Transforms&#039; =&gt; [
                    &#039;Transform&#039; =&gt; [
                        &#039;@attributes&#039; =&gt; [
                            &#039;Algorithm&#039; =&gt; &#039;http://www.w3.org/2000/09/xmldsig#enveloped-signature&#039;,
                        ],
                    ],
                ],
                &#039;DigestMethod&#039; =&gt; [
                    &#039;@attributes&#039; =&gt; [
                        &#039;Algorithm&#039; =&gt; &#039;http://www.w3.org/2000/09/xmldsig#sha1&#039;,
                    ],
                ],
                &#039;DigestValue&#039; =&gt; &#039;&#039;, // Will contain the digest value.
            ],
        ],
        &#039;SignatureValue&#039; =&gt; &#039;&#039;, // Will contain the signature value.
        &#039;KeyInfo&#039; =&gt; [
            &#039;KeyValue&#039; =&gt; [
                &#039;RSAKeyValue&#039; =&gt; [
                    &#039;Modulus&#039; =&gt; &#039;&#039;, // Will contain the certificate modulus.
                    &#039;Exponent&#039; =&gt; &#039;&#039;, // Will contain the certificate exponent.
                ],
            ],
            &#039;X509Data&#039; =&gt; [
                &#039;X509Certificate&#039; =&gt; &#039;&#039;, // Will contain the certificate.
            ],
        ],
    ],
]
```

## Implementation Details

### XML Invalidation

The `Signature` class automatically invalidates the XML representation when data is modified:

```php
// This will cause the internal XML to be invalidated.
$signatureNode-&gt;setData($newData);

// This will also invalidate the XML.
$signatureNode-&gt;setSignatureValue($newSignatureValue);
```

After invalidation, the XML must be regenerated (typically by the `SignatureGenerator` class) before `getXml()` can be called again.

### Reference URIs

Reference URIs are handled according to the XML-DSIG standard:

- An empty URI (`&quot;&quot;`) means the entire document is signed.
- A URI starting with `#` refers to an element with the specified ID.
- The `getReference()` method returns the reference without the `#` prefix.
- The `configureSignatureData()` method automatically adds the `#` prefix if not present.

### Transformation Algorithm

The transformation algorithm changes based on whether a reference is provided:

- With a reference: `http://www.w3.org/TR/2001/REC-xml-c14n-20010315` (standard C14N).
- Without a reference: `http://www.w3.org/2000/09/xmldsig#enveloped-signature` (enveloped signature transformation).

This ensures that the signature is correctly calculated for both whole-document signatures and element signatures.




---

### Signature Generator

SignatureGenerator Class

# SignatureGenerator Class

The `SignatureGenerator` class is responsible for creating digital signatures, both for general data and specifically for XML documents according to the XML-DSIG standard.

## Overview

The `SignatureGenerator` implements the `SignatureGeneratorInterface` and provides mechanisms to:

1. Sign any data using a private key.
2. Sign XML documents using a certificate.
3. Calculate digest values for XML documents or specific elements within them.

This class is a core component of the Derafu Signature library and is typically used through the `SignatureService` facade.

## Basic Usage

### Initialization

```php
use Derafu\Signature\Service\SignatureGenerator;
use Derafu\Xml\Service\XmlDecoder;
use Derafu\Xml\Service\XmlEncoder;
use Derafu\Xml\Service\XmlService;
use Derafu\Xml\Service\XmlValidator;

// Initialize required XML services.
$xmlEncoder = new XmlEncoder();
$xmlDecoder = new XmlDecoder();
$xmlValidator = new XmlValidator();
$xmlService = new XmlService($xmlEncoder, $xmlDecoder, $xmlValidator);

// Create the signature generator.
$generator = new SignatureGenerator($xmlService);
```

### Signing Data

```php
// Sign data with a private key.
$data = &#039;Data to be signed&#039;;
$signature = $generator-&gt;sign($data, $privateKey);

// Optional: Specify a different signature algorithm.
$signature = $generator-&gt;sign($data, $privateKey, OPENSSL_ALGO_SHA256);
```

### Signing XML Documents

```php
use Derafu\Certificate\Service\CertificateLoader;

// Load a certificate.
$loader = new CertificateLoader();
$certificate = $loader-&gt;loadFromFile(&#039;/path/to/certificate.p12&#039;, &#039;password&#039;);

// Sign an XML string.
$xmlString = &#039;&lt;root&gt;&lt;element&gt;data&lt;/element&gt;&lt;/root&gt;&#039;;
$signedXml = $generator-&gt;signXml($xmlString, $certificate);

// Sign a specific element in the XML (identified by ID).
$xmlWithIds = &#039;&lt;root&gt;&lt;element ID=&quot;myElement&quot;&gt;data&lt;/element&gt;&lt;/root&gt;&#039;;
$signedXml = $generator-&gt;signXml($xmlWithIds, $certificate, &#039;myElement&#039;);
```

### Calculating Digest Values

```php
// Create an XML document.
$xmlDoc = new \Derafu\Xml\XmlDocument();
$xmlDoc-&gt;loadXml(&#039;&lt;root&gt;&lt;element ID=&quot;myElement&quot;&gt;data&lt;/element&gt;&lt;/root&gt;&#039;);

// Calculate digest value for the entire document.
$digestValue = $generator-&gt;generateXmlDigestValue($xmlDoc);

// Calculate digest value for a specific element.
$digestValue = $generator-&gt;generateXmlDigestValue($xmlDoc, &#039;myElement&#039;);
```

## API Reference

### Data Signing

```php
/**
 * Sign the provided data using a private key.
 *
 * @param string $data Data to be signed.
 * @param string $privateKey Private key to be used for signing.
 * @param string|int $signatureAlgorithm Algorithm to be used for signing (default SHA1).
 * @return string Digital signature in base64.
 * @throws SignatureException If the signing operation fails.
 */
public function sign(
    string $data,
    string $privateKey,
    string|int $signatureAlgorithm = OPENSSL_ALGO_SHA1
): string;
```

### XML Signing

```php
/**
 * Sign an XML document using RSA and SHA1.
 *
 * @param XmlDocumentInterface|string $xml XML document to be signed.
 * @param CertificateInterface $certificate Digital certificate to be used for signing.
 * @param ?string $reference Reference to which the signature is made. If not
 * specified, the digest of the entire XML document will be signed.
 * @return string String XML with the generated signature included in the
 * &quot;Signature&quot; tag at the end of the XML (last element within the root node).
 * @throws SignatureException If any problem occurs while signing.
 */
public function signXml(
    XmlDocumentInterface|string $xml,
    CertificateInterface $certificate,
    ?string $reference = null
): string;
```

### Digest Value Generation

```php
/**
 * Generate the SHA1 (&quot;DigestValue&quot;) of a node of the XML with a certain
 * reference. This can be used later to generate the XML signature.
 *
 * If no reference is specified, the &quot;DigestValue&quot; will be calculated over the
 * entire XML (root node).
 *
 * @param XmlDocumentInterface $doc XML document to be signed.
 * @param ?string $reference Reference to which the signature is made.
 * @return string Data of the XML that must be digested.
 * @throws XmlException If the reference is not found in the XML.
 */
public function generateXmlDigestValue(
    XmlDocumentInterface $doc,
    ?string $reference = null
): string;
```

## Implementation Details

### Digital Signature Process

For general data, the signing process is straightforward:

1. The data is signed using the private key and the specified algorithm.
2. The resulting binary signature is base64-encoded.
3. The base64-encoded signature is returned.

### XML Signature Process

For XML documents, the signing process follows the XML-DSIG standard:

1. The XML document is loaded and parsed.
2. If a reference is provided, the referenced element is located.
3. The digest value (SHA1 hash) of the canonicalized (C14N) content is calculated.
4. A `Signature` node is created with the digest value and certificate information.
5. The `SignedInfo` element of the signature is canonicalized.
6. The canonicalized `SignedInfo` is signed using the certificate&#039;s private key.
7. The signature value is added to the `SignatureValue` element.
8. The complete signature node is added to the XML document.
9. The signed XML document is returned.

### Canonicalization

The class uses the `C14NWithIso88591Encoding` method for canonicalization, which:

1. Applies XML canonicalization (C14N) according to the W3C standard.
2. Converts the result to ISO-8859-1 encoding.
3. Ensures consistent representation across different systems.

This process is crucial for ensuring that the same signature is generated regardless of the XML document&#039;s formatting or encoding.

### Reference Handling

When a reference is provided:

1. The reference must be an ID attribute value in the XML document.
2. The digest is calculated only for the referenced element.
3. The signature&#039;s `Reference` element includes a URI attribute (`#elementId`).
4. The transform algorithm is set to standard C14N.

When no reference is provided:

1. The digest is calculated for the entire document (excluding any existing signature).
2. The signature&#039;s `Reference` element has an empty URI attribute.
3. The transform algorithm is set to &quot;enveloped signature transformation&quot;.

### Signature Node Creation

The class creates a `Signature` node with the following components:

1. `SignedInfo`: Contains information about what data was signed.
   - `CanonicalizationMethod`: Specifies the C14N algorithm.
   - `SignatureMethod`: Specifies RSA-SHA1.
   - `Reference`: Points to the signed content and includes the digest value.

2. `SignatureValue`: Contains the actual signature of the `SignedInfo` element.

3. `KeyInfo`: Contains information about the certificate used for signing.
   - `KeyValue/RSAKeyValue`: Contains the modulus and exponent from the certificate.
   - `X509Data/X509Certificate`: Contains the certificate itself.




---

### Signature Validator

SignatureValidator Class

# SignatureValidator Class

The `SignatureValidator` class is responsible for validating digital signatures, both for general data and specifically for XML documents that follow the XML-DSIG standard.

## Overview

The `SignatureValidator` implements the `SignatureValidatorInterface` and provides mechanisms to:

1. Validate signatures for any data against a public key.
2. Validate signatures in XML documents.
3. Extract and parse signature nodes from XML documents.
4. Validate specific aspects of XML signatures (digest values and signature values).

This class is a core component of the Derafu Signature library and is typically used through the `SignatureService` facade.

## Basic Usage

### Initialization

```php
use Derafu\Signature\Service\SignatureGenerator;
use Derafu\Signature\Service\SignatureValidator;
use Derafu\Xml\Service\XmlDecoder;
use Derafu\Xml\Service\XmlEncoder;
use Derafu\Xml\Service\XmlService;
use Derafu\Xml\Service\XmlValidator;

// Initialize required XML services.
$xmlEncoder = new XmlEncoder();
$xmlDecoder = new XmlDecoder();
$xmlValidator = new XmlValidator();
$xmlService = new XmlService($xmlEncoder, $xmlDecoder, $xmlValidator);

// Create the signature generator (needed by validator).
$generator = new SignatureGenerator($xmlService);

// Create the signature validator.
$validator = new SignatureValidator($generator, $xmlService);
```

### Validating Data Signatures

```php
// Validate a signature for some data.
$data = &#039;Data that was signed&#039;;
$signature = &#039;base64EncodedSignature&#039;;
$publicKey = &#039;publicKeyPEM&#039;;

$isValid = $validator-&gt;validate($data, $signature, $publicKey);

// Optional: Specify the signature algorithm.
$isValid = $validator-&gt;validate($data, $signature, $publicKey, OPENSSL_ALGO_SHA256);

if ($isValid) {
    echo &quot;Signature is valid!&quot;;
} else {
    echo &quot;Signature is invalid!&quot;;
}
```

### Validating XML Signatures

```php
use Derafu\Signature\Exception\SignatureException;

// Validate an XML document with a signature.
$signedXml = file_get_contents(&#039;signed_document.xml&#039;);

try {
    $validator-&gt;validateXml($signedXml);
    echo &quot;XML signature is valid!&quot;;
} catch (SignatureException $e) {
    echo &quot;XML signature validation failed: &quot; . $e-&gt;getMessage();
}
```

### Working with Signature Nodes

```php
// Extract and parse a signature node from XML.
$signatureXml = &#039;&lt;Signature xmlns=&quot;http://www.w3.org/2000/09/xmldsig#&quot;&gt;...&lt;/Signature&gt;&#039;;
$signatureNode = $validator-&gt;createSignatureNode($signatureXml);

// Access signature components.
$reference = $signatureNode-&gt;getReference();
$digestValue = $signatureNode-&gt;getDigestValue();
$signatureValue = $signatureNode-&gt;getSignatureValue();
$x509Certificate = $signatureNode-&gt;getX509Certificate();
```

### Detailed XML Signature Validation

```php
// Create a signature node from a signed XML document.
$signatureNode = $validator-&gt;createSignatureNode($signatureXml);

// Validate just the digest value (content integrity).
try {
    $validator-&gt;validateXmlDigestValue($xmlDoc, $signatureNode);
    echo &quot;Content integrity verified!&quot;;
} catch (SignatureException $e) {
    echo &quot;Content may have been tampered with: &quot; . $e-&gt;getMessage();
}

// Validate just the signature value (signer authenticity).
try {
    $validator-&gt;validateXmlSignatureValue($signatureNode);
    echo &quot;Signature authenticity verified!&quot;;
} catch (SignatureException $e) {
    echo &quot;Signature validation failed: &quot; . $e-&gt;getMessage();
}
```

## API Reference

### Data Signature Validation

```php
/**
 * Validate the digital signature of data.
 *
 * @param string $data Data to be verified.
 * @param string $signature Digital signature of the data in base64.
 * @param string $publicKey Public key of the signature of the data.
 * @param string|int $signatureAlgorithm Algorithm used to sign (default SHA1).
 * @return bool `true` if the signature is valid, `false` if it is invalid.
 * @throws SignatureException If there was an error while validating.
 */
public function validate(
    string $data,
    string $signature,
    string $publicKey,
    string|int $signatureAlgorithm = OPENSSL_ALGO_SHA1
): bool;
```

### XML Signature Validation

```php
/**
 * Validate the validity of an XML signature using RSA and SHA1.
 *
 * @param XmlDocumentInterface|string $xml XML string to be validated.
 * @return void
 * @throws SignatureException If there was an error while validating.
 */
public function validateXml(XmlDocumentInterface|string $xml): void;
```

### Signature Node Handling

```php
/**
 * Creates the `Signature` instance from a string XML with the signature node.
 *
 * @param string $xml String with the XML of the `Signature` node.
 * @return SignatureInterface
 */
public function createSignatureNode(string $xml): SignatureInterface;
```

### Detailed Validation Methods

```php
/**
 * Validate the DigestValue of the signed data.
 *
 * @param XmlDocumentInterface|string $xml Document to be validated.
 * @param SignatureInterface $signatureNode Signature node to be validated.
 * @return void
 * @throws SignatureException If the DigestValue is invalid.
 */
public function validateXmlDigestValue(
    XmlDocumentInterface|string $xml,
    SignatureInterface $signatureNode
): void;

/**
 * Validate the signature of the `SignedInfo` node of the XML using the X509
 * certificate.
 *
 * @param SignatureInterface $signatureNode Signature node to be validated.
 * @throws SignatureException If the XML signature is invalid.
 */
public function validateXmlSignatureValue(
    SignatureInterface $signatureNode
): void;
```

## Implementation Details

### Signature Validation Process

For general data signatures, the validation process is as follows:

1. The public key is normalized (headers and footers are added if missing).
2. The base64-encoded signature is decoded to binary.
3. The `openssl_verify` function is called with the data, decoded signature, and public key.
4. If the result is 1, the signature is valid; if 0, it&#039;s invalid; if -1, an error occurred.

### XML Signature Validation Process

For XML signatures, the validation process follows the XML-DSIG standard:

1. The XML document is loaded and parsed.
2. All `Signature` elements in the document are located.
3. For each signature element:
   - A `Signature` node object is created from the element&#039;s XML.
   - The digest value is validated to ensure content integrity.
   - The signature value is validated to ensure signer authenticity.

### Digest Value Validation

The digest value validation ensures that the content being signed hasn&#039;t been modified:

1. The XML document is loaded.
2. The reference from the signature node is extracted (if any).
3. The digest value is calculated for the referenced content or the entire document.
4. The calculated digest value is compared to the one in the signature.
5. If they don&#039;t match, a `SignatureException` is thrown.

### Signature Value Validation

The signature value validation ensures that the signature was created with the corresponding private key:

1. The `SignedInfo` element is extracted and canonicalized.
2. The base64-encoded signature value is obtained from the `SignatureValue` element.
3. The public key is extracted from the `X509Certificate` element.
4. The signature is validated using the canonicalized `SignedInfo`, the signature value, and the public key.
5. If the validation fails, a `SignatureException` is thrown.

### Error Handling

The `SignatureValidator` provides detailed error messages when validation fails:

- For data signatures, it indicates when an error occurred during validation.
- For XML signatures, it indicates whether the problem is with the digest value (content integrity) or the signature value (signer authenticity).
- For missing signatures or malformed XML, it provides clear error messages.

These detailed error messages help diagnose the exact cause of validation failures.

### Dependency on SignatureGenerator

The `SignatureValidator` requires an instance of `SignatureGeneratorInterface` to calculate digest values for XML documents. This dependency ensures that the same digest calculation algorithm is used for both signing and validating.

### XML Handling

The class uses the Derafu XML library for XML operations:

- Loading and parsing XML documents.
- Extracting signature elements.
- Canonicalizing XML for digest and signature validation.
- Converting between XML and PHP arrays.

This integration ensures consistent handling of XML across the library.




---

### Signature Service

SignatureService Class

# SignatureService Class

The `SignatureService` class is the main entry point for working with digital signatures in the Derafu Signature library. It provides a unified interface for generating and validating signatures for both general data and XML documents.

## Overview

The `SignatureService` implements the `SignatureServiceInterface`, which combines the functionality of:

- `SignatureGeneratorInterface`: For creating digital signatures.
- `SignatureValidatorInterface`: For validating digital signatures.

This service acts as a facade over the signature generation and validation components, making it easy to use the library&#039;s full functionality through a single interface.

## Basic Usage

### Setting Up the Service

```php
use Derafu\Signature\Service\SignatureGenerator;
use Derafu\Signature\Service\SignatureService;
use Derafu\Signature\Service\SignatureValidator;
use Derafu\Xml\Service\XmlDecoder;
use Derafu\Xml\Service\XmlEncoder;
use Derafu\Xml\Service\XmlService;
use Derafu\Xml\Service\XmlValidator;

// Initialize required XML services.
$xmlEncoder = new XmlEncoder();
$xmlDecoder = new XmlDecoder();
$xmlValidator = new XmlValidator();
$xmlService = new XmlService($xmlEncoder, $xmlDecoder, $xmlValidator);

// Create generator and validator.
$generator = new SignatureGenerator($xmlService);
$validator = new SignatureValidator($generator, $xmlService);

// Create the signature service.
$signatureService = new SignatureService($generator, $validator);
```

### Signing Data

```php
// Sign simple data with a private key.
$data = &#039;Data to be signed&#039;;
$signature = $signatureService-&gt;sign($data, $privateKey);

// Optional: Specify a different signature algorithm.
$signature = $signatureService-&gt;sign($data, $privateKey, OPENSSL_ALGO_SHA256);
```

### Validating a Signature

```php
// Validate a signature using a public key.
$isValid = $signatureService-&gt;validate($data, $signature, $publicKey);

// Optional: Specify the same signature algorithm used for signing.
$isValid = $signatureService-&gt;validate($data, $signature, $publicKey, OPENSSL_ALGO_SHA256);

if ($isValid) {
    echo &quot;Signature is valid!&quot;;
} else {
    echo &quot;Signature is invalid!&quot;;
}
```

### Signing XML

```php
use Derafu\Certificate\Service\CertificateLoader;
use Derafu\Xml\XmlDocument;

// Load a certificate.
$loader = new CertificateLoader();
$certificate = $loader-&gt;loadFromFile(&#039;/path/to/certificate.p12&#039;, &#039;password&#039;);

// Sign an XML string.
$xmlString = &#039;&lt;root&gt;&lt;element&gt;data&lt;/element&gt;&lt;/root&gt;&#039;;
$signedXml = $signatureService-&gt;signXml($xmlString, $certificate);

// Sign an XmlDocument object.
$xmlDoc = new XmlDocument();
$xmlDoc-&gt;loadXml($xmlString);
$signedXml = $signatureService-&gt;signXml($xmlDoc, $certificate);

// Sign a specific element in the XML (identified by ID).
$xmlWithIds = &#039;&lt;root&gt;&lt;element ID=&quot;myElement&quot;&gt;data&lt;/element&gt;&lt;/root&gt;&#039;;
$signedXml = $signatureService-&gt;signXml($xmlWithIds, $certificate, &#039;myElement&#039;);
```

### Validating XML Signatures

```php
use Derafu\Signature\Exception\SignatureException;

try {
    // Validate a signed XML document.
    $signatureService-&gt;validateXml($signedXml);
    echo &quot;XML signature is valid!&quot;;
} catch (SignatureException $e) {
    echo &quot;XML signature validation failed: &quot; . $e-&gt;getMessage();
}
```

## Advanced Usage

### Calculating Digest Values

```php
// Calculate the digest value for an XML document.
$digestValue = $signatureService-&gt;generateXmlDigestValue($xmlDoc);

// Calculate the digest value for a specific element.
$digestValue = $signatureService-&gt;generateXmlDigestValue($xmlDoc, &#039;elementId&#039;);
```

### Working with Signature Nodes

```php
// Extract signature node from a signed XML.
$signatureXml = $signedXml; // The signature node XML.
$signatureNode = $signatureService-&gt;createSignatureNode($signatureXml);

// Validate just the digest value (content integrity).
try {
    $signatureService-&gt;validateXmlDigestValue($xmlDoc, $signatureNode);
    echo &quot;XML content integrity verified!&quot;;
} catch (SignatureException $e) {
    echo &quot;Digest validation failed: &quot; . $e-&gt;getMessage();
}

// Validate just the signature value (signer authenticity).
try {
    $signatureService-&gt;validateXmlSignatureValue($signatureNode);
    echo &quot;Signature authenticity verified!&quot;;
} catch (SignatureException $e) {
    echo &quot;Signature validation failed: &quot; . $e-&gt;getMessage();
}
```

## API Reference

### Data Signing and Validation

```php
// Generate a digital signature for data.
public function sign(
    string $data,
    string $privateKey,
    string|int $signatureAlgorithm = OPENSSL_ALGO_SHA1
): string;

// Validate a digital signature for data.
public function validate(
    string $data,
    string $signature,
    string $publicKey,
    string|int $signatureAlgorithm = OPENSSL_ALGO_SHA1
): bool;
```

### XML Signing and Validation

```php
// Sign an XML document.
public function signXml(
    XmlDocumentInterface|string $xml,
    CertificateInterface $certificate,
    ?string $reference = null
): string;

// Validate an XML signature.
public function validateXml(XmlDocumentInterface|string $xml): void;

// Calculate the digest value for an XML document or element.
public function generateXmlDigestValue(
    XmlDocumentInterface $doc,
    ?string $reference = null
): string;
```

### Signature Node Operations

```php
// Create a signature node from XML.
public function createSignatureNode(string $xml): SignatureInterface;

// Validate the digest value of a signature.
public function validateXmlDigestValue(
    XmlDocumentInterface|string $xml,
    SignatureInterface $signatureNode
): void;

// Validate the signature value of a signature.
public function validateXmlSignatureValue(
    SignatureInterface $signatureNode
): void;
```

## Implementation Details

### Dependency Injection

The `SignatureService` uses dependency injection to receive its required components:

```php
public function __construct(
    private readonly SignatureGeneratorInterface $generator,
    private readonly SignatureValidatorInterface $validator
)
```

This design allows for flexibility and testability, as the generator and validator components can be replaced with custom implementations if needed.

### Delegation Pattern

The service uses the delegation pattern to forward method calls to the appropriate components:

- Signing methods are delegated to the `SignatureGeneratorInterface` implementation.
- Validation methods are delegated to the `SignatureValidatorInterface` implementation.

This separation of concerns keeps the codebase clean and maintainable.

### Error Handling

Validation methods throw `SignatureException` when validation fails, providing detailed error messages about what went wrong:

- Invalid digest values (content has been modified).
- Invalid signature values (signature was not created with the expected private key).
- Missing signature nodes.
- Malformed XML.

These exceptions should be caught and handled appropriately in your application code.




---

### XML-DSIG Standard

XML Digital Signature (XML-DSIG)

# XML Digital Signature (XML-DSIG)

This document provides an in-depth explanation of how the Derafu Signature library implements the XML Digital Signature (XML-DSIG) standard, including the structure of signature elements, the signing process, and the validation process.

## XML-DSIG Overview

XML Digital Signatures provide integrity, message authentication, and signer authentication for XML data. The XML-DSIG standard is defined by the W3C in the [XML Signature Syntax and Processing](https://www.w3.org/TR/xmldsig-core/) specification.

## Signature Structure

The Derafu Signature library creates XML signatures with the following structure:

```xml
&lt;Signature xmlns=&quot;http://www.w3.org/2000/09/xmldsig#&quot;&gt;
  &lt;SignedInfo xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;&gt;
    &lt;CanonicalizationMethod Algorithm=&quot;http://www.w3.org/TR/2001/REC-xml-c14n-20010315&quot;/&gt;
    &lt;SignatureMethod Algorithm=&quot;http://www.w3.org/2000/09/xmldsig#rsa-sha1&quot;/&gt;
    &lt;Reference URI=&quot;#elementId&quot;&gt;
      &lt;Transforms&gt;
        &lt;Transform Algorithm=&quot;http://www.w3.org/2000/09/xmldsig#enveloped-signature&quot;/&gt;
      &lt;/Transforms&gt;
      &lt;DigestMethod Algorithm=&quot;http://www.w3.org/2000/09/xmldsig#sha1&quot;/&gt;
      &lt;DigestValue&gt;base64EncodedDigestValue&lt;/DigestValue&gt;
    &lt;/Reference&gt;
  &lt;/SignedInfo&gt;
  &lt;SignatureValue&gt;base64EncodedSignatureValue&lt;/SignatureValue&gt;
  &lt;KeyInfo&gt;
    &lt;KeyValue&gt;
      &lt;RSAKeyValue&gt;
        &lt;Modulus&gt;base64EncodedModulus&lt;/Modulus&gt;
        &lt;Exponent&gt;base64EncodedExponent&lt;/Exponent&gt;
      &lt;/RSAKeyValue&gt;
    &lt;/KeyValue&gt;
    &lt;X509Data&gt;
      &lt;X509Certificate&gt;base64EncodedCertificate&lt;/X509Certificate&gt;
    &lt;/X509Data&gt;
  &lt;/KeyInfo&gt;
&lt;/Signature&gt;
```

### Key Components

1. **SignedInfo**: Contains information about what data is being signed.
   - **CanonicalizationMethod**: Specifies how the XML is normalized before signing.
   - **SignatureMethod**: Specifies the algorithm used for signing (RSA-SHA1).
   - **Reference**: Points to the data being signed.
     - **Transforms**: Describes transformations applied to the data before digesting.
     - **DigestMethod**: Specifies the algorithm used for the digest (SHA1).
     - **DigestValue**: Contains the base64-encoded digest of the data.

2. **SignatureValue**: Contains the base64-encoded signature of the canonicalized SignedInfo element.

3. **KeyInfo**: Contains information about the key used to validate the signature.
   - **KeyValue/RSAKeyValue**: Contains the RSA key parameters (modulus and exponent).
   - **X509Data/X509Certificate**: Contains the X.509 certificate used for signing.

## Signing Process

The Derafu Signature library implements XML signing following these steps:

### 1. Preparing the Data

- If a reference ID is specified, the referenced element is located in the XML document.
- Otherwise, the entire XML document is used (excluding any existing Signature elements).

### 2. Calculating the Digest Value

- The data is canonicalized using the C14N algorithm.
- The canonicalized data is converted to ISO-8859-1 encoding.
- The SHA1 digest of the data is calculated and base64-encoded.

### 3. Creating the SignedInfo Element

- The SignedInfo element is created with the appropriate CanonicalizationMethod, SignatureMethod, and Reference elements.
- The DigestValue is included in the Reference element.

### 4. Calculating the Signature Value

- The SignedInfo element is canonicalized and converted to ISO-8859-1 encoding.
- The canonicalized SignedInfo is signed using the private key from the certificate.
- The resulting signature is base64-encoded and included in the SignatureValue element.

### 5. Including Key Information

- The public key components (modulus and exponent) are extracted from the certificate.
- The certificate itself is included in the X509Certificate element.

### 6. Adding the Signature to the Document

- The complete Signature element is added to the XML document, typically as the last child of the root element.

## Validation Process

The Derafu Signature library validates XML signatures following these steps:

### 1. Locating Signature Elements

- All Signature elements in the XML document are located.
- Each signature is validated independently.

### 2. Validating the Digest Value

- The reference in the signature is extracted.
- The referenced data (or the entire document) is canonicalized and converted to ISO-8859-1 encoding.
- The SHA1 digest of the data is calculated and base64-encoded.
- The calculated digest is compared to the DigestValue in the signature.
- If they don&#039;t match, the content integrity check fails.

### 3. Validating the Signature Value

- The SignedInfo element is canonicalized and converted to ISO-8859-1 encoding.
- The X.509 certificate is extracted from the X509Certificate element.
- The signature value is validated using the canonicalized SignedInfo, the SignatureValue, and the public key from the certificate.
- If the validation fails, the signer authenticity check fails.

## Reference Types

The Derafu Signature library supports two types of references:

### 1. Element References

- Specified by a URI attribute with a value starting with `#` (e.g., `URI=&quot;#elementId&quot;`).
- Only the referenced element is signed.
- The transform algorithm is set to standard C14N.

### 2. Whole Document References

- Specified by an empty URI attribute (`URI=&quot;&quot;`).
- The entire document is signed, excluding any Signature elements.
- The transform algorithm is set to &quot;enveloped signature transformation&quot;.

## Canonicalization

The Derafu Signature library uses the following canonicalization algorithms:

### 1. XML Canonicalization (C14N)

- Algorithm: `http://www.w3.org/TR/2001/REC-xml-c14n-20010315`
- Ensures consistent XML representation regardless of formatting differences.
- Applied to data before calculating digests and to SignedInfo before calculating signatures.

### 2. Enveloped Signature Transform

- Algorithm: `http://www.w3.org/2000/09/xmldsig#enveloped-signature`
- Excludes the signature itself when signing the entire document.
- Prevents circular references in the signing process.

## Working with References and IDs

When using element references, the referenced element must have an `ID` attribute:

```xml
&lt;root&gt;
  &lt;element ID=&quot;myElement&quot;&gt;data&lt;/element&gt;
&lt;/root&gt;
```

The reference in the signature would be:

```xml
&lt;Reference URI=&quot;#myElement&quot;&gt;
  &lt;!-- ... --&gt;
&lt;/Reference&gt;
```

The `SignatureGenerator.signXml()` method accepts the ID as a parameter:

```php
$signedXml = $signatureGenerator-&gt;signXml($xml, $certificate, &#039;myElement&#039;);
```

## Security Considerations

### Digest Algorithm

The library uses SHA1 for digest calculation. While SHA1 is considered cryptographically weak for certain applications, it remains the standard algorithm specified in the XML-DSIG specification.

### Signature Algorithm

The library uses RSA-SHA1 for signature calculation, which is the standard algorithm specified in the XML-DSIG specification.

### Certificate Handling

The library includes the entire X.509 certificate in the signature, which allows for complete validation including certificate chain verification (although this is not currently implemented in the library).

## Compatibility

The XML signatures generated by the Derafu Signature library follow the W3C XML-DSIG standard and should be compatible with other XML-DSIG implementations. However, different implementations may have subtle differences in canonicalization or other aspects of the signing process.

## Common Issues

### Invalid References

If a reference ID is specified but doesn&#039;t exist in the XML document, the signing process will fail.

### Malformed XML

If the XML document is not well-formed, both signing and validation will fail.

### Character Encoding

The library uses ISO-8859-1 encoding for canonicalized XML to ensure consistent digest and signature calculation across different systems.

## Example Usage

### Signing with a Reference

```php
$xml = &#039;&lt;root&gt;&lt;element ID=&quot;myElement&quot;&gt;data&lt;/element&gt;&lt;/root&gt;&#039;;
$signedXml = $signatureService-&gt;signXml($xml, $certificate, &#039;myElement&#039;);
```

### Signing the Entire Document

```php
$xml = &#039;&lt;root&gt;&lt;element&gt;data&lt;/element&gt;&lt;/root&gt;&#039;;
$signedXml = $signatureService-&gt;signXml($xml, $certificate);
```

### Validating a Signature

```php
try {
    $signatureService-&gt;validateXml($signedXml);
    echo &quot;Signature is valid!&quot;;
} catch (SignatureException $e) {
    echo &quot;Signature validation failed: &quot; . $e-&gt;getMessage();
}
```





---
Last updated on 09/09/2026

