Cấu hình ứng dụng (Bài trước)
(Bài tiếp) Plugin Manager

Service Manager

Bạn hình dùng trong ứng dụng của bạn sẽ có nhiều dịch vụ (service), như chức dịch vụ chuyên xác thực login, dịch vụ chuyên đảm trách truy cập cơ sở dữ liệu, dịch vụ quản lý các sự kiện ứng dụng ... Các dịch vụ như vậy được xây dựng từ các lớp khác nhau, các dịch vụ này lại phụ thuộc vào một dịch vụ khác, nên rất cần có một cơ chế quả lý tập trung.

Trong Zend Framework, lớp có tên là ServiceManager (\Zend\ServiceManager\ServiceManager) chính là trung tâm chứa tất cả các dịch vụ và quản lý các dịch vụ của ứng dụng. Lớp này kế thừa các giao diện interop\Container\ContainerInterfaceZend\ServiceManager\ServiceLacatorInterface, có nghĩa là nó hỗ trợ tiêu chuẩn PSR-11: xem psr-11

Lớp ServiceManager chính là triển khai mã lập trình theo mô hình Service Locator của Zend Framework, tức triển khai nguyên lý IoC - Nguyên lý lập trình IoC Inversion of control

Tích hợp Zend ServiceManager vào dự án thì gõ lệnh Composer:

composer require zendframework/zend-servicemanager

Ở đây ta sẽ tìm hiểu Zend ServiceManager trong dự án độc lập sau đó xem nó áp dụng vào chương trình khung Zend Framework như thế nào. Tạo một thư mục dự án, đặt tên SM-Example, trong thư mục đó gõ lệnh:

composer require zendframework/zend-servicemanager
zf

Mở thư mục code bằng VSC ra để biên tập, đầu tiên sẽ thiết lập Autoload của dự án, với namespace có tên SMExample sẽ nạp từ thư mục src (hãy tạo thư mục này). Vậy mở file composer.json thêm vào Autoload như sau:

{
    "require": {
        "zendframework/zend-servicemanager": "^3.4"
    }, 
    "autoload": {
        "psr-4": {
            "SMExample\\": "src/"
        }
    }
}

Tiếp theo để xuất thiết lập cho autoload.php của vendor gõ lệnh sau:

composer dump

Cấu hình và lấy dịch vụ từ Service Manager

Mỗi dịch vụ đăng ký vào Service Manager có một chuỗi là tên, key trỏ đến dịch vụ đó. Dịch vụ đó có thể là một đối tượng, một lớp có khả năng khởi tạo, một lớp khởi tạo qua Factory ...

Khởi tạo một Service Manager, cần cung cấp cho nó một mảng cấu hình, trong mảng này có định nghĩa tên và dịch vụ, mảng có cấu trúc như sau:

// Triển khai thực tế, mảng config thương lưu ở một file .php, rồi include vào
$config = [
    'services' => [
        // Nơi cấu hình đăng ký đối tượng là dịch vụ:
        // tên-dịch-vụ => đối-tượng
    ],
    'invokables' => [
        // Nơi  đăng ký các lớp dạng invokable, khởi tạo không cần tham  số
        // tên-dịch-vụ => lớp-tạo-dịch-vụ
    ],
    'factories' => [
        // Nơi Đăng ký các lớp dạng factory, khởi tạo cần tham số
        // tên-dịch-vụ => lớp-tạo-dịch-vụ
    ],
    'abstract_factories' => [
        // Đăng ký các lớp dạng abstract factory, tạo đối tượng căn cứ theo tên
    ],
    'aliases' => [
        // Khóa tắt cho tên các dịch vụ (tên tắt)
        // tên-dịch-vụ-tắt => tên-dịch-vụ
    ],
    'shared' => [
        // Các dịch vụ dạng non-shared (tạo mới mỗi lần get)
    ]
];

$servicemanager = new ServiceManager($config);

Để kiểm tra có một dịch vụ nào đó không dùng phương thức has, để lấy dịch vụ dùng phương thức get

if ($servicemanager->has("nameservice"))
{
    $service = $servicemanager->get("nameservice");
    //... other code
}

