จะเพิ่มวิธีการใหม่ให้กับวัตถุ php ได้อย่างไร?


101

ฉันจะเพิ่มวิธีการใหม่ให้กับวัตถุ "ทันที" ได้อย่างไร

$me= new stdClass;
$me->doSomething=function ()
 {
    echo 'I\'ve done something';
 };
$me->doSomething();

//Fatal error: Call to undefined method stdClass::doSomething()

นี่เป็นการเฉพาะโดยไม่ใช้คลาสหรือไม่?
thomas-peter

1
คุณสามารถใช้ __call แต่โปรดอย่าใช้ __call การเปลี่ยนแปลงพฤติกรรมของวัตถุแบบไดนามิกเป็นวิธีที่ง่ายมากในการทำให้โค้ดของคุณไม่สามารถอ่านได้และไม่สามารถเข้าถึงได้
GordonM

คำตอบ:


96

คุณสามารถควบคุม__callสิ่งนี้:

class Foo
{
    public function __call($method, $args)
    {
        if (isset($this->$method)) {
            $func = $this->$method;
            return call_user_func_array($func, $args);
        }
    }
}

$foo = new Foo();
$foo->bar = function () { echo "Hello, this function is added at runtime"; };
$foo->bar();

6
มากฉลาดมาก แต่มีความแตกต่างในโครงการ IMO ในโลกแห่งความเป็นจริง! (และ +1 เพื่อตอบโต้ผู้โหวตที่ไม่ระบุชื่อ)
Pekka

2
@pekka - แต่มันเป็น kludgy ได้อย่างไร? แน่นอนฉันไม่ได้บอกว่าฉันเป็นผู้เชี่ยวชาญในการขยายวัตถุในระหว่างรันไทม์ใน PHP แต่ฉันไม่สามารถพูดได้ว่าฉันเห็นผิดมาก (บางทีฉันอาจจะมีรสชาติไม่ดี)
karim79

16
ฉันจะเพิ่มการเช็คอิน is_callable ด้วยเช่นกัน (ดังนั้นคุณจะไม่พยายามโทรหาสตริงโดยไม่ได้ตั้งใจ) และโยน BadMethodCallException () หากคุณไม่พบวิธีการดังนั้นคุณจึงไม่ได้รับคืนและคิดว่ามันกลับมาสำเร็จ นอกจากนี้ให้สร้างพารามิเตอร์ตัวแรกของฟังก์ชันเป็นวัตถุเองจากนั้นทำarray_unshift($args, $this);ก่อนการเรียกใช้เมธอดเพื่อให้ฟังก์ชันได้รับการอ้างอิงถึงวัตถุโดยไม่จำเป็นต้องผูกมัดอย่างชัดเจน ...
ircmaxell

4
ฉันคิดว่าผู้คนกำลังพลาดประเด็นความต้องการเดิมคือการใช้วัตถุที่ไม่มีคลาส
thomas-peter

1
ฉันอยากจะแนะนำให้คุณลบการทดสอบ isset () อย่างสมบูรณ์เพราะหากไม่ได้กำหนดฟังก์ชันนั้นไว้คุณอาจไม่ต้องการกลับแบบเงียบ ๆ
Alexis Wilke


22

การใช้เพียง __call เพื่อให้สามารถเพิ่มเมธอดใหม่ในรันไทม์มีข้อเสียเปรียบที่สำคัญคือวิธีการเหล่านั้นไม่สามารถใช้การอ้างอิงอินสแตนซ์ $ นี้ได้ ทุกอย่างใช้งานได้ดีจนกว่าวิธีการที่เพิ่มเข้ามาจะไม่ใช้ $ this ในโค้ด

class AnObj extends stdClass
{
    public function __call($closure, $args)
    {
        return call_user_func_array($this->{$closure}, $args);
    }
 }
 $a=new AnObj();
 $a->color = "red";
 $a->sayhello = function(){ echo "hello!";};
 $a->printmycolor = function(){ echo $this->color;};
 $a->sayhello();//output: "hello!"
 $a->printmycolor();//ERROR: Undefined variable $this

ในการแก้ปัญหานี้คุณสามารถเขียนรูปแบบใหม่ได้ด้วยวิธีนี้

class AnObj extends stdClass
{
    public function __call($closure, $args)
    {
        return call_user_func_array($this->{$closure}->bindTo($this),$args);
    }

