将当前对象($this)转换为一个子类

40

我有一个类,可能需要在后面的某个时候将对象更改为一个更具体的子类。这是可能的吗?我知道一种选项是返回一个使用子类替代的副本,但实际上修改当前对象会更好……所以:

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

2
我有一个可以做到这一点的函数,但这不是通常应该做的事情(对于其他人来说是无法理解的代码)。实现标准工厂方法有什么问题吗? - Mark Baker
@Mark Baker:你能发一下你的代码吗?我很好奇你是如何在不使用类似runkit这样的东西的情况下完成它的... - ircmaxell
2
请告诉我们您为什么想要这样做。 - rojoca
5个回答

41
在PHP中,无法进行对象类型转换(除非使用不好的扩展)。一旦实例化了一个对象,就无法再更改类(或其他实现细节)了...
可以通过以下方法模拟它:
public function castAs($newClass) {
    $obj = new $newClass;
    foreach (get_object_vars($this) as $key => $name) {
        $obj->$key = $name;
    }
    return $obj;
}

使用方法:

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

但要注意,它实际上并没有改变原始类。它只是创建了一个新的类。而且要注意,这要求属性是公共的或者具有getter和setter的魔术方法...
如果你想要更多的检查(我建议这样),我会在castAs的第一行添加这行代码来防止问题的发生:
if (!$newClass instanceof self) {
    throw new InvalidArgumentException(
        'Can\'t change class hierarchy, you must cast to a child class'
    );
}

好吧,既然Gordon提出了一个非常黑魔法的解决方案,我也会采取同样的方法(使用RunKit PECL扩展(警告:这里有危险):)
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);
}

所以,不要使用new Foo,你可以像这样做:
$obj = getInstance('MyClass');
echo $obj instanceof MyChildClass; //false
castAs($obj, 'myChildClass');
echo $obj instanceof MyChildClass; //true

只要使用getInstance创建的类内部:
echo $this instanceof MyChildClass; //false
castAs($this, 'myChildClass');
echo $this instanceof MyChildClass; //true

免责声明:不要这样做。真的,不要。虽然可能,但这是一个非常糟糕的主意...

2
这个技巧是让代码变丑的好方法。我宁愿坚持语言本身并尝试“优先使用组合而非继承”... - smentek
1
PHP 5.5+ 在2013年仍存在“NO CAST”问题吗? - Peter Krauss

12

重新定义类

你可以使用runkit PECL扩展,也就是“地狱工具包”来实现这个功能:


重新定义实例

runkit函数无法直接作用于对象实例。如果你想对对象实例进行操作,你可以尝试修改序列化后的对象字符串。
不过这是一种黑魔法。

下面的代码允许你将一个实例更改为另一个类:

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), '"'), ':')
        )
    );
}

例子:

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);

结果:

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

可以看到,得到的对象是一个Bar对象,所有属性的可见性都保留了下来,但是prop2NULL。构造函数不允许这种情况发生,所以严格来说,虽然你拥有一个Foo的子类Bar,但它并不处于有效状态。你可以添加一个魔术__wakeup方法来处理这个问题,但说实话,你不想要这样做,它也展示了为什么类型转换是一个丑陋的过程。

声明:我绝对不鼓励任何人在生产环境中使用这些解决方案。


@ircmaxell 工具包地狱没有适用的吗? :) - Gordon
2
好吧,我觉得这有点过于同情了(考虑到它有多么恶劣)... - ircmaxell
@Mark 谢谢。顺便说一下,你可能想向版主标记你的问题并询问他们是否可以永久删除它。我已经标记了,但当作者要求时,版主可能更倾向于这样做。不过我不确定他们是否能够这样做。 - Gordon
1
@Gordon - 已经标记了...它从来不是意味着要公开的代码 - 我会更高兴知道没有足够积分可以看到已删除答案的人能够使用它并在真实应用中开始使用它。我肯定不会发布我的disinherit()反转函数(将子对象更改为其父对象)。 - Mark Baker
你知道的,你附上了免责声明,但并没有太多解释支持它 ;) - ashgromnies

10

如其他回答所述,您可以使用糟糕的 黑魔法 PECL扩展来实现。

然而,您真的不想这样做。任何您想在面向对象编程中解决的问题都有符合面向对象编程标准的解决方法。

运行时类型层次结构修改不符合面向对象编程标准(事实上,这是有意避免的)。有设计模式可以满足您的需求。

请告诉我们为什么您需要它,我相信一定有更好的方法来解决;)


12
这是我第一次听说多态性被避免而且不符合面向对象的规范。我原以为它是基本原则之一,难道这是一种叫做"类型多态"的不同形式的多态性吗? - Gherman
我所知道的唯一类型安全的方式,可以在某些情况下非常有用(不是在PHP中),被称为Scala中的self-types。它之所以有效,是因为你可以在实际执行之前提供类型信息,因此它是类型安全的,但这可能已经超出了面向对象编程的范畴。 - ssice
5
我不同意。多态性是面向对象编程的关键原则之一。当然,伴随着强大的功能而来的是巨大的责任。 - mwieczorek
我认为这更多是一种设计选择。多态性以极其充分的理由而广受欢迎(在保持代码完整性的同时具有灵活性)。然而,任何类型的强制转换都有其优点和缺点。正如一位评论者所说,“伟大的力量伴随着巨大的责任”。我认为PHP应该允许向下转型。 - Rick Mac Gillis
PHP已经允许您将对象用作后代类(又名“downcast”)如果原始对象后代类(您可以使用instanceof进行检查),但是在运行时交换对象的类绝对不是面向对象编程。如果您想将对象包装到可能是子类的东西上,可以使用装饰器模式,并使装饰器成为父对象的子类。 - ssice
显示剩余2条评论

2
这是不可能的,因为子类的实例也是父类的实例,但反过来不成立。
你可以创建一个新的子类实例,并将旧对象的值复制到它上面。然后返回新实例,它将是myChildClass类型的。

1
对于简单的类,这可能会起作用(在某些罕见情况下我成功使用了这种方法):
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);
    }
}

在我的情况下使用:
$indentFormMapper = castAs($formMapper, IndentedFormMapper::class);

更加抽象:
$castedObject = castAs($sourceObject, TargetClass::class);

当然,TargetClass 必须继承自 sourceObject 的类,并且您必须将 TargetClass 中的所有受保护和私有属性公开以使其工作。

我使用这个方法来在运行时将 FormMapperhttps://github.com/sonata-project/SonataAdminBundle/blob/3.x/src/Form/FormMapper.php)更改为 IndentedFormMapper,通过添加一个名为 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;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接