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
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 DTO class looking like:
class ParamsDto
{
#[Assert\Type(ArrayCollection::class)]
#[Assert\All([
new Assert\Type('digit'),
new Assert\Positive(),
])]
private ?ArrayCollection $tagIds = null;
public function getTagIds(): ?ArrayCollection
{
return $this->tagIds;
}
public function setTagIds(?ArrayCollection $tagIds): self
{
$this->tagIds = $tagIds;
return $this;
}
}
Given a request to a url like https://domain.php?tag-ids[]=2, I'd like to parse the tag-ids request param into this DTO's tagIds property.
First step I did, I created a name converter, so I can convert between tag-ids and tagIds, so my serializer instantiation looks like:
$nameConverter = new EducationEntrySearchParamNameConverter();
$serializer = new Serializer([
new ArrayDenormalizer(),
new ObjectNormalizer(null, $nameConverter, null, new ReflectionExtractor()),
], [new JsonEncoder()]);
$params = $serializer->denormalize($requestParams, ParamsDto::class);
where $params shows as:
^ App\DataTransferObject\ParamsDto {#749
-tagIds: Doctrine\Common\Collections\ArrayCollection {#753
-elements: []
}
}
So it is instantiated but it is empty.
Most likely because my request does not include the elements key in it.
If I do a bit of preprocessing, like:
$requestParams = [];
foreach ($request->query->all() as $key => $value) {
if (in_array($key, ['tag-ids'])) {
$requestParams[$key] = ['elements' => $value];
} else {
$requestParams[$key] = $value;
}
}
$params = $serializer->denormalize($requestParams, ParamsDto::class);
then I get the right output:
^ App\DataTransferObject\ParamsDto {#749
-tagIds: Doctrine\Common\Collections\ArrayCollection {#757
-elements: array:1 [
0 => "2"
]
}
}
How do I do it in a way that the serializer translate the request into the DTO in a way where I don't have to do this pre-processing?
L.E: No need for using a custom name converter, I am now using SerializedName
Short answer: I would strongly suggest to replace the collection with just a standard php built-in array instead - at least on the setter! -, I bet that would help.
Long answer:
My guess is, since your DTO isn't an entity, that the serializer won't use something doctrine-related, which would tell it to convert arrays to collections.
IMHO, Collections are object-internal solutions that should never be exposed directly to the outside, the interface to the outside world are arrays. Very opinionated maybe.
From a non-doctrine Serializer's perspective, a collection is an object. Objects have properties. To map an array onto an object is to use the keys of the array and match them to the properties of the object, using reflection: Using the property directly, if it is public, else using adders/removers, else using setters/getters. (IIRC in that order) - That's why adding elements as a key to your input with the array of ids "works".
Using an array as the interface in the setter/getter pair will help the property accessor greatly to just set the array. You can still have the property internally as an ArrayCollection, if that's what you want/need.
This is just a guess: adding docblock type hint /** #param array<int> $tagIds */ to your setter might even trigger conversion from "2" to 2. Having addTagId+removeTagId+getTagIds instead with a php type hint might also work instead.
I'll add this in order to help others struggling with same problem, but I was able to come up with it only after #Jakumi's answer, this is also why I am going to award them the bounty.
Therefore, after some research, it seems that indeed, the simplest way to go about this is to just use array as the type hint. This way, things will work out of the box, without you doing anything else, but you won't be using collections, so:
#[Assert\Type('array')]
#[Assert\All([
new Assert\Type('numeric'),
new Assert\Positive(),
])]
#[SerializedName('tag-ids')]
private ?array $tagIds = null;
public function getTagIds(): ?array
{
return $this->tagIds;
}
public function setTagIds(?array $tagIds): self
{
$this->tagIds = $tagIds;
return $this;
}
However, if you want to keep using collections, then you need to do some extra work:
#[Assert\Type(ArrayCollection::class)]
#[Assert\All([
new Assert\Type('numeric'),
new Assert\Positive(),
])]
#[SerializedName('tag-ids')]
private ?ArrayCollection $tagIds = null;
public function getTagIds(): ?ArrayCollection
{
return $this->tagIds;
}
public function setTagIds(array|ArrayCollection|null $tagIds): self
{
if (null !== $tagIds) {
$tagIds = $tagIds instanceof ArrayCollection ? $tagIds : new ArrayCollection($tagIds);
}
$this->tagIds = $tagIds;
return $this;
}
// Make sure our numeric values are treated as integers not strings
public function addTagId(int $tagId): void
{
if (null === $this->getTagIds()) {
$this->setTagIds(new ArrayCollection());
}
$this->getTagIds()?->add($tagId);
}
public function removeTagId(int $tagId): void
{
$this->getTagIds()?->remove($tagId);
}
So beside the setter and getter, you'll need to add a adder and a remover as well.
This will deserialize into a ArrayCollection of integers.
Bonus, if your object property is a collection of DTO's, not just integers, and you want to denormalize those as well, then you'd do something like:
#[Assert\Type(TagCollection::class)]
#[Assert\All([
new Assert\Type(TagDto::class),
])]
#[Assert\Valid]
private ?TagCollection $tags = null;
private PropertyAccessorInterface $propertyAccessor;
public function __construct()
{
$this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
->disableExceptionOnInvalidIndex()
->disableExceptionOnInvalidPropertyPath()
->getPropertyAccessor()
;
}
public function getTags(): ?TagCollection
{
return $this->tags;
}
public function setTags(array|TagCollection|null $tags): self
{
if (null !== $tags) {
$tags = $tags instanceof TagCollection ? $tags : new TagCollection($tags);
}
$this->tags = $tags;
return $this;
}
public function addTag(array $tag): void
{
if (null === $this->getTags()) {
$this->setTags(new TagCollection());
}
$tagDto = new TagDto();
foreach ($tag as $key => $value) {
$this->propertyAccessor->setValue($tagDto, $key, $value);
}
$this->getTags()?->add($tagDto);
}
public function removeTag(TagDto $tag): void
{
$this->getTags()?->remove($tag);
}
This will denormalize the tags property into a ArrayCollection instance (TagCollection extends it) and each item in the collection will be a TagDto in this case.
I've been working on a custom WordPress plugin for integrating YouTube content into a gaming blog, and in order to limit the strain on the daily API quota, I've been trying to come up with a way to store temporary cache objects which expire after 24 hours.
I created the following class to manage the data:
public class YouTubeDataCache {
protected $dataObject;
protected $expirationDate;
__construct($dataObject) {
$this->dataObject = $dataObject;
$this->expirationDate = time() + 86400;
}
public function isExpired() {
return $this->expirationDate < time();
}
public function getDataObject() {
return $this->dataObject;
}
}
And then, I call json_encode($dataCache) on instances of this class to generate a JSON representation of the instance that I can store in the DB.
Once I started testing this, though, I noticed two significant problems:
The database entries were blank despite verifyin that Google's API returned actual results.
calling json_decode() on the strings pulled out of the database returned fatal errors for undefined methods when I tried calling isExpired() on the decoded object.
My question is two-fold:
How can I make sure that all the necessary data elements get encoded into the JSON string?
How can I retain access to the object's methods after calling json_decode()?
It took some time to track this down, but here is the solution that I came up with.
How do I store all the necessary data elements in the cache?
The specifics of this will vary slightly depending on individual use-cases but the gist of it is this: json_encode() will only encode public variables.
Because my $dataObject and $expirationDate were defined as protected, json_encode() had no access to the values, and therefore encoded blank objects. Oops.
Digging a little deeper, the objects returned from the YouTube API also contained a lot of protected data elements, so even if I changed my class variables to public, I'd still encounter a similar problem trying to store things like video thumbnails.
Ultimately, I had to create my own serialization function.
Newer versions of PHP can utilize the JsonSerializable interface and define a class-specific implementation which, according to the documentation, "returns data which can be serialized by json_encode()"
Since I can't accurately predict what version of PHP users of this plugin will be running, I opted to just create a toJSON() method instead, and modified the constructor to pull specific data elements out of the $dataObject.
First, the constructor/class variables:
For my particular use-case, I'm only concerned about the ID, snippet, status and the thumbnail information. If you need additional data elements, you'll want to dig into the structure of the response object to see what your serialization function needs to manually include.
YouTubeDataCache {
protected $objectId;
protected $snippet;
protected $status;
protected $thumbnails = array();
protected $expirationDate;
__construct($dataObject = null) {
$this->expirationDate = time() + 86400;
if ($dataObject === null) {
return; // I'll explain later
}
$this->objectId = $dataObject->getId();
$this->snippet = $dataObject->getSnippet();
$this->status = $dataObject->getStatus();
$thumbs = $dataObject->getThumbnails();
if ($thumbs->getDefault()) {
$this->addThumbnail('default', $thumbs->getDefault());
}
if ($thumbs->getMedium()) {
$this->addThumbnail('medium', $thumbs->getMedium());
}
if ($thumbs->getHigh()) {
$this->addThumbnail('high', $thumbs->getHigh());
}
if ($thumbs->getMaxRes()) {
$this->addThumbnail('maxRes', $thumbs->getMaxRes());
}
}
public function setExpirationDate($expirationDate) {
$this->expirationDate = $expirationDate;
}
public function addThumbnail($name, $data) {
$this->thumbnails[$name] = $data;
}
public function getObjectId() {
return $this->objectId;
}
public function setObjectId($objectId) {
$this->objectId = $objectId;
}
public function getSnippet() {
return $this->snippet;
}
public function setSnippet($snippet) {
$this->snippet = $snippet;
}
public function getStatus() {
return $this->status;
}
public function setStatus($status) {
$this->status = $status;
}
}
NOTE: There is one caveat here. Technically, the thumbnail information is contained in the Snippet, but it's tied up in a protected array. Since I'll need to pull it out of that array eventually, doing it here provides a more consistent access pattern for what comes next.
Next I added the custom serialization method
NOTE: This is a class method. I just pulled it out for readability
public function toJSON() {
$array = array(
'objectId' => $this->objectId,
'snippet' => $this->snippet,
'status' => $this->status,
'thumbnails' => $this->thumbnails,
'expirationDate' => $this->expirationDate
);
return json_encode($array);
}
With this function, I can now call $cacheObject->toJson() to generate an accurately encoded JSON representation of the data that I can then write to the database, which brings us to the second part of the solution.
How do I retain access to the cache object's methods?
It turns out json_decode creates either a stdClass instance or an associative array (depending on how you choose to call the function). In either scenario, it doesn't have any of the methods present on the class originally used to create the JSON string, so I ultimately had to create a custom deserialization method as well.
I chose to make this particular function static so that I didn't need an instance of the cache object to use it.
public static function fromJSON($json) {
$data = json_decode($json);
$cacheObject = new YouTubeDataCache();
/* This is why the constructor now accepts a null argument.
I won't always have access to an API response object from
Google, and in such cases, it's faster to just call setters
on this class rather than jumping through hoops to create the
Google class instance. */
$cacheObject->setExpirationDate($data->expirationDate);
$cacheObject->setObjectId($data->objectId);
$cacheObject->setSnippet($data->snippet);
$cacheObject->setStatus($data->status);
foreach($data->thumbnails as $name => $thumbnail) {
$cacheObject->addThumbnail($name, $thumbnail);
}
return $cacheObject;
}
The final class definition
Just for reference purposes, here is the final class definition all in one place:
YouTubeDataCache {
protected $objectId;
protected $snippet;
protected $status;
protected $thumbnails = array();
protected $expirationDate;
__construct($dataObject = null) {
$this->expirationDate = time() + 86400;
if ($dataObject === null) {
return;
}
$this->objectId = $dataObject->getId();
$this->snippet = $dataObject->getSnippet();
$this->status = $dataObject->getStatus();
$thumbs = $dataObject->getThumbnails();
if ($thumbs->getDefault()) {
$this->addThumbnail('default', $thumbs->getDefault());
}
if ($thumbs->getMedium()) {
$this->addThumbnail('medium', $thumbs->getMedium());
}
if ($thumbs->getHigh()) {
$this->addThumbnail('high', $thumbs->getHigh());
}
if ($thumbs->getMaxRes()) {
$this->addThumbnail('maxRes', $thumbs->getMaxRes());
}
}
public function setExpirationDate($expirationDate) {
$this->expirationDate = $expirationDate;
}
public function addThumbnail($name, $data) {
$this->thumbnails[$name] = $data;
}
public function getObjectId() {
return $this->objectId;
}
public function setObjectId($objectId) {
$this->objectId = $objectId;
}
public function getSnippet() {
return $this->snippet;
}
public function setSnippet($snippet) {
$this->snippet = $snippet;
}
public function getStatus() {
return $this->status;
}
public function setStatus($status) {
$this->status = $status;
}
public function toJSON() {
$array = array(
'objectId' => $this->objectId,
'snippet' => $this->snippet,
'status' => $this->status,
'thumbnails' => $this->thumbnails,
'expirationDate' => $this->expirationDate
);
return json_encode($array);
}
public static function fromJSON($json) {
$data = json_decode($json);
$cacheObject = new YouTubeDataCache();
$cacheObject->setExpirationDate($data->expirationDate);
$cacheObject->setObjectId($data->objectId);
$cacheObject->setSnippet($data->snippet);
$cacheObject->setStatus($data->status);
foreach($data->thumbnails as $name => $thumbnail) {
$cacheObject->addThumbnail($name, $thumbnail);
}
return $cacheObject;
}
}
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
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.