38

我有一个类,可能需要将对象更改为更进一步的后代类。这可能吗?我知道一种选择是返回它的副本,但改用子类,但实际上修改当前对象会很好......所以:

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
4

5 回答 5

38

在 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 扩展(警告:这里是 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);
}

所以,而不是做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

免责声明:不要这样做。真的,不要。这是可能的,但这是一个可怕的想法......

于 2010-11-02T17:15:51.193 回答
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对象现在是一个所有属性都保留其可见性prop2的对象,但是NULL. ctor 不允许这样做,所以从技术上讲,当你有一个 的Bar孩子时Foo,它不是处于有效状态。你可以添加一个神奇的__wakeup方法来以某种方式处理这个问题,但说真的,你不想要那个,它说明了为什么铸造是丑陋的事情。

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

于 2010-11-02T17:29:26.097 回答
10

如其他答案中所述,您可以使用讨厌的黑魔法PECL 扩展来做到这一点。

不过,你真的不想要它。您想在 OOP 中解决的任何问题都可以通过符合 OOP 的方式来解决。

运行时类型层次结构修改不符合 OOP(事实上,这是有意识地避免的)。有一些设计模式应该适合你想要的。

请告诉我们您为什么要这样做,我相信一定有更好的方法来做到这一点;)

于 2010-11-03T00:00:48.460 回答
2

这是不可能的,因为虽然子类的实例也是父类的实例,但反之则不然。

您可以做的是创建子类的新实例并将旧对象中的值复制到它上面。然后,您可以返回类型为 的新对象myChildClass

于 2010-11-02T17:17:23.327 回答
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才能完成这项工作。

FormMapper通过添加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;
    }
}
于 2018-10-31T10:17:29.420 回答