Có thể dùng build($name, $options) khởi tạo và lấy dịch vụ mới $name

Để thực hành, trước tiên tạo ra một lớp dịch vụ, ClassA như sau:

src/ClassA.php
<?php
declare(strict_types=1);

namespace SMExample;

class ClassA {
    private $mgs;
    public function  __construct() {
        $this->mgs = uniqid();
    }
};

Đăng ký đối tượng và lớp Invokable vào SM

Đối với các đối tượng, để đăng ký vào SM thì khai báo trong key services của mảng cấu hình config hoặc gọi trực tiếp $servicemanager->setService("nameservice", $service_object). Đối với các lớp dịch vụ có thể khởi tạo mà phương thức khởi tạo không cần tham số thì có thể đăng ký lớp đó như là dịch vụ bằng cách khai báo trong phần invokables của cấu hình hoặc dùng phương thức $servicemanager->setInvokableClass("nameservice", "classService");

Ví dụ: test1.php

<?php
    declare(strict_types=1);
    use SMExample\ClassA;
    use Zend\ServiceManager\ServiceManager;

    require "vendor/autoload.php";


    // KHỞI TẠO SM VÀ ĐĂNG KÝ DỊCH VỤ VÀO SM

    $classA = new ClassA();                             // Đối tượng dịch vụ

    $config = [
        'services' => [
            'service1'  =>  $classA                     // đăng  ký đối tượng với tên service1
        ],

        'invokables'  => [
            'service2'    =>  ClassA::class,            // đăng ký lớp dịch vụ invokable, với tên service2
            'service3'    =>  "SMExample\ClassA",       // đăng ký invokable với tên service3
            ClassA::class => ClassA::class              // đăng ký invokable với tên trùng tên đầy đủ lớp
        ]
    ];

    $servicemanager = new ServiceManager($config);



    // ĐỌC CÁC DỊCH VỤ TỪ SM

    $s1 = $servicemanager->get('service1');
    $s2 = $servicemanager->get('service2');
    $s3 = $servicemanager->get('service3');
    $s4 = $servicemanager->get(ClassA::class);


    var_dump($s1, $s2, $s3, $s4);
zf

Như vậy những tên dịch vụ invokable trỏ đến cùng tên lớp chỉ khởi tạo một đối tượng dịch vụ, dịch vụ service2, service3, ClassA::class xác định 1 dịch vụ.

Đăng ký dịch vụ với Factory

Với loại dịch vụ invokable ở trên, lớp khởi tạo dịch vụ phải có hàm tạo không tham số, còn nếu lớp đó khởi tạo có tham số thì cần dùng kỹ thuật Factory, một lớp gọi là Factory thì nó được dùng để khởi tạo ra lớp khác.

Ví dụ, tạo ra lớp dịch vụ /src/ClassB.php

<?php
    declare(strict_types=1);

    namespace SMExample;

    class ClassB {
        /***
         * @var ClassB - lưu dịch vụ ClassA, Inject qua khởi tạo
         */
        private $service;
        private $mgs;
        public function  __construct($mgs, ClassA $service) {
            $this->mgs     = $mgs;
            $this->service = $service;
        }
    };

Do lớp này khởi tạo cần có 2 tham số, nên không thể đăng ký là Invokable như ví dụ trên, nó cần được đăng ký vào khu vực factories, tuy nhiên cần có một lớp chuyên để khởi tạo gọi là Factory, lớp này triển khai từ giao diện Zend\ServiceManager\Factory\FactoryInterface, cần định nghĩa phương thức __invoke trả về đối tượng cần tạo. Ví dụ định nghĩa lớp ClassBFactory dùng để tạo đối tượng lớp ClassB như sau:

// src/ClassBFactory.php
<?php
declare(strict_types=1);

namespace SMExample;

use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;

class ClassBFactory implements FactoryInterface {

    public function __invoke(ContainerInterface $container, $requestedName, array $options = null) {
        // $requestedName - tên dịch vụ cần tạo (tham khảo tên này nếu cần)
        $serviceA = $container->get(ClassA::class);      // lấy dịch vụ từ SM
        $serviceB = new ClassB("Dịch vụ B", $serviceA);
        return $serviceB;
    }
}

