Привести текущий объект ($ this) к классу-потомку - PullRequest
32 голосов
/ 02 ноября 2010

У меня есть класс, в котором может потребоваться изменить объект на потомок в дальнейшем.Это возможно?Я знаю, что один из вариантов - вернуть его копию, но вместо этого использовать дочерний класс, но было бы неплохо изменить текущий объект ... так:

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

Ответы [ 5 ]

34 голосов
/ 02 ноября 2010

Приведение к изменению типа объекта в 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

Но учтите, что на самом деле это не меняет первоначальный класс. Это просто создает новый. И помните, что для этого необходимо, чтобы свойства были общедоступными или имели магические методы получения и установки ...

И если бы вы хотели еще несколько проверок (я бы предложил), я бы добавил эту строку в качестве первой строки castAs, чтобы предотвратить проблемы:

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

Хорошо, поскольку Гордон опубликовал очень черное магическое решение, я сделаю то же самое (используя расширение 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

Отказ от ответственности: не делайте этого. На самом деле нет. Это возможно, но это такая ужасная идея ...

11 голосов
/ 02 ноября 2010

Переопределение классов

Вы можете сделать это с расширением PECL runkit он же «Инструментарий из ада»:

  • 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 объектом, теперь все свойства сохраняют свою видимость, но prop2 равно NULL. Ctor этого не допускает, так что технически, пока у вас есть Bar дочерний элемент Foo, он не находится в допустимом состоянии. Вы можете добавить волшебный метод __wakeup, чтобы как-то справиться с этим, но если серьезно, вы этого не хотите, и это показывает, почему кастинг - это уродливый бизнес.

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Я абсолютно не рекомендую никому использовать любое из этих решений в производстве.

9 голосов
/ 03 ноября 2010

Вы можете, как описано в других ответах, сделать это с помощью противного черной магии расширений PECL.

Хотя ты серьезно не хочешь этого. Любую проблему, которую вы хотите решить в ООП, есть совместимый с ООП способ сделать это.

Изменения иерархии типов времени выполнения не соответствуют ООП (на самом деле этого сознательно избегают). Есть шаблоны дизайна, которые должны соответствовать тому, что вы хотите.

Пожалуйста, скажите нам, почему вы этого хотите, я уверен, что должны быть лучшие способы сделать это;)

2 голосов
/ 02 ноября 2010

Это невозможно, потому что, хотя экземпляр дочернего класса также является экземпляром родительского класса, обратное неверно.

Что вы можете сделать, это создать новый экземпляр дочернего класса и скопировать в него значения из старого объекта. Затем вы можете вернуть новый объект, который будет иметь тип myChildClass.

1 голос
/ 31 октября 2018

Для простых классов это может работать (я использую это успешно в некоторых редких случаях):

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 (https://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;
    }
}
...