Внесите текущий объект ($ 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
Ответы
Ответ 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;
}
}