Можно ли динамически расширять класс? - PullRequest
26 голосов
/ 08 октября 2009

У меня есть класс, который мне нужно использовать для расширения различных классов (до сотен) в зависимости от критериев. Есть ли в PHP способ расширить класс динамическим именем класса?

Я предполагаю, что для определения расширения с помощью экземпляра потребуется метод.

Идеи? * * 1005

Ответы [ 8 ]

27 голосов
/ 27 мая 2013

Хотя это все еще невозможно, и не совсем ваш ответ, мне нужно то же самое, и я не хотел использовать eval, мартышки-патчи и т. Д. Поэтому я использовал класс по умолчанию, расширяя его в условиях.

Конечно, это означает, что если у вас есть 100 классов для расширения, вам нужно добавить 100 условий с помощью другой операции расширения, но для меня это выглядело как правильный путь.

<?php
if(class_exists('SolutionClass')) {
    class DynamicParent extends SolutionClass {}
} else {
    class DynamicParent extends DefaultSolutionClass {}
}

class ProblemChild extends DynamicParent {}
?>
8 голосов
/ 05 июля 2013

Возможно создать динамическое наследование в PHP, используя силу магической функции __call. Для работы требуется немного кода инфраструктуры, но это не так сложно.

Ответственность

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

Единственная причина, по которой я использую эту технику, заключается в том, что я не хочу ни создавать определения интерфейса, ни настраивать внедрения зависимостей при создании шаблонов для сайта. Я хочу просто иметь возможность просто определить пару функциональных «блоков» в шаблоне, а затем наследовать автоматически использовать правильный «блок».

Осуществление

Необходимые шаги:

  • Дочерний класс теперь расширяет класс DynamicExtender. Этот класс перехватывает любые вызовы, сделанные дочерним классом, к методам, которых нет в дочернем классе, и перенаправляет их на родительский экземпляр.

  • Каждый «ParentClass» расширяется до «ProxyParentClass». Для каждого доступного метода в родительском классе существует эквивалентный метод в ProxyParentClass. Каждый из этих методов в 'ProxyParentClass' проверяет, существует ли метод в ChildClass, и вызывает дочернюю версию функции, если она существует, в противном случае он вызывает версию из ParentClass

  • Когда создается класс DynamicExtender, вы передаете требуемый родительский класс, DynamicExtender создает новый экземпляр этого класса и устанавливает себя в качестве дочернего для ParentClass.

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

Это может быть легче понять как пару изображений:

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

enter image description here

Динамическое наследование с прокси

enter image description here

Демонстрационная реализация

Код для этого решения доступен на Github и немного более полное объяснение того, как это можно использовать здесь , но код для приведенного выше изображения:

//An interface that defines the method that must be implemented by any renderer.
interface Render {
    public function render();
}


/**
 * Class DynamicExtender
 */
class DynamicExtender implements Render {

    var $parentInstance = null;

    /**
     * Construct a class with it's parent class chosen dynamically.
     *
     * @param $parentClassName The parent class to extend.
     */
    public function __construct($parentClassName) {
        $parentClassName = "Proxied".$parentClassName;

        //Check that the requested parent class implements the interface 'Render'
        //to prevent surprises later.
        if (is_subclass_of($parentClassName, 'Render') == false) {
            throw new Exception("Requested parent class $parentClassName does not implement Render, so cannot extend it.");
        }

        $this->parentInstance = new $parentClassName($this);
    }

    /**
     * Magic __call method is triggered whenever the child class tries to call a method that doesn't
     * exist in the child class. This is the case whenever the child class tries to call a method of
     * the parent class. We then redirect the method call to the parentInstance.
     *
     * @param $name
     * @param array $arguments
     * @return mixed
     * @throws PHPTemplateException
     */
    public function __call($name, array $arguments) {
        if ($this->parentInstance == null) {
            throw new Exception("parentInstance is null in Proxied class in renderInternal.");
        }

        return call_user_func_array([$this->parentInstance, $name], $arguments);
    }

    /**
     * Render method needs to be defined to satisfy the 'implements Render' but it
     * also just delegates the function to the parentInstance.
     * @throws Exception
     */
    function render() {
        $this->parentInstance->render();
    }
}



