web-dev-qa-db-ja.com

現在のオブジェクト($ this)を子孫クラスにキャストします

オブジェクトを子孫クラスに変更する必要があるかもしれないクラスがあります。これは可能ですか? 1つのオプションはそのコピーを返すが、代わりに子クラスを使用することですが、現在のオブジェクトを実際に変更するのはいいことです...

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
34
Nathan MacInnes

他の回答で説明されているように、厄介なブラックマジック PECL拡張機能でそれを行うことができます。

しかし、あなたは真剣にそれを望んでいません。 OOPで解決したい問題があれば、それを行うにはOOP準拠の方法があります。

ランタイムタイプの階層の変更はOOPに準拠していません(実際、これは意識的に回避されています)。あなたが望むものに合うはずのデザインパターンがあります。

なぜそうしたいのか教えてください。それを行うにはもっと良い方法があるはずだと思います;)

9
ssice

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

了解しました。ゴードンは非常にブラックマジックのソリューションを投稿したので、同じことを行います( RunKit PECL拡張機能を使用します(警告: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);
}

したがって、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

免責事項:これを行わないでください。本当に、しないでください。それは可能ですが、それはとても恐ろしい考えです...

36
ircmaxell

クラスの再定義

runkit PECL extension aka the "Toolkit from Hell"でこれを行うことができます:

  • runkit_class_adopt —基本クラスを継承クラスに変換し、必要に応じて祖先メソッドを追加します
  • runkit_class_emancipate —継承されたクラスを基本クラスに変換し、スコープが祖先であるメソッドを削除します

インスタンスの再定義

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です。 Actorはこれを許可しないため、技術的には、BarFooの子があるにもかかわらず、有効な状態ではありません。魔法を追加することができます__wakeup何らかの方法でこれを処理する方法ですが、真剣に、それを望まず、キャストが醜いビジネスである理由を示しています。

免責事項:私は絶対にこれらのソリューションのいずれかを本番環境で使用することを推奨しません。

12
Gordon

子クラスのインスタンスは親クラスのインスタンスでもあるので、これは不可能です。その逆は当てはまりません。

できることは、子クラスの新しいインスタンスを作成し、古いオブジェクトからその値をそれにコピーすることです。その後、myChildClassタイプの新しいオブジェクトを返すことができます。

2
Alan Geleynse

単純なクラスの場合、これは機能する可能性があります(まれにこれを正常に使用しています)。

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

もちろんTargetClasssourceObjectのクラスから継承する必要があり、この作業を行うにはTargetClassですべての保護プロパティとプライベートプロパティをパブリックにする必要があります。

これを使用してFormMapperhttps://github.com/sonata-project/SonataAdminBundle/blob/3.x/src/Form/FormMapper.php )をオンザフライで変更しますIndentedFormMapperchainという新しいメソッドを追加して:

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;
    }
}
1
Thomas Kekeisen