ThinkPHP6 Core Analysis: System Services

Keywords: PHP Database JSON Attribute

What are system services?System services are bindings that identify the class used by the program before it is used, so that the container can parse it (via the register method of the service class), initialize some parameters, register routes, etc. (not limited to these operations, but mainly look at the needs of a class before it is used, configure it, and use the boot of the service class)Method).For example, the ModelService class, which provides services as described below, initializes some of the model class's member variables (in the boot method) and sets the stage for the subsequent Model class's "coming out".

Let's first look at the services that come with the system and see how they are implemented.

 

Built-in Services

The services built into the system are ModelService, PaginatorService, and ValidateService classes. Let's see how they are registered and initialized.

In App::initialize(), there is a period like this:

1 foreach ($this->initializers as $initializer) {
2     $this->make($initializer)->init($this);
3 }

 

Here you loop the value of App::initializers, use the make method of the container class to get each instance of $initializer, and then call the init method corresponding to the instance.App::initializers member variable value is:

1 protected $initializers = [
2     Error::class,
3     RegisterService::class,
4     BootService::class,
5 ];

 

This focuses on the latter two: service registration and service initialization.

 

Service Registration

When executing $this->make($initializer) ->init($this), $initializer equals RegisterService::class, the init method in that class is called with the following code:

 1 public function init(App $app)
 2 {
 3     // Service to load extension packs
 4     $file = $app->getRootPath() . 'vendor/services.php';
 5 
 6     $services = $this->services;
 7 
 8     //Merge to get all services that need to be registered
 9     if (is_file($file)) {
10         $services = array_merge($services, include $file);
11     }
12     // Register services one by one
13     foreach ($services as $service) {
14         if (class_exists($service)) {
15             $app->register($service);
16         }
17     }
18 }

 

In the service registration class, the values of the system built-in services are defined:

1 protected $services = [
2     PaginatorService::class,
3     ValidateService::class,
4     ModelService::class,
5 ];

 

The services defined by these three services and extension packs will be registered one by one, using the register code as follows:

 1 public function register($service, bool $force = false)
 2 {
 3     // such as think\service\PaginatorService
 4     // The getService method determines if an instance of a service exists in the App::$services member variable
 5     // If so, return the instance directly
 6     $registered = $this->getService($service);
 7     // If the service is registered and not forced to re-register, return directly to the service instance
 8     if ($registered && !$force) {
 9         return $registered;
10     }
11     // Instantiate the service
12     // Like think\service\Paginator Service,
13     // This class does not have a constructor, its parent Service class has a constructor, and an instance of the App class needs to be passed in
14     // So here you pass in $this (an instance of the App class) for instantiation
15     if (is_string($service)) {
16         $service = new $service($this);
17     }
18     // If it exists「register」Method is called
19     if (method_exists($service, 'register')) {
20         $service->register();
21     }
22     // If it exists「bind」Property, add container identity binding
23     if (property_exists($service, 'bind')) {
24         $this->bind($service->bind);
25     }
26     // Save service instance
27     $this->services[] = $service;
28 }

 

For detailed analysis, see the code comments.If a service class defines a register method, it is executed when a service is registered, which is typically used to bind a service to a container; in addition, you can bind a service to a container by defining the value of the bind property.

After the services have been registered one by one, the value of App::services is approximately the same:

 

 

 


 

Instances of each service contain an instance of the App class.

 

Service Initialization

When executing $this->make($initializer) ->init($this), $initializer equals BootService::class, the init method in that class is called with the following code:

 1 public function init(App $app)
 2 {
 3     $app->boot();
 4 }
 5 Actually execution App::boot():
 6 
 7 public function boot(): void
 8 {
 9     array_walk($this->services, function ($service) {
10         $this->bootService($service);
11     });
12 }

 

Here, each service instance is passed into the bootService method.Focus on the bootService method:

1 public function bootService($service)
2 {
3     if (method_exists($service, 'boot')) {
4         return $this->invoke([$service, 'boot']);
5     }
6 }

 

This calls the boot method corresponding to the service instance.Next, let's take the boot method of ModelService as an example to see what the boot method might do.The boot method code for ModelService is as follows:

 1 public function boot()
 2 {
 3     // Set up Db object
 4     Model::setDb($this->app->db);
 5     // Set up Event object
 6     Model::setEvent($this->app->event);
 7     // Setting Dependency Injection Method for Container Objects
 8     Model::setInvoker([$this->app, 'invoke']);
 9     // Save Closure to Model::maker
10     Model::maker(function (Model $model) {
11         //Preservation db object
12         $db     = $this->app->db;
13         //Preservation $config object
14         $config = $this->app->config;
15         // Whether automatic timestamp writing is required If set to string, it indicates the type of time field
16         $isAutoWriteTimestamp = $model->getAutoWriteTimestamp();
17 
18         if (is_null($isAutoWriteTimestamp)) {
19             // Auto-write timestamp (obtained from configuration file)
20             $model->isAutoWriteTimestamp($config->get('database.auto_timestamp', 'timestamp'));
21         }
22         // Time field display format
23         $dateFormat = $model->getDateFormat();
24 
25         if (is_null($dateFormat)) {
26             // Set timestamp format (obtained from configuration file)
27             $model->setDateFormat($config->get('database.datetime_format', 'Y-m-d H:i:s'));
28         }
29 
30     });
31 }

 

You can see that the static members of the Model class are all initialized here.The access properties of these static member variables are protected, so they can be used in subclasses of the Model class.

 

Customize System Services

