Use JSM Serializer to serialise to JSON 'primitives' - php

Perhaps I am going at it in the wrong way, but I would like to be able to serialise an object (which represents a primitive) into JSON 'primitives'. With the help of JsonSerializable Interface and json_encode that is easy but I would like to be able to generalise this with help of JSM Serializer.
I played with the inline annotation, but whatever I try it seems when serialising an object an string representing of an object is mandatory? Is this correct? How would I be able to do this if possible?
class ATest implements SingularValueObjectInterface, JsonSerializable
{
/**
* #JMS\Accessor(getter="get",setter="set")
*/
private $value = 45;
/**
* #return string
* #JMS\Inline
*/
public function get()
{
return $this->value * $this->value;
}
public function jsonSerialize()
{
return $this->get();
}
}
$serializer = \JMS\Serializer\SerializerBuilder::create()->build();
$a = new ATest();
var_dump(json_encode($a), $serializer->serialize($a, 'json'));
string(4) "2025" <- I want this.
vs
"{"value":2025}"
I have more complex objects too representing arrays/collections for example

Related

Deep-copy Object with Array of Object Properties - how to or alternative?

That's gonna be a tough one to explain.
I have a class Tree which is rather complex, I try to simplify:
class Tree {
/**
* #var Node[]
*/
private $nodes;
/**
* #var Edge[]
*/
private $edges;
}
class Node {
/**
* #var Value[]
*/
private $values;
/**
* #var array
*/
private $someArray;
}
class Value {
/**
* #var float
*/
private $result;
}
So you can see I have an object Tree containing two arrays of Objects again (Node and Edge) and every Node has an array of objects (Value), some other 'simple array' and every Value has a property result.
To calculate the property result I basically need to run up and down my tree etc... so some have business logic which will end up in still the same Tree but having some calculated results for my nodes.
So what I do so far is something like:
$tree = myCalculationFunction($tree, $calcParameter);
return $tree->getNode(1)->getValue(1)->getResult();
But no when I call additionally the same function with a different calcParameter of course my Tree operates on referenced Nodes, Values etc.
So I cannot:
$initialTree = myCalculationFunction($tree, $calcParameter);
$treeOne = myCalculationFunction($initialTree, $calcParameterOne);
$treeTwo = myCalculationFunction($initialTree, $calcParameterTwo);
$result1 = $treeOne->getNode(1)->getValue(1)->getResult();
$result2 = $treeTwo->getNode(1)->getValue(1)->getResult();
So by I have no deep-copy of my $initialTree because all objects in it are byReference. I cannot clone and I don't see how some manual/custom deep-copy like here will work for this case.
How can I achieve this here? I basically need the initialTree to be stable and every calculation function call manipulates a fully copy of the initially 'calculated tree'.
You could extend the approach from this question, and implement a custom __clone method for each of your classes. Since there don't seem to be any relations between the nodes or edges themselves, it should be enough to accomplish what you want.
It might be worth mentioning that, as described in the documentation, __clone is invoked on the new object just after cloning. It isn't actually responsible for cloning the object which might seem logical at first.
So, simplified for the Tree and Node class:
class Tree
{
private $nodes;
private $edges;
public function __clone()
{
$this->nodes = array_map(function ($node) {
return clone $node;
}, $this->nodes);
$this->edges = array_map(function ($edge) {
return clone $edge;
}, $this->edges);
// clone other properties as necessary
}
}
class Node
{
private $values;
private $someArray;
public function __clone()
{
$this->values = array_map(function ($value) {
return clone $value;
}, $this->values);
$this->someArray = array_map(function ($elem) {
return clone $elem;
}, $this->someArray);
// clone other properties as necessary
}
}
Just follow this pattern for each class down the road and it will recursively deep clone your whole tree.

What is the best practice in PHP to create multiple objects if you know only their interface

I have a class to manipulate objects with defined interface
class TaskManager
{
/**
* #param TaskInterface $task
* #param string $command
* #return TaskInterface
*/
public static function editTask($task, $command)
{
$task->setStatus(TaskInterface::TASK_STATUS_ACTIVE);
$task->setCommand($command);
$task->taskSave();
return $task;
}
}
I can create single object by passing its instance as an method argument. This is pretty straightforward. But how should I create many of them?
public static function export()
{
$commands = self::getCommandsToAdd();
foreach($commands as $c){
//This is wrong.
$task = new TaskInterface();
$task->setCommand($c);
$task->save();
//don't need to return it if it's saved
}
}
I can't create it this way. And it's obviously a bad idea to pass array of new objects. Another way is to pass a class name as a string and call its method to retrieve new object. But it seems wrong as well
I think I figured out a solution by myself. Can use a factory interface to pass a factory object.
interface TaskFactoryInterface
{
public static function createNew();
}
/**
* #param TaskFactoryInterface $task_factory
*/
public static function export($task_factory)
{
$commands = self::getCommandsToAdd();
foreach($commands as $c){
$task = $task_factory::createNew();
$task->setCommand($c);
$task->save();
//don't need to return it if it's saved
}
}
What do you think?

