PHP advanced features - Reflection and the combination of factory design patterns [explained in combination with the example of laravel admin code]
Use reflection to realize the production of factory mode without creating specific factory classes
Article address http://janrs.com/?p=833 Reprint without authorization of the author
Please indicate the source of reprint
Reflection
JANRS.COM - PHP Reflection
What is Reflection
Reflection is reflection. Reflection provides object-oriented programming with the ability to introspect
This understanding is a little too conceptual. Generally speaking, it can find out the cause according to the result of the event. In programming, you can find out the class to which the object belongs and all the properties and methods of the class according to an instantiated object, and even read document comments. This process of backchecking is called reflection
PHP provides a complete reflection API and the ability to introspect classes, interfaces, functions, methods and extensions. In addition, the reflection API provides methods to extract document comments from functions, classes, and methods. See PHP official website for details Introduction to PHP reflection
What can Reflection do
As mentioned above, you can use reflection to obtain all properties and methods of a class, comment documents, and even access permissions [protected/private] of class properties and methods. These features greatly improve the flexibility of PHP. For example:
-The so-called elegance of the Laravel framework, namely container, dependency injection and IOC control inversion, is realized by these features
-Annotation routing in Hyperf framework is also implemented by obtaining annotations based on reflection
-Generate documents because reflection can obtain access to class properties and methods. You can scan all files of the whole project and then use reflection to generate documents
-Test driven development uses reflection to obtain the characteristics of all methods of this class for test driven development
-The development plug-in uses the reflection to obtain the internal structure of the class to realize the Hook function, such as the implementation of the framework plug-in
Advantages and disadvantages of Reflection
Advantage reflection provides anti parsing of classes, which obtains high flexibility compared with the original object-oriented programming method, and reasonable use can make the code look more elegant and concise. Originally, in object-oriented programming, when using an instance of a class, you need to create an object first and then use the method. However, the reflection mechanism is used. You only need to provide a method of the class, and then use the reflection mechanism to use the object or method. Laravel framework gained its reputation for elegance by using a lot of reflection. The implementation of annotation routing in Swoole's Hyperf framework also uses reflection
At the same time, because reflection is the anti process of class instantiation, it destroys the object-oriented encapsulation and directly exposes the whole internal structure of the class, which leads to the abuse of reflection, the code will be difficult to manage, the whole project will be very chaotic, and even the business execution will be disordered. Especially in a team of dozens of people on large projects, imagine that the original object-oriented only tells what can be used and what can not be used. CTO writes the underlying code and others inherit it and then use it. Others don't know the internal structure. Once reflection is used, if a programmer accidentally sets the originally protected or private properties or methods to be accessible, and other programmers call the hidden data or methods unknowingly, it will lead to unpredictable disasters [see the following example code]
Secondly, due to the high flexibility of reflection, it is impossible to trace the source by directly clicking on the code in the IDE. It is really painful for novices, as is the case with Laravel and Hyperf
In the following code, the reflection mechanism directly sets the private method to be externally accessible
#Example: <?php class Foo { private function myPrivateMethod() { return 7; } } $method = new ReflectionMethod('Foo', 'myPrivateMethod'); //This reflection function directly sets the method that was originally private permission to be accessible $method->setAccessible(true); echo $method->invoke(new Foo); // echos "7" ?>
Factory design mode
Three factory design patterns [simple factory pattern] [factory pattern] [Abstract Factory pattern]
The simple factory pattern is also called the static factory method pattern. In short, the method of creating objects is implemented through a static method. In the simple factory pattern, instances of different classes are returned according to the parameters passed
In PHP, in the simple factory mode, there is an abstract product class [i.e. abstract class Calculate], which can be interface / abstract class / ordinary class. This abstract product class can derive multiple specific product classes, namely class CalculateAdd and class CalculateSub. Finally, a specific factory class [class CalculateFactory] is used to obtain the instance of the required product class
code implementation
1) Abstract product production class: Operation abstract class
//Production abstract class abstract class Calculate{ //Number A protected $number_a = null; //Number B protected $number_b = null; //Set number A public function setNumberA( $number ){ $this->number_a = $number; } //Set number B public function setNumberB( $number ){ $this->number_b = $number; } //Get number A public function getNumberA(){ return $this->number_a; } //Get number B public function getNumberB(){ return $this->number_b; } //Obtain the calculation results [obtain the produced products] public function getResult(){ return null; } }
2) Specific product categories: addition / subtraction, etc
//Addition operation class CalculateAdd extends Calculate{ //Obtain operation results [obtain specific products] public function getResult(){ return $this->number_a + $this->number_b; } } //Subtraction operation class CalculateSub extends Calculate{ //Obtain operation results [obtain specific products] public function getResult(){ return $this->number_a - $this->number_b; } } //Multiplication / division and other operations [other products]
3) Factory: factory. That is, a single class is used to create the instantiation process. This class is the factory. That is, the simple factory model
In PHP, the implementation method is actually a switch function or php8's new match function to instantiate the required product production class
//Instantiate different objects according to different operations //[that is, instantiate the corresponding product class for production according to the required products] //The corresponding implementation is actually a switch or php8 function and a new match function //Let's demonstrate with the latest match function class CalculateFactory{ public static function setCalculate( $type = null ){ return match( $type ){ 'add' => (function(){ return new CalculateAdd(); })(), 'sub' => (function(){ return new CalculateSub(); })(), default => null; }; } } //Specific use $calculate = CalculateFactory::setCalculate('add'); $calculate->setNumberA = 1; $calculate->setNumberB = 2; //calculation echo $calculate->getResult;//echo 3
Summary: the simple factory mode is actually to create a base class [abstract], which stores the common code of all specific production product classes, but does not execute the process. Then all specific production product classes inherit the base class and implement their own production processes. Finally, create a factory class, which is used to obtain the required production class according to the passed in parameters
Factory method mode is also called factory mode, which belongs to creative mode. In the factory mode, the parent class of the factory class is only responsible for defining the public interface and does not perform the actual production action. The actual production action is handed over to the subclass of the factory. In this way, the instantiation of the class is delayed to the subclass of the factory, and the instantiation of the specific product, that is, production, is completed through the subclass of the factory
In the factory mode, unlike the simple factory mode, there is an abstract factory class [interface CalculateFactory], which can be an interface / abstract class. This abstract factory class can derive multiple specific factory classes [FactoryAdd and FactorySub]
Code implementation [the following code needs to use the above production abstract class]
The following code needs to use the above production abstract class: abstract class Calculate
And specific production classes, i.e. CalculateAdd and CalculateSub. The implementation will not be repeated below
interface CalculateFactory{ public function CreateCalculate(); } class FactoryAdd implements CalculateFactory{ public function CreateCalculate(){ return new CalculateAdd(); } } class FactorySub implements CalculateFactory{ public function CreateCalculate(){ return new CalculateSub(); } } //Specific use //Create factory instance $calculateFactory = new FactoryAdd(); $add = $calculateFactory->CreateCalculate(); $add->setNumberA( 1 ); $add->setNumberB( 2 ); //calculation echo $add->getResult();//echo 3
Summary: the difference between the factory mode and the simple factory mode is that in the simple factory mode, there is only one factory to produce the corresponding production object [CalculateFactory]. In the factory mode, each production object is produced by its own factory, and these factories inherit from the same interface [interface CalculateFactory]
Abstract factory pattern abstract factory pattern provides an interface to create a series of related or interdependent objects without specifying their specific classes. That's abstract. A popular explanation is that compared with the above factory mode, the abstract factory mode has a super factory on each different factory. This super factory is an abstract interface used to produce specific factories
In the abstract factory mode, there are multiple abstract product classes [i.e. abstract class Phone and abstract class Android], which can be interface / abstract class / ordinary class. Each abstract product class can derive multiple specific product classes [i.e. class IPhone / class MiPhone and class IOS / class Android]. An abstract factory class [i.e. interface AbstractFactory] can derive multiple specific factory classes [i.e. class iPhoneFactory and class MiFactory], and each specific factory class can create multiple instances of product classes [i.e. createPhone and createSystem]
code implementation
//Abstract product class abstract class Phone{} abstract class System{} //Specific product categories class IPhone extends Phone{} class MiPhone extends Phone{} //Specific product categories class IOS extends System{} class Android extends System{} //Super factory interface AbstractFactory{ public function createPhone(); public function createSystem(); } //Specific Apple factory class iPhoneFactory implements AbstractFactory{ //Production of Apple phones public function createPhone(){ return new IPhone(); } //Apple production system public function createSystem(){ return new IOS(); } } //Specific millet factory class MiFactory implements AbstractFactory{ //Production of Xiaomi mobile phone public function createPhone(){ return new MiPhone(); } //Production Android system public function createSystem(){ return new Android(); } }
Summary: compared with the factory pattern, the abstract factory pattern provides an interface to specify the products to be produced. Each factory inherited from this interface can produce according to the specified mode [AbstarctFactory in the code]
The above three factory modes are ultimately designed to extract duplicate codes, summarize them according to specific demand scenarios, decouple and reuse them, so that they can be used directly in the required scenarios
The three modes are summarized as follows:
Simple factory:
- An abstract product class (which can be interface, abstract class and ordinary class) can derive multiple concrete product classes
- A single specific factory class
Each specific factory class can only create one instance of a specific product class
Factory mode:
- An abstract product class (which can be interface, abstract class and ordinary class) can derive multiple concrete product classes
- An abstract factory class (can be: interface, abstract class) can derive multiple concrete factory classes
Each specific factory class can only create one instance of a specific product class
Abstract factory:
- Multiple abstract product classes (can be: interface, abstract class, ordinary class), and each abstract product class can derive multiple specific product classes
- An abstract factory class (can be: interface, abstract class) can derive multiple concrete factory classes
- Each specific factory class can create multiple instances of specific product classes
Differences between the three modes:
- The simple factory pattern has only one abstract product class and only one concrete factory class
- The factory method pattern has only one abstract product class, while the abstract factory pattern has multiple abstract product classes
- The concrete factory class of factory method pattern can only create one instance of concrete product class, while the abstract factory pattern can create multiple instances of concrete product class
Combination of factory mode and reflection
The reflection feature can be used to realize the production process of factory mode. An example is given in combination with larevel admin
Let's take a look at the following code. Requirement background: different permission buttons need to be displayed according to different roles
<?php class TaskController extends BaseController { use HasResourceActions; /** * Make a grid builder. * * @return Grid */ protected function grid() { //Grid Columns... if (Admin::user()->inRoles([AdminUserModel::getAssignmentRole()])) { $grid->disableBatchActions(); $grid->disableEditButton(); $grid->disableCreateButton(); $grid->disableDeleteButton(); } elseif (Admin::user()->inRoles([AdminUserModel::getEvaluatorRole()])) { $grid->disableBatchActions(); $grid->disableEditButton(); $grid->disableCreateButton(); $grid->disableDeleteButton(); $grid->actions(function (Grid\Displayers\Actions $actions) { $actions->append(new ConfirmCloseTaskAction()); }); } else { $grid->disableCreateButton(); $grid->disableDeleteButton(); $grid->disableEditButton(); $grid->disableBatchActions(); $grid->disableViewButton(); $grid->disableActions(); } } }
The above code is obviously bloated at a glance. With the increase of business [i.e. the increase of Controller] and roles, more repeated judgments and repeated codes need to be written
Solution: different roles need different permissions. Each role can use a fixed method to set permissions. This fixed method can set permissions for different roles. These conditions just meet the usage scenario of factory mode: namely:
- Abstract a product class to derive permission product classes of multiple roles
- Abstract a factory class to derive multiple specific factory classes, which are represented as the scene corresponding to the permission button to be used
- Each specific factory can create multiple specific product classes (i.e. instantiate permission products of multiple roles) in the scenario of using permission buttons
The code is as follows [in the following code, reflection will be used instead of factory production]
1) Abstract a product class to derive permission product classes of multiple roles
<?php namespace App\GridActionFactory; use Dcat\Admin\Grid; /** * Factory interface */ interface GridActionInterface { //Permission of operator role function salesmanAction(Grid $grid); //Assign permissions to the role of administrator function assignmentAction(Grid $grid); //Authority of financial role function financeAction(Grid $grid); //... permissions for other roles }
2,3) the two steps are included together. Abstract a factory class to derive multiple specific factory classes, which are represented as the scene corresponding to the permission button to be used. Among them, the setRoleAction method uses reflection to directly produce, that is, it replaces the process of creating an instance of each specific factory class
<?php namespace App\GridActionFactory; use Dcat\Admin\Admin; use Dcat\Admin\Grid; /** * Set Action permission abstract class */ abstract class GridActionAbstract { // abstract public static function setAction(Grid $grid, string $role); /** * Filter role * * @param string $role * @return bool */ protected static function isInRoles(string $role): bool { return Admin::user()->inRoles([$role]); } /** * Call the corresponding method * [This method is actually a factory in the factory mode, which is specially produced] * [Multiple factories correspond to each Controller that needs Action permission] * [Each Controller produces its own Action permissions] * [This production is achieved through reflection] * * @param Grid $grid * @param string $role * @param string $class * @throws \ReflectionException */ protected static function setRoleAction(Grid $grid, string $role, string $class) { $r = new \ReflectionClass($class); $methodName = $role . 'Action'; if (!$r->hasMethod($methodName)) throw new \Exception('Method Not Found [ method : ' . $methodName . ' ] '); $method = $r->getMethod($methodName); $method->invoke($r->newInstance(), $grid); } }
According to the above reflection to implement the instantiation process, the permissions of the above TaskController can be simplified into the following code:
<?php namespace App\GridActionFactory; use Dcat\Admin\Grid; class TaskAction extends GridActionAbstract implements GridActionInterface { /** * @param Grid $grid * @param string $role * @throws \ReflectionException */ public static function setAction(Grid $grid, string $role) { if (!parent::isInRoles($role)) return; //The production process is directly realized by calling setRoleAction of the parent class parent::setRoleAction($grid, $role, self::class); } //There are roles that require permission buttons under TaskController //Assignor role public function assignmentAction(Grid $grid) { //Permission button $grid->showActions(); $grid->showViewButton(); } //There are roles that require permission buttons under TaskController //Financial role public function financeAction(Grid $grid) { $grid->showActions(); $grid->showViewButton(); } //There are roles that require permission buttons under TaskController //Salesman role public function salesmanAction(Grid $grid) { } //... other roles }
After encapsulation with design patterns, the code for controlling permissions in the TaskController above is directly optimized as follows: [much more elegant ~]
<?php class TaskController extends BaseController { use HasResourceActions; /** * Make a grid builder. * * @return Grid */ protected function grid() { //Grid Columns... //Financial role button TaskAction::setAction($grid, AdminUserModel::getFinanceRole()); //Assignor role button TaskAction::setAction($grid, AdminUserModel::getAssignmentRole()); //... other role buttons /* if (Admin::user()->inRoles([AdminUserModel::getAssignmentRole()])) { $grid->disableBatchActions(); $grid->disableEditButton(); $grid->disableCreateButton(); $grid->disableDeleteButton(); } elseif (Admin::user()->inRoles([AdminUserModel::getEvaluatorRole()])) { $grid->disableBatchActions(); $grid->disableEditButton(); $grid->disableCreateButton(); $grid->disableDeleteButton(); $grid->actions(function (Grid\Displayers\Actions $actions) { $actions->append(new ConfirmCloseTaskAction()); }); } else { $grid->disableCreateButton(); $grid->disableDeleteButton(); $grid->disableEditButton(); $grid->disableBatchActions(); $grid->disableViewButton(); $grid->disableActions(); } */ } }