/**
 * Class PageLayout
 *
 * Implements render with a full HTML layout.
 */
class PageLayout implements Render {

    //renders the whole page.
    public function render() {
        $this->renderHeader();
        $this->renderMainContent();
        $this->renderFooter();
    }

    //Start HTML page
    function renderHeader() {
        echo "<html><head></head><body>";
        echo "<h2>Welcome to a test server!</h2>";

        echo "<span id='mainContent'>";
    }

    //Renders the main page content. This method should be overridden for each page
    function renderMainContent(){
        echo "Main content goes here.";
    }

    //End the HTML page, including Javascript
    function renderFooter(){
        echo "</span>";
        echo "<div style='margin-top: 20px'>Dynamic Extension Danack@basereality.com</div>";
        echo "</body>";
        echo "<script type='text/javascript' src='jquery-1.9.1.js' ></script>";
        echo "<script type='text/javascript' src='content.js' ></script>";
        echo "</html>";
    }

    //Just to prove we're extending dynamically.
    function getLayoutType() {
        return get_class($this);
    }
}

/**
 * Class ProxiedPageLayout
 *
 * Implements render for rendering some content surrounded by the opening and closing HTML
 * tags, along with the Javascript required for a page.
 */
class ProxiedPageLayout extends PageLayout {

    /**
     * The child instance which has extended this class.
     */
    var $childInstance = null;

    /**
     * Construct a ProxiedPageLayout. The child class must be passed in so that any methods
     * implemented by the child class can override the same method in this class.
     * @param $childInstance
     */
    function __construct($childInstance){
        $this->childInstance = $childInstance;
    }

    /**
     * Check if method exists in child class or just call the version in PageLayout
     */
    function renderHeader() {
        if (method_exists ($this->childInstance, 'renderHeader') == true) {
            return $this->childInstance->renderHeader();
        }
        parent::renderHeader();
    }

    /**
     * Check if method exists in child class or just call the version in PageLayout
     */
    function renderMainContent(){
        if (method_exists ($this->childInstance, 'renderMainContent') == true) {
            return $this->childInstance->renderMainContent();
        }
        parent::renderMainContent();
    }

    /**
     * Check if method exists in child class or just call the version in PageLayout
     */
    function renderFooter(){
        if (method_exists ($this->childInstance, 'renderFooter') == true) {
            return $this->childInstance->renderFooter();
        }
        parent::renderFooter();
    }
}


/**
 * Class AjaxLayout
 *
 * Implements render for just rendering a panel to replace the existing content.
 */
class AjaxLayout implements Render {

    //Render the Ajax request.
    public function render() {
        $this->renderMainContent();
    }

    //Renders the main page content. This method should be overridden for each page
    function renderMainContent(){
        echo "Main content goes here.";
    }

    //Just to prove we're extending dynamically.
    function getLayoutType() {
        return get_class($this);
    }
}

/**
 * Class ProxiedAjaxLayout
 *
 * Proxied version of AjaxLayout. All public functions must be overridden with a version that tests
 * whether the method exists in the child class.
 */
class ProxiedAjaxLayout extends AjaxLayout {

    /**
     * The child instance which has extended this class.
     */
    var $childInstance = null;

    /**
     * Construct a ProxiedAjaxLayout. The child class must be passed in so that any methods
     * implemented by the child class can override the same method in this class.
     * @param $childInstance
     */
    function __construct($childInstance){
        $this->childInstance = $childInstance;
    }

    /**
     * Check if method exists in child class or just call the version in AjaxLayout
     */
    function renderMainContent() {
        if (method_exists ($this->childInstance, 'renderMainContent') == true) {
            return $this->childInstance->renderMainContent();
        }
        parent::renderMainContent();
    }
}



/**
 * Class ImageDisplay
 *
 * Renders some images on a page or Ajax request.
 */
class ImageDisplay extends DynamicExtender {