Callback on serializer Symfony

I'm running Symfony 2.7 and I'm trying output an object (Doctrine entity) as JSON.
When I'm normalizing the object I want to convert some of it's values. To do this I found the "setCallbacks" method in the documentation but I'm kinda stumped on how to apply it to my case.
Is there any way to call the "setCallbacks" method on the normalizer that is set when calling Symfonys serializer service?
Here is a short example of what I'm trying to achieve:
//ExampleController.php
public function getJSONOrderByIdAction($id) {
$serializer = $this->get('serializer');
$normalizer = $serializer->getNormalizer(); // <- This is what I'm unable to do
$dateTimeToString = function ($dateTime) {
return $dateTime instanceof \DateTime ? $dateTime->format(\DateTime::ISO8601) : '';
};
$normalizer->setCallbacks(['time' => $dateTimeToString]);
$order = $this->getDoctrine()->find("AppBundle:Order", $id);
return new JsonResponse(["order" => $serializer->normalize($order, null, ["groups" => ["public"]])]);
}
I'm aware that most people have switched to the JMS serializer. It just seems as if the built in serializer should be able to handle what I'm trying to achieve.
The default Serializer service is created during dependency injection phase, and the Serializer interface do not allow editing of (full) retrieval of normalizers.
I think you have (at least) three choice here:
add your custom normalizer to the default Serializer service
add NormalizableInterface to your entities
create a new Serializer service (or a local object as suggested by the docs) as you were trying to do.
I think in your scenario, case 1 is preferred (since 2 becomes boring pretty fast).
I would do something like this; first create a custom Normalizer
<?php
namespace AppBundle;
class DateTimeNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface
{
/**
* {#inheritdoc}
*/
public function normalize($object, $format = null, array $context = array())
{
return $object->format(\DateTime::ISO8601);
}
/**
* {#inheritdoc}
*/
public function denormalize($data, $class, $format = null, array $context = array())
{
return new $class($data);
}
/**
* Checks if the given class is a DateTime.
*
* #param mixed $data Data to normalize.
* #param string $format The format being (de-)serialized from or into.
*
* #return bool
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof \DateTime;
}
/**
* Checks if the given class is a DateTime.
*
* #param mixed $data Data to denormalize from.
* #param string $type The class to which the data should be denormalized.
* #param string $format The format being deserialized from.
*
* #return bool
*/
public function supportsDenormalization($data, $type, $format = null)
{
$class = new \ReflectionClass($type);
return $class->isSubclassOf('\DateTime');
}
}
Then register it to your services:
# app/config/services.yml
services:
datetime_normalizer:
class: AppBundle\DateTimeNormalizer
tags:
- { name: serializer.normalizer }
My own solution
Following the advice from giosh94mhz I tried switching to JMS Serializer but ended up going back to Symfonys serializer.
JMS Serializer presented it's own issues and while searching for answers for those I stumbled upon a blog post by Thomas Jarrand that did an excellent job explaining how to make and implement your own normalizers in Symfony.
You can use callback normalizer of K. Dunglas component.
You can see that in ObjectNormalizer (in normalize method)
if (isset($this->callbacks[$attribute])) {
$attributeValue = call_user_func($this->callbacks[$attribute], $attributeValue);
}
This mean that you must use in callback array key the name of property you wan't to normalize.
For example in my entity I have field named "name" of type "pgarray" (like array for postgresql). I wan't to normalize this data. Instead array I want a string.
/**
* $object represent the property "name" because callback is attached to name property (setCallback)
*/
$nameCallback = function ($object, $outerObject = null) {
return $object[0];
};
$this->normalizer->setCallbacks(['name' => $dateCallback]);
Just remember since Symfony 4.2 you must use $context in DI to use callback.
In my opinion, you seem to be trying to over-complicate things. Here's the approach I've taken when I needed to serialize my entities as JSON:
PHP 2.5 and above allows you to implement the jsonSerialize method on your objects and just call json_encode directly on your object.
If you are still using PHP 2.4, you just need to manually call jsonSerialize() on your objects.
For example:
/**
* #ORM\Entity
*/
class MyEntity {
...
public function jsonSerialize() {
$data = array("foo" => $this->bar());
// add other data here ...
return $data
}
}
And then in calling code:
// for PHP 2.5 and up:
$normalized = json_encode($myEntityInstance);
// for PHP 2.4 and below
$normalized = json_encode($myEntityInstance->jsonSerialize());