    public function __toString()
    {
        return call_user_func($this->{"__toString"}->bindTo($this));
    }
}

ด้วยวิธีนี้คุณสามารถเพิ่มวิธีการใหม่ที่สามารถใช้การอ้างอิงอินสแตนซ์ได้

$a=new AnObj();
$a->color="red";
$a->sayhello = function(){ echo "hello!";};
$a->printmycolor = function(){ echo $this->color;};
$a->sayhello();//output: "hello!"
$a->printmycolor();//output: "red"

12

ปรับปรุง:แนวทางที่แสดงในที่นี้มีข้อบกพร่องที่สำคัญ: ฟังก์ชันใหม่ไม่ใช่สมาชิกที่มีคุณสมบัติครบถ้วนของคลาส $thisไม่มีอยู่ในเมธอดเมื่อเรียกด้วยวิธีนี้ ซึ่งหมายความว่าคุณจะต้องส่งผ่านวัตถุไปยังฟังก์ชันเป็นพารามิเตอร์หากคุณต้องการทำงานกับข้อมูลหรือฟังก์ชันจากอินสแตนซ์วัตถุ! นอกจากนี้คุณจะไม่สามารถเข้าถึงprivateหรือprotectedสมาชิกของชั้นเรียนได้จากฟังก์ชันเหล่านี้

คำถามที่ดีและความคิดที่ชาญฉลาดโดยใช้ฟังก์ชันใหม่ที่ไม่ระบุตัวตน!

สิ่งนี้ใช้ได้ผล: แทนที่

$me->doSomething();    // Doesn't work

โดย call_user_func บนฟังก์ชั่นตัวเอง :

call_user_func($me->doSomething);    // Works!

สิ่งที่ไม่ได้ผลคือวิธีที่ "ถูกต้อง":

call_user_func(array($me, "doSomething"));   // Doesn't work

ถ้าเรียกแบบนั้น PHP ต้องใช้วิธีการประกาศในนิยามคลาส

นี่คือ private/ public/protectedปัญหาการมองเห็นหรือไม่

อัปเดต:ไม่ เป็นไปไม่ได้ที่จะเรียกใช้ฟังก์ชันตามปกติแม้จะมาจากภายในคลาสก็ตามดังนั้นนี่จึงไม่ใช่ปัญหาด้านการมองเห็น การส่งผ่านฟังก์ชันจริงไปcall_user_func()เป็นวิธีเดียวที่ฉันสามารถทำให้มันใช้งานได้


2

คุณยังสามารถบันทึกฟังก์ชันในอาร์เรย์:

<?php
class Foo
{
    private $arrayFuncs=array();// array of functions
    //
    public function addFunc($name,$function){
        $this->arrayFuncs[$name] = $function;
    }
    //
    public function callFunc($namefunc,$params=false){
        if(!isset($this->arrayFuncs[$namefunc])){
            return 'no function exist';
        }
        if(is_callable($this->arrayFuncs[$namefunc])){
            return call_user_func($this->arrayFuncs[$namefunc],$params);
        }
    }

}
$foo = new Foo();

//Save function on array variable with params
$foo->addFunc('my_function_call',function($params){
    return array('data1'=>$params['num1'],'data2'=>'qwerty','action'=>'add');
});
//Save function on array variable
$foo->addFunc('my_function_call2',function(){
    return 'a simple function';
});
//call func 1
$data = $foo->callFunc('my_function_call',array('num1'=>1224343545));
var_dump($data);
//call func 2
$data = $foo->callFunc('my_function_call2');
var_dump($data);
?>

1

เพื่อดูวิธีการทำเช่นนี้กับ EVAL คุณสามารถดูที่ PHP ไมโครกรอบของฉัน Halcyon ซึ่งสามารถใช้ได้บนGitHub มันเล็กพอที่คุณจะคิดออกได้โดยไม่มีปัญหาใด ๆ - มุ่งเน้นไปที่คลาส HalcyonClassMunger


ฉันสมมติ (และรหัสของฉันก็เช่นกัน) ว่าคุณกำลังทำงานบน PHP <5.3.0 ดังนั้นจึงต้องการ kludges และแฮ็กเพื่อให้ทำงานได้
ivans

การถามว่าใครต้องการแสดงความคิดเห็นเกี่ยวกับการโหวตลงนั้นไม่มีประโยชน์ฉันคิดว่า ...
ivans

