Convert/cast an stdClass object to another class
Asked Answered
P

11

107

I'm using a third party storage system that only returns me stdClass objects no matter what I feed in for some obscure reason. So I'm curious to know if there is a way to cast/convert an stdClass object into a full fledged object of a given type.

For instance something along the lines of:

//$stdClass is an stdClass instance
$converted = (BusinessClass) $stdClass;

I am just casting the stdClass into an array and feed it to the BusinessClass constructor, but maybe there is a way to restore the initial class that I am not aware of.

Note: I am not interested in 'Change your storage system' type of answers since it is not the point of interest. Please consider it more an academic question on the language capacities.

Cheers

Pedestrianize answered 14/7, 2010 at 6:43 Comment(3)
It's explained in my post after the pseudo code sample. I am casting into an array and feeding to a automated constructor.Pedestrianize
@Adam Puza 's answer is much better than the hack shown in the accepted answer. although I am sure a mapper would still be the prefered methodErleneerlewine
Well how does PDOStatement::fetchObject accomplish this task?Weasand
C
97

See the manual on Type Juggling on possible casts.

The casts allowed are:

  • (int), (integer) - cast to integer
  • (bool), (boolean) - cast to boolean
  • (float), (double), (real) - cast to float
  • (string) - cast to string
  • (array) - cast to array
  • (object) - cast to object
  • (unset) - cast to NULL (PHP 5)

You would have to write a Mapper that does the casting from stdClass to another concrete class. Shouldn't be too hard to do.

Or, if you are in a hackish mood, you could adapt the following code:

function arrayToObject(array $array, $className) {
    return unserialize(sprintf(
        'O:%d:"%s"%s',
        strlen($className),
        $className,
        strstr(serialize($array), ':')
    ));
}

which pseudocasts an array to an object of a certain class. This works by first serializing the array and then changing the serialized data so that it represents a certain class. The result is unserialized to an instance of this class then. But like I said, it's hackish, so expect side-effects.

For object to object, the code would be

function objectToObject($instance, $className) {
    return unserialize(sprintf(
        'O:%d:"%s"%s',
        strlen($className),
        $className,
        strstr(strstr(serialize($instance), '"'), ':')
    ));
}
Caslon answered 14/7, 2010 at 6:54 Comment(8)
This hack is a clever technique. Won't be using it since my current way of solving the issue is more stable, but interesting nonetheless.Pedestrianize
You end up with a __PHP_Incomplete_Class object by using this method (at least as of PHP 5.6).Anything
@Anything no, you don't. See codepad.org/spGkyLzL. Make sure the class to cast to was included prior to calling the function.Caslon
@Anything not sure what you mean. it worked. it's an example\Foo now.Caslon
Yeah the problem is it appends the stdClass property. So you have two fooBars (the private one from example\Foo and the public one from stdClass). Instead that it replaces the value.Anything
@Anything yes, that will not work. Compare the two serialized strings: O:8:"stdClass":1:{s:6:"fooBar";b:0;} and O:11:"example\Foo":1:{s:19:" example\Foo fooBar";b:1;}. What the code above really does is messing with those two strings. Like said in the description: "it's hackish, so expect side-effects". The better solution is a dedicated Data Mapper that knows how to convert class Foo to class Bar.Caslon
sprintf() is more memory efficient way to do this than preg_replace that I found elsewhere, gg!Anesthetic
Guys, please, don't do this. It's a dirty hack. Use hydrators.Concatenate
C
59

You can use above function for casting not similar class objects (PHP >= 5.3)

/**
 * Class casting
 *
 * @param string|object $destination
 * @param object $sourceObject
 * @return object
 */
function cast($destination, $sourceObject)
{
    if (is_string($destination)) {
        $destination = new $destination();
    }
    $sourceReflection = new ReflectionObject($sourceObject);
    $destinationReflection = new ReflectionObject($destination);
    $sourceProperties = $sourceReflection->getProperties();
    foreach ($sourceProperties as $sourceProperty) {
        $sourceProperty->setAccessible(true);
        $name = $sourceProperty->getName();
        $value = $sourceProperty->getValue($sourceObject);
        if ($destinationReflection->hasProperty($name)) {
            $propDest = $destinationReflection->getProperty($name);
            $propDest->setAccessible(true);
            $propDest->setValue($destination,$value);
        } else {
            $destination->$name = $value;
        }
    }
    return $destination;
}

EXAMPLE:

class A 
{
  private $_x;   
}

class B 
{
  public $_x;   
}

$a = new A();
$b = new B();