Handling array of object and intellisense

I am finding a lot of the time I pass an array of custom objects to a function for processing. In my function I check to make sure it is the right class before accessing internal properties/methods.
Because I am taking an array and as far as I am aware PHP doesn't have any typed generic list I cannot use type hinting so intellisense in my IDE doesn't work and code inspection throws warnings.
I came across an old post that gave the idea of throwing in this line of code in order to get intellisense working while developing:
if (false) $myObj = new MyObject();
So I end up with a function that looks like:
function ProcessObjectArray(array $arrayOfObject)
{
foreach ($arrayOfObject as $key => $myObj) {
if (get_class($myObj) == 'MyNamespace\MyObject') {
if (false) $myObj = new MyObject();
// I can now access intellisense for MyObject in here
} else {
trigger_error('is not right object');
}
}
}
It seems like a bit of a weird hack so I am wondering if this is a sign that I am not handling arrays of objects in the best way or if there is a better way to get my intellisense working. I had a look at the arrayobject interface to see if I could create a class implementing arrayobject that would hold a typed list. While it makes it nice to put all validation inside its constructor or append like functions I couldn't get it working with intellisense as the internal container is still just a standard array. Also using get_class doesn't seem good as if a class name or namespace is renamed then the IDE refactoring features do not pick this reference up (well, PHPStorm doesn't at least and it is one I am using).
I of course don't need intellisense but the weirdness of this hack made me wonder if I have the right approach to OOP in PHP and thought I might be missing something. Is this the right way of doing it or have I missed something?
with phpstorm when you add your annotations it will correctly do the code completion for you.
/**
* #param MyNamespace\MyObject[] $arrayOfObject
*/
function ProcessObjectArray(array $arrayOfObject)
{
foreach ($arrayOfObject as $key => $myObj) {
if (get_class($myObj) == 'MyNamespace\MyObject') {
if (false) $myObj = new MyObject();
// I can now access intellisense for MyObject in here
} else {
trigger_error('is not right object');
}
}
}
However intellisense won't stop you from passing in incorrect objects, it will only help you while coding to prevent passing in the incorrect objects.
Perhaps you may wish to consider using collections instead of using generic arrays.
There are lots of examples out there, however here is a simple one which you can expand upon
class Collection
{
/** #var array */
private $items = [];
/**
* #param MyNamespace\MyObject $obj
* #return $this
*/
public function addItem(MyNamespace\MyObject $obj) {
$this->items[] = $obj;
return $this;
}
/**
* #param $index
* #return $this
*/
public function deleteItem($index) {
unset($this->items[$index]);
return $this;
}
/**
* #param $index
* #return MyNamespace\MyObject|null
*/
public function getItem($index) {
return array_key_exists($index, $this->items) ? $this->items[$index] : null;
}
/**
* #return MyNamespace\MyObject[]
*/
public function getItems() {
return $this->items;
}
/**
* #return int
*/
public function count() {
return sizeOf($this->items);
}
}
here is an example how it all works:
$collection = new \Collection;
$obj = new MyNamespace\MyObject;
$collection->addItem($obj);
foreach($collection->getItems() as $item) {
// this will already know that $item is MyNamespace\MyObject because of the annotation on the getItems() method
$item->doSomething();
}
class MyCollection implements \Iterator
{
/**
* #var CollectionElement[]
*/
private $elements = [];
public function addElement(CollectionElement $element)
{
$this->elements[] = $element;
}
/**
* #return CollectionElement
*/
public function current()
{
return current($this->elements);
}
//... Rest of the Iterator implementation
}
This way you cannot add to the collection anything other than CollectionElement. And after:
foreach($collection as $element){
/** #var CollectionElement $element */
$element->//here you will have autocompletion
}
You don't need to check anything since your collection will never contain anything you are not expecting.
If you are going this route, I would avoid using the are native arrays. I would make classes that contain arrays of a certain type. That way, you can validate the array contents in the constructor of the class, and then you can use actual type hinting for the objects, which PHP allows.

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