อาจเป็นได้ว่ามีการลงคะแนนเสียงจำนวนมากเกิดขึ้นที่นี่ แต่คุณอาจต้องการอธิบายเพิ่มเติมเกี่ยวกับสิ่งที่คุณทำ? อย่างที่คุณเห็นนี่เป็นคำถามที่น่าสนใจและยังไม่มีคำตอบที่แน่ชัด
Pekka

1

หากไม่มี__callวิธีแก้ไขคุณสามารถใช้bindTo(PHP> = 5.4) เพื่อเรียกเมธอดที่มี$thisขอบเขต$meดังนี้:

call_user_func($me->doSomething->bindTo($me, null)); 

สคริปต์ที่สมบูรณ์อาจมีลักษณะดังนี้:

$me = new stdClass;
// Property for proving that the method has access to the above object:
$me->message = "I\'ve done something"; 
$me->doSomething = function () {
    echo $this->message;
};
call_user_func($me->doSomething->bindTo($me)); // "I've done something"

หรือคุณสามารถกำหนดฟังก์ชันขอบเขตให้กับตัวแปรแล้วเรียกโดยไม่ใช้ call_user_func :

$f = $me->doSomething->bindTo($me);
$f(); 

0

มีโพสต์ที่คล้ายกันใน stackoverflowซึ่งทำให้ชัดเจนว่าสิ่งนี้ทำได้ผ่านการใช้รูปแบบการออกแบบบางอย่างเท่านั้น

วิธีเดียวคือการใช้classkitซึ่งเป็นส่วนขยาย php ทดลอง (ยังอยู่ในโพสต์)

ใช่เป็นไปได้ที่จะเพิ่มวิธีการลงในคลาส PHP หลังจากกำหนดแล้ว คุณต้องการใช้ classkit ซึ่งเป็นส่วนขยาย "ทดลอง" ดูเหมือนว่าส่วนขยายนี้ไม่ได้เปิดใช้งานตามค่าเริ่มต้นดังนั้นจึงขึ้นอยู่กับว่าคุณสามารถคอมไพล์ไบนารี PHP ที่กำหนดเองหรือโหลด PHP DLLs ได้หากบน windows (เช่น Dreamhost อนุญาตให้ใช้ไบนารี PHP ที่กำหนดเองและติดตั้งได้ง่ายมาก ).


0

คำตอบkarim79 ใช้งานได้ แต่จะเก็บฟังก์ชันที่ไม่ระบุตัวตนไว้ในคุณสมบัติของวิธีการและการประกาศของเขาสามารถเขียนทับคุณสมบัติที่มีอยู่ของชื่อเดียวกันหรือไม่ทำงานหากคุณสมบัติที่มีอยู่privateทำให้วัตถุข้อผิดพลาดร้ายแรงใช้ไม่ได้ฉันคิดว่าการจัดเก็บไว้ในอาร์เรย์แยกต่างหากและการใช้ setter เป็นวิธีที่สะอาดกว่า วิธีการฉีดหมาสามารถเพิ่มไปยังทุกวัตถุโดยอัตโนมัติโดยใช้ลักษณะ

ปล. แน่นอนว่านี่คือการแฮ็กที่ไม่ควรใช้เนื่องจากเป็นการละเมิดหลักการปิดแบบเปิดของ SOLID

class MyClass {

    //create array to store all injected methods
    private $anon_methods = array();

    //create setter to fill array with injected methods on runtime
    public function inject_method($name, $method) {
        $this->anon_methods[$name] = $method;
    }

    //runs when calling non existent or private methods from object instance
    public function __call($name, $args) {
        if ( key_exists($name, $this->anon_methods) ) {
            call_user_func_array($this->anon_methods[$name], $args);
        }
    }

}


$MyClass = new MyClass;

//method one
$print_txt = function ($text) {
    echo $text . PHP_EOL;
};

$MyClass->inject_method("print_txt", $print_txt);

//method
$add_numbers = function ($n, $e) {
    echo "$n + $e = " . ($n + $e);
};

$MyClass->inject_method("add_numbers", $add_numbers);


//Use injected methods
$MyClass->print_txt("Hello World");
$MyClass->add_numbers(5, 10);

หากเป็นการแฮ็กที่ไม่ควรใช้ก็ไม่ควรโพสต์เป็นคำตอบ
miken32
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.