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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ name: CI

on:
push:
branches: [master]
branches: [master, v1]
pull_request:
branches: [master]
branches: [master, v1]

jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.2', '8.4']
php: ['8.1', '8.5']

name: PHP ${{ matrix.php }}

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand All @@ -40,7 +40,7 @@ jobs:
name: Coding Standards

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Latest Stable Version](https://img.shields.io/packagist/v/php-collective/toml?style=flat-square)](https://packagist.org/packages/php-collective/toml)
[![Total Downloads](https://img.shields.io/packagist/dt/php-collective/toml?style=flat-square)](https://packagist.org/packages/php-collective/toml)
[![PHPStan](https://img.shields.io/badge/PHPStan-level%208-brightgreen.svg?style=flat-square)](https://phpstan.org/)
[![PHP Version](https://img.shields.io/badge/php-%3E%3D8.2-8892BF.svg?style=flat-square)](https://php.net)
[![PHP Version](https://img.shields.io/badge/php-%3E%3D8.1-8892BF.svg?style=flat-square)](https://php.net)
[![Software License](https://img.shields.io/badge/license-MIT-green.svg?style=flat-square)](LICENSE)

A [TOML](https://toml.io/) (v1.0 and v1.1) parser and encoder for PHP with AST access and collected parse errors.
Expand All @@ -22,7 +22,7 @@ A [TOML](https://toml.io/) (v1.0 and v1.1) parser and encoder for PHP with AST a

## Requirements

- PHP 8.2 or higher
- PHP 8.1 or higher

## Installation

Expand Down Expand Up @@ -185,7 +185,7 @@ The library currently supports:

See the [Support Matrix](https://php-collective.github.io/toml/reference/support-matrix) for current coverage and known gaps.

Versioned behavior is available through `TomlVersion` and `EncoderOptions(version: ...)`. The default remains TOML 1.1-compatible; use `TomlVersion::V10` when you need strict TOML 1.0 parsing or output rules.
Versioned behavior is available through `TomlVersion` and `EncoderOptions(version: ...)`. Parsing defaults to TOML 1.1-compatible input, while encoding defaults to TOML 1.0-compatible output for interoperability. Use `TomlVersion::V10` when you need strict TOML 1.0 parsing, or `EncoderOptions(version: TomlVersion::V11)` when you want TOML 1.1 output.

For explicit local temporal encoding, use:

Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
],
"homepage": "https://php-collective.github.io/toml/",
"require": {
"php": ">=8.2",
"php": ">=8.1",
"symfony/polyfill-mbstring": "^1.30"
},
"require-dev": {
"phpunit/phpunit": "^11.5 || ^12.5 || ^13.0",
"phpunit/phpunit": "^10.5 || ^11.5 || ^12.5 || ^13.0",
"phpstan/phpstan": "^2.0",
"php-collective/code-sniffer": "dev-master"
},
Expand Down
18 changes: 18 additions & 0 deletions docs/guide/encoding.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,24 @@ message = "Hello\nWorld"
path = "C:\\Users\\name"
```

Literal or multiline string output can be enabled explicitly:

```php
use PhpCollective\Toml\Encoder\StringStyle;

$toml = Toml::encode([
'path' => 'C:\Users\name',
], new EncoderOptions(stringStyle: StringStyle::Literal));
```

Output:

```toml
path = 'C:\Users\name'
```

The default remains basic strings. Literal styles fall back to basic strings when a value cannot be represented safely in the requested style.

## Re-encoding from AST

```php
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Install via Composer:
composer require php-collective/toml
```

**Requirements:** PHP 8.2+
**Requirements:** PHP 8.1+

## Quick Start

Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ layout: home
hero:
name: PHP Toml
text: TOML 1.0/1.1 Parser
tagline: A modern PHP 8.2+ parser and encoder with AST access, trivia preservation, and error recovery
tagline: A modern PHP 8.1+ parser and encoder with AST access, trivia preservation, and error recovery
image:
src: /logo.svg
alt: PHP Toml
Expand All @@ -31,7 +31,7 @@ features:
details: Full abstract syntax tree for analysis, transformation, or editor integrations
- icon: ⚡
title: Zero Dependencies
details: No required extensions - pure PHP 8.2+ with optional php-ds for performance
details: No required extensions - pure PHP 8.1+ with optional php-ds for performance
- icon: 🔄
title: Structured Re-Encode
details: Parse TOML to an AST, modify nodes, and re-encode with partial trivia and layout preservation
Expand Down
42 changes: 34 additions & 8 deletions docs/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ $config = Toml::decodeFile('/path/to/config.toml');
public static function parse(
string $input,
bool $preserveTrivia = false,
TomlVersion $version = TomlVersion::V11,
TomlVersion $version = TomlVersion::V10,
): Document
```

Expand Down Expand Up @@ -91,9 +91,8 @@ Encodes a PHP array to a TOML string.
$toml = Toml::encode(['key' => 'value']);
// key = "value"

$strict = Toml::encode(
$toml = Toml::encode(
['time' => new \PhpCollective\Toml\Value\LocalTime('07:32')],
new EncoderOptions(version: TomlVersion::V10),
);
// time = 07:32:00
```
Expand Down Expand Up @@ -250,6 +249,7 @@ public function __construct(
?int $multilineThreshold = null,
?int $inlineTableThreshold = null,
string $indent = ' ',
StringStyle $stringStyle = StringStyle::Basic,
)
```

Expand All @@ -275,7 +275,7 @@ $toml = Toml::encode($data, EncoderOptions::diffFriendly());
| `newline` | `string` | `"\n"` | Newline sequence to use (`"\n"` or `"\r\n"`) |
| `documentFormatting` | `DocumentFormattingMode` | `Normalized` | `Normalized` or `SourceAware` for `encodeDocument()` |
| `skipNulls` | `bool` | `false` | Omit `null` values instead of throwing `EncodeException` |
| `version` | `TomlVersion` | `V11` | TOML version for output rules |
| `version` | `TomlVersion` | `V10` | TOML version for output rules |
| `integerGrouping` | `bool` | `false` | Add underscores to large integers (e.g., `1_000_000`) |
| `integerBase` | `IntegerBase` | `Decimal` | Radix for integers in `encode()` output (`Hexadecimal`/`Octal`/`Binary` emit `0x`/`0o`/`0b`; negatives stay decimal) |
| `trailingComma` | `bool` | `false` | Add trailing commas to inline arrays |
Expand All @@ -285,6 +285,7 @@ $toml = Toml::encode($data, EncoderOptions::diffFriendly());
| `multilineThreshold` | `?int` | `null` | Opt-in string length threshold for multiline basic strings in `encode()` |
| `inlineTableThreshold` | `?int` | `null` | Opt-in key count threshold for small flat nested arrays to encode as inline tables |
| `indent` | `string` | `' '` | Indentation string for multiline arrays |
| `stringStyle` | `StringStyle` | `Basic` | String formatting style for plain PHP strings |

### ArrayStyle

Expand Down Expand Up @@ -387,18 +388,43 @@ $toml = Toml::encode(
// database.port = 5432
```

### TOML 1.0 Mode
### StringStyle

Controls how plain PHP strings are formatted during `encode()`.

```php
use PhpCollective\Toml\Encoder\StringStyle;
```

| Style | Description |
|-------|-------------|
| `StringStyle::Basic` | Basic double-quoted strings. This is the default and preserves current output behavior |
| `StringStyle::Literal` | Single-quoted literal strings when representable; falls back to basic strings when needed |
| `StringStyle::MultiLineBasic` | Multiline basic strings |
| `StringStyle::MultiLineLiteral` | Multiline literal strings when representable; falls back to multiline basic strings when needed |

```php
$toml = Toml::encode(
['path' => 'C:\Users\name'],
new EncoderOptions(stringStyle: StringStyle::Literal),
);
// path = 'C:\Users\name'
```

### TOML Version Modes

Parsing and decoding default to TOML 1.1-compatible input. Encoding defaults to TOML 1.0-compatible output for interoperability and normalizes local times and local datetimes to include seconds where possible.

In strict TOML 1.0 mode, `encode()` normalizes local times and local datetimes to include seconds where possible. `encodeDocument()` in `DocumentFormattingMode::SourceAware` throws `EncodeException` if preserving the parsed source would keep TOML 1.1-only syntax.
`encodeDocument()` in `DocumentFormattingMode::SourceAware` throws `EncodeException` if preserving the parsed source would keep TOML 1.1-only syntax while `EncoderOptions(version: TomlVersion::V10)` is active.

## TomlVersion

Controls version-specific parser and encoder behavior.

- `TomlVersion::V11`
Default behavior. Accepts TOML 1.1 syntax and emits TOML 1.1-compatible output.
Default parser/decoder behavior. Accepts TOML 1.1 syntax and emits TOML 1.1-compatible output when passed to `EncoderOptions`.
- `TomlVersion::V10`
Strict TOML 1.0 mode for parsing, decoding, and encoding.
Strict TOML 1.0 mode for parsing and decoding. Default encoder behavior.

## DocumentFormattingMode

Expand Down
4 changes: 2 additions & 2 deletions docs/reference/support-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ It is intentionally narrower than a blanket "full TOML support" claim. The goal

The sections below intentionally separate parsing/decoding, encoding, and round-trip editing so support claims stay scoped to the actual surface being described.

The default API behavior is TOML 1.1-compatible. Strict TOML 1.0 parsing/decoding and encoding are available through `TomlVersion::V10` and `EncoderOptions(version: TomlVersion::V10)`.
The default parser/decoder behavior is TOML 1.1-compatible. The default encoder behavior is TOML 1.0-compatible for interoperability; TOML 1.1 output is available through `EncoderOptions(version: TomlVersion::V11)`.

## Status Legend

Expand Down Expand Up @@ -68,7 +68,7 @@ The default API behavior is TOML 1.1-compatible. Strict TOML 1.0 parsing/decodin

| Feature | Status | Notes |
|---------|--------|-------|
| Strings | Supported | Encoded as basic strings; control characters are escaped (`\uXXXX` / shorthand) so output stays valid TOML |
| Strings | Supported | Encoded as basic strings by default; literal and multiline styles are opt-in via `EncoderOptions`. Control characters are escaped (`\uXXXX` / shorthand) so output stays valid TOML |
| Integers | Supported | |
| Floats | Supported | Round-tripped at full `double` precision (shortest exact representation) |
| Booleans | Supported | |
Expand Down
1 change: 0 additions & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
colors="true"
cacheDirectory=".phpunit.cache"
executionOrder="depends,defects"
shortenArraysForExportThreshold="10"
displayDetailsOnTestsThatTriggerWarnings="true"
displayDetailsOnTestsThatTriggerDeprecations="true"
displayDetailsOnIncompleteTests="true"
Expand Down
74 changes: 64 additions & 10 deletions src/Encoder/Encoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
use PhpCollective\Toml\Ast\Value\LocalDateTime;
use PhpCollective\Toml\Ast\Value\LocalTime;
use PhpCollective\Toml\Ast\Value\OffsetDateTime;
use PhpCollective\Toml\Ast\Value\StringStyle;
use PhpCollective\Toml\Ast\Value\StringStyle as AstStringStyle;
use PhpCollective\Toml\Ast\Value\StringValue;
use PhpCollective\Toml\Ast\Value\Value;
use PhpCollective\Toml\Exception\EncodeException;
Expand Down Expand Up @@ -198,11 +198,7 @@ private function encodeValue(mixed $value): string
}

if (is_string($value)) {
if ($this->options->multilineThreshold !== null && mb_strlen($value) > $this->options->multilineThreshold) {
return $this->encodeMultilineBasicString($value);
}

return $this->encodeString($value);
return $this->encodeStringValue($value);
}

if ($value instanceof DateTimeInterface) {
Expand Down Expand Up @@ -372,10 +368,10 @@ private function encodeAstStringValue(StringValue $value): string
}

return match ($value->style) {
StringStyle::Basic => $this->encodeString($value->value),
StringStyle::Literal => "'" . $value->value . "'",
StringStyle::MultiLineBasic => $this->encodeMultilineBasicString($value->value),
StringStyle::MultiLineLiteral => "'''\n" . $value->value . "'''",
AstStringStyle::Basic => $this->encodeString($value->value),
AstStringStyle::Literal => "'" . $value->value . "'",
AstStringStyle::MultiLineBasic => $this->encodeMultilineBasicString($value->value),
AstStringStyle::MultiLineLiteral => "'''\n" . $value->value . "'''",
};
}

Expand Down Expand Up @@ -1356,6 +1352,64 @@ private function escapeControlChars(string $value): string
) ?? $value;
}

private function encodeStringValue(string $value): string
{
$style = $this->options->stringStyle;
if ($this->options->multilineThreshold !== null && mb_strlen($value) > $this->options->multilineThreshold) {
$style = $style === StringStyle::Literal
? StringStyle::MultiLineLiteral
: StringStyle::MultiLineBasic;
}

return match ($style) {
StringStyle::Basic => $this->encodeString($value),
StringStyle::Literal => $this->encodeLiteralString($value),
StringStyle::MultiLineBasic => $this->encodeMultilineBasicString($value),
StringStyle::MultiLineLiteral => $this->encodeMultilineLiteralString($value),
};
}

private function encodeLiteralString(string $value): string
{
if (!$this->canUseSingleLineLiteralString($value)) {
return $this->encodeString($value);
}

return "'" . $value . "'";
}

private function encodeMultilineLiteralString(string $value): string
{
if (!$this->canUseMultilineLiteralString($value)) {
return $this->encodeMultilineBasicString($value);
}

return "'''\n" . $value . "'''";
}

private function canUseSingleLineLiteralString(string $value): bool
{
return !str_contains($value, "'")
&& !str_contains($value, "\n")
&& !str_contains($value, "\r")
&& !$this->containsDisallowedControlCharacter($value);
}

private function canUseMultilineLiteralString(string $value): bool
{
return !str_contains($value, "'''")
&& !$this->containsDisallowedControlCharacter($value, allowNewline: true);
}

private function containsDisallowedControlCharacter(string $value, bool $allowNewline = false): bool
{
if ($allowNewline) {
return preg_match('/[\x00-\x08\x0B-\x1F\x7F]/', $value) === 1;
}

return preg_match('/[\x00-\x08\x0A-\x1F\x7F]/', $value) === 1;
}

/**
* @param array<mixed> $value
*/
Expand Down
31 changes: 16 additions & 15 deletions src/Encoder/EncoderOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,24 @@
use PhpCollective\Toml\Ast\Value\IntegerBase;
use PhpCollective\Toml\TomlVersion;

final readonly class EncoderOptions
final class EncoderOptions
{
public function __construct(
public bool $sortKeys = false,
public string $newline = "\n",
public DocumentFormattingMode $documentFormatting = DocumentFormattingMode::Normalized,
public bool $skipNulls = false,
public TomlVersion $version = TomlVersion::V11,
public bool $integerGrouping = false,
public IntegerBase $integerBase = IntegerBase::Decimal,
public bool $trailingComma = false,
public bool $dottedKeys = false,
public ArrayStyle $arrayStyle = ArrayStyle::Inline,
public int $arrayAutoThreshold = 3,
public ?int $multilineThreshold = null,
public ?int $inlineTableThreshold = null,
public string $indent = ' ',
public readonly bool $sortKeys = false,
public readonly string $newline = "\n",
public readonly DocumentFormattingMode $documentFormatting = DocumentFormattingMode::Normalized,
public readonly bool $skipNulls = false,
public readonly TomlVersion $version = TomlVersion::V10,
public readonly bool $integerGrouping = false,
public readonly IntegerBase $integerBase = IntegerBase::Decimal,
public readonly bool $trailingComma = false,
public readonly bool $dottedKeys = false,
public readonly ArrayStyle $arrayStyle = ArrayStyle::Inline,
public readonly int $arrayAutoThreshold = 3,
public readonly ?int $multilineThreshold = null,
public readonly ?int $inlineTableThreshold = null,
public readonly string $indent = ' ',
public readonly StringStyle $stringStyle = StringStyle::Basic,
) {
}

Expand Down
Loading