Skip to content

Commit e804dc6

Browse files
committed
feat(jsonmapper): add factory helper for default services
1 parent c02ea14 commit e804dc6

File tree

6 files changed

+128
-25
lines changed

6 files changed

+128
-25
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
## Unreleased
22

3+
### Added
4+
- Introduced `JsonMapper::createWithDefaults()` to bootstrap the mapper with Symfony reflection, PhpDoc extractors, and a default property accessor.
5+
36
### Changed
47
- Marked `MagicSunday\\JsonMapper\\JsonMapper` as `final` and promoted constructor dependencies to `readonly` properties for consistent visibility.
58
- Declared `MagicSunday\\JsonMapper\\Converter\\CamelCasePropertyNameConverter` as `final` and immutable.

README.md

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,23 +65,13 @@ require __DIR__ . '/vendor/autoload.php';
6565
use App\Dto\Article;
6666
use App\Dto\ArticleCollection;
6767
use MagicSunday\JsonMapper;
68-
use Symfony\Component\PropertyAccess\PropertyAccess;
69-
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
70-
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
71-
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
7268

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

77-
// Configure JsonMapper with reflection and PhpDoc support.
78-
$propertyInfo = new PropertyInfoExtractor(
79-
listExtractors: [new ReflectionExtractor()],
80-
typeExtractors: [new PhpDocExtractor()],
81-
);
82-
$propertyAccessor = PropertyAccess::createPropertyAccessor();
83-
84-
$mapper = new JsonMapper($propertyInfo, $propertyAccessor);
73+
// Bootstrap JsonMapper with reflection and PhpDoc extractors.
74+
$mapper = JsonMapper::createWithDefaults();
8575

8676
// Map a single DTO and an entire collection in one go.
8777
$article = $mapper->map($single, Article::class);
@@ -93,6 +83,8 @@ var_dump($article, $articles);
9383

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

86+
`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.
87+
9688
Test coverage: `tests/JsonMapper/DocsQuickStartTest.php`.
9789

9890
### PHP classes

docs/API.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ This document summarises the public surface of the JsonMapper package. All class
55
## JsonMapper (final)
66
The `JsonMapper` class is the main entry point for mapping arbitrary JSON structures to PHP objects. The class is `final`; prefer composition over inheritance.
77

8+
### Factory helper
9+
```php
10+
<?php
11+
declare(strict_types=1);
12+
13+
use MagicSunday\JsonMapper;
14+
15+
$mapper = JsonMapper::createWithDefaults();
16+
```
17+
18+
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.
19+
820
### Constructor
921
```php
1022
<?php
@@ -81,24 +93,13 @@ declare(strict_types=1);
8193

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

84-
use MagicSunday\JsonMapper\Converter\CamelCasePropertyNameConverter;
8596
use MagicSunday\JsonMapper;
86-
use Symfony\Component\PropertyAccess\PropertyAccess;
87-
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
88-
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
89-
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
90-
91-
// Collect metadata and build the property accessor.
92-
$propertyInfo = new PropertyInfoExtractor(
93-
listExtractors: [new ReflectionExtractor()],
94-
typeExtractors: [new PhpDocExtractor()],
95-
);
96-
$propertyAccessor = PropertyAccess::createPropertyAccessor();
97+
use MagicSunday\JsonMapper\Converter\CamelCasePropertyNameConverter;
9798

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

101-
$mapper = new JsonMapper($propertyInfo, $propertyAccessor, $nameConverter);
102+
$mapper = JsonMapper::createWithDefaults($nameConverter);
102103

