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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Unreleased

### Added
- Introduced `JsonMapper::createWithDefaults()` to bootstrap the mapper with Symfony reflection, PhpDoc extractors, and a default property accessor.

### Changed
- Marked `MagicSunday\\JsonMapper\\JsonMapper` as `final` and promoted constructor dependencies to `readonly` properties for consistent visibility.
- Declared `MagicSunday\\JsonMapper\\Converter\\CamelCasePropertyNameConverter` as `final` and immutable.
Expand Down
16 changes: 4 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,23 +65,13 @@ require __DIR__ . '/vendor/autoload.php';
use App\Dto\Article;
use App\Dto\ArticleCollection;
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;

// Decode a single article and a list of articles, raising on malformed JSON.
$single = json_decode('{"title":"Hello world","comments":[{"message":"First!"}]}', associative: false, flags: JSON_THROW_ON_ERROR);
$list = json_decode('[{"title":"Hello world","comments":[{"message":"First!"}]},{"title":"Second","comments":[]}]', associative: false, flags: JSON_THROW_ON_ERROR);

// Configure JsonMapper with reflection and PhpDoc support.
$propertyInfo = new PropertyInfoExtractor(
listExtractors: [new ReflectionExtractor()],
typeExtractors: [new PhpDocExtractor()],
);
$propertyAccessor = PropertyAccess::createPropertyAccessor();

$mapper = new JsonMapper($propertyInfo, $propertyAccessor);
// Bootstrap JsonMapper with reflection and PhpDoc extractors.
$mapper = JsonMapper::createWithDefaults();

// Map a single DTO and an entire collection in one go.
$article = $mapper->map($single, Article::class);
Expand All @@ -93,6 +83,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.

`JsonMapper::createWithDefaults()` wires the default Symfony `PropertyInfoExtractor` (reflection + PhpDoc) and a `PropertyAccessor`. When you need custom extractors, caching, or a specialised accessor you can still instantiate `JsonMapper` manually with your preferred services.

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

### PHP classes
Expand Down
27 changes: 14 additions & 13 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ This document summarises the public surface of the JsonMapper package. All class
## JsonMapper (final)
The `JsonMapper` class is the main entry point for mapping arbitrary JSON structures to PHP objects. The class is `final`; prefer composition over inheritance.

### Factory helper
```php
<?php
declare(strict_types=1);

use MagicSunday\JsonMapper;

$mapper = JsonMapper::createWithDefaults();
```

The helper wires the Symfony `PropertyInfoExtractor` (reflection + PhpDoc) and a default `PropertyAccessor`. Use the constructor described below when you need custom extractors, caches, or accessors.

### Constructor
```php
<?php
Expand Down Expand Up @@ -81,24 +93,13 @@ declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use MagicSunday\JsonMapper\Converter\CamelCasePropertyNameConverter;
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 build the property accessor.
$propertyInfo = new PropertyInfoExtractor(
listExtractors: [new ReflectionExtractor()],
typeExtractors: [new PhpDocExtractor()],
);
$propertyAccessor = PropertyAccess::createPropertyAccessor();
use MagicSunday\JsonMapper\Converter\CamelCasePropertyNameConverter;

// Translate snake_case JSON keys into camelCase DTO properties.
$nameConverter = new CamelCasePropertyNameConverter();

$mapper = new JsonMapper($propertyInfo, $propertyAccessor, $nameConverter);
$mapper = JsonMapper::createWithDefaults($nameConverter);

var_dump($mapper::class);
```
Expand Down
33 changes: 33 additions & 0 deletions src/JsonMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@
use ReflectionNamedType;
use ReflectionProperty;
use ReflectionUnionType;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\BuiltinType;
Expand Down Expand Up @@ -161,6 +165,35 @@ function (mixed $value, string $resolvedClass, MappingContext $context): mixed {
$this->valueConverter->addStrategy(new PassthroughValueConversionStrategy());
}

/**
* Creates a mapper with sensible default Symfony services.
*
* @param PropertyNameConverterInterface|null $nameConverter Optional converter to normalise incoming property names.
* @param array<class-string, class-string|Closure(mixed):class-string|Closure(mixed, MappingContext):class-string> $classMap Optional class map forwarded to the mapper constructor.
* @param CacheItemPoolInterface|null $typeCache Optional cache for resolved type information.
* @param JsonMapperConfiguration|null $config Default mapper configuration cloned for new mapping contexts.
*/
public static function createWithDefaults(
?PropertyNameConverterInterface $nameConverter = null,
array $classMap = [],
?CacheItemPoolInterface $typeCache = null,
?JsonMapperConfiguration $config = null,
): self {
$extractor = new PropertyInfoExtractor(
[new ReflectionExtractor()],
[new PhpDocExtractor()],
);

return new self(
$extractor,
PropertyAccess::createPropertyAccessor(),
$nameConverter,
$classMap,
$typeCache,
$config ?? new JsonMapperConfiguration(),
);
}

/**
* Registers a custom type handler.
*
Expand Down
20 changes: 20 additions & 0 deletions tests/Classes/CamelCasePerson.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/**
* This file is part of the package magicsunday/jsonmapper.
*
* For the full copyright and license information, please read the
* LICENSE file that was distributed with this source code.
*/

declare(strict_types=1);

namespace MagicSunday\Test\Classes;

/**
* Simple DTO with a camelCase property.
*/
final class CamelCasePerson
{
public string $firstName;
}
54 changes: 54 additions & 0 deletions tests/JsonMapper/JsonMapperFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

/**
* This file is part of the package magicsunday/jsonmapper.
*
* For the full copyright and license information, please read the
* LICENSE file that was distributed with this source code.
*/

declare(strict_types=1);

namespace MagicSunday\Test\JsonMapper;

use MagicSunday\JsonMapper;
use MagicSunday\JsonMapper\Converter\CamelCasePropertyNameConverter;
use MagicSunday\Test\Classes\CamelCasePerson;
use MagicSunday\Test\Classes\Simple;
use PHPUnit\Framework\TestCase;

/**
* @covers \MagicSunday\JsonMapper::createWithDefaults
*/
final class JsonMapperFactoryTest extends TestCase
{
public function testCreateWithDefaultsReturnsConfiguredMapper(): void
{
$mapper = JsonMapper::createWithDefaults();

$payload = (object) [
'id' => 42,
'name' => 'Example',
];

$result = $mapper->map($payload, Simple::class);

self::assertInstanceOf(Simple::class, $result);
self::assertSame(42, $result->id);
self::assertSame('Example', $result->name);
}

public function testCreateWithDefaultsUsesProvidedNameConverter(): void
{
$mapper = JsonMapper::createWithDefaults(new CamelCasePropertyNameConverter());

$payload = (object) [
'first_name' => 'Ada',
];

$result = $mapper->map($payload, CamelCasePerson::class);

self::assertInstanceOf(CamelCasePerson::class, $result);
self::assertSame('Ada', $result->firstName);
}
}
Loading