Changed behavior of (un)serialize()?

2019-07-05 15:22发布

问题:

EDIT: Problem is a documented php bug by now: https://bugs.php.net/bug.php?id=71617 thanks to for finding that one @Danack

I'm just migrating an application from PHPH 5.5 to PHP 7 and stumbled over some strange behavior when it comes to serializing objects.

I have tried to cook it down to a minimal, complete and verifiable example which can be found at http://sandbox.onlinephpfunctions.com/code/e926a7398119ea715531cafe4ce6a22c329e53b8

The problem is that if a class extends ArrayObject then all private properties seem to just vanish if you serialize() and then unserialize() that object:

  1. create a class with a private property and getter/setter method for that property
  2. create an object of that class
  3. set private property via setter method
  4. serialize() object
  5. unserialize() result of step 4
  6. call getter method of private property, result depends on your PHP version
    • PHP 5.3 - PHP 5.6: result is value set in step 3
    • PHP 7: result is null

I have tried to cook it down to a minimal, complete and verifiable example which can be found at http://sandbox.onlinephpfunctions.com/code/e926a7398119ea715531cafe4ce6a22c329e53b8 where you can test the code with different PHP versions.

<?php
class demoStdObject {
    public $public = ''; protected $protected = ''; private $private = '';

    public function getPublic() { return $this->public; }    
    public function getProtected() { return $this->protected; }    
    public function getPrivate() { return $this->private; }        
    public function setPublic($public) { $this->public = $public; }    
    public function setProtected($protected) { $this->protected = $protected; }    
    public function setPrivate($private) { $this->private = $private; }
}

class demoArrayObject extends ArrayObject {
    public $public = ''; protected $protected = ''; private $private = '';
    public function getPublic() { return $this->public; }    
    public function getProtected() { return $this->protected; }    
    public function getPrivate() { return $this->private; }        
    public function setPublic($public) { $this->public = $public; }    
    public function setProtected($protected) { $this->protected = $protected; }    
    public function setPrivate($private) { $this->private = $private; }
}


$arrayObject = new demoArrayObject();
$stdObject = new demoStdObject();

testSerialize($arrayObject);
echo str_repeat('-',30) . "\n";
testSerialize($stdObject);

function testSerialize($object) {
  $object->setPublic('public');
  $object->setProtected('protected');
  $object->setPrivate('private');

  $serialized = serialize($object);

  $unserialized = unserialize($serialized);

  echo get_class($object) . ":\n";
  echo $unserialized->getPublic() . "\n";
  echo $unserialized->getProtected() . "\n";
  echo $unserialized->getPrivate() . "\n";
}

Output for PHP 5.6:

demoArrayObject:
public
protected
private
------------------------------
demoStdObject:
public
protected
private

Output for PHP 7:

demoArrayObject:
public
protected

------------------------------
demoStdObject:
public
protected
private

I could not find any documented changes related to serialize(), unserialize() or the ArrayObject class so I am wondering what's going on. Is it a bug? Undocumented feature? ;-)

As we do a lot of serialize() / unserialize() in our project I really need to make sure bahavior of PHP 7 is 100% compatible to PHP 5.3+ behavior.

Question: How can I make PHP 7 behave like PHP 5.3+??

回答1:

Although this is fixed for the next release of PHP, the fact that your code is relying on an undocumented behaviour is an error known as "Programming by Coincidence". From the fine article:

How to Program by Coincidence

Suppose Fred is given a programming assignment. Fred types in some code, tries it, and it seems to work. Fred types in some more code, tries it, and it still seems to work. After several weeks of coding this way, the program suddenly stops working, and after hours of trying to fix it, he still doesn’t know why. Fred may well spend a significant amount of time chasing this piece of code around without ever being able to fix it. No matter what he does, it just doesn’t ever seem to work right.

Accidents of Implementation

Accidents of implementation are things that happen simply because that’s the way the code is currently written. You end up relying on undocumented error or boundary conditions.

In this case, there is no guarantee that when you extend ArrayObject the values of the child class will be unserialized correctly.

Either using composition rather than inheritance would be much safer, or writing serialize/unserialize methods on the child method would allow you to control the serialization/deserialization. Alternatively, just not using serialize/unserialize and instead using your own interface can also be more predictable than the "magic" internal methods.