103104
var_dump($mapper::class);
104105
```

src/JsonMapper.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@
5050
use ReflectionNamedType;
5151
use ReflectionProperty;
5252
use ReflectionUnionType;
53+
use Symfony\Component\PropertyAccess\PropertyAccess;
5354
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
55+
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
56+
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
57+
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
5458
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
5559
use Symfony\Component\TypeInfo\Type;
5660
use Symfony\Component\TypeInfo\Type\BuiltinType;
@@ -161,6 +165,35 @@ function (mixed $value, string $resolvedClass, MappingContext $context): mixed {
161165
$this->valueConverter->addStrategy(new PassthroughValueConversionStrategy());
162166
}
163167

168+
/**
169+
* Creates a mapper with sensible default Symfony services.
170+
*
171+
* @param PropertyNameConverterInterface|null $nameConverter Optional converter to normalise incoming property names.
172+
* @param array<class-string, class-string|Closure(mixed):class-string|Closure(mixed, MappingContext):class-string> $classMap Optional class map forwarded to the mapper constructor.
173+
* @param CacheItemPoolInterface|null $typeCache Optional cache for resolved type information.
174+
* @param JsonMapperConfiguration|null $config Default mapper configuration cloned for new mapping contexts.
175+
*/
176+
public static function createWithDefaults(
177+
?PropertyNameConverterInterface $nameConverter = null,
178+
array $classMap = [],
179+
?CacheItemPoolInterface $typeCache = null,
180+
?JsonMapperConfiguration $config = null,
181+
): self {
182+
$extractor = new PropertyInfoExtractor(
183+
[new ReflectionExtractor()],
184+
[new PhpDocExtractor()],
185+
);
186+
187+
return new self(
188+
$extractor,
189+
PropertyAccess::createPropertyAccessor(),
190+
$nameConverter,
191+
$classMap,
192+
$typeCache,
193+
$config ?? new JsonMapperConfiguration(),
194+
);
195+
}
196+
164197
/**
165198
* Registers a custom type handler.
166199
*

tests/Classes/CamelCasePerson.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the package magicsunday/jsonmapper.
5+
*
6+
* For the full copyright and license information, please read the
7+
* LICENSE file that was distributed with this source code.
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace MagicSunday\Test\Classes;
13+
14+
/**
15+
* Simple DTO with a camelCase property.
16+
*/
17+
final class CamelCasePerson
18+
{
19+
public string $firstName;
20+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the package magicsunday/jsonmapper.
5+
*
6+
* For the full copyright and license information, please read the
7+
* LICENSE file that was distributed with this source code.
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace MagicSunday\Test\JsonMapper;
13+
14+
use MagicSunday\JsonMapper;
15+
use MagicSunday\JsonMapper\Converter\CamelCasePropertyNameConverter;
16+
use MagicSunday\Test\Classes\CamelCasePerson;
17+
use MagicSunday\Test\Classes\Simple;
18+
use PHPUnit\Framework\TestCase;
19+
20+
/**
21+
* @covers \MagicSunday\JsonMapper::createWithDefaults
22+
*/
23+
final class JsonMapperFactoryTest extends TestCase
24+
{
25+
public function testCreateWithDefaultsReturnsConfiguredMapper(): void
26+
{
27+
$mapper = JsonMapper::createWithDefaults();
28+
29+
$payload = (object) [
30+
'id' => 42,
31+
'name' => 'Example',
32+
];
33+
34+
$result = $mapper->map($payload, Simple::class);
35+
36+
self::assertInstanceOf(Simple::class, $result);
37+
self::assertSame(42, $result->id);
38+
self::assertSame('Example', $result->name);
39+
}
40+
41+
public function testCreateWithDefaultsUsesProvidedNameConverter(): void
42+
{
43+
$mapper = JsonMapper::createWithDefaults(new CamelCasePropertyNameConverter());
44+
45+
$payload = (object) [
46+
'first_name' => 'Ada',
47+
];
48+
49+
$result = $mapper->map($payload, CamelCasePerson::class);
50+
51+
self::assertInstanceOf(CamelCasePerson::class, $result);
52+
self::assertSame('Ada', $result->firstName);
53+
}
54+
}

0 commit comments

Comments
 (0)