Array to dynamic object - php

I'm trying to make some very rudimental database mapping conversion where I'm fetching data from the database and then trying to convert that array to an instance of an arbitrary class. This should be dynamic so that I can use the same function no matter the output object class/properties.
Let's say I have CASE1:
$student = [
'name' => 'Jhon',
'surname' => 'Wick',
'age' => 40
];
class Student{
private string $name;
private string $surname;
private int $age;
... getter and setters
}
CASE2:
$car = [
'manufacturer' => 'Toyota',
'model' => 'one of their model',
'commercialName' => 'the name'
];
class Car{
private $manufacturer;
private $model;
private $commercialName;
// Getter and Setter
}
And I want something that transforms the $student array var to a Student instance or the $car to Car. How can I do that?
I know I can do that using something like:
$funcName = 'get' . ucfirst($camelCaseName);
if (method_exists($this, $funcName)) {
$funcValue = $this->$funcName();
}
But I'm searching for something a little more modern like using Reflection.
What is the best approach to this? What could be an efficient solution?
To give further info, this is needed for an extension of the WordPress $wpdb object. If possible I wouldn't like to use public class properties because I may need to actually call the class setter in some case to change some other class value. Let's say something like giving birth date should calculate age

As I stated out in the comments already, all that you need is the process of hydration. The boys and girls from Laminas got a good maintained package called laminas/laminas-hydrator which can do the job for you.
An easy example:
<?php
declare(strict_types=1);
namespace Marcel;
use Laminas\Hydrator\ClassMethodsHydrator;
use Marcel\Model\MyModel;
$hydrator = new ClassMethodsHydrator();
// hydrate arry data into an instance of your entity / data object
// using setter methods
$object = $hydrator->hydrate([ ... ], new MyModel());
// vice versa using the getter methods
$array = $hydrator->extract($object);
Your approach is not so wrong. It is at least going in the right direction. In my eyes you should not use private properties. What kind of advantage do you expect from using private properties? They only bring disadvantages. Think of extensions of your model. Protected properties do the same job for just accessing the properties via getters and setters. Protected properties are much more easy to handle.
<?php
declare(strict_types=1);
namespace Marcel;
class MyDataObject
{
public ?string $name = null;
public ?int $age = null;
public function getName(): ?string
{
return $name;
}
public function setName(?string $name): void
{
$this->name = $name;
}
public function getAge(): ?int
{
return $this->age;
}
public function setAge(?int $age): void
{
$this->age = $age;
}
}
class MyOwnHydrator
{
public function hydrate(array $data, object $object): object
{
foreach ($data as $property => $value) {
// beware of naming your properties in the data array the right way
$setterName = 'set' . ucfirst($property);
if (is_callable([$object, $setterName])) {
$object->{$setterName}($value);
}
}
return $object;
}
}
// example
$data = [
'age' => 43,
'name' => 'Marcel',
'some_other_key' => 'blah!', // won 't by hydrated
];
$hydrator = new MyOwnHydrator();
$object = new MyDataObject();
$object = $hydrator->hydrate($data, $object);
This is the simplest hydrator you can get. It iterates through the data array, creates setter method names and checks if they are callable. If so the value will be hydrated into the object by calling the setter method. At the end you 'll get a hydrated object.
But beware. There are some stumbling blocks with this solution that need to be considered. The keys of your data array must be named exactly like the properties of your data object or entity. You have to use some naming strategies, when you want to use underscore separated keys in your array but your object properties are camel cased, e.g. Another problem could be type hints. What if the example had a birthday and only accepts DateTime instances and your data array only contains the birhtday as string "1979-12-19"? What if your data array contains sub arrays, that should be hydrated in n:m relations?
All that is done already in the menetiond laminas package. If you don 't need all that fancy stuff, follow your first thought and build your own hydrator. ;)

I would say drop setters/getters and use readonly properties:
class Car{
public function __construct(
public readonly string $manufacturer,
public readonly string $model,
public readonly string $commercialName,
);
}
...
new Car(...$car);

Related

PHP PDO Fetch MySQL DateTime

