Reputation: 1370
My project communicates with an API that returns a string with a PHP serialized object. Normally, the response is a serialized array & there's no issue unserializing it. However, we just encountered an endpoint where it returns an object we had not encountered before and I'm not sure how to create a Class that can be used to unserialize the object.
This is what comes back from the API (just showing the part that is confusing):
s:8:" * _data";a:1:{
i:4057353;C:25:"Transfer_Sales_Order_Item":201:{
a:5:{
s:8:"quantity";i:1;
s:19:"id_sales_order_item";s:7:"4057353";
s:4:"name";s:44:"Empacadora Selladora FoodSaver al Alto Vacio";
s:10:"paid_price";d:796.13999999999999;
s:3:"sku";s:15:"LI200HL37WWCLMX";
}
}
}
Full serialized string:
O:16:"Service_Response":8:{s:11:" * _success";b:1;s:17:" * _errorMessages";a:0:{}s:19:" * _successMessages";a:0:{}s:22:" * _validationMessages";a:0:{}s:18:" * _noticeMessages";a:0:{}s:14:" * redirectUrl";N;s:23:" Service_Abstract _type";N;s:14:" * _resultData";a:1:{s:24:"NotAvailablesForTracking";O:40:"Transfer_Sales_Order_ItemCollection_test":6:{s:19:" * _objectClassName";s:25:"Transfer_Sales_Order_Item";s:15:" * _idAttribute";s:19:"id_sales_order_item";s:47:" Vendor_Transfer_AbstractCollection _indexDirty";b:0;s:8:" * _data";a:1:{i:4057353;C:25:"Transfer_Sales_Order_Item":201:{a:5:{s:8:"quantity";i:1;s:19:"id_sales_order_item";s:7:"4057353";s:4:"name";s:44:"Empacadora Selladora FoodSaver al Alto Vacio";s:10:"paid_price";d:796.13999999999999;s:3:"sku";s:15:"LI200HL37WWCLMX";}}}s:17:" * _setDataStrict";b:1;s:9:" * locale";N;}}}
It seems like it's an array of Objects / Classes (?) that just has an array? I've tried to look for what the C
symbol means, but no success. Any idea of what the structure of Transfer_Sales_Order_Item
should look like?
My guess is because the original class implements ArrayAccess
, but I tried that & unserialized still complained it didn't have a target to unserialize to.
For the moment we solved it by implementing Serializable
on the class & the unserialize()
method which gets the contents of the class as a serialized string. This then unserializes normally to an array & we went from there.
This is the code we're using to unserialize the string:
class Transformer
{
public static function debugUnserialize($className)
{
throw new \RuntimeException(sprintf('Cannot unserialize object %s', $className, 500);
}
public function transform($value)
{
$previousCallback = ini_get('unserialize_callback_func');
ini_set('unserialize_callback_func', self::class . '::debugUnserialzie');
$data = unserialize($value);
ini_set('unserialize_callback_func', $previousCallback);
return $data;
}
}
After adding an empty class Transfer_Sales_Order_Item
, PHP throws
Warning: Class Transfer_Sales_Order_Item has no unserializer
Note that this is not the exception from the unserialize_callback_func
callback. So it doesn't even get there.
Upvotes: 0
Views: 605
Reputation: 2564
Add this somewhere in your script and it should solve your problem
<?php
function unserialize_callback_func($fqcn){
$explode = explode("\\", $fqcn);
$namespace = implode("\\", array_slice($explode, 0, count($explode)-1));
$className = end($explode);
$dev = "
namespace $namespace;
class $className{
}
";
eval($dev);
};
ini_set('unserialize_callback_func', 'unserialize_callback_func');
once your ini_set is registered, it will call unserialize_callback_func()
each time unserialize
runs into an object that doesn't exist. However, it needs to be registered prior to calling unserialize()
this callback function will create a class named whatever you want, allowing unserialize to set each property as a publicly accessible property.
if eval()
is not allowed on your server, you can define each class needed inside the function
Since you gave me your class, and the full serialize string, I've fixed your class up. This is the script I used to test it all out:
<?php
$data = 'O:16:"Service_Response":8:{s:11:" * _success";b:1;s:17:" * _errorMessages";a:0:{}s:19:" * _successMessages";a:0:{}s:22:" * _validationMessages";a:0:{}s:18:" * _noticeMessages";a:0:{}s:14:" * redirectUrl";N;s:23:" Service_Abstract _type";N;s:14:" * _resultData";a:1:{s:24:"NotAvailablesForTracking";O:40:"Transfer_Sales_Order_ItemCollection_test":6:{s:19:" * _objectClassName";s:25:"Transfer_Sales_Order_Item";s:15:" * _idAttribute";s:19:"id_sales_order_item";s:47:" Vendor_Transfer_AbstractCollection _indexDirty";b:0;s:8:" * _data";a:1:{i:4057353;C:25:"Transfer_Sales_Order_Item":201:{a:5:{s:8:"quantity";i:1;s:19:"id_sales_order_item";s:7:"4057353";s:4:"name";s:44:"Empacadora Selladora FoodSaver al Alto Vacio";s:10:"paid_price";d:796.13999999999999;s:3:"sku";s:15:"LI200HL37WWCLMX";}}}s:17:" * _setDataStrict";b:1;s:9:" * locale";N;}}}';
class Transformer
{
public static function debugUnserialize($className)
{
$explode = explode("\\", $className);
$namespace = implode("\\", array_slice($explode, 0, count($explode)-1));
$className = end($explode);
$dev = "
".(!empty($namespace)?"namespace $namespace;":"")."
class $className implements Serializable {
public function unserialize(".'$data'.") {
".'$this->data'." = unserialize(".'$data'.");
}
public function serialize() {
return serialize(".'$this->data'.");
}
public function getData(){
return ".'$this->data'.";
}
}
";
eval($dev);
}
public function transform($value)
{
$previousCallback = ini_get('unserialize_callback_func');
ini_set('unserialize_callback_func', self::class . '::debugUnserialize');
$data = unserialize($value);
ini_set('unserialize_callback_func', $previousCallback);
return $data;
}
}
$trans = new Transformer();
var_dump($trans->transform($data));exit;
you're receiving data serialized in that way because, when serialize()
is called on a class, without implementing a serialize difference, it will pull out all of the properties in such a way that it looks like an array. I used part of your provided information to create a test script:
<?php
class Transfer_Sales_Order_Item {
public
$quantity = 1
, $id_sales_order_item = '4057353'
, $name = 'Empacadora Selladora FoodSaver al Alto Vacio'
, $paid_price = 796.14
, $sku = 'LI200HL37WWCLMX'
;
}
echo serialize(new Transfer_Sales_Order_Item);
will output:
O:25:"Transfer_Sales_Order_Item":5:{s:8:"quantity";i:1;s:19:"id_sales_order_item";s:7:"4057353";s:4:"name";s:44:"Empacadora Selladora FoodSaver al Alto Vacio";s:10:"paid_price";d:796.13999999999999;s:3:"sku";s:15:"LI200HL37WWCLMX";}
however using this class instead:
class Transfer_Sales_Order_Item {
private
$quantity = 1
, $id_sales_order_item = '4057353'
, $name = 'Empacadora Selladora FoodSaver al Alto Vacio'
, $paid_price = 796.14
, $sku = 'LI200HL37WWCLMX'
;
}
will output the class name prefixed to each property to show that the property belongs solely to the class.
O:25:"Transfer_Sales_Order_Item":5:{s:35:"Transfer_Sales_Order_Itemquantity";i:1;s:46:"Transfer_Sales_Order_Itemid_sales_order_item";s:7:"4057353";s:31:"Transfer_Sales_Order_Itemname";s:44:"Empacadora Selladora FoodSaver al Alto Vacio";s:37:"Transfer_Sales_Order_Itempaid_price";d:796.13999999999999;s:30:"Transfer_Sales_Order_Itemsku";s:15:"LI200HL37WWCLMX";}
and for consistency sake if it were changed to protected
, an *
will be prefixed to each property to show that the property belongs to all classes inside the class inheritance structure.
the reason for all of this, is a simple way of showing how the properties exist in a class, and a string representation to show such a thing.
Upvotes: 0