Next, we'll write a simple system service by ourselves.

  • Define the object (class) being served

    Create a file: app\common\MyServiceDemo.php with the following code:

     1 <?php
     2 namespace app\common;
     3 class MyServiceDemo
     4 {
     5     //Define a static member variable
     6     protected static $myStaticVar = '123';
     7     // Set the value of this variable
     8     public static function setVar($value){
     9         self::$myStaticVar = $value;
    10     }
    11     //Used to display the variable
    12     public function showVar()
    13     {
    14         var_dump(self::$myStaticVar);
    15     }
    16 }

     

  • Define service providers

    In the project root directory, the command line executes php think make:service MyService, which generates an app\serviceMyService.php file in which to write code:

     1 <?php
     2 namespace app\service;
     3 use think\Service;
     4 use app\common\MyServiceDemo;
     5 class MyService  extends Service
     6 {
     7     // When system service is registered, execute register Method
     8     public function register()
     9     {
    10         // Identifies the binding to the corresponding class
    11         $this->app->bind('my_service', MyServiceDemo::class);
    12     }
    13     // After system service registration, execute boot Method
    14     public function boot()
    15     {
    16         // Set one static member of the service class to another value
    17         MyServiceDemo::setVar('456');
    18     }
    19 }

     

  • Configure System Services

    In the app\service.php file (created without it), write:

    1 <?php
    2     return [
    3         '\app\service\MyService'
    4     ];

     

  • Called in Controller
    Create a controller file, app\controllerDemo.php, and write the code:

     1 <?php
     2 namespace app\controller;
     3 use app\BaseController;
     4 use app\common\MyServiceDemo;
     5 class Demo extends BaseController
     6 {
     7     public function testService(MyServiceDemo $demo){
     8         // Because the class is provided in the service app\service\MyService Of boot Method is set $myStaticVar='456'\
     9         // So here's output'456'
    10         $demo->showVar();
    11     }
    12 
    13     public function testServiceDi(){
    14         // Because the register Method has bound the mapping of class identity to the serviced class
    15         // So here you can use an instance of a container class to access the identity to get an instance of the class being served
    16         // '456'is also output here
    17         $this->app->my_service->showVar();
    18     }
    19 }

     

    See the code Notes for principles of execution and analysis.What's more, how does a custom service configuration load? App::initialize() calls the App::load() method, which ends with a paragraph like this:

    1 if (is_file($appPath . 'service.php')) {
    2     $services = include $appPath . 'service.php';
    3     foreach ($services as $service) {
    4         $this->register($service);
    5     }
    6 }

     

    It is here that our custom services are loaded and registered.

 

Using services in Composer extensions

Here is an example of the think-captcha extension package, which uses system services, where the service provider is the think\captchaCaptchaService class and the served class is the think\captchaCaptcha.

First, the project root directory runs composer require topthink/think-captcha to install the extension pack; when the installation is complete, we look at the vendor\services.php file and find a new line:

1 return array (
2   0 => 'think\\captcha\\CaptchaService',  //Newly added
3 );

 

How does this work?This is because the vendor\topthink\think-captchacomposer.json file is configured:

 1 "extra": {
 2     "think": {
 3         "services": [
 4             "think\\captcha\\CaptchaService"
 5         ]
 6     }
 7 },
 8 In composer.json under the project root directory, there is such a configuration:
 9 
10 "scripts": {
11     "post-autoload-dump": [
12         "@php think service:discover",
13         "@php think vendor:publish"
14     ]
15 }

 

After the extension pack is installed, the script here is executed, where php think service:discover is associated with adding a system service configuration here.The code executed by this directive is vendortopthink\framework\src\think\console\command\ServiceDiscover.php, with the following code:

 1 foreach ($packages as $package) {
 2     if (!empty($package['extra']['think']['services'])) {
 3         $services = array_merge($services, (array) $package['extra']['think']['services']);
 4     }
 5 }
 6 
 7 $header = '// This file is automatically generated at:' . date('Y-m-d H:i:s') . PHP_EOL . 'declare (strict_types = 1);' . PHP_EOL;
 8 
 9 $content = '<?php ' . PHP_EOL . $header . "return " . var_export($services, true) . ';';
10 
11 file_put_contents($this->app->getRootPath() . 'vendor/services.php', $content);

 

You can see that if an extension package has a configuration ['extra']['think']['services'], that is, the system service configuration, it will be written to the vendor\services.php file, and eventually all services will be loaded, registered, and initialized at system initialization.

After analyzing the implementation and principles of the service configuration in the extension pack, let's see what initialization has been done by the CaptchaService service provider class.This class has only one boot method with the following code:

 1 public function boot(Route $route)
 2 {
 3     // Configure Routing
 4     $route->get('captcha/[:config]', "\\think\\captcha\\CaptchaController@index");
 5     // Add a validator
 6     Validate::maker(function ($validate) {
 7         $validate->extend('captcha', function ($value) {
 8             return captcha_check($value);
 9         }, ':attribute error!');
10     });
11 }

 

With the preconfiguration above, we can happily use the Captcha class.

 

summary

Using system services offers great benefits and avoids the disadvantage of directly modifying classes.From the above analysis, I think that using system services, a class can be non-invasive "configured". If on any day certain settings of a class need to be modified, we do not need to modify this class directly, just modify the service provider class.For extension packs, system services enable them to flexibly configure programs in extensions for out-of-the-box use.However, there is a disadvantage that system service classes are instantiated during program initialization. If there are many service classes in a system, the performance of the program will be affected.

Posted by amelhedi on Sat, 16 Nov 2019 00:48:56 -0800