I have a PHP class that represent MySQL table. One of that column table type is DateTime. Previously I use string and everything work fine, because I don't have to deal with the date type. I just use fetchAll function and the column table automatically mapping to a propriate field.
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_CLASS, MyPHPClass::class);
Now I want to use the DateTime type in my PHP script. Is this possible to automatically convert MySQL DateTime to PHP DateTime when use PDO fetchAll? If yes, how?
Note:
I know how to convert the DateTime string from MySQL to PHP DateTime, I just wonder if this is possible to add something like #Annotation, or converter.
For this purpose the concept of so called hydrators is very common. Especially for data fields of the type DateTime, which will most likely be repeated in other models, it makes sense to use hydrators. This keeps the logic away from the models and works with reusable code.
Why Hydrators?
If you are considering using your entire development with another database system, or if you simply want to maintain the greatest possible flexibility with your data models, hydrators make perfect sense. As mentioned earlier, hydrators can ensure that the models remain free of any logic. In addition, hydrators can be used to represent flexible scenarios. In addition, the hydration of data solely on the basis of the possibilities offered by the PHP PDO class is very weak. Just handle the raw data from the database as array and let the hydrator do the magic.
The Logic Behind Hydrators
Each hydrator can apply different strategies to the properties of the object to be hydrated. These hydrator strategies can be used to change values or perform other functions in the model before the actual hydration.
<?php
declare(strict_types=1);
namespace Marcel\Hydrator;
interface HydratorInterface
{
public function hydrate(array $data, object $model): object;
public function extract(object $model): array;
}
The above shown interface should be implemented in every hydrator class. Every hydrator should have a hydrate method, which pushes a given array of data into a given model. Furthermore there has to be the turnaround which is the extract method, which extracts data out of an model into an array.
<?php
declare(strict_types=1);
namespace Marcel\Hydrator\Strategy;
interface StrategyInterface
{
public function hydrate($value);
}
Both interfaces define the methods that hydrators and hydrator strategies must bring. These interfaces are mainly used to achieve secure type hinting for the identification of objects.
The Hydrator Strategy
<?php
declare(strict_types=1);
namespace Marcel\Hydrator\Strategy;
use DateTime;
class DateTimeStrategy implements StrategyInterface
{
public function hydrate($value)
{
$value = new DateTime($value);
return $value;
}
}
This simple example of an hydrator strategy does nothing more than taking the original value and initializing a new DateTime object with it. For the sake of simple illustration, I have omitted the error handling here. In production, you should always check at this point whether the DateTime object was really created and did not generate any errors.
The Hydrator
<?php
declare(strict_types=1);
namespace Marcel\Hydrator;
use Marcel\Hydrator\Strategy\StrategyInterface;
use ReflectionClass;
class ClassMethodsHydrator implements HydratorInterface
{
protected ?ReflectionClass $reflector = null;
protected array $strategies = [];
public function hydrate(array $data, object $model): object
{
if ($this->reflector === null) {
$this->reflector = new ReflectionClass($model);
}
foreach ($data as $key => $value) {
if ($this->hasStrategy($key)) {
$strategy = $this->strategies[$key];
$value = $strategy->hydrate($value);
}
$methodName = 'set' . ucfirst($key);
if ($this->reflector->hasMethod($methodName)) {
$model->{$methodName}($value);
}
}
return $model;
}
public function extract(object $model): array
{
return get_object_vars($model);
}
public function addStrategy(string $name, StrategyInterface $strategy): void
{
$this->strategies[$name] = $strategy;
}
public function hasStrategy(string $name): bool
{
return array_key_exists($name, $this->strategies);
}
}
This hydrator requires that your models have getter and setter methods. In this example, it requires at least that there is a corresponding setter method for each property. To avoid errors and to name methods correctly, the names of the column names should be filtered from the database. Normally, the names in the database are noted with an underscore and the properties of a model follow the camel case convention. (Example: "fancy_string" => "setFancyString")
The Example
class User
{
protected int $id;
protected DateTime $birthday;
public function getId(): int
{
return $this->id;
}
public function setId(int $id): void
{
$this->id = $id;
}
public function getBirtday(): DateTime
{
return $this->birthday;
}
public function setBirthday(DateTime $birthday): void
{
$this->birthday = $birthday;
}
}
$data = [
'id' => 1,
'birthday' => '1979-12-19',
];
$hydrator = new ClassMethodsHydrator();
$hydrator->addStrategy('birthday', new DateTimeStrategy());
$user = $hydrator->hydrate($data, new User());
The result of this code will be a fine hydrated user model.
object(Marcel\Model\User)#3 (2) {
["id":protected] => int(1)
["birthday":protected] => object(DateTime)#5 (3) {
["date"] => string(26) "1979-12-19 00:00:00.000000"
["timezone_type"] => int(3)
["timezone"] => string(13) "Europe/Berlin"
}
We can take advantage of the fact that PHP will call __set magic method for all undefined properties so we can initialize our DateTime object there.
class User {
public string $name;
public DateTime $dateObject;
public function __set($property, $value) {
if ($property === 'date') {
$this->dateObject = new DateTime($value);
} else {
$this->$property = $value;
}
}
}
$stmt->fetchAll(PDO::FETCH_CLASS, User::class);
Note: the column name in the database must differ from the property name in the User object, otherwise __set method will not be called.
MySQL save datetime as unix timestamp and return all dates as timestamps if we get it as string , then we are convert in timestamp
Example: - date('m/d/Y H:i:s', 1541843467);

