Подтвердить что ты не робот

Внесите текущий объект ($ this) в класс потомков

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

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
4b9b3361

Ответ 1

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

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

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

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

Ответ 2

Кастинг для изменения типа объекта невозможно в 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 magic...

И если вы хотите еще несколько проверок (я бы так предложил), я бы добавил эту строку в первую строку 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

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

Ответ 3

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

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

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

Ответ 4

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

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

Ответ 5

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

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