Giờ có thể đăng ký vào SM ở khu vực factories như sau (test2.php):

<?php
    declare(strict_types=1);

    use SMExample\ClassA;
    use SMExample\ClassB;
    use SMExample\ClassBFactory;
    use Zend\ServiceManager\ServiceManager;

    require "vendor/autoload.php";


    $config = [
        'invokables'  => [
            ClassA::class =>  ClassA::class,
        ],
        'factories' => [
            ClassB::class =>  ClassBFactory::class
        ],
    ];

    $servicemanager = new ServiceManager($config);

    // Lấy dịch vụ từ Service Manager
    $sb = $servicemanager->get(ClassB::class);
    var_dump($sb);

Cũng cần lưu ý, có sẵn lớp tên là InvokableFactory, cho phép đưa lớp khởi tạo không tham số vào khu vực factories

<?php
    declare(strict_types=1);

    use SMExample\ClassA;
    use SMExample\ClassB;
    use SMExample\ClassBFactory;
    use Zend\ServiceManager\Factory\InvokableFactory;
    use Zend\ServiceManager\ServiceManager;

    require "vendor/autoload.php";


    $config = [
        'factories' => [
            ClassB::class =>  ClassBFactory::class,
            ClassA::class =>  InvokableFactory::class
        ],
    ];

    $servicemanager = new ServiceManager($config);
    // Lấy dịch vụ từ Service Manager
    $sb = $servicemanager->get(ClassB::class);
    var_dump($sb);
zf

Đăng ký dịch vụ với FactoryAbstract

Bạn có thể triên khai giao diện Zend\ServiceManager\Factory\AbstractFactoryInterface để xây dựng một lớp gọi là AbstractFactory, lớp triển khai này được dùng để đăng ký các dịch vụ, khai báo ở khu vực abstract_factories. Lớp này, khi nhận một tên truy cập (tên lớp), nó cho biết có tạo được dịch vụ không (phương thức canCreate) và thực hiện tạo dịch vụ theo tên đó (phương thức __invoke)

Ví dụ triển khai một lớp có tên MyAbstractFactory

<?php
    namespace SMExample;

    use Interop\Container\ContainerInterface;
    use Zend\ServiceManager\Factory\AbstractFactoryInterface;

    class MyAbstractFactory implements AbstractFactoryInterface
    {
        /**
         * Trả về true (tạo  được dịch vụ) nếu  tên truy  vấn  dịch  vụ trùng với tên lớp ClassA, ClassB
         */
        public function canCreate(ContainerInterface $container, $requestedName)
        {

            return in_array($requestedName,
              [
                  ClassA::class,
                  ClassB::class
              ]);
        }

        // Tạo dịch vụ theo tên lớp truy vấn $requestedName
        public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
        {
            $ob = null;
            switch ($requestedName) {
                case ClassA::class :
                    $ob = new ClassA();
                break;
                case ClassB::class :
                    $serviceA = $container->get(ClassA::class);
                    $ob = new ClassB("ClassB", $serviceA);
                break;
                default: break;
            }

            return $ob;

        }
    }

Áp dung: (test3.php)

<?php
    declare(strict_types=1);

    use SMExample\ClassA;
    use SMExample\ClassB;
    use SMExample\MyAbstractFactory;
    use Zend\ServiceManager\ServiceManager;

    require "vendor/autoload.php";


    $config = [
        'abstract_factories' => [
           MyAbstractFactory::class
        ],
    ];

    $servicemanager = new ServiceManager($config);
    // Lấy dịch vụ từ Service Manager
    $s1 = $servicemanager->get(ClassA::class);
    $s2 = $servicemanager->get(ClassB::class);
    var_dump($s1, $s2);

Như vậy chỉ cần đưa một lớp MyAbstractFactory::class đăng ký vào SM, thì truy vấn đến ClassA::class nó sẽ tạo ra dịch vụ tương tứng, truy vấn đến ClassB::class thì tạo dịch vụ lớp ClassB

Service Manager trong Zend Framework Skeleton

ServiceManager là một trung tâm quan trọng trong logic ứng dụng, mặc định Zend Skeleton Application được cấu hình để ServiceManager đưa vào trung tâm của nó các dịch vụ khởi tạo sẵn gồm, có thể lấy bằng $ServiceManager->get($Key):

