能否动态地扩展一个类?

31

我有一个类需要根据条件扩展不同的类(多达数百个)。在 PHP 中,有没有一种方式可以通过动态类名称来扩展一个类?

我认为需要一种方法来指定实例化时的扩展。

有什么思路吗?

9个回答

32

虽然目前还不可能,也不完全是我需要的答案,但我也需要同样的东西,并且不想使用eval、monkey-patching等方法。所以我通过在条件中扩展默认类来解决问题。

当然,这意味着如果你有100个类要扩展,你需要添加100个带有另一个扩展操作的条件,但对我来说,这看起来像是正确的方式。

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

class ProblemChild extends DynamicParent {}
?>

2
我在Zend Framework(特别是Magento)中遇到了一个问题,即第三方模块扩展了我已经扩展的类。使用这种方法,我可以更改我的类以动态地扩展第三方类,而不是我们都想要的核心类。 - Tyler V.
2
优秀的解决方案,而且如此简单!(“冗长”?伙计们,你们看到其他选择了吗? :) )无法停止喜爱它并且一直在桌子上敲头,因为我没有想出来... - Sz.

13

是的,我喜欢使用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 {}

好主意,缺点是您只能为一个父类创建一个子类。 - Black Mantha

12
可以使用魔术 __call 函数在 PHP 中创建动态继承。需要一些基础设施代码才能实现,但并不太困难。
免责声明
在使用此技术之前,您真的应该认真考虑至少两次,因为这实际上是一种不好的做法。
我使用此技术的唯一原因是,我不想在为站点创建模板时创建接口定义或设置依赖注入。我只想在模板中定义几个函数“块”,然后让继承自动使用正确的“块”。
实现
所需步骤为:
  • 子类现在扩展了一个'DynamicExtender'类。这个类拦截了子类调用不存在于子类中的方法的任何调用,并将它们重定向到父实例。

  • 每个'ParentClass'都被扩展为'ProxyParentClass'。对于父类中的每个可访问方法,在'ProxyParentClass'中都存在一个等效的方法。在'ProxyParentClass'中的每个这些方法都会检查方法是否存在于ChildClass中,如果存在,则调用该函数的child版本,否则调用来自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();

谢谢您,我在想是否可以实现类似 Threaded::extend 的东西,找到了这个答案,非常有帮助。 - Alex Barker
1
聪明,但会破坏所有类型提示和继承检查。 - I Want Answers
1
@IWantAnswers 是的,九年后,我将这个留下作为一种非常微妙的挑衅行为,而不是因为我认为这是一个好主意。 - Danack

8

关于组合模式的PHP实现,你有简单的例子吗?这里有一个例子,够好吗? - Peter Krauss

7

我已经解决了类似的问题。第一个参数定义class_alias函数的原始类名,第二个参数定义新类名。然后我们可以在if和else条件语句中使用该函数。

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

class Apple extends C{
      ...
}

苹果类扩展到虚拟类"C",可以根据if和else条件定义为"A"或"B"类。

更多信息,请查看此链接https://www.php.net/manual/en/function.class-alias.php


1
对于困惑的谷歌用户,如果您正在命名空间中工作,则别名将在全局命名空间中创建,因此您需要使用它。 - Finnbar M

7
你能直接使用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
    . "}"
);
?>

6
在任何生产环境中,我通常都很犹豫使用 eval()。 - Spot
1
如果您正在使用带有PHP加速器(例如APC)的Web服务器,则eval将不会存储在opcode缓存中。 - Francesco Casula
@OhhMee https://dev59.com/b3NA5IYBdhLWcg3wdtpd - Jared
2
@Jared 在这种情况下,eval函数并不是邪恶的。其次,我们应该时刻记住“伟大的力量带来伟大的责任”。 - Aniruddh
1
我使用class_alias函数设置了一个动态类名,例如class_alias('TCPDF', 'TCPDF2');。我解决了同类型的问题。 第一个参数定义了原始类名,第二个参数定义了新类名。然后我们可以在if和else条件中使用此函数。如果有人想要使用这种方法解决继承问题,我可以给出演示。 - Bhavin Thummar
显示剩余2条评论

0

我必须使用一个处理器类来扩展两个抽象类中的一个。

有效的解决方案如下:

if (class_exists('MODX\Revolution\Processors\Processor')) {
    abstract class DynamicProcessorParent extends 
        MODX\Revolution\Processors\Processor {}
} else {
    abstract class DynamicProcessorParent extends modProcessor {}
}

class NfSendEmailProcessor extends DynamicProcessorParent {
  /* Concrete class */
}

如果抽象父类包含抽象方法,则不需要在任何动态父类中实现它们。

如果您正在处理大型项目,除非类被命名空间化,否则可能不希望使用DynamicParent作为类名。 您需要更具体的内容以避免冲突。


0
  1. 获取所有已声明的类
  2. 类将被声明的位置。

class myClass { public $parentVar; function __construct() { $all_classes = get_declared_classes(); // 所有类 $parent = $parent[count($parent) -2]; //-2 是位置 $this->parentVar = new $parent(); } }


0

我有一个非常简单的想法,你可以试一试

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 :)
   }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接