Skip to content
Open
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
25 changes: 22 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,28 @@ parameters into the call.
$jm->map(...);


Constructor map
---------
Using JsonMapper's ``$constructorMap`` property, you can override how classes
shall get instantiated:

.. code:: php

$jm = new JsonMapper();
$jm->constructorMap[\DateTime::class] = function ($jvalue) {
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $jvalue)) {
return new \DateTime($jvalue);
} else {
throw new \Exception('Invalid date pattern');
}
};
$jm->map(...);

.. note::
This can be used to map JSON datetime strings to ``DateTime`` objects when
`$bStrictObjectTypeChecking`__ is enabled.


Nullables
---------
JsonMapper throws an exception when a JSON property is ``null``,
Expand Down Expand Up @@ -490,9 +512,6 @@ when configured to do so:
$jm->bStrictObjectTypeChecking = false;
$jm->map(...);

This can be used to automatically initialize DateTime objects
from date strings.

Disabling this strict object type checks may lead to problems, though:

- When a class does not have a constructor or no constructor parameter,
Expand Down
44 changes: 42 additions & 2 deletions src/JsonMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ class JsonMapper
*/
public array $classMap = [];

/**
* Override constructors that JsonMapper uses to create objects.
*

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing whitespace

* @var array<class-string, callable>
*/
public array $constructorMap = [];

/**
* Callback used when an undefined property is found.
*
Expand Down Expand Up @@ -312,6 +319,7 @@ public function map($json, $object)
// but only a flat type (i.e. string, int)
if ($this->bStrictObjectTypeChecking
&& !is_subclass_of($type, \BackedEnum::class)
&& !$this->getMappedConstructor($type)
) {
throw new JsonMapper_Exception(
'JSON property "' . $key . '" must be an object, '
Expand Down Expand Up @@ -466,6 +474,7 @@ public function mapArray(
$array[$key] = $jvalue;
} else if ($this->bStrictObjectTypeChecking
&& !is_subclass_of($class, \BackedEnum::class)
&& !$this->getMappedConstructor($class)
) {
throw new JsonMapper_Exception(
'JSON property'
Expand Down Expand Up @@ -707,13 +716,21 @@ protected function setProperty(
protected function createInstance(
string $class, bool $useParameter = false, mixed $jvalue = null
): object {
$constructor = $this->getMappedConstructor($class);

if ($useParameter) {
if (is_subclass_of($class, \BackedEnum::class)) {
return $class::from($jvalue);
}

return new $class($jvalue);
return null === $constructor
? new $class($jvalue)
: $constructor($jvalue);
} else {
if ($constructor) {
return $constructor();
}

$reflectClass = new ReflectionClass($class);
$constructor = $reflectClass->getConstructor();
if (null === $constructor
Expand All @@ -740,7 +757,7 @@ protected function getMappedType(?string $type, mixed $jvalue = null): ?string
{
if (isset($this->classMap[$type ?? ''])) {
$target = $this->classMap[$type];
} else if (is_string($type) && $type !== '' && $type[0] == '\\'
} else if (is_string($type) && $type !== '' && str_starts_with($type, '\\')

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this deprecated in some new PHP version? If not, please keep the old code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm just retested and it works fine. I thought I had some issues with this while testing the new getMappedConstructor logic but apparently I was mistaken. Will revert!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With PHP 8.1 as the minimum, str_starts_with() is available and improves expressiveness. The array access of a string immediately triggers my unicode feeling of "something might be odd", as it only checks the first byte.

If it's reverted, I don't mind, but I do see it as an improvement - if it should go into this PR is a different discussion.

&& isset($this->classMap[substr($type, 1)])
) {
$target = $this->classMap[substr($type, 1)];
Expand All @@ -758,6 +775,29 @@ protected function getMappedType(?string $type, mixed $jvalue = null): ?string
return $type;
}

/**
* Get the mapped constructor for this class.
* Returns null if not mapped.
*
* Lets you override constructors via the $constructorMap property.
*
* @param $class Class name to map
*
* @return ?callable The mapped constructor
*/
protected function getMappedConstructor(string $class): ?callable
{
if (isset($this->constructorMap[$class])) {
return $this->constructorMap[$class];
} else if ($class !== '' && str_starts_with($class, '\\')
&& isset($this->constructorMap[substr($class, 1)])
) {
return $this->constructorMap[substr($class, 1)];

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need that fallback for classes without backslash. People shall simply use the class names with their correct namespace.

@djairhogeuens djairhogeuens Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure? I used it here to be consistent with getMappedType. If we omit this case, you cannot use $jm->constructorMap[\DateTime::class] = because\DateTime::classbecomesDateTimeas key in the map. My testtestConstructorMapWithoutLeadingBackslash` fails then as well.

} else {
return null;
}
}

/**
* Checks if the given type is a "simple type"
*
Expand Down
95 changes: 95 additions & 0 deletions tests/ConstructorMapTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php
/**
* Unit tests for JsonMapper's constructorMap
*
* @category Tests
* @package JsonMapper
* @license OSL-3.0 http://opensource.org/licenses/osl-3.0
* @link https://github.com/cweiske/jsonmapper
*/
class ConstructorMapTest extends \PHPUnit\Framework\TestCase
{
public function testConstructorMapWithoutLeadingBackslash()
{
$jm = new JsonMapper();
$jm->constructorMap[\DateTime::class] = function ($jvalue) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test that passes a callable to the constructor map:

$jm->constructorMap[\DateTime::class] = 'myfunctionname';
$jm->constructorMap[\DateTime::class] = [$obj, 'mymethod'];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $jvalue)) {
return new \DateTime($jvalue);
} else {
throw new \Exception('Invalid date pattern');
}
};
$sn = $jm->map(
json_decode('{"datetime": "2026-06-05"}'),
new JsonMapperTest_Object()
);

$this->assertInstanceOf(DateTime::class, $sn->datetime);
$this->assertSame(
'2026-06-05',
$sn->datetime->format('Y-m-d')
);
}

public function testConstructorMapWithLeadingBackslash()
{
$jm = new JsonMapper();
$jm->constructorMap['\\DateTime'] = function ($jvalue) {
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $jvalue)) {
return new \DateTime($jvalue);
} else {
throw new \Exception('Invalid date pattern');
}
};
$sn = $jm->map(
json_decode('{"datetime": "2026-06-05"}'),
new JsonMapperTest_Object()
);

$this->assertInstanceOf(DateTime::class, $sn->datetime);
$this->assertSame(
'2026-06-05',
$sn->datetime->format('Y-m-d')
);
}

public function testConstructorMapError()
{
$jm = new JsonMapper();
$jm->constructorMap[\DateTime::class] = function ($jvalue) {
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $jvalue)) {
return new \DateTime($jvalue);
} else {
throw new \Exception('Invalid date pattern');
}
};

$this->expectException(\Exception::class);
$this->expectExceptionMessage('Invalid date pattern');

$jm->map(
json_decode('{"datetime": "05/06/2026"}'),
new JsonMapperTest_Object()
);
}

public function testConstructorMapWithClassNameBased()
{
$jm = new JsonMapper();
$jm->constructorMap[JsonMapperTest_Simple::class] = function () {
$obj = new JsonMapperTest_Simple();
$obj->str = 'initial';
return $obj;
};
$sn = $jm->map(
json_decode('{"pbool": true}'),
JsonMapperTest_Simple::class
);

$this->assertSame(
'initial',
$sn->str
);
}
}
?>
Loading