Serializing Phalcon\Mvc\Model loses object property that's not a part of schema.
I have the following Model, which upon load sets array of states:
class Country extends Phalcon\Mvc\Model
{
protected $states;
public function initialize()
{
$this->setSource('countries');
}
public function afterFetch()
{
if ($this->id) {
$this->states = ['AL', 'AZ', 'NV', 'NY'];
}
}
}
I do this:
$country = Country::findFirst($countryId);
$serialized = serialize($country);
$unserialized = unserialize($serialized);
$serialized string does not even contain "states" substring. Hence, "states" are missing in unserialized object.
I have discovered this while working on user authentication and persistence in session (which involved serialization/unserialization). My User object was losing all properties that were loaded in afterFetch() phase.
Two questions:
Why did "states" property disappear upon serialization?
Is it a bad practice in Phalcon world to persist models (which I thought is a convenient way of storing user object in session)?
I am on Phalcon 1.3.0.
Thanks,
Temuri
\Phalcon\Mvc\Model implements Serializable interface.
To serialize your own properties (which \Phalcon\Mvc\Model is unaware of), you will need to use a trick like this: http://ua1.php.net/manual/en/class.serializable.php#107194
public function serialize()
{
$data = array(
'states' => $this->states,
'parent' => parent::serialize(),
);
return serialize($data);
}
public function unserialize($str)
{
$data = unserialize($str);
parent::unserialize($data['parent']);
unset($data['parent']);
foreach ($data as $key => $value) {
$this->$key = $value;
}
}
The answer is - Phalcon serializer currently ignores all non-Model properties in order to make serialized objects light.
I've filed a new NFR: https://github.com/phalcon/cphalcon/issues/1285.
Related
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);
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);
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();
I have such model for my company with have method setOptions for creating object from array or Zend_DB_Table_Row
<?php
class Model_Company
{
protected $c_id;
protected $c_shortname;
protected $c_longname;
public function __constructor(array $options = null)
{
if(is_array($options)) {
$this->setOptions($options);
}
}
public function setOptions($data)
{
if($data instanceof Zend_Db_Table_Row_Abstract) {
$data = $data->toArray();
}
if(is_object($data)) {
$data = (array)$data;
}
if(!is_array($data)) {
throw new Exception('Initial data must be object or array');
}
$methods = get_class_methods($this);
foreach($data as $key => $value) {
$method = 'set'.ucfirst($key);
if(in_array($method, $methods)) {
$this->{'c_'.$key} = $value;
}
}
return $this;
}
I also have company manager with param like model and adapter for different db/soap/rest
class Model_CompanyManager
{
private $_adapter;
public function __construct(Model_Company $model,$adapter)
{
$this->_adapter = $adapter;
$this->_model = $model;
return $this;
}
/**
* Save data
* #var Model_Company $data
*/
public function save(array $data)
{
$data = $this->_model->setOptions($data);
$this->_adapter->save($data);
}
}
And DBTable in DBTable
class Model_DbTable_Company extends Zend_Db_Table_Abstract
{
protected $_name = 'company';
protected $_id = 'c_id';
public function save(Model_Company $data)
{
try {
if(isset($data['c_id'])) {
$this->update($data, 'c_id = :c_id',array('c_id' => $data['c_id']));
} else {
$this->insert($data);
}
} catch (Zend_Db_Exception $e) {
throw new Exception($e->getMessage());
}
}
}
How can I insert into db because model properties are protected and I can't do (array)$data
$this->_companyManager = new Model_CompanyManager(new Model_Company,new Model_DbTable_Company);
$this->_companyManager->save($values);
I don't what to create array with fields name inside save like this:
$data = array(
'c_shortname' => $company->getShortname(),
'c_longname' => $company->getLongName(),
'c_description' => $company->getDescription(),
'c_salt' => $company->getSalt(),
'c_password' => $company->getPassword(),
'c_updated_at' => date('YmdHis')
);
Because when I gonna change model fields names and other stuff I have to remember also to change here... Is there is simple approach , pattern that model keep everything and it clean
If you simply want to get a list of all your objects properties with reflection you could use get_class_vars. With a recent version of PHP this will return all the class variables regardless of scope. But since you are tossing in this Manager, usually called a Data Mapper, I assume you expect your model objects will not align exactly to a database table. Otherwise you should just stick to the table-row-gateway pattern of the Zend_Db_Table classes.
I personally am a big fan of the Data Mapper pattern in conjunction with ZF applications. Only the most simple apps are going to line up to relational database tables. It encourages richer objects and writing models and business logic before database schemas.
The job of the mapper is exactly what it suggests, to map your entity to a persistence layer, so handling in the mappers save() method, the actual assignments for the SQL statement (probably building an array with your tables field names and assigning the values, maybe even saving to multiple tables) is perfectly acceptable.
If some of your objects are more simple and do align better with a table, as will certainly be the case, what I like to do is have a __toArray() method for the object. It can be responsible for returning a representation suitable for building an insert/update statement. Also useful for needing a JSON representation for serving it via AJAX. You can be as lazy as you like in writing these - use that get_class_vars function or other reflection. I usually have two mapper base classes. One with CRUD functions that essentially do what Zend_Db_Table does, and another more skeletal where I am responsible for writing more of the code. They should follow a common interface. Kinda gets you the best of both worlds.
I found this link to be a good resource for some ideas.
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