Is there any way to define the structure of Array or Object in PHP?

My question is basically exactly the same as the title says.
I was playing with TypeScript for a while now and there's a simple way to define the structure of the Object which is defining properties inside the Interface. I know that PHP does not support the properties in Interfaces, but is there any way to somehow define the structure of the Object (without using some abstract class) I'm passing or at least the Array (which keys need to be presented inside).
What I mean exactly is:
// I already sanitized that this method returns the exact same structure every time
$data = $this->storage->get($some);
// here I'm passing the data I obtained to my Builder
Builder::createFromArray($data);
// or
Builder::create($data);
class Builder {
public static function createFromArray(\ArrayOfSomeType $array) {}
public static function create(\ObjectOfSomeTypeWithPropertiesSpecified $obj) {}
}
Hope I explained it well.
As you said, there is no concept in PHP, that does exactly what you want. There is an ArrayObject class, that would fit your demands of an object with a loose amount of members.
class Builder {
public function create(iterable $data) : \ArrayObject {
$object = (new \ArrayObject())->setFlags(\ArrayObject::ARRAY_AS_PROPS);
foreach ($data as $key => $value) {
$object->offsetSet($key, $value);
}
return $object;
}
}
}
Example 1: Populate object with array
$object = Builder::create([ 'bla' => 'blubb', 'yadda' => 'blubb' ]);
var_dump($object->bla);
The builder returns an object with exact the same properties as the array had keys. All the properties contain the values the array has. You can iterate over the new object with foreach or an Iterator object.
Example 2: Populate object with another object
$class = new \stdClass();
$class->propertyA = 'B';
$class->propertyB = 'B';
$object = Builder::Create($class);
var_dump($object->propertyA);
With PHP nearly all objects are iterable. That means you can iterate over the properties of one object and pass them to our ArrayObject instance.
Common approach of hydration with PHP
There is another approach in PHP which is called hydration. It is a bit more complex than the shown example but pretty handy, if you got it.
// your entity / data model
class Car implements EntityInterface {
protected $horsepower;
public function getHorsepower() : int
{
if ($this->horsepower === null) {
$this->horsepower = 0;
}
return $this->horsepower;
}
public function setHorsepower(int $horsepower) : self
{
$this->horsepower = $horsepower;
return $this;
}
}
This is our data model (entity) of the type car. Our Car inherits from an interface. This is just for type hinting reasons in the hydrator. Our car has one property called horsepower. Of course a car can have more properties. But this is just for example. ;)
class ClassMethodsHydrator
{
public function hydrate(array $data, EntityInterface $entity) : EntityInterface {
foreach ($data as $key => $value) {
$methodName = 'set' . ucwords(strtolower($key));
if (method_exists($entity, $methodName) {
$entity->{$methodName}($value);
}
}
return $entity;
}
}
This is our small hydrator example. This class does, what your builder does. But in a more specific way. It takes an entity and hydrates it with the given data, if it matches a method of the entity.
$entity = (new Hydrator())->hydrate(
[
'horsepower' => 172,
'notexistingproperty' => 'bla',
],
new Car()
);
var_dump($entity->getHorsepower()); // 172
The advantage of hydration is simple. You only have models with known properties. That 's pretty safe and you know at any time, what your model can do for you. It would be senseless, that a car has a rudder for example. In this case a car only got what a car got. Like in the example the car only takes the horsepower member of the array.
I think you could force properites by creating getters and setter in interface. And then instead of
$item->something;
you could use
$item->getSomething();

Formatting of Numbers with PDO

I have recently moved a large php application from using mssql_ functions to the PDO function using the mssql driver.
I wrote a simple library that allows drop in replacement. It all seems to work pretty well considering.
However one thing that is a bit annoying is default format of numbers and particularly numbers defined as money in the database.
Most of my smarty template pages previous simply output the number as it came from the database so someones balance might be show as
125.00
however since changing to PDO this is returned as
125.0000
This is a little annoying and off putting, but obviously not the end of the world.
My Question. Is there a workaround / trick / formatting Constant or method that I can use to get PDO to format values differently, or do I need to go an manually set the format for every number in every template throughout my app?
So basically, what I'd do is create models that represent a result-set for each table, and use PDO::FETCH_CLASS to load the data into instances of the corresponding class. For example:
class UserTable //extends AbstractTable <-- see below
{
protected $id = null;
protected $name = null;
protected $email = null;
protected $money = null;
}
Then add getters and setters that format/validate the data accordingly eg:
public function getMoney()
{
return sprintf('%.2f', $this->money);//check if not null first, obviously
}
Next, have an abstract class for these models, and implement the ArrayAccess interface in there. For example, using a simple mapping array:
protected $getterMap = [
'email' => 'getEmail',
'id' => 'getId',
'money' => 'getMoney',
];
Define a tailor-made map in each child, then have the abstract class use it like so:
//in abstract class AbstracTable implements ArrayAccess
public function offsetGet($offset)
{
if (!isset($this->getterMap[$offset])) {
throw new RuntimeException(
sprintf('%s not a member of %s', $offset, get_class($this));
);
}
$getter = $this->getterMap[$offset];
return $this->{$getter}();//use the getter, it formats the data!
}
Do something similar for all 4 methods in the interface, and now you can use this:
$row = $stmt->fetch(PDO::FETCH_CLASS, 'User');
$row['money'];//will call getMoney, and return the formatted number
A more complete example:
abstract class AbstractTable implements ArrayAccess
{
protected $id = null;//very likely to be defined in all tables
protected $getterMap = [
'id' => 'getId',
];
protected $setterMap = [
'id' => 'setId',
];
//force child classes to define a constructor, which sets up the getter/setter maps
abstract public function __construct();
public offsetExists($offset)
{
return isset($this->getterMap[$offset]);
//optionally, check if value if not null: isset($arr['keyWithNullVal']) returns null, too:
return isset($this->getterMap[$offset]) && $this->{$offset} !== null;
}
public offsetGet ( mixed $offset )
{
if (!isset($this->getterMap[$offset])) {
throw new RuntimeException('member does not exist');
}
$getter = $this->getterMap[$offset];
return $this->{$getter}();
}
public offsetSet($offset, $value )
{
if (!isset($this->setterMap[$offset])) {
throw new RuntimeException('Trying to set non-existing member');
}
$setter = $this->setterMap[$offset];
$this->{$setter}($value);
}
public offsetUnset ($offset)
{
//same as setter, but call:
//or just leave blank
$this->{$setter}(null);
}
}
class UserTable extends AbstractTable
{
//protected $id = null; in parent already
protected $name = null;
protected $email = null;
protected $money = null;
public function __construct()
{
$fields = [
'name' => 'etName',
'email' => 'etEmail',
'money' => 'etMoney',
];
foreach ($fields as $name => $method) {
$this->getterMap[$name] = 'g' . $method;
$this->setterMap[$name] = 's' . $method;
}
}
}
Obviously, you'll have to write the getters and setters for all the fields. Not to worry, though: most IDE's will helpfully generate the getters and setters for predefined properties at the click of a button

Why not put all class properties in an associative array for easy set/get/etc.?

I have a class with 23 different properties of various types, but I'll only show three in this example. A typical way to construct this class would be:
class Item
{
public $name;
public $price;
public $date;
// Getters and setters
public function get_name() {return $name;}
public function get_price() {return $price;}
public function get_date() {return $date;}
public function set_name($val) {$name = $val;}
public function set_price($val) {$price = $val;}
public function set_date($val) {$date = $val;}
}
With 23 different properties I would need 46 different set/get functions, and my code would have to call all these different functions by specific name, so it's difficult to create loops.
Can I do this instead:
class Item
{
public $props = array (
'name' => NULL,
'price' => NULL,
'date' => NULL
);
// Getters and setters
public function get( $key ) { return $props[$key]; }
public function set( $key, $val ) { $props[$key] = $val; }
public function getKeys() { return array_keys( $props ); }
}
To get a property such as $name I'd simply use Item->get('name');
rather than the specific getter function Item->get_name();
This way, even if I have 23 different properties of different types, I still use only one getter and one setter for all of them. Plus, it's easy to loop through all (or a subset) of the properties by doing a foreach on the getKeys() array.
It seems so convenient I would think it would be a pretty standard format for PHP classes, yet in all the tutorials and examples of PHP classes that I've seen I have never seen this construct used. Is there a problem with it?
This is fine if you don't care what type your properties are or you don't care if they should be restricted to a certain set of known properties (ie, you don't care about encapsulation).
If however you want to use specific types, you will need some control.
class Foo {
/**
* #var Bar
*/
private $bar;
public function setBar(Bar $bar) {
$this->bar = $bar;
}
/**
* #return Bar
*/
public function getBar() {
return $this->bar;
}
}
The other real benefit to setters in particular is the ability to perform transformations on input data. For example
class Foo {
/**
* #var DateTime
*/
private $date;
public function setDate($date) {
if (!$date instanceof DateTime) {
$date = new DateTime((string) $date);
}
$this->date = $date;
}
}
it's difficult to create loops.
What meaningful loop would you make over name, price and date?
The reason individual getters and setters are used is encapsulation: prohibiting things outside the class insight into its insides. For example, say you have age property. Then you extend your class with birth_date property. Then you think it would be a good idea to automatically calculate age instead of having to assign both values. If you have a getter for age, it is trivial: just have it stop returning $props["age"], and start returning the difference between the birth date and now. But if you're not using the age getter but a direct access, you would have to change every single place in your codebase where you're accessing age.
Or say that you want to issue a log statement every time a property changes. Trivial to put it inside a setter, but you're screwed if you have a public property that anyone is allowed to mess with at will.

Deserializing from JSON into PHP, with casting?

Suppose I have a User class with 'name' and 'password' properties, and a 'save' method. When serializing an object of this class to JSON via json_encode, the method is properly skipped and I end up with something like {'name': 'testName', 'password': 'testPassword'}.
However, when deserializing via json_decode, I end up with a StdClass object instead of a User object, which makes sense but this means the object lacks the 'save' method. Is there any way to cast the resultant object as a User, or to provide some hint to json_decode as to what type of object I'm expecting?
Old question, but maybe someone will find this useful.
I've created an abstract class with static functions that you can inherit on your object in order to deserialize any JSON into the inheriting class instance.
abstract class JsonDeserializer
{
/**
* #param string|array $json
* #return $this
*/
public static function Deserialize($json)
{
$className = get_called_class();
$classInstance = new $className();
if (is_string($json))
$json = json_decode($json);
foreach ($json as $key => $value) {
if (!property_exists($classInstance, $key)) continue;
$classInstance->{$key} = $value;
}
return $classInstance;
}
/**
* #param string $json
* #return $this[]
*/
public static function DeserializeArray($json)
{
$json = json_decode($json);
$items = [];
foreach ($json as $item)
$items[] = self::Deserialize($item);
return $items;
}
}
You use it by inheriting it on a class which has the values that your JSON will have:
class MyObject extends JsonDeserializer
{
/** #var string */
public $property1;
/** #var string */
public $property2;
/** #var string */
public $property3;
/** #var array */
public $array1;
}
Example usage:
$objectInstance = new MyObject();
$objectInstance->property1 = 'Value 1';
$objectInstance->property2 = 'Value 2';
$objectInstance->property3 = 'Value 3';
$objectInstance->array1 = ['Key 1' => 'Value 1', 'Key 2' => 'Value 2'];
$jsonSerialized = json_encode($objectInstance);
$deserializedInstance = MyObject::Deserialize($jsonSerialized);
You can use the ::DeserializeArray method if your JSON contains an array of your target object.
Here's a runnable sample.
Short answer: No (not that I know of*)
Long answer: json_encode will only serialize public variables. As you can see per the JSON spec, there is no "function" datatype. These are both reasons why your methods aren't serialized into your JSON object.
Ryan Graham is right - the only way to re-create these objects as non-stdClass instances is to re-create them post-deserialization.
Example
<?php
class Person
{
public $firstName;
public $lastName;
public function __construct( $firstName, $lastName )
{
$this->firstName = $firstName;
$this->lastName = $lastName;
}
public static function createFromJson( $jsonString )
{
$object = json_decode( $jsonString );
return new self( $object->firstName, $object->lastName );
}
public function getName()
{
return $this->firstName . ' ' . $this->lastName;
}
}
$p = new Person( 'Peter', 'Bailey' );
$jsonPerson = json_encode( $p );
$reconstructedPerson = Person::createFromJson( $jsonPerson );
echo $reconstructedPerson->getName();
Alternatively, unless you really need the data as JSON, you can just use normal serialization and leverage the __sleep() and __wakeup() hooks to achieve additional customization.
* In a previous question of my own it was suggested that you could implement some of the SPL interfaces to customize the input/output of json_encode() but my tests revealed those to be wild goose chases.
I think the best way to handle this would be via the constructor, either directly or via a factory:
class User
{
public $username;
public $nestedObj; //another class that has a constructor for handling json
...
// This could be make private if the factories below are used exclusively
// and then make more sane constructors like:
// __construct($username, $password)
public function __construct($mixed)
{
if (is_object($mixed)) {
if (isset($mixed->username))
$this->username = $mixed->username;
if (isset($mixed->nestedObj) && is_object($mixed->nestedObj))
$this->nestedObj = new NestedObject($mixed->nestedObj);
...
} else if (is_array($mixed)) {
if (isset($mixed['username']))
$this->username = $mixed['username'];
if (isset($mixed['nestedObj']) && is_array($mixed['nestedObj']))
$this->nestedObj = new NestedObj($mixed['nestedObj']);
...
}
}
...
public static fromJSON_by_obj($json)
{
return new self(json_decode($json));
}
public static fromJSON_by_ary($json)
{
return new self(json_decode($json, TRUE));
}
}
You could create a FactoryClass of some sort:
function create(array $data)
{
$user = new User();
foreach($data as $k => $v) {
$user->$k = $v;
}
return $user;
}
It's not like the solution you wanted, but it gets your job done.
Have a look at this class I wrote:
https://github.com/mindplay-dk/jsonfreeze/blob/master/mindplay/jsonfreeze/JsonSerializer.php
It reserves a JSON object-property named '#type' to store the class-name, and it has some limitations that are described here:
Serialize/unserialize PHP object-graph to JSON
Bit late but another option is to use symfony serializer to deserialize xml, json, whatever to Object.
here is documentation:
http://symfony.com/doc/current/components/serializer.html#deserializing-in-an-existing-object
I'm aware that JSON doesn't support the serialization of functions, which is perfectly acceptable, and even desired. My classes are currently used as value objects in communicating with JavaScript, and functions would hold no meaning (and the regular serialization functions aren't usable).
However, as the functionality pertaining to these classes increases, encapsulating their utility functions (such as a User's save() in this case) inside the actual class makes sense to me. This does mean they're no longer strictly value objects though, and that's where I run into my aforementioned problem.
An idea I had would have the class name specified inside the JSON string, and would probably end up like this:
$string = '{"name": "testUser", "password": "testPassword", "class": "User"}';
$object = json_decode ($string);
$user = ($user->class) $object;
And the reverse would be setting the class property during/after json_encode. I know, a bit convoluted, but I'm just trying to keep related code together. I'll probably end up taking my utility functions out of the classes again; the modified constructor approach seems a bit opaque and runs into trouble with nested objects.
I do appreciate this and any future feedback, however.
Maybe the hydration pattern can be of help.
Basically you instantiate an new empty object (new User()) and then you fill in the properties with values from the StdClass object. For example you could have a hydrate method in User.
If possible in your case, you can make the User constructor accept an optional parameter of type StdClass and take the values at instantiation.
Old question, new answer.
What about creating your own interface to match JsonSerializable? ;)
/**
* Interface JsonUnseriablizable
*/
interface JsonUnseriablizable {
/**
* #param array $json
*/
public function jsonUnserialize(array $json);
}
example:
/**
* Class Person
*/
class Person implements JsonUnseriablizable {
public $name;
public $dateOfBirth;
/**
* #param string $json
*/
public function jsonUnserialize(array $json)
{
$this->name = $json['name'] ?? $this->name;
$this->dateOfBirth = $json['date_of_birth'] ?? $this->dateOfBirth;
}
}
$json = '{"name":"Bob","date_of_birth":"1970-01-01"}';
$person = new Person();
if($person instanceof JsonUnseriablizable){
$person->jsonUnserialize(json_decode($json, true, 512, JSON_THROW_ON_ERROR));
}
var_dump($person->name);
To answer your direct question, no, there's no was to do this with json_encode/json_decode. JSON was designed and specified to be a format for encoding information, and not for serializing objects. The PHP function don't go beyond that.
If you're interested in recreating objects from JSON, one possible solution is a static method on all the objects in your hierarchy that accepts a stdClass/string and populates variables that looks something like this
//semi pseudo code, not tested
static public function createFromJson($json){
//if you pass in a string, decode it to an object
$json = is_string($json) ? json_decode($json) : $json;
foreach($json as $key=>$value){
$object = new self();
if(is_object($value)){
$object->{$key} = parent::createFromJson($json);
}
else{
$object->{$key} = $value;
}
}
return $object;
}
I didn't test that, but I hope it gets the idea across. Ideally, all your objects should extend from some base object (usually named "class Object") so you can add this code in one place only.
Below is an example of using both static (i.e. you know the class type in code) and dynamic (i.e. you only know the class type at runtime) to deserialize JSON back into a PHP object:
Code
<?php
class Car
{
private $brand;
private $model;
private $year;
public function __construct($brand, $model, $year)
{
$this->brand = $brand;
$this->model = $model;
$this->year = $year;
}
public function toJson()
{
$arr = array(
'brand' => $this->brand,
'model' => $this->model,
'year' => $this->year,
);
return json_encode($arr);
}
public static function fromJson($json)
{
$arr = json_decode($json, true);
return new self(
$arr['brand'],
$arr['model'],
$arr['year']
);
}
}
// original object
echo 'car1: ';
$car1 = new Car('Hyundai', 'Tucson', 2010);
var_dump($car1);
// serialize
echo 'car1class: ';
$car1class = get_class($car1); // need the class name for the dynamic case below. this would need to be bundled with the JSON to know what kind of class to recreate.
var_dump($car1class);
echo 'car1json: ';
$car1Json = $car1->toJson();
var_dump($car1Json);
// static recreation with direct invocation. can only do this if you know the class name in code.
echo 'car2: ';
$car2 = Car::fromJson($car1Json);
var_dump($car2);
// dynamic recreation with reflection. can do this when you only know the class name at runtime as a string.
echo 'car3: ';
$car3 = (new ReflectionMethod($car1class, 'fromJson'))->invoke(null, $car1Json);
var_dump($car3);
Output
car1: object(Car)#1 (3) {
["brand":"Car":private]=>
string(7) "Hyundai"
["model":"Car":private]=>
string(6) "Tucson"
["year":"Car":private]=>
int(2010)
}
car1class: string(3) "Car"
car1json: string(48) "{"brand":"Hyundai","model":"Tucson","year":2010}"
car2: object(Car)#2 (3) {
["brand":"Car":private]=>
string(7) "Hyundai"
["model":"Car":private]=>
string(6) "Tucson"
["year":"Car":private]=>
int(2010)
}
car3: object(Car)#4 (3) {
["brand":"Car":private]=>
string(7) "Hyundai"
["model":"Car":private]=>
string(6) "Tucson"
["year":"Car":private]=>
int(2010)
}
PHP 8.1 allows to do it easily.
Suppose you have a class A:
Class A {
public function __construct(
public string $firstName,
public int $age) {
}
}
The easiest way to import from JSON:
$a = new A(...json_decode('{"firstName": "John", "age": 73}', true));
And that's all, folks.
Another way is to use the JMS (de)serializer: https://github.com/schmittjoh/serializer . It can be loaded using Composer.
This library allows you to (de-)serialize data of any complexity.
Currently, it supports XML and JSON.
It also provides you with a rich tool-set to adapt the output to your
specific needs.
Built-in features include:
(De-)serialize data of any complexity; circular references and complex exclusion strategies are handled gracefully.
Supports many built-in PHP types (such as dates, intervals)
Integrates with Doctrine ORM, et. al.
Supports versioning, e.g. for APIs
Configurable via XML, YAML, or Annotations

Categories