Skip to content

Commit b86b040

Browse files
committed
Adding the EnumFormatter
1 parent b092e2a commit b86b040

7 files changed

Lines changed: 345 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
All Notable changes to `Csv` will be documented in this file
44

5+
## [Next](https://github.com/thephpleague/csv/compare/9.27.0...9.27.1) - TBD
6+
7+
### Added
8+
9+
- `EnumFormatter` to format Enum for CSV serialization
10+
11+
### Deprecated
12+
13+
- None
14+
15+
### Fixed
16+
17+
- None
18+
19+
### Remove
20+
21+
- None
22+
523
## [9.27.1](https://github.com/thephpleague/csv/compare/9.27.0...9.27.1) - 2025-10-25
624

725
### Added

composer.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@
3232
"require-dev": {
3333
"ext-dom": "*",
3434
"ext-xdebug": "*",
35-
"friendsofphp/php-cs-fixer": "^3.75.0",
36-
"phpbench/phpbench": "^1.4.1",
37-
"phpstan/phpstan": "^1.12.27",
35+
"friendsofphp/php-cs-fixer": "^3.92.3",
36+
"phpbench/phpbench": "^1.4.3",
37+
"phpstan/phpstan": "^1.12.32",
3838
"phpstan/phpstan-deprecation-rules": "^1.2.1",
3939
"phpstan/phpstan-phpunit": "^1.4.2",
4040
"phpstan/phpstan-strict-rules": "^1.6.2",
41-
"phpunit/phpunit": "^10.5.16 || ^11.5.22 || ^12.3.6",
42-
"symfony/var-dumper": "^6.4.8 || ^7.3.0"
41+
"phpunit/phpunit": "^10.5.16 || ^11.5.22 || ^12.5.4",
42+
"symfony/var-dumper": "^6.4.8 || ^7.4.0 || ^8.0"
4343
},
4444
"autoload": {
4545
"psr-4": {

docs/9.0/writer/helpers.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,89 @@ $writer->insertOne(["foo", "bar"]); //will trigger a CannotInsertRecord exceptio
3232
## Charset formatter
3333

3434
[League\Csv\CharsetConverter](/9.0/converter/charset/) will help you encode your records depending on your settings.
35+
36+
## Enum Formatter
37+
38+
The `League\Csv\EnumFormatter` class allows serializing `Enum` introduced in PHP8.1 as CSV field.
39+
40+
The class is **an immutable formatter** designed to convert PHP enums (`UnitEnum`) into scalar or
41+
serializable values suitable for CSV export (or similar flat formats).
42+
43+
It can be used directly as a callable and supports multiple exclusive formatting strategies.
44+
45+
| Strategy | Description | Named constructors |
46+
|--------------|-----------------------------------------------------|--------------------|
47+
| **Name** | Uses the enum case name (`UnitEnum::name`) | `usingName` |
48+
| **Value** | Uses the backed value of a `BackedEnum` | `usingValue` |
49+
| **JSON** | Uses `jsonSerialize()` for `JsonSerializable` enums | `usingJson` |
50+
| **Callback** | Uses a user-defined callable | `usingCallback` |
51+
52+
### Encoding a single enum
53+
54+
You may also encode a single enum manually:
55+
56+
```php
57+
use League\Csv\EnumFormatter;
58+
59+
$value = EnumFormatter::useJson()->encode(Pure::Foo);
60+
// returns "foo"
61+
```
62+
63+
### Encoding Enum found in records
64+
65+
```php
66+
use League\Csv\EnumFormatter;
67+
68+
enum Status
69+
{
70+
case Active;
71+
case Inactive;
72+
}
73+
74+
$record = [
75+
'id' => 1,
76+
'status' => Status::Active,
77+
];
78+
79+
$formatter = EnumFormatter::usingName();
80+
$result = $formatter($record);
81+
// ['id' => 1, 'status' => 'Active']
82+
```
83+
84+
The class can be used as a Formatter by the `Writer` as follows:
85+
86+
```php
87+
use League\Csv\Writer;
88+
use League\Csv\EnumFormatter;
89+
90+
enum Pure implements JsonSerializable
91+
{
92+
case Foo;
93+
case Bar;
94+
95+
public function jsonSerialize(): string
96+
{
97+
return strtolower($this->name);
98+
}
99+
}
100+
101+
$arr = ['city' => Pure::Foo, 'habitants' => 7_000_000];
102+
$doc = Writer::fromString();
103+
$doc->addFormatter(EnumFormatter::usingCallback(fn (UnitEnum $value) => 'fourty-two'));
104+
$doc->insertOne($arr);
105+
106+
$doc->toString();
107+
// returns "fourty-two,7000000 \n",
108+
```
109+
110+
### Error Handling
111+
112+
If an enum cannot be serialized using the selected strategy, a `TypeError` is thrown:
113+
114+
```php
115+
use League\Csv\EnumFormatter;
116+
117+
$formatter = EnumFormatter::useNative();
118+
$value = $formatter->encode(Pure::Foo);
119+
// Enum `Pure` cannot be serialized for CSV.
120+
```

docs/assets/img/csv_large_logo.png

20.8 KB
Loading

docs/assets/img/site.webmanifest

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name":"",
3+
"short_name":"",
4+
"icons":[
5+
{
6+
"src":"/android-chrome-192x192.png",
7+
"sizes":"192x192",
8+
"type":"image/png"
9+
},
10+
{
11+
"src":"/android-chrome-512x512.png",
12+
"sizes":"512x512",
13+
"type":"image/png"
14+
}
15+
],
16+
"theme_color":"#ffffff",
17+
"background_color":"#ffffff",
18+
"display":"standalone"
19+
}

src/EnumFormatter.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
/**
4+
* League.Csv (https://csv.thephpleague.com)
5+
*
6+
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace League\Csv;
15+
16+
use BackedEnum;
17+
use Closure;
18+
use JsonSerializable;
19+
use TypeError;
20+
use UnitEnum;
21+
22+
/**
23+
* Formatting strategies (exclusive):
24+
* - Name (UnitEnum::name)
25+
* - Value (BackedEnum::value)
26+
* - JSON (JsonSerializable)
27+
* - callback
28+
*/
29+
final class EnumFormatter
30+
{
31+
private const VALUE_FORMAT = 1;
32+
private const JSON_FORMAT = 2;
33+
private const CALLBACK_FORMAT = 3;
34+
private const NAME_FORMAT = 4;
35+
36+
private function __construct(
37+
private int $format = self::VALUE_FORMAT,
38+
private ?Closure $callback = null
39+
) {
40+
}
41+
42+
public static function usingValue(): self
43+
{
44+
return new self(self::VALUE_FORMAT);
45+
}
46+
47+
public static function usingName(): self
48+
{
49+
return new self(self::NAME_FORMAT);
50+
}
51+
52+
public static function usingJson(): self
53+
{
54+
return new self(self::JSON_FORMAT);
55+
}
56+
57+
/**
58+
* @param callable(UnitEnum): mixed $callback
59+
*/
60+
public static function usingCallback(callable $callback): self
61+
{
62+
return new self(self::CALLBACK_FORMAT, $callback instanceof Closure ? $callback : $callback(...));
63+
}
64+
65+
/**
66+
* Enable using the class as a formatter for the {@link Writer}.
67+
*
68+
* @throws TypeError if encoding is invalid
69+
*/
70+
public function __invoke(array $record): array
71+
{
72+
return array_map(fn (mixed $value) => !$value instanceof UnitEnum ? $value : $this->encode($value), $record);
73+
}
74+
75+
/**
76+
* Encodes the UnitEnum in a representation suitable to be used
77+
* by PHP's fputcsv and fwrite functions without emitting errors
78+
*
79+
* @throws TypeError If the encoding does not work
80+
*/
81+
public function encode(UnitEnum $value): mixed
82+
{
83+
return match (true) {
84+
self::NAME_FORMAT === $this->format => $value->name,
85+
self::VALUE_FORMAT === $this->format && $value instanceof BackedEnum => $value->value,
86+
self::JSON_FORMAT === $this->format && $value instanceof JsonSerializable => $value->jsonSerialize(),
87+
self::CALLBACK_FORMAT === $this->format && null !== $this->callback => ($this->callback)($value),
88+
default => throw new TypeError('Enum `'.$value::class.'` cannot be serialized for CSV.'),
89+
};
90+
}
91+
}

src/EnumFormatterTest.php

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
3+
/**
4+
* League.Csv (https://csv.thephpleague.com)
5+
*
6+
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace League\Csv;
15+
16+
use JsonSerializable;
17+
use PHPUnit\Framework\TestCase;
18+
use TypeError;
19+
use UnitEnum;
20+
21+
use function strtolower;
22+
23+
final class EnumFormatterTest extends TestCase
24+
{
25+
public function test_it_can_convert_backed_enum(): void
26+
{
27+
$arr = ['city' => City::Brussels, 'habitants' => 7_000_000];
28+
$doc = Writer::fromString();
29+
$doc->addFormatter(EnumFormatter::usingValue());
30+
$doc->insertOne($arr);
31+
32+
self::assertSame('Brussels,7000000'."\n", $doc->toString());
33+
}
34+
35+
public function test_it_can_convert_backed_enum_using_json_serializable(): void
36+
{
37+
$arr = ['city' => City::Brussels, 'habitants' => 7_000_000];
38+
$doc = Writer::fromString();
39+
$doc->addFormatter(EnumFormatter::usingJson());
40+
$doc->insertOne($arr);
41+
42+
self::assertSame('brussels,7000000'."\n", $doc->toString());
43+
}
44+
45+
public function test_it_can_convert_backed_enum_using_callback(): void
46+
{
47+
$arr = ['city' => City::Brussels, 'habitants' => 7_000_000];
48+
$doc = Writer::fromString();
49+
$doc->addFormatter(EnumFormatter::usingCallback(fn (UnitEnum $value) => 'fourty-two'));
50+
$doc->insertOne($arr);
51+
52+
self::assertSame('fourty-two,7000000'."\n", $doc->toString());
53+
}
54+
55+
public function test_it_can_convert_backed_enum_using_name(): void
56+
{
57+
$arr = ['city' => City::KINSHASA, 'habitants' => 7_000_000];
58+
$doc = Writer::fromString();
59+
$doc->addFormatter(EnumFormatter::usingName());
60+
$doc->insertOne($arr);
61+
62+
self::assertSame('KINSHASA,7000000'."\n", $doc->toString());
63+
}
64+
65+
public function test_it_fails_to_convert_an_unit_enum(): void
66+
{
67+
$arr = ['city' => Pure::Foo, 'habitants' => 7_000_000];
68+
$doc = Writer::fromString();
69+
$doc->addFormatter(EnumFormatter::usingValue());
70+
$this->expectException(TypeError::class);
71+
$doc->insertOne($arr);
72+
}
73+
74+
public function test_it_uses_json_serializable_representation_to_convert_an_unit_enum(): void
75+
{
76+
$arr = ['city' => Pure::Foo, 'habitants' => 7_000_000];
77+
$doc = Writer::fromString();
78+
$doc->addFormatter(EnumFormatter::usingJson());
79+
$doc->insertOne($arr);
80+
81+
self::assertSame('foo,7000000'."\n", $doc->toString());
82+
}
83+
84+
public function test_it_uses_name_representation_to_convert_an_unit_enum(): void
85+
{
86+
$arr = ['city' => Pure::Foo, 'habitants' => 7_000_000];
87+
$doc = Writer::fromString();
88+
$doc->addFormatter(EnumFormatter::usingName());
89+
$doc->insertOne($arr);
90+
91+
self::assertSame('Foo,7000000'."\n", $doc->toString());
92+
}
93+
94+
public function test_it_uses_callback_representation_to_convert_an_unit_enum(): void
95+
{
96+
$arr = ['city' => Pure::Foo, 'habitants' => 7_000_000];
97+
$doc = Writer::fromString();
98+
$doc->addFormatter(EnumFormatter::usingCallback(fn (UnitEnum $value) => 'fourty-two'));
99+
$doc->insertOne($arr);
100+
101+
self::assertSame('fourty-two,7000000'."\n", $doc->toString());
102+
}
103+
}
104+
105+
enum City: string implements JsonSerializable
106+
{
107+
case Kigali = 'Kigali';
108+
case KINSHASA = 'Kinshasa';
109+
case Brussels = 'Brussels';
110+
111+
public function jsonSerialize(): string
112+
{
113+
return strtolower($this->value);
114+
}
115+
}
116+
117+
enum Pure implements JsonSerializable
118+
{
119+
case Foo;
120+
case Bar;
121+
122+
public function jsonSerialize(): string
123+
{
124+
return strtolower($this->name);
125+
}
126+
}

0 commit comments

Comments
 (0)