39

I'm writing a module for a php cms. In a function (a callback) I can access an object that comes from the framework code.

This object is of type __PHP_Incomplete_Class because the needed header file is not included before the session starts. I cannot include it without hacking the core cms code.

I wonder if is possibile to access the object properties anyway (casting to array does not work). I ask this because I can see the values with var_dump() but using $object->var I always get nulls.

Mike Mackintosh
  • 13,917
  • 6
  • 60
  • 87
gpilotino
  • 13,055
  • 9
  • 48
  • 61
  • I tried a few of the answers and couldn't get any of them to work. The only thing I could get to work was calling `get_object_vars` (this was in php5). – jbobbins Jun 22 '23 at 22:02

8 Answers8

66

This issue appends when you un serialize an object of a class that hasn't been included yet. For exemple, if you call session_start before include the class.

A PHPIncompleteClass object can't be accessed directly, but it's ok with foreach, serialize and gettype. Calling is_object with an PHPIncompleteClass object will result false.

So, if you find a '__PHP_Incomplete_Class' object in your session and you've included your class after the session_load, you can use this function :

function fixObject (&$object)
{
  if (!is_object ($object) && gettype ($object) == 'object')
    return ($object = unserialize (serialize ($object)));
  return $object;
}

This will results a usable object :

fixObject($_SESSION['member']);
23hush
  • 129
  • 1
  • 7
  • On that second chunk of code, did you mean fixObject instead of fixclass? Could be confusing. – Cyprus106 May 15 '11 at 18:44
  • 2
    you could also use autoloader to load the class, which would make the whole problem go away. – StasM May 29 '12 at 00:26
  • after unserialized, I can't still access the properties, but foreach is ok for me. – Sithu Jul 30 '12 at 05:48
  • 1
    not sure why but it just seems silly to me that `unserialize(serialize($data))` should do anything at all but hey it works. thanks – But those new buttons though.. Jul 07 '17 at 06:22
  • if you are looking for a much simpler solution, just to get things working in the spirit of php, cast the object to an array. – Erick Robertson Feb 04 '20 at 18:43
  • 3
    This will no longer work since PHP 7.2 because is_object() will return true for incomplete objects. You need to use a different method to detect if the object is incomplete. https://www.php.net/manual/en/function.is-object.php#refsect1-function.is-object-changelog – Sybille Peters May 25 '20 at 10:00
19

I found this hack which will let you cast an object:

function casttoclass($class, $object)
{
  return unserialize(preg_replace('/^O:\d+:"[^"]++"/', 'O:' . strlen($class) . ':"' . $class . '"', serialize($object)));
}

From http://blog.adaniels.nl/articles/a-dark-corner-of-php-class-casting/

So you can do:

$obj = casttoclass('stdClass', $incompleteObject);

and then access properties as normal.


You could also define an unserialize_callback_func in a .htaccess/Apache configuration file. That way you wouldn't need to hack any PHP but you could include the file on demand.

Tom Haigh
  • 57,217
  • 21
  • 114
  • 142
6

As an addition here is my version of the fix_object() function: The main change is step 3 in the code: Make all properties public.

When PHP serializes an object, all private and protected properties are prefixed with two null-bytes! These null-bytes are the actual reason, why the property cannot be accessed via $obj->key because actually it is something like $obj->{NULL*NULL}key.

/**
 * Takes an __PHP_Incomplete_Class and casts it to a stdClass object.
 * All properties will be made public in this step.
 *
 * @since  1.1.0
 * @param  object $object __PHP_Incomplete_Class
 * @return object
 */
function fix_object( $object ) {
    // preg_replace_callback handler. Needed to calculate new key-length.
    $fix_key = create_function(
        '$matches',
        'return ":" . strlen( $matches[1] ) . ":\"" . $matches[1] . "\"";'
    );

    // 1. Serialize the object to a string.
    $dump = serialize( $object );

    // 2. Change class-type to 'stdClass'.
    $dump = preg_replace( '/^O:\d+:"[^"]++"/', 'O:8:"stdClass"', $dump );

    // 3. Make private and protected properties public.
    $dump = preg_replace_callback( '/:\d+:"\0.*?\0([^"]+)"/', $fix_key, $dump );

    // 4. Unserialize the modified object again.
    return unserialize( $dump );
}

var_dump will not display these NULL byte prefixes to you, but you can see them with this code:

class Test {
    private $AAA = 1;
    protected $BBB = 2;
    public $CCC = 3;
}

$test = new Test();
echo json_encode( serialize( $test ) );

// Output:
// "O:4:\"Test\":3:{s:9:\"\u0000Test\u0000AAA\";i:1;s:6:\"\u0000*\u0000BBB\";i:2;s:3:\"CCC\";i:3;}"

