40

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
BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
Nathan MacInnes
  • 11,033
  • 4
  • 35
  • 50
  • 2
    I have a function that will do this, but it's not something that you should normally do (impossible code for anybody else to understand). What's wrong with implementing a standard factory method? – Mark Baker Nov 02 '10 at 17:16
  • @Mark Baker: Could you post your code? I'd be curious how you do it without using something like runkit... – ircmaxell Nov 02 '10 at 17:20
  • 2
    Please tell us why you want to do this – rojoca Nov 02 '10 at 18:01

5 Answers5

41

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...

ircmaxell
  • 163,128
  • 34
  • 264
  • 314
12

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.

Gordon
  • 312,688
  • 75
  • 539
  • 559
  • @ircmaxell none for Toolkit of Hell? :) – Gordon Nov 02 '10 at 18:37
  • 2
    Well, I thought that was being a bit sympathetic (considering how nasty it is)... – ircmaxell Nov 02 '10 at 18:38
  • @Mark thanks. Btw, you might want to flag your question to mods and ask if they can permadelete it. I already flagged it but mods might be more inclined to do so when author ask for it. Not sure if they can do that at all though. – Gordon Nov 02 '10 at 23:03
  • 1
    @Gordon - duly flagged... it wasn't ever code meant to see the light of day anyway - and I'd be happier knowing that nobody with enough points to see deleted answers can ever pick up on it and start using it in a real application. I certainly won't publish my disinherit() reversal function (change child object to its parent). – Mark Baker Nov 02 '10 at 23:09
  • y'know, you give a disclaimer but not much justification with it ;) – ashgromnies Jan 02 '14 at 20:18
10

You can, as described in other answers, do it with nasty black magic PECL extensions.

Though, you seriously don't want it. Any problem you want to solve in OOP there's an OOP-compliant way to do it.

Runtime type hierarchy modifications are not OOP-compliant (in fact, this is consciously avoided). There are design patterns that should fit what you want.

Please, tell us why do you want that, I'm sure there must be better ways to do it ;)

ssice
  • 3,564
  • 1
  • 26
  • 44
  • Thanks. The method I'm going with is to simulate it by making the parent class a mapper for the child classes. It sounds messy, but I'm pretty sure I can make it clean. I've got the idea in my head of what it should look like, and I'm just working on getting it onto the computer screen. – Nathan MacInnes Nov 03 '10 at 09:19
  • 12
    This is the first time I hear polymorphism is avoided and not OOP-complient. I thought it was contrary, one the basic principles. Is that soem different polymorphism called "type polymorphism". – Gherman Mar 16 '15 at 10:57
  • I meant here "type swapping" or whatever you'd better want to call it. Polymorphism the OOP way is availability for using specialized subtypes in place of supertypes, which is neat; but not the other way around. – ssice Mar 16 '15 at 12:49
  • The only type-safe way I know for some sort of this happening (which can be very useful in some contexts) (is not on PHP) is called self-types in Scala. And it works because you can provide type information prior to the actual execution, so it's type-safe, but that probably falls a bit out of OOP already. – ssice Mar 16 '15 at 12:51
  • 5
    I disagree. Polymorphism is one of the key principles of OOP. Of course, with great power comes great responsibility. – mwieczorek Aug 05 '17 at 03:15
  • I think it's more of a design choice. Polymorphism is overwhelmingly popular with very good reason. (Flexibility while maintaining code integrity) However, CASTING of any kind has it's benefits and drawbacks. As one commenter stated, "great power comes with great responsibility". I believe that PHP should allow for downcasting. – Rick Mac Gillis Nov 20 '20 at 20:29
  • PHP already allows you to use the object as the descendant class (aka "downcast") *if* the original object *is* of the descendant class (you can use instanceof to check), but **swapping the class of an object at runtime is definitely not OOP**. If you want to wrap an object onto something that could be a subclass, you could use the Decorator Pattern and have the Decorator be a subclass of your parent object. – ssice Nov 22 '20 at 12:37
2

This is not possible because while an instance of a child class is also an instance of a parent class, the reverse is not true.

What you could do is create a new instance of the child class and copy the values from the old object onto it. You can then return the new object which will be of type myChildClass.

Alan Geleynse
  • 24,821
  • 5
  • 46
  • 55
1

For simple classes this may work (I am using this successfully in some rare cases):

function castAs($sourceObject, $newClass)
{
    $castedObject                    = new $newClass();
    $reflectedSourceObject           = new \ReflectionClass($sourceObject);
    $reflectedSourceObjectProperties = $reflectedSourceObject->getProperties();

    foreach ($reflectedSourceObjectProperties as $reflectedSourceObjectProperty) {
        $propertyName = $reflectedSourceObjectProperty->getName();

        $reflectedSourceObjectProperty->setAccessible(true);

        $castedObject->$propertyName = $reflectedSourceObjectProperty->getValue($sourceObject);
    }
}

Usage in my case:

$indentFormMapper = castAs($formMapper, IndentedFormMapper::class);

More abstract:

$castedObject = castAs($sourceObject, TargetClass::class);

Of course TargetClass has to inherit from the class of sourceObject and you have to make all protected and private properties public in TargetClass to get this work.

I use this to change FormMapper (https://github.com/sonata-project/SonataAdminBundle/blob/3.x/src/Form/FormMapper.php) on the fly to IndentedFormMapper by adding a new method called chain:

class IndentedFormMapper extends FormMapper
{
    /**
     * @var AdminInterface
     */
    public $admin;

    /**
     * @var BuilderInterface
     */
    public $builder;

    /**
     * @var FormBuilderInterface
     */
    public $formBuilder;

    /**
     * @var string|null
     */
    public $currentGroup;

    /**
     * @var string|null
     */
    public $currentTab;

    /**
     * @var bool|null
     */
    public $apply;

    public function __construct()
    {
    }

    /**
     * @param $callback
     * @return $this
     */
    public function chain($callback)
    {
        $callback($this);

        return $this;
    }
}
Thomas Kekeisen
  • 4,355
  • 4
  • 35
  • 54