Key - Tên dịch vụ Description
Application Dối tượng Zend\Mvc\Application
ApplicationConfig Mảng cấu hình application.config.php
Config Mảng cấu hình tổng hợp từ các file module.config.php, autoload/global.php, autoload/local.php.
EventManager Đối tượng Zend\EventManager\EventManager quản lý các sự kiện, các listener
SharedEventManager Đối tượng Zend\EventManager\SharedEventManager
ModuleManager Đối tượng lớp Zend\ModuleManager\ModuleManager, quản lý các module
Request Đối tượng Zend\Http\Request biểu diễn HTTP Request khách gửi đến
Response Đối tượng Zend\Http\Response biểu diễn HTTP response sẽ trả về cho khách.
Router Đối tượng Zend\Router\Http\TreeRouteStack, phân tích định tuyển URL
ServiceManager Chính là ServiceManager
ViewManager Đối tượng Zend\Mvc\View\Http\ViewManager biểu diễn layer của mô hình MVC, quản lý hệ thống Render HTML
ControllerManager Đối tượng Zend\Mvc\Controller\ControllerManager biểu diễn layer Controller của mô hình MVC, quả lý các Controller

Ngoài ra còn nhiều dịch vụ tiện ích khác. Nhưng nên nhớ ServiceManager lưu trữ một phiên bản duy nhất của từng đối tượng lớp.

Lấy Service Manager từ onBootstrap

Bạn có thể lấy SM của ứng dụng khung bằng cách thêm phương thức onBootstrap vào lớp Module của module nào bạn cần lấy tùy thích, ví dụng module Application mở file Module.php và thêm phương thức onBootstrap như sau:

public function onBootstrap(\Zend\Mvc\MvcEvent $e)
{
    //Lấy SM
    $serviceManager =  $e->getTarget()->getServiceManager();

    //Có $serviceManager sử dụng tùy tình huống, ví dụ
    //lấy mảng ApplicationConfig, danh sách các module

    $appConfig = $serviceManager->get('ApplicationConfig');
    $modules   = $appConfig['modules'];
    var_dump($modules);
}

Ngoài ra Service Manager còn lấy được thông qua quá trình Inject khi khởi tạo các loại dịch vụ, ví dụ Inject vào Controller, Inject vào View ... những kỹ thuật này sẽ trình bày sau khi học về Controller, View ...

Cấu hình các dịch vụ cho Service Manager của ZF Skeleton

Cấu hình đăng ký các dịch vụ vào SM của ứng dụng khung khai báo trong các file config (như trong module.config.php của Module nào đó, trong autoload/global.php, autoload/local.php ...). Mảng config của SM khai báo sau key service_manager

<?php
return [
    //..
    // Đăng ký dịch vụ dưới key này
    'service_manager' => [
        'services' => [
           // Đăng ký đối tượng dịch vụ
        ],
        'invokables' => [
           // Đăng ký các lớp dạng invokable
        ],
        'factories' => [
           // Đăng ký các lớp dạng factory
        ],
        'abstract_factories' => [
           // Đăng ký các lớp dạng abstract factory

        ],
        'aliases' => [
           // Khóa tắt cho tên các dịch vụ (tên tắt)

        ],
        'shared' => [
            // Các dịch vụ dạng non-shared (tạo mới mỗi lầ get)
        ]
    ],

    //...
];

Ví dụ: mở file module.config.php thêm vào nội dung:

//...
    'service_manager' => [
        'factories' => [
            \Application\MyModel\CircleApp::class => \Application\MyModel\CircleAppFactory::class
        ],

        'aliases' => [
            //gọi get(\Application\MyModel\CircleApp::class) tương đương get('Abc')
            'Abc' => \Application\MyModel\CircleApp::class
        ]
    ]
//...

Ngoài ra trong ứng dụng khung Zend Framework có nhiều lớp nhất là các PluginManager chính là lớp kế thừa Service Manager


Đăng ký nhận bài viết mới
Cấu hình ứng dụng (Bài trước)
(Bài tiếp) Plugin Manager