Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cast the current object ($this) to a descendent class

I have a class where it may be necessary to change the object to a descendent class further down the line. Is this possible? I know that one option is to return a copy of it but using the child class instead, but it'd be nice to actually modify the current object... so:

class myClass {   protected $var;    function myMethod()   {     // function which changes the class of this object     recast(myChildClass);    } }  class myChildClass extends myClass { }  $obj = new myClass(); $obj->myMethod(); get_class_name($obj); // => myChildClass 
like image 844
Nathan MacInnes Avatar asked Nov 02 '10 17:11

Nathan MacInnes


2 Answers

Casting to change the object's type is not possible in PHP (without using a nasty extension). Once you instantiate a object, you can't change the class (or other implementation details) anymore...

You can simulate it with a method like so:

public function castAs($newClass) {     $obj = new $newClass;     foreach (get_object_vars($this) as $key => $name) {         $obj->$key = $name;     }     return $obj; } 

Usage:

$obj = new MyClass(); $obj->foo = 'bar'; $newObj = $obj->castAs('myChildClass'); echo $newObj->foo; // bar 

But beware that it doesn't actually change the original class. It just creates a new one. And beware that this requires that the properties are public or have getter and setter magic methods...

And if you wanted some more checks (I'd suggest so), I'd add this line as the first line of castAs to prevent issues:

if (!$newClass instanceof self) {     throw new InvalidArgumentException(         'Can\'t change class hierarchy, you must cast to a child class'     ); } 

Alright, since Gordon posted a very black-magic solution, I will do the same (using the RunKit PECL extension (warning: here be dragons):

class myClass {} class myChildClass extends MyClass {}  function getInstance($classname) {     //create random classname     $tmpclass = 'inheritableClass'.rand(0,9);     while (class_exists($tmpclass))         $tmpclass .= rand(0,9);     $code = 'class '.$tmpclass.' extends '.$classname.' {}';     eval($code);     return new $tmpclass(); }  function castAs($obj, $class) {     $classname = get_class($obj);     if (stripos($classname, 'inheritableClass') !== 0)         throw new InvalidArgumentException(             'Class is not castable'         );     runkit_class_emancipate($classname);     runkit_class_adopt($classname, $class); } 

So, instead of doing new Foo, you'd do something like this:

$obj = getInstance('MyClass'); echo $obj instanceof MyChildClass; //false castAs($obj, 'myChildClass'); echo $obj instanceof MyChildClass; //true 

And from within the class (as long as it was created with getInstance):

echo $this instanceof MyChildClass; //false castAs($this, 'myChildClass'); echo $this instanceof MyChildClass; //true 

Disclaimer: Don't do this. Really, don't. It's possible, but it's such a horrible idea...

like image 150
ircmaxell Avatar answered Oct 04 '22 03:10

ircmaxell


Redefining Classes

You can do this with the runkit PECL extension aka the "Toolkit from Hell":

  • runkit_class_adopt — Convert a base class to an inherited class, add ancestral methods when appropriate
  • runkit_class_emancipate — Convert an inherited class to a base class, removes any method whose scope is ancestral

Redefining Instances

The runkit functions do not work on object instances. If you want to do that on object instances, you could theoretically do that by messing with the serialized object strings.
This is the realms of black magic though.

The code below allows you to change an instance to whatever other class:

function castToObject($instance, $className) {     if (!is_object($instance)) {         throw new InvalidArgumentException(             'Argument 1 must be an Object'         );     }     if (!class_exists($className)) {         throw new InvalidArgumentException(             'Argument 2 must be an existing Class'         );     }     return unserialize(         sprintf(             'O:%d:"%s"%s',             strlen($className),             $className,             strstr(strstr(serialize($instance), '"'), ':')         )     ); } 

Example:

class Foo {     private $prop1;     public function __construct($arg)     {         $this->prop1 = $arg;     }     public function getProp1()     {         return $this->prop1;     } } class Bar extends Foo {     protected $prop2;     public function getProp2()     {         return $this->prop2;     } } $foo = new Foo('test'); $bar = castToObject($foo, 'Bar'); var_dump($bar); 

Result:

object(Bar)#3 (2) {   ["prop2":protected]=>   NULL   ["prop1":"Foo":private]=>   string(4) "test" } 

As you can see, the resulting object is a Bar object now with all properties retaining their visibility but prop2 is NULL. The ctor doesnt allow this, so technically, while you have a Bar child of Foo, it is not in a valid state. You could add a magic __wakeup method to handle this somehow, but seriously, you dont want that and it shows why casting is ugly business.

DISCLAIMER: I absolutely do not encourage anyone to use any of these solutions in production.

like image 28
Gordon Avatar answered Oct 04 '22 03:10

Gordon