$test2 = fix_object( $test );
echo json_encode( serialize( $test2 ) );

// Output:
// "O:8:\"stdClass\":3:{s:3:\"AAA\";i:1;s:3:\"BBB\";i:2;s:3:\"CCC\";i:3;}"

There you see:

  • The private property is prefixed with NULL + classname + NULL
  • The protected property is prefixed with NULL + "*" + NULL
Philipp
  • 10,240
  • 8
  • 59
  • 71
5

None of the above answers actually worked for me, except this solution:

$object = unserialize(serialize($object));

$object->function();

Hope it helps someone

2

I tried the answer of Tom Haigh here, but discovered 2 problems.

  1. when you have other "Incomplete_Class" objects as properties of the top-level-class they stay untouched as __PHP_Incomplete_Class Object
  2. if you have private properties, they will still be private in your stdClass object

So I rewrote the function handle this:

/**
 * @see: https://stackoverflow.com/a/965704/2377961
 *
 * @param  object  $object  The object that should be casted
 * @param  String  $class   The name of the class
 * @return mixed   The new created object
 */
function casttoclass($object, $class = 'stdClass')
{
  $ser_data = serialize($object);
  # preg_match_all('/O:\d+:"([^"]++)"/', $ser_data, $matches); // find all classes

  /*
   * make private and protected properties public
   *   privates  is stored as "s:14:\0class_name\0property_name")
   *   protected is stored as "s:14:\0*\0property_name")
   */
  $ser_data = preg_replace_callback('/s:\d+:"\0([^\0]+)\0([^"]+)"/',
    function($prop_match) {
      list($old, $classname, $propname) = $prop_match;
      return 's:'.strlen($propname) . ':"' . $propname . '"';
  }, $ser_data);

  // replace object-names
  $ser_data = preg_replace('/O:\d+:"[^"]++"/', 'O:' . strlen($class) . ':"' . $class . '"', $ser_data);
  return unserialize($ser_data);
}

I switch the function arguments too, so that you can use

$obj = casttoclass($incompleteObject);

And you get an stdClass object with only public properties. Even if you have objects in childclasses they are converted to stdClass with public properties too.

Radon8472
  • 4,285
  • 1
  • 33
  • 41
1

If you just need to access raw data (like class variables) from a PHP_Incomplete_Class object, you can use the foreach hack, or you can also do:

$result_array = (array)$_SESSION['incomplete_object_index'];
echo $result_array['desired_item'];
Eric
  • 123
  • 6
  • 1
    it's a general observation, perhaps i should have added it only as a comment. i worked for a large company that used the above to extract simple vars. might help others to know about. – Eric Jun 20 '13 at 15:32
  • its usefull when u need to migrate database and need only simple data stored in seriealized object. – El' Aug 12 '14 at 16:59
  • This worked great. I'm doing a migration and had a pretty complex situation. This solved it easily! Thanks! – Erick Robertson Feb 04 '20 at 18:42
1

Put the session_start() after your require to the class of the object you are trying to read from the SESSION

Mahder
  • 409
  • 3
  • 9
0

I've read a lot of suggestions on how to fix incomplete classobjects and I actually needed to fix those problems myself, in a ecommerce-project.

One suggestion I've found is to simply use json_decode/json_encode to convert incomplete classes without preloading anything. However, I didn't want to take the risk using this, if there are older PHP versions that are dependent in for example PECL, that is described at http://php.net/manual/en/function.json-encode.php - so I finally succeeded to make my own solution.

However, the code is a way to get the data out of the object properly, so it may not fit all needs - and it will primarily, use the json-solution first, if it is available in the environment and fail over to manual handling if needed.

It also works recursively, which in my own case is required, to save the whole array.

/**
 * Convert a object to a data object (used for repairing __PHP_Incomplete_Class objects)
 * @param array $d
 * @return array|mixed|object
 */
function arrayObjectToStdClass($d = array())
{
    /**
     * If json_decode and json_encode exists as function, do it the simple way.
     * http://php.net/manual/en/function.json-encode.php
     */
    if (function_exists('json_decode') && function_exists('json_encode')) {
        return json_decode(json_encode($d));
    }
    $newArray = array();
    if (is_array($d) || is_object($d)) {
        foreach ($d as $itemKey => $itemValue) {
            if (is_array($itemValue)) {
                $newArray[$itemKey] = (array)$this->arrayObjectToStdClass($itemValue);
            } elseif (is_object($itemValue)) {
                $newArray[$itemKey] = (object)(array)$this->arrayObjectToStdClass($itemValue);
            } else {
                $newArray[$itemKey] = $itemValue;
            }
        }
    }
    return $newArray;
}