    private $images = array(
        "6E6F0115.jpg",
        "6E6F0294.jpg",
        "6E6F0327.jpg",
        "6E6F0416.jpg",
        "6E6F0926.jpg",
        "6E6F1061.jpg",
        "6E6F1151.jpg",
        "IMG_4353_4_5_6_7_8.jpg",
        "IMG_4509.jpg",
        "IMG_4785.jpg",
        "IMG_4888.jpg",
        "MK3L5774.jpg",
        "MK3L5858.jpg",
        "MK3L5899.jpg",
        "MK3L5913.jpg",
        "MK3L7764.jpg",
        "MK3L8562.jpg",
    );

    //Renders the images on a page, along with a refresh button
    function renderMainContent() {
        $totalImages = count($this->images);
        $imagesToShow = 4;
        $startImage = rand(0, $totalImages - $imagesToShow);

        //Code inspection will not be available for 'getLayoutType' as it
        //doesn't exist statically in the class hierarchy
        echo "Parent class is of type: ".$this->getLayoutType()."<br/>";

        for($x=0 ; $x<$imagesToShow ; $x++) {
            echo "<img src='images/".$this->images[$startImage + $x]."'/>";
        }

        echo "<br/>&nbsp;<br/>";
        echo "<span onclick='loadImagesDynamic();' style='border: 2px solid #000000; padding: 4px:'>Click to refresh images</span>";
    }
}


$parentClassName = 'PageLayout';

if (isset($_REQUEST['panel']) && $_REQUEST['panel']) {
    //YAY! Dynamically set the parent class.
    $parentClassName = 'AjaxLayout';
}

$page = new ImageDisplay($parentClassName);

$page->render();
7 голосов
/ 08 октября 2009

Я не думаю, что можно динамически расширять класс (однако, если я ошибаюсь, я бы хотел посмотреть, как это делается). Задумывались ли вы об использовании шаблона Composite (http://en.wikipedia.org/wiki/Composite_pattern, http://devzone.zend.com/article/7)?). Вы можете динамически комбинировать другой класс (даже несколько классов - это часто используется в качестве обходного пути к множественному наследованию), чтобы «внедрить» методы / Свойства вашего родительского класса в дочерний класс.

4 голосов
/ 18 июня 2016

Да. Мне нравится ответ с eval, но многие люди боятся любого eval в своем коде, поэтому вот один без eval:

<?php //MyClass.php 
namespace my\namespace;
function get_dynamic_parent() {
    return 'any\other\namespace\ExtendedClass';// return what you need
}
class_alias(get_dynamic_parent(), 'my\namespace\DynamicParent');

class MyClass extends DynamicParent {}
3 голосов
/ 09 октября 2009

Не могли бы вы просто использовать eval?

<?php
function dynamic_class_name() {
    if(time() % 60)
        return "Class_A";
    if(time() % 60 == 0)
        return "Class_B";
}
eval(
    "class MyRealClass extends " . dynamic_class_name() . " {" . 
    # some code string here, possibly read from a file
    . "}"
);
?>
1 голос
/ 24 июня 2019

Я решил свою проблему такого же типа. Первый параметр определяет исходное имя класса, а второй параметр определяет новое имя класса функции class_alias. Тогда мы можем использовать эту функцию в условиях if и else.

if(1==1){
  class_alias('A', 'C');
}
else{
  class_alias('B', 'C');
}

class Apple extends C{
      ...
}

Класс Apple распространяется на виртуальный класс «C», который можно определить как класс «A» или «B», в зависимости от условия if и else.

Для получения дополнительной информации вы можете проверить эту ссылку https://www.php.net/manual/en/function.class-alias.php

0 голосов
/ 03 декабря 2016

У меня такая простая идея, вы можете попробовать

class A {} 
class B {}
$dynamicClassName = "A";
eval("class DynamicParent extends $dynamicClassName {}");

class C extends DynamicParent{
   // extends success
   // Testing
   function __construct(){
        echo get_parent_class('DynamicParent'); exit; //A :)
   }
}
0 голосов
/ 27 августа 2016
  1. Получить все объявленные_классы
  2. Позиция, где будет объявлен класс.

class myClass { public $parentVar; function __construct() { $all_classes = get_declared_classes(); // all classes $parent = $parent[count($parent) -2]; //-2 is the position $this->parentVar = new $parent(); } }

...