******************************************************** JsonMapper - map nested JSON structures onto PHP classes ******************************************************** Takes data retrieved from a JSON__ web service and converts them into nested object and arrays - using your own model classes. Starting from a base object, it maps JSON data on class properties, converting them into the correct simple types or objects. It's a bit like the native SOAP parameter mapping PHP's ``SoapClient`` gives you, but for JSON. It does not rely on any schema, only your PHP class definitions. Type detection works by parsing type declarations and ``@var`` docblock annotations of class properties, as well as type hints in setter methods. You do not have to modify your model classes by adding JSON specific code; it works automatically by parsing already-existing docblocks. This library has no dependencies. Keywords: deserialization, hydration __ http://json.org/ .. contents:: ============ Pro & contra ============ Benefits ======== - Autocompletion in IDEs - It's easy to add comfort methods to data model classes - Your JSON API may change, but your models can stay the same - not breaking applications that use the model classes. Drawbacks ========= - Model classes need to be written by hand Since JsonMapper does not rely on any schema information (e.g. from `json-schema`__), model classes cannot be generated automatically. __ http://json-schema.org/ ===== Usage ===== Basic usage =========== #. `Install`__ ``netresearch/jsonmapper`` with composer #. Create a ``JsonMapper`` object instance #. Call the ``map`` or ``mapArray`` method, depending on your data Map a normal object: .. code:: php map($jsonContact, new Contact()); // or as classname $contactObject = $mapper->map($jsonContact, Contact::class); Map an array of objects: .. code:: php mapArray( $jsonContacts, array(), 'Contact' ); Instead of ``array()`` you may also use ``ArrayObject`` and derived classes, as well as classes implementing ``ArrayAccess``. .. __: #installation Example ======= JSON from an address book web service: .. code:: javascript { "name":"Sheldon Cooper", "address": { "street": "2311 N. Los Robles Avenue", "city": "Pasadena" } } Your local ``Contact`` class: .. code:: php map($json, new Contact()); echo "Geo coordinates for " . $contact->name . ": " . var_export($contact->address->getGeoCoords(), true); Property type mapping ===================== ``JsonMapper`` uses several sources to detect the correct type of a property in the following order: #. Setter method (``set`` + ``ucwords($propertyname)``) Underscores "``_``" and hyphens "``-``" make the next letter uppercase. Property ``foo_bar-baz`` leads to setter method ``setFooBarBaz``. #. If it has a type hint in the method signature then its type used:: public function setPerson(Contact $person) {...} #. The method's docblock is inspected for ``@param $type`` annotations:: /** * @param Contact $person Main contact for this application */ public function setPerson($person) {...} #. If no type could be detected, the plain JSON value is passed to the setter method. #. Class property types (since PHP 7.4):: public Contact $person; #. Constructor property promotion types (since PHP 8.0):: public function __construct(protected Contact $person) {} #. ``@var $type`` docblock annotation of class properties:: /** * @var \my\application\model\Contact */ public $person; The property has to be public to be used directly. You may also use `$bIgnoreVisibility`__ to utilize protected and private properties. .. __: #prop-bignorevisibility If no type could be detected, the property gets the plain JSON value set. If a property can not be found, JsonMapper tries to find the property in a case-insensitive manner. A JSON property ``isempty`` would then be mapped to a PHP property ``isEmpty``. .. note:: You have to provide the fully qualified namespace for the type to work. Relative class names are evaluated in the context of the current classes namespace, NOT respecting any imports that may be present. PHP does not provide the imports via Reflection; the comment text only contains the literal text of the type. For performance reasons JsonMapper does not parse the source code on its own to detect and expand any imports. Supported type names -------------------- - Simple types - ``string`` - ``bool``, ``boolean`` - ``int``, ``integer`` - ``double``, ``float`` - ``array`` - ``object`` - ``mixed`` - Class names, with and without namespaces - ``Contact`` - exception will be thrown if the JSON value is ``null`` - Arrays of simple types and class names: - ``int[]`` - ``Contact[]`` - Multidimensional arrays: - ``int[][]`` - ``TreeDeePixel[][][]`` - ArrayObjects of simple types and class names: - ``ContactList[Contact]`` - ``NumberList[int]`` - Backed enums, with and without namespaces - ``Suit:string|Suit:int`` - exception will be thrown if the JSON value is not present in the enum - Nullable types: - ``int|null`` or ``?int`` - will be ``null`` if the value in JSON is ``null``, otherwise it will be an integer - ``Contact|null`` or ``?Contact`` - will be ``null`` if the value in JSON is ``null``, otherwise it will be an object of type ``Contact`` ArrayObjects and extending classes are treated as arrays. Variables without a type or with type ``mixed`` will get the JSON value set directly without any conversion. See `phpdoc's type documentation`__ for more information. __ http://phpdoc.org/docs/latest/references/phpdoc/types.html Simple type mapping ------------------- .. note:: This feature is disabled by default for security reasons since version 5. See `$bStrictObjectTypeChecking`__ for details. .. __: #prop-bstrictobjecttypechecking When an object shall be created but the JSON contains a simple type only (e.g. string, float, boolean), this value is passed to the classes' constructor. Example: PHP code: .. code:: php public DateTime $date; JSON: .. code:: js {"date":"2014-05-15"} This will result in ``new DateTime('2014-05-15')`` being called. Class map --------- When variables are defined as objects of abstract classes or interfaces, JsonMapper would normally try to instantiate those directly and crash. Using JsonMapper's ``$classMap`` property, you can specify which classes shall get instantiated instead: .. code:: php $jm = new JsonMapper(); $jm->classMap['Foo'] = 'Bar'; $jm->map(...); This would create objects of type ``Bar`` when a variable is defined to be of type ``Foo``. It is also possible to use a callable in case the actual implementation class needs to be determined dynamically (for example in case of a union). The mapped class ('Foo' in the example below) and the Json data are passed as parameters into the call. .. code:: php $mapper = function ($class, $jvalue) { // examine $class and $jvalue to figure out what class to use... return 'DateTime'; }; $jm = new JsonMapper(); $jm->classMap['Foo'] = $mapper; $jm->map(...); .. _prop-classfactories: Class factories --------------- Using JsonMapper's ``$classFactories`` property, you can override how classes get instantiated: .. code:: php $jm = new JsonMapper(); $jm->classFactories[\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(...); This can be used to map JSON datetime strings to ``DateTime`` objects when `$bStrictObjectTypeChecking`__ is enabled. .. __: #prop-bstrictobjecttypechecking Nullables --------- JsonMapper throws an exception when a JSON property is ``null``, unless the PHP class property has a nullable type - e.g. ``Contact|null`` or ``?Contact``. If your API contains many fields that may be ``null`` and you do not want to make all your type definitions nullable, set: .. code:: php $jm->bStrictNullTypes = false; Since version 5.0.0, ``null`` values in arrays lead to a ``JsonMapper_Exception`` unless the type is nullable - e.g. ``array[?string]`` or ``array[string|null]``. To get the previous behavior back (allowing nulls even when not declared so) set: .. code:: php $jm->bStrictNullTypesInArrays = false; Logging ======= JsonMapper's ``setLogger()`` method supports all PSR-3__ compatible logger instances. Events that get logged: - JSON data contain a key, but the class does not have a property or setter method for it. - Neither setter nor property can be set from outside because they are protected or private __ http://www.php-fig.org/psr/psr-3/ Handling invalid or missing data ================================ During development, APIs often change. To get notified about such changes, JsonMapper can be configured to throw exceptions in case of either missing or yet unknown data. Unknown properties ------------------ When JsonMapper sees properties in the JSON data that are not defined in the PHP class, you can let it throw an exception by setting ``$bExceptionOnUndefinedProperty``: .. code:: php $jm = new JsonMapper(); $jm->bExceptionOnUndefinedProperty = true; $jm->map(...); You may also choose to handle those properties yourself by setting a callable__ to ``$undefinedPropertyHandler``: __ http://php.net/manual/en/language.types.callable.php .. code:: php /** * Handle undefined properties during JsonMapper::map() * * @param object $object Object that is being filled * @param string $propName Name of the unknown JSON property * @param mixed $jsonValue JSON value of the property * * @return void */ function setUndefinedProperty($object, $propName, $jsonValue) { $object->{'UNDEF' . $propName} = $jsonValue; } $jm = new JsonMapper(); $jm->undefinedPropertyHandler = 'setUndefinedProperty'; $jm->map(...); Or if you would let JsonMapper handle the setter for you, you can return a string from the ``$undefinedPropertyHandler`` which will be used as property name. .. code:: php /** * Handle undefined properties during JsonMapper::map() * * @param object $object Object that is being filled * @param string $propName Name of the unknown JSON property * @param mixed $jsonValue JSON value of the property * * @return void */ function fixPropName($object, $propName, $jsonValue) { return ucfirst($propName); } $jm = new JsonMapper(); $jm->undefinedPropertyHandler = 'fixPropName'; $jm->map(...); Missing properties ------------------ .. note:: This only works when `$bStrictObjectTypeChecking`__ stays enabled. .. __: #prop-bstrictobjecttypechecking Properties in your PHP classes can be marked as "required" by putting ``@required`` in their docblock: .. code:: php /** * @var string * @required */ public $someDatum; When the JSON data do not contain this property, JsonMapper will throw a ``JsonMapper_Exception`` when ``$bExceptionOnMissingData`` is activated: .. code:: php $jm = new JsonMapper(); $jm->bExceptionOnMissingData = true; $jm->map(...); Option ``$bRemoveUndefinedAttributes`` causes JsonMapper to remove properties from the final object if they have not been in the JSON data: .. code:: php $jm = new JsonMapper(); $jm->bRemoveUndefinedAttributes = true; $jm->map(...); .. _prop-bignorevisibility: Private properties and functions ================================ You can allow mapping to private and protected properties and setter methods by setting ``$bIgnoreVisibility`` to true: .. code:: php $jm = new JsonMapper(); $jm->bIgnoreVisibility = true; $jm->map(...); .. _prop-bstrictobjecttypechecking: Simple types instead of objects =============================== When a variable's type is a class and JSON data is a simple type like ``string``, JsonMapper can pass this value to the class' constructor when configured to do so: .. code:: php $jm = new JsonMapper(); $jm->bStrictObjectTypeChecking = false; $jm->map(...); Disabling this strict object type checks may lead to problems, though: - When a class does not have a constructor or no constructor parameter, the value will get lost. - When the constructor has more than 1 required parameter, it will crash. - When the constructor's parameter type does not match the one of the data in JSON, it will crash. - ``@required`` properties will not be filled. A better alternative to disabling strict object type checking is to use the `$classFactories`__ property. .. __: #prop-classfactories .. note:: The default value changed from ``false`` to ``true`` in version 5 to increase security. Now you have to opt in if you want to pass simple types to the class constructor. Passing arrays to ``map()`` =========================== You may wish to pass array data into ``map()`` that you got by calling .. code:: php json_decode($jsonString, true) By default, JsonMapper will throw an exception because ``map()`` requires an object as first parameter. You can circumvent that by setting ``$bEnforceMapType`` to ``false``: .. code:: php $jm = new JsonMapper(); $jm->bEnforceMapType = false; $jm->map(...); Post-mapping callback ===================== JsonMapper is able to call a custom method directly on each object after mapping it is finished: .. code:: php $jm = new JsonMapper(); $jm->postMappingMethod = 'afterMapping'; $jm->map(...); Now ``afterMapping()`` is called on each mapped object (if the class has that method). You may pass additional arguments to the post-mapping callback: .. code:: php $jm = new JsonMapper(); $jm->postMappingMethod = 'afterMapping'; $jm->postMappingMethodArguments = [23, 'foo']; $jm->map(...); .. _installation: ============ Installation ============ Via Composer from Packagist__:: $ composer require netresearch/jsonmapper __ https://packagist.org/packages/netresearch/jsonmapper ================ Related software ================ Alternatives - `Jackson's data binding`__ for Java - `Johannes Schmitt Serializer`__ for PHP - `metassione`__ for PHP - `Cartographer`__ for PHP - `Data Transfer Object`__ for PHP - `Valinor`__ for PHP - An equally named `JsonMapper`__ library that has dependencies. __ https://fasterxml.github.io/jackson-databind/ __ http://jmsyst.com/libs/serializer __ https://github.com/drbonzo/metassione __ https://github.com/jonjomckay/cartographer __ https://github.com/spatie/data-transfer-object __ https://github.com/cuyz/valinor __ https://jsonmapper.net/ ================ About JsonMapper ================ License ======= JsonMapper is licensed under the `OSL 3.0`__. __ http://opensource.org/licenses/osl-3.0 Coding style ============ JsonMapper follows the `PEAR Coding Standards`__. __ http://pear.php.net/manual/en/standards.php Author ====== `Christian Weiske`__, `cweiske.de`__ __ mailto:cweiske+jsonmapper@cweiske.de __ http://cweiske.de/