PHP memiliki sekumpulan API refleksi yang dapat digunakan untuk mengintrospeksi kelas, antarmuka, fungsi, metode, dan ekstensi. Di antara banyak fitur lainnya, refleksi memungkinkan kita untuk mengakses properti kelas dan memanggil metode kelas meskipun dideklarasikan sebagai pribadi/dilindungi. Saya banyak menggunakannya untuk menulis pengujian unit untuk metode privat di beberapa proyek lawas di mana pengujian metode publik terlalu rumit
Jadi, bagaimana cara melakukannya?
class Foo { private $bar = 'a'; protected function call() { return 'bazz'; } } $obj = new Foo; // lets create reflection for private $bar property $reflectedProperty = new ReflectionProperty(Foo::class, 'bar'); // set it as accessible $reflectedProperty->setAccessible(true); // now we can read the value of the property echo ($reflectedProperty)->getValue($obj); // now reflection for protected call() method $reflectedMethod = new ReflectionMethod(Foo::class, 'call'); // set it as accessible $reflectedMethod->setAccessible(true); // now we can invoke/call that method as if it is public method echo $reflectedMethod->invoke($obj);Seperti yang dapat kita lihat, untuk mengakses properti atau metode pribadi/dilindungi, kita perlu memanggil metode setAccessible(true) sebelum mengakses. Jika kami tidak memanggil metode ini, kami akan mendapatkan pengecualian
Uncaught ReflectionException: Trying to invoke protected method Foo::call() from scope ReflectionMethod _Agak mengganggu karena jika kita menerima objek ReflectionProperty atau ReflectionMethod dari pustaka atau modul pihak ketiga mana pun, kita tidak tahu apakah setAccessible() telah dipanggil pada objek itu atau tidak. Jadi agar lebih aman, kita perlu memanggil setAccessible() lagi
PHP 8. 1 memecahkan masalah itu
Mulai sekarang, kita dapat mengakses properti atau memanggil metode melalui API refleksi tanpa perlu memanggil setAccessible() secara eksplisit, meskipun dilindungi/pribadi
Setiap kali kami mencoba mengakses properti pribadi/dilindungi atau memanggil metode, itu akan berperilaku seolah-olah setAccessible(true) telah dipanggil di muka
Saya sedang mendiskusikan teknik ini selama tinjauan kode baru-baru ini, dan saya menyadari teknik keren ini mungkin tidak begitu dikenal. Saya menemukannya secara tidak sengaja beberapa tahun yang lalu
Kadang-kadang, semata-mata untuk tujuan pengujian, kita mungkin perlu mengakses properti atau metode pribadi atau yang dilindungi. Kecenderungan kita yang biasa adalah menggunakan Refleksi untuk melakukan ini. Refleksi agak rumit, karena ada banyak boilerplate tambahan untuk menyiapkannya
Tapi penutupan sebenarnya memberi kita cara keren untuk melakukannya dengan lebih mudah. Jika Anda pernah menggunakan metode (fn (int $newAge) => $this->age = $newAge)->call($me, 30); // Changes $me->age to 30 4 JavaScript, kita dapat melakukan hal yang sama di PHP
Mari kita mulai dengan kelas sampel
class Person { public string $name = 'Dan'; protected int $age = 40; private bool $cool = true; protected static string $species = 'homo sapien'; protected static function evolve(): void { static::$species = 'homo superior'; } protected function birthday(): void { $this->age++; } public function howOldAmI(): int { return $this->age; } } $me = new Person();
Masuk ke mode layar penuh Keluar dari mode layar penuh
Properti non-statis
Kami tahu cara membaca dan mengubah (fn (int $newAge) => $this->age = $newAge)->call($me, 30); // Changes $me->age to 30 5. Ini publik, jadi tidak ada masalah di sana
Sekarang, bagaimana jika kita perlu, di suatu tempat dalam ujian, untuk menetapkan usia yang tepat. Itu adalah properti yang dilindungi, jadi kita tidak bisa langsung mengubahnya, tetapi Penutupan bisa memberi kita jalan keluarnya
(fn (int $newAge) => $this->age = $newAge)->call($me, 30); // Changes $me->age to 30
Masuk ke mode layar penuh Keluar dari mode layar penuh
Jadi, apa yang kita lakukan di sini?
$changeAge = (fn (int $newAge) => $this->age = $newAge); _
Masuk ke mode layar penuh Keluar dari mode layar penuh
Pertama, kami membuat penutupan yang mengubah (fn (int $newAge) => $this->age = $newAge)->call($me, 30); // Changes $me->age to 30 6 menjadi apa pun yang diteruskan ke sana. Jika kami mencoba melakukan (fn (int $newAge) => $this->age = $newAge)->call($me, 30); // Changes $me->age to 30 _7, kami akan mendapatkan kesalahan, karena kami mendefinisikan ini di luar kelas. (fn (int $newAge) => $this->age = $newAge)->call($me, 30); // Changes $me->age to 30 8 sebenarnya tidak ada di sini, atau, jika kita memasukkan kode ini ke dalam pengujian unit, (fn (int $newAge) => $this->age = $newAge)->call($me, 30); // Changes $me->age to 30 8 akan merujuk ke kelas pengujian unit itu sendiri
Di situlah $changeAge = (fn (int $newAge) => $this->age = $newAge);
_0 masuk. $changeAge = (fn (int $newAge) => $this->age = $newAge);
1 mengikat penutupan ke objek baru, dan memanggilnya dengan argumen apa pun yang Anda berikan padanya. Dengan kata lain, argumen pertama diteruskan ke $changeAge = (fn (int $newAge) => $this->age = $newAge);
0 menjadi (fn (int $newAge) => $this->age = $newAge)->call($me, 30); // Changes $me->age to 30
8 dalam penutupan. Argumen lain apa pun diteruskan ke fungsi itu sendiri
$changeAge->call($me, 30);
Masuk ke mode layar penuh Keluar dari mode layar penuh
Kami juga dapat dengan mudah mengakses properti $changeAge = (fn (int $newAge) => $this->age = $newAge);
_4 dan $changeAge = (fn (int $newAge) => $this->age = $newAge);
5 tanpa mengubahnya. Jadi, saya bisa melakukannya
$amICool = (fn () => $this->cool)->call($me); // true $myAge = (fn () => $this->age)->call($me);
Masuk ke mode layar penuh Keluar dari mode layar penuh
Sifat statis
Itu berfungsi dengan baik untuk properti atau metode non-statis. Bagaimana dengan yang statis?
$mySpecies = (fn () => static::$species)->bindTo(null, Person::class)(); // homo sapien (fn () => static::evolve())->bindTo(null, Person::class)(); // Changes to homo superior (fn (string $newSpecies) => static::$species = $newSpecies)->bindTo(null, Person::class)('homo erectus'); // devolve to homo erectus
Masuk ke mode layar penuh Keluar dari mode layar penuh
Jadi, ini sedikit berbeda. Pertama, kami menggunakan $changeAge = (fn (int $newAge) => $this->age = $newAge); _6 yang mengembalikan penutupan baru, yang telah diikat ulang ke objek atau kelas yang ditentukan. Dengan $changeAge = (fn (int $newAge) => $this->age = $newAge); _7, Anda dapat meneruskan sebuah objek (seperti $changeAge = (fn (int $newAge) => $this->age = $newAge); 0), atau Anda dapat membiarkan null itu dan meneruskan kelas, yang mengubah pengikatan statis. Itulah yang telah kami lakukan di sini. Dan karena mengembalikan penutupan baru, Anda kemudian harus memanggilnya, itulah sebabnya kami memiliki tambahan $changeAge = (fn (int $newAge) => $this->age = $newAge); 9 setelah
Jadi, mari kita uraikan ini selangkah demi selangkah juga
$getSpecies = (fn () => static::$species);
Masuk ke mode layar penuh Keluar dari mode layar penuh
Penutupan yang mengembalikan $changeAge->call($me, 30);
_0 dari cakupan saat ini
$boundGetSpecies = $getSpecies->bindTo(null, Person::class);
Masuk ke mode layar penuh Keluar dari mode layar penuh
Penutupan baru, yang mengubah cakupan menjadi $changeAge->call($me, 30);
1, artinya setiap referensi ke $changeAge->call($me, 30);
2 mengacu pada $changeAge->call($me, 30);
1
$mySpecies = $boundGetSpecies();
Masuk ke mode layar penuh Keluar dari mode layar penuh
Atau, jika kita mengubah spesies menjadi nilai arbitrer, seperti yang kita lakukan pada yang ketiga
// Create species changing closure, initially bound to the current scope $changeSpecies = (fn (string $newSpecies) => static::$species = $newSpecies); // Create a new Closure, bound to the Person scope $changePersonSpecies = $changeSpecies->bindTo(null, Person::class); // Change it to whatever $changePersonSpecies('homo erectus');
Masuk ke mode layar penuh Keluar dari mode layar penuh
Menggunakan $changeAge = (fn (int $newAge) => $this->age = $newAge); _7 dengan objek
Anda dapat menggunakan $changeAge = (fn (int $newAge) => $this->age = $newAge);
_7 dengan cara yang mirip dengan $changeAge = (fn (int $newAge) => $this->age = $newAge);
0
(fn (int $newAge) => $this->age = $newAge)->call($me, 30); // Changes $me->age to 30 0
Masuk ke mode layar penuh Keluar dari mode layar penuh
Mari kita hancurkan
(fn (int $newAge) => $this->age = $newAge)->call($me, 30); // Changes $me->age to 30 1
Masuk ke mode layar penuh Keluar dari mode layar penuh
Penutupan yang memanggil $changeAge->call($me, 30);
_7 pada objek dalam lingkup saat ini
(fn (int $newAge) => $this->age = $newAge)->call($me, 30); // Changes $me->age to 30 2
Masuk ke mode layar penuh Keluar dari mode layar penuh
Buat penutupan baru dengan $changeAge->call($me, 30);
_8 sebagai ruang lingkup. Dengan cara ini, setiap referensi ke (fn (int $newAge) => $this->age = $newAge)->call($me, 30); // Changes $me->age to 30
_8 merujuk ke $changeAge->call($me, 30);
8
(fn (int $newAge) => $this->age = $newAge)->call($me, 30); // Changes $me->age to 30 _3
Masuk ke mode layar penuh Keluar dari mode layar penuh
Karena $amICool = (fn () => $this->cool)->call($me); // true $myAge = (fn () => $this->age)->call($me); _1 adalah penutupan, kita harus benar-benar menyebutnya
Kesimpulan
Tentu, kita dapat menggunakan $amICool = (fn () => $this->cool)->call($me); // true $myAge = (fn () => $this->age)->call($me); 2 dan $amICool = (fn () => $this->cool)->call($me); // true $myAge = (fn () => $this->age)->call($me); 3 untuk melakukan semua ini, tetapi teknik ini sangat menyederhanakannya, karena memanggil satu metode pribadi adalah satu baris kode
Saya benar-benar menggunakan ini dalam paket yang baru-baru ini saya tulis yang seharusnya membuat ini lebih mudah
Dan untuk mengulangi, saya tidak menyarankan melakukan ini dalam kode produksi. Ada alasan visibilitas ada. Kita tidak boleh menghindarinya seperti ini dalam kode di server kita. Namun, jika kita perlu mengutak-atik beberapa objek untuk pengujian, teknik ini dapat menyederhanakannya untuk kita