$x = cast('A',$b);
$x = cast('B',$a);
Cleodel answered 21/3, 2012 at 20:8 Comment(10)
That's a pretty elegant solution, I must say! I just wonder how well it scales... Reflection scares me.Misogyny
Hey Adam, this solution solved a similar problem for me here: #35351085 If you want to score an easy answer, head on over and I'll check it off. Thanks!Vinculum
It doesn't work for properties inherited from parent classes.Concepcionconcept
I've just used this as a basis of my solution for seeding my database with known IDs for API functional testing with Behat. My issue was that my normal IDs are generated UUIDs and I didn't want to add a setId() method in my entity just for the sake of my testing layer, and I didn't want to load fixtures files and slow down tests. Now I can include @Given the user :username has the id :id in my feature, and handle it with reflections in the context classDrinking
The need of having to implement such hacks makes me wanna leave php now. :xAcquisitive
Great solution, I do want to add that $destination = new $destination(); can be swapped with $destination = ( new ReflectionClass( $destination ) )->newInstanceWithoutConstructor(); if you need to avoid calling the constructor.Rochdale
@scuzzy Yes. This is good improvement (PHP >= 5.4.0). Thanks.Cleodel
As @Rochdale pointed out, you might want to avoid calling the constructor since it might throw exceptions or lead to an error. After all I think this answer is underrated.Scyphozoan
@Rochdale So since your improvement doesn't call the constructor, does that mean that it works even if the constructor has parameters defined (that would otherwise need to be passed or the original implementation with just new $destination(); wouldn't work)?Adactylous
@Adactylous whatever happens in __construct($foo,$bar) is not executed when newInstanceWithoutConstructor is used basically, not sure what else I can add here? Also using this is still kinda a hack as far as OOP goes :DRochdale
L
18

To move all existing properties of a stdClass to a new object of a specified class name:

/**
 * recast stdClass object to an object with type
 *
 * @param string $className
 * @param stdClass $object
 * @throws InvalidArgumentException
 * @return mixed new, typed object
 */
function recast($className, stdClass &$object)
{
    if (!class_exists($className))
        throw new InvalidArgumentException(sprintf('Inexistant class %s.', $className));

    $new = new $className();

    foreach($object as $property => &$value)
    {
        $new->$property = &$value;
        unset($object->$property);
    }
    unset($value);
    $object = (unset) $object;
    return $new;
}

Usage:

$array = array('h','n');

$obj=new stdClass;
$obj->action='auth';
$obj->params= &$array;
$obj->authKey=md5('i');

class RestQuery{
    public $action;
    public $params=array();
    public $authKey='';
}

$restQuery = recast('RestQuery', $obj);

var_dump($restQuery, $obj);

Output:

object(RestQuery)#2 (3) {
  ["action"]=>
  string(4) "auth"
  ["params"]=>
  &array(2) {
    [0]=>
    string(1) "h"
    [1]=>
    string(1) "n"
  }
  ["authKey"]=>
  string(32) "865c0c0b4ab0e063e5caa3387c1a8741"
}
NULL

This is limited because of the new operator as it is unknown which parameters it would need. For your case probably fitting.

Lavine answered 20/1, 2012 at 19:10 Comment(3)
To inform others attempting to use this method. There is a caveat to this function in that iterating over an instantiated object outside of itself will not be able to set private or protected properties within the casted object. EG: Setting public $authKey=''; to private $authKey=''; Results in E_ERROR : type 1 -- Cannot access private property RestQuery::$authKeyAhders
A stdClass with private properties though?Christal
@Christal The OP specifically indicates this requirement... cast/convert an stdClass object into a full fledged object of a given typeVinculum
S
12

I have a very similar problem. Simplified reflection solution worked just fine for me:

public static function cast($destination, \stdClass $source)
{
    $sourceReflection = new \ReflectionObject($source);
    $sourceProperties = $sourceReflection->getProperties();
    foreach ($sourceProperties as $sourceProperty) {
        $name = $sourceProperty->getName();
        $destination->{$name} = $source->$name;
    }
    return $destination;
}
Scrobiculate answered 28/8, 2012 at 12:29 Comment(0)
C
11

Hope that somebody find this useful

// new instance of stdClass Object
$item = (object) array(
    'id'     => 1,
    'value'  => 'test object',
);

// cast the stdClass Object to another type by passing
// the value through constructor
$casted = new ModelFoo($item);

// OR..

// cast the stdObject using the method
$casted = new ModelFoo;
$casted->cast($item);
class Castable
{
    public function __construct($object = null)
    {
        $this->cast($object);
    }

    public function cast($object)
    {
        if (is_array($object) || is_object($object)) {
            foreach ($object as $key => $value) {
                $this->$key = $value;
            }
        }
    }
} 
class ModelFoo extends Castable
{
    public $id;
    public $value;
}
Calix answered 12/12, 2014 at 11:46 Comment(1)
Can you explain why "is_array($object) || is_array($object)" ?Trembles
P
5

Changed function for deep casting (using recursion)

/**
 * Translates type
 * @param $destination Object destination
 * @param stdClass $source Source
 */
private static function Cast(&$destination, stdClass $source)
{
    $sourceReflection = new \ReflectionObject($source);
    $sourceProperties = $sourceReflection->getProperties();
    foreach ($sourceProperties as $sourceProperty) {
        $name = $sourceProperty->getName();
        if (gettype($destination->{$name}) == "object") {
            self::Cast($destination->{$name}, $source->$name);
        } else {
            $destination->{$name} = $source->$name;
        }
    }
}
Pecten answered 17/7, 2013 at 10:55 Comment(0)
M
3

consider adding a new method to BusinessClass:

public static function fromStdClass(\stdClass $in): BusinessClass
{
  $out                   = new self();
  $reflection_object     = new \ReflectionObject($in);
  $reflection_properties = $reflection_object->getProperties();
  foreach ($reflection_properties as $reflection_property)
  {
    $name = $reflection_property->getName();
    if (property_exists('BusinessClass', $name))
    {
      $out->{$name} = $in->$name;
    }
  }
  return $out;
}

then you can make a new BusinessClass from $stdClass:

$converted = BusinessClass::fromStdClass($stdClass);
Miscellanea answered 12/11, 2018 at 1:7 Comment(0)
P
2

And yet another approach using the decorator pattern and PHPs magic getter & setters:

// A simple StdClass object    
$stdclass = new StdClass();
$stdclass->foo = 'bar';

// Decorator base class to inherit from
class Decorator {

    protected $object = NULL;

    public function __construct($object)
    {
       $this->object = $object;  
    }

    public function __get($property_name)
    {
        return $this->object->$property_name;   
    }

    public function __set($property_name, $value)
    {
        $this->object->$property_name = $value;   
    }
}

class MyClass extends Decorator {}

$myclass = new MyClass($stdclass)

// Use the decorated object in any type-hinted function/method
function test(MyClass $object) {
    echo $object->foo . '<br>';
    $object->foo = 'baz';
    echo $object->foo;   
}

test($myclass);
Pyorrhea answered 19/9, 2019 at 14:20 Comment(0)
M
1

Yet another approach.

The following is now possible thanks to the recent PHP 7 version.

$theStdClass = (object) [
  'a' => 'Alpha',
  'b' => 'Bravo',
  'c' => 'Charlie',
  'd' => 'Delta',
];

$foo = new class($theStdClass)  {
  public function __construct($data) {
    if (!is_array($data)) {
      $data = (array) $data;
    }

    foreach ($data as $prop => $value) {
      $this->{$prop} = $value;
    }
  }
  public function word4Letter($letter) {
    return $this->{$letter};
  }
};

print $foo->word4Letter('a') . PHP_EOL; // Alpha
print $foo->word4Letter('b') . PHP_EOL; // Bravo
print $foo->word4Letter('c') . PHP_EOL; // Charlie
print $foo->word4Letter('d') . PHP_EOL; // Delta
print $foo->word4Letter('e') . PHP_EOL; // PHP Notice:  Undefined property

In this example, $foo is being initialized as an anonymous class that takes one array or stdClass as only parameter for the constructor.

Eventually, we loop through the each items contained in the passed object and dynamically assign then to an object's property.

To make this approch event more generic, you can write an interface or a Trait that you will implement in any class where you want to be able to cast an stdClass.

Mosier answered 30/5, 2019 at 14:51 Comment(0)
S
0

BTW: Converting is highly important if you are serialized, mainly because the de-serialization breaks the type of objects and turns into stdclass, including DateTime objects.

I updated the example of @Jadrovski, now it allows objects and arrays.

example

$stdobj=new StdClass();
$stdobj->field=20;
$obj=new SomeClass();
fixCast($obj,$stdobj);

example array

$stdobjArr=array(new StdClass(),new StdClass());
$obj=array(); 
$obj[0]=new SomeClass(); // at least the first object should indicates the right class.
fixCast($obj,$stdobj);

code: (its recursive). However, i don't know if its recursive with arrays. May be its missing an extra is_array

public static function fixCast(&$destination,$source)
{
    if (is_array($source)) {
        $getClass=get_class($destination[0]);
        $array=array();
        foreach($source as $sourceItem) {
            $obj = new $getClass();
            fixCast($obj,$sourceItem);
            $array[]=$obj;
        }
        $destination=$array;
    } else {
        $sourceReflection = new \ReflectionObject($source);
        $sourceProperties = $sourceReflection->getProperties();
        foreach ($sourceProperties as $sourceProperty) {
            $name = $sourceProperty->getName();
            if (is_object(@$destination->{$name})) {
                fixCast($destination->{$name}, $source->$name);
            } else {
                $destination->{$name} = $source->$name;
            }
        }
    }
}
Suellen answered 16/1, 2018 at 18:5 Comment(0)
D
0

Convert it to an array, return the first element of that array, and set the return param to that class. Now you should get the autocomplete for that class as it will regconize it as that class instead of stdclass.

/**
 * @return Order
 */
    public function test(){
    $db = new Database();

    $order = array();
    $result = $db->getConnection()->query("select * from `order` where productId in (select id from product where name = 'RTX 2070')");
    $data = $result->fetch_object("Order"); //returns stdClass
    array_push($order, $data);

    $db->close();
    return $order[0];
}
Dahabeah answered 29/5, 2020 at 9:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.