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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 101 additions & 5 deletions docs/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,14 +218,110 @@ public function __construct(
DocumentFormattingMode $documentFormatting = DocumentFormattingMode::Normalized,
bool $skipNulls = false,
TomlVersion $version = TomlVersion::V11,
bool $integerGrouping = false,
bool $trailingComma = false,
bool $dottedKeys = false,
ArrayStyle $arrayStyle = ArrayStyle::Inline,
int $arrayAutoThreshold = 3,
string $indent = ' ',
)
```

- `$sortKeys`: Sort keys alphabetically in output
- `$newline`: Newline sequence to use during encoding
- `$documentFormatting`: `Normalized` or `SourceAware` for `encodeDocument()`
- `$skipNulls`: Omit `null` values during `encode()` instead of throwing `EncodeException`
- `$version`: default TOML output mode. Use `TomlVersion::V10` for strict TOML 1.0 encoding rules
### Options Reference

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `sortKeys` | `bool` | `false` | Sort keys alphabetically in output |
| `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 |
| `integerGrouping` | `bool` | `false` | Add underscores to large integers (e.g., `1_000_000`) |
| `trailingComma` | `bool` | `false` | Add trailing commas to inline arrays |
| `dottedKeys` | `bool` | `false` | Use dotted keys instead of table sections |
| `arrayStyle` | `ArrayStyle` | `Inline` | Array formatting style (see below) |
| `arrayAutoThreshold` | `int` | `3` | Item count threshold for `ArrayStyle::Auto` |
| `indent` | `string` | `' '` | Indentation string for multiline arrays |

### ArrayStyle

Controls how arrays are formatted in output.

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

| Style | Description |
|-------|-------------|
| `ArrayStyle::Inline` | Arrays on a single line: `[1, 2, 3]` |
| `ArrayStyle::Multiline` | One item per line with indentation |
| `ArrayStyle::Auto` | Multiline if items exceed `arrayAutoThreshold` |

**Examples:**

```php
// Inline (default)
$toml = Toml::encode(['ports' => [8080, 8081, 8082]]);
// ports = [8080, 8081, 8082]

// Multiline
$toml = Toml::encode(
['ports' => [8080, 8081, 8082]],
new EncoderOptions(arrayStyle: ArrayStyle::Multiline),
);
// ports = [
// 8080,
// 8081,
// 8082,
// ]

// Auto (multiline when > 3 items)
$toml = Toml::encode(
['ports' => [8080, 8081, 8082, 8083, 8084]],
new EncoderOptions(arrayStyle: ArrayStyle::Auto, arrayAutoThreshold: 3),
);
// ports = [
// 8080,
// 8081,
// 8082,
// 8083,
// 8084,
// ]

// Custom indent (2 spaces)
$toml = Toml::encode(
['items' => [1, 2, 3]],
new EncoderOptions(arrayStyle: ArrayStyle::Multiline, indent: ' '),
);
// items = [
// 1,
// 2,
// 3,
// ]
```

### Integer Grouping

```php
$toml = Toml::encode(
['large' => 1000000],
new EncoderOptions(integerGrouping: true),
);
// large = 1_000_000
```

### Dotted Keys

```php
$toml = Toml::encode(
['database' => ['host' => 'localhost', 'port' => 5432]],
new EncoderOptions(dottedKeys: true),
);
// database.host = "localhost"
// database.port = 5432
```

### TOML 1.0 Mode

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.

Expand Down
31 changes: 31 additions & 0 deletions src/Encoder/ArrayStyle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace PhpCollective\Toml\Encoder;

enum ArrayStyle: string
{
/**
* Arrays are encoded inline on a single line.
* Example: ports = [8080, 8081, 8082]
*/
case Inline = 'inline';

/**
* Arrays are always encoded with one item per line.
* Example:
* ports = [
* 8080,
* 8081,
* 8082,
* ]
*/
case Multiline = 'multiline';

/**
* Arrays with more than the threshold number of items are multiline.
* Short arrays remain inline for readability.
*/
case Auto = 'auto';
}
37 changes: 37 additions & 0 deletions src/Encoder/Encoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -1240,13 +1240,50 @@ private function encodeArray(array $value): string
if ($this->options->skipNulls) {
$value = array_filter($value, static fn ($v) => $v !== null);
}

$items = array_map(fn ($v) => $this->encodeValue($v), $value);

if ($this->shouldUseMultilineArray($value)) {
return $this->encodeMultilineArrayFromValues($items);
}

$trailing = $this->options->trailingComma && $items !== [] ? ',' : '';

return '[' . implode(', ', $items) . $trailing . ']';
}

/**
* @param array<mixed> $value
*/
private function shouldUseMultilineArray(array $value): bool
{
return match ($this->options->arrayStyle) {
ArrayStyle::Inline => false,
ArrayStyle::Multiline => true,
ArrayStyle::Auto => count($value) > $this->options->arrayAutoThreshold,
};
}

/**
* @param array<string> $items
*/
private function encodeMultilineArrayFromValues(array $items): string
{
if ($items === []) {
return '[]';
}

$newline = $this->options->newline;
$indent = $this->options->indent;
$output = '[' . $newline;

foreach ($items as $item) {
$output .= $indent . $item . ',' . $newline;
}

return $output . ']';
}

/**
* @param array<string, mixed> $value
*/
Expand Down
3 changes: 3 additions & 0 deletions src/Encoder/EncoderOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public function __construct(
public bool $integerGrouping = false,
public bool $trailingComma = false,
public bool $dottedKeys = false,
public ArrayStyle $arrayStyle = ArrayStyle::Inline,
public int $arrayAutoThreshold = 3,
public string $indent = ' ',
) {
}
}
108 changes: 108 additions & 0 deletions tests/Encoder/EncoderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace PhpCollective\Toml\Test\Encoder;

use DateTimeImmutable;
use PhpCollective\Toml\Encoder\ArrayStyle;
use PhpCollective\Toml\Encoder\DocumentFormattingMode;
use PhpCollective\Toml\Encoder\Encoder;
use PhpCollective\Toml\Encoder\EncoderOptions;
Expand Down Expand Up @@ -450,4 +451,111 @@ public function testEncodeDottedKeysDisabledByDefault(): void
$this->assertStringContainsString('host = "localhost"', $result);
$this->assertStringNotContainsString('database.host', $result);
}

public function testEncodeArrayStyleInlineByDefault(): void
{
$encoder = new Encoder(new EncoderOptions());

$result = $encoder->encode([
'items' => [1, 2, 3, 4, 5],
]);

$this->assertStringContainsString('items = [1, 2, 3, 4, 5]', $result);
$this->assertStringNotContainsString("\n ", $result);
}

public function testEncodeArrayStyleMultiline(): void
{
$encoder = new Encoder(new EncoderOptions(arrayStyle: ArrayStyle::Multiline));

$result = $encoder->encode([
'items' => [1, 2, 3],
]);

$expected = "items = [\n 1,\n 2,\n 3,\n]";
$this->assertStringContainsString($expected, $result);
}

public function testEncodeArrayStyleMultilineEmpty(): void
{
$encoder = new Encoder(new EncoderOptions(arrayStyle: ArrayStyle::Multiline));

$result = $encoder->encode([
'empty' => [],
]);

$this->assertStringContainsString('empty = []', $result);
}

public function testEncodeArrayStyleAutoAboveThreshold(): void
{
$encoder = new Encoder(new EncoderOptions(
arrayStyle: ArrayStyle::Auto,
arrayAutoThreshold: 3,
));

$result = $encoder->encode([
'items' => [1, 2, 3, 4],
]);

$expected = "items = [\n 1,\n 2,\n 3,\n 4,\n]";
$this->assertStringContainsString($expected, $result);
}

public function testEncodeArrayStyleAutoBelowThreshold(): void
{
$encoder = new Encoder(new EncoderOptions(
arrayStyle: ArrayStyle::Auto,
arrayAutoThreshold: 3,
));

$result = $encoder->encode([
'items' => [1, 2, 3],
]);

$this->assertStringContainsString('items = [1, 2, 3]', $result);
$this->assertStringNotContainsString("[\n", $result);
}

public function testEncodeArrayStyleCustomIndent(): void
{
$encoder = new Encoder(new EncoderOptions(
arrayStyle: ArrayStyle::Multiline,
indent: ' ',
));

$result = $encoder->encode([
'items' => [1, 2],
]);

$expected = "items = [\n 1,\n 2,\n]";
$this->assertStringContainsString($expected, $result);
}

public function testEncodeArrayStyleWithTabIndent(): void
{
$encoder = new Encoder(new EncoderOptions(
arrayStyle: ArrayStyle::Multiline,
indent: "\t",
));

$result = $encoder->encode([
'items' => [1, 2],
]);

$expected = "items = [\n\t1,\n\t2,\n]";
$this->assertStringContainsString($expected, $result);
}

public function testEncodeArrayStyleMultilineWithStrings(): void
{
$encoder = new Encoder(new EncoderOptions(arrayStyle: ArrayStyle::Multiline));

$result = $encoder->encode([
'hosts' => ['alpha', 'beta', 'gamma'],
]);

$expected = "hosts = [\n \"alpha\",\n \"beta\",\n \"gamma\",\n]";
$this->assertStringContainsString($expected, $result);
}
}
Loading