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
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ final class ArticleCollection extends ArrayObject
final class Article
{
public string $title;

/**
* @var CommentCollection<int, Comment>
*/
public CommentCollection $comments;
}
```
Expand All @@ -60,7 +64,7 @@ require __DIR__ . '/vendor/autoload.php';

use App\Dto\Article;
use App\Dto\ArticleCollection;
use MagicSunday\JsonMapper\JsonMapper;
use MagicSunday\JsonMapper;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
Expand Down Expand Up @@ -89,6 +93,8 @@ var_dump($article, $articles);

The first call produces an `Article` instance with a populated `CommentCollection`; the second call returns an `ArticleCollection` containing `Article` objects.

Test coverage: `tests/JsonMapper/DocsQuickStartTest.php`.

### PHP classes
In order to guarantee a seamless mapping of a JSON response into PHP classes you should prepare your classes well.
Annotate all properties with the requested type.
Expand Down Expand Up @@ -170,7 +176,7 @@ To use the `PhpDocExtractor` extractor you need to install the `phpdocumentor/re
require __DIR__ . '/vendor/autoload.php';

use MagicSunday\JsonMapper\Converter\CamelCasePropertyNameConverter;
use MagicSunday\JsonMapper\JsonMapper;
use MagicSunday\JsonMapper;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
Expand Down Expand Up @@ -219,7 +225,7 @@ You may alternatively implement `\MagicSunday\JsonMapper\Value\TypeHandlerInterf
require __DIR__ . '/vendor/autoload.php';

use DateTimeImmutable;
use MagicSunday\JsonMapper\JsonMapper;
use MagicSunday\JsonMapper;
use MagicSunday\JsonMapper\Value\ClosureTypeHandler;
use stdClass;
use Symfony\Component\PropertyAccess\PropertyAccess;
Expand Down Expand Up @@ -294,7 +300,7 @@ and optional the name of a collection class to the method.
require __DIR__ . '/vendor/autoload.php';

use ArrayObject;
use MagicSunday\JsonMapper\JsonMapper;
use MagicSunday\JsonMapper;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
Expand Down Expand Up @@ -333,7 +339,7 @@ A complete set-up may look like this:
require __DIR__ . '/vendor/autoload.php';

use MagicSunday\JsonMapper\Converter\CamelCasePropertyNameConverter;
use MagicSunday\JsonMapper\JsonMapper;
use MagicSunday\JsonMapper;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
Expand Down Expand Up @@ -372,7 +378,7 @@ Use `JsonMapper::addCustomClassMapEntry()` when the target class depends on runt
```php
require __DIR__ . '/vendor/autoload.php';

use MagicSunday\JsonMapper\JsonMapper;
use MagicSunday\JsonMapper;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
Expand Down Expand Up @@ -413,7 +419,7 @@ The mapper operates in a lenient mode by default. Switch to strict mapping when
require __DIR__ . '/vendor/autoload.php';

use MagicSunday\JsonMapper\Configuration\JsonMapperConfiguration;
use MagicSunday\JsonMapper\JsonMapper;
use MagicSunday\JsonMapper;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
Expand Down Expand Up @@ -453,7 +459,7 @@ Type resolution is the most expensive part of a mapping run. Provide a PSR-6 cac
```php
require __DIR__ . '/vendor/autoload.php';

use MagicSunday\JsonMapper\JsonMapper;
use MagicSunday\JsonMapper;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
Expand Down
4 changes: 2 additions & 2 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The `JsonMapper` class is the main entry point for mapping arbitrary JSON struct
<?php
declare(strict_types=1);

use MagicSunday\JsonMapper\JsonMapper;
use MagicSunday\JsonMapper;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
Expand Down Expand Up @@ -82,7 +82,7 @@ declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';

use MagicSunday\JsonMapper\Converter\CamelCasePropertyNameConverter;
use MagicSunday\JsonMapper\JsonMapper;
use MagicSunday\JsonMapper;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
Expand Down
10 changes: 5 additions & 5 deletions docs/recipes/custom-name-converter.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,23 @@ declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';

use App\Converter\UpperSnakeCaseConverter;
use MagicSunday\JsonMapper\Converter\CamelCasePropertyNameConverter;
use MagicSunday\JsonMapper\JsonMapper;
use MagicSunday\JsonMapper;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;

// Collect metadata and choose the converter implementation.
// Collect metadata and provide the custom converter.
$propertyInfo = new PropertyInfoExtractor(
listExtractors: [new ReflectionExtractor()],
typeExtractors: [new PhpDocExtractor()],
);
$propertyAccessor = PropertyAccess::createPropertyAccessor();
$converter = new CamelCasePropertyNameConverter();
// or $converter = new UpperSnakeCaseConverter();
$converter = new UpperSnakeCaseConverter();

$mapper = new JsonMapper($propertyInfo, $propertyAccessor, $converter);
```

Name converters are stateless and should be declared `final`. They are applied to every property access during mapping, so keep the implementation idempotent and efficient.

Test coverage: `tests/JsonMapper/DocsCustomNameConverterTest.php`.
11 changes: 6 additions & 5 deletions docs/recipes/mapping-with-enums.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ require __DIR__ . '/vendor/autoload.php';

use App\Dto\Article;
use App\Dto\Status;
use MagicSunday\JsonMapper\Configuration\JsonMapperConfiguration;
use MagicSunday\JsonMapper\JsonMapper;
use MagicSunday\JsonMapper;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
Expand All @@ -48,12 +47,14 @@ $propertyInfo = new PropertyInfoExtractor(
typeExtractors: [new PhpDocExtractor()],
);
$propertyAccessor = PropertyAccess::createPropertyAccessor();
$configuration = JsonMapperConfiguration::lenient();

$mapper = new JsonMapper($propertyInfo, $propertyAccessor);
$article = $mapper->map($json, Article::class, configuration: $configuration);
$article = $mapper->map($json, Article::class);

assert($article instanceof Article);
assert($article->status === Status::Published);
```
The mapper validates enum values. When strict mode is enabled (`JsonMapperConfiguration::strict()`), an invalid enum value results in a `TypeMismatchException`.

The mapper validates enum values. In strict mode (`JsonMapperConfiguration::strict()`), an invalid enum value results in a `TypeMismatchException` instead of populating the property.

Test coverage: `tests/JsonMapperTest.php::mapBackedEnumFromString` and `tests/JsonMapper/JsonMapperErrorHandlingTest.php::itReportsInvalidEnumValuesInLenientMode`.
5 changes: 4 additions & 1 deletion docs/recipes/nested-collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ use App\Dto\Article;
use App\Dto\ArticleCollection;
use App\Dto\NestedTagCollection;
use App\Dto\TagCollection;
use MagicSunday\JsonMapper\JsonMapper;
use MagicSunday\JsonMapper;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
Expand Down Expand Up @@ -87,3 +87,6 @@ assert($articles[0]->tags instanceof NestedTagCollection);
```

Each custom collection advertises its value type through the `@extends` PHPDoc annotation, allowing the mapper to recurse through nested structures.

Test coverage: `tests/JsonMapper/DocsNestedCollectionsTest.php`.

14 changes: 11 additions & 3 deletions docs/recipes/using-attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ use MagicSunday\JsonMapper\Attribute\ReplaceNullWithDefaultValue;

final class User
{
/**
* @var list<string>
*/
#[ReplaceNullWithDefaultValue]
public array $roles = [];
}
```

When a payload contains `{ "roles": null }`, the mapper keeps the default empty array.
When a payload contains `{ "roles": null }`, the mapper keeps the default empty array instead of overwriting it with `null`.

Test coverage: `tests/JsonMapperTest.php::mapNullToDefaultValueUsingAttribute`.

## `ReplaceProperty`
Apply this attribute at class level to redirect one or more incoming property names to a different target property.
Expand All @@ -33,13 +38,16 @@ namespace App\Dto;

use MagicSunday\JsonMapper\Attribute\ReplaceProperty;

#[ReplaceProperty('fullName', replaces: ['first_name', 'name'])]
#[ReplaceProperty('fullName', replaces: 'first_name')]
#[ReplaceProperty('fullName', replaces: 'name')]
final class Contact
{
public string $fullName;
}
```

Both `first_name` and `name` keys will populate the `$fullName` property. Order matters: the first matching alias wins.
Both `first_name` and `name` keys populate the `$fullName` property. Declare one attribute per alias to express the precedence order explicitly.

Test coverage: `tests/Attribute/ReplacePropertyTest.php::replaceProperty`.

Attributes can be combined with PHPDoc annotations and work alongside the classic DocBlock metadata.