带有设计模式的迪斯科:依赖注入的全新视角

Dependency Injection is all about code reusability. It’s a design pattern aiming to make high-level code reusable, by separating the object creation / configuration from usage.

依赖注入与代码可重用性有关。 这是一种设计模式,旨在通过将对象的创建/配置与使用分开来使可重复使用的高级代码。

Illustration of people's outlines dancing in a disco

Consider the following code:

考虑以下代码:

<?php

class Test {

    protected $dbh;

    public function __construct(\PDO $dbh)
    {
        $this->dbh = $dbh;
    }

}

$dbh  = new PDO('mysql:host=localhost;dbname=test', 'username', 'password');
$test = new Test($dbh)

As you can see, instead of creating the PDO object inside the class, we create it outside of the class and pass it in as a dependency – via the constructor method. This way, we can use the driver of our choice, instead of having to to use the driver defined inside the class.

如您所见,我们没有在类内部创建PDO对象,而是在类外部创建了PDO对象,然后通过构造函数方法将其作为依赖项传入。 这样,我们可以使用我们选择的驱动程序,而不必使用在类中定义的驱动程序。

Our very own Alejandro Gervasio has explained the DI concept fantastically, and Fabien Potencier also covered it in a series.

我们自己的Alejandro Gervasio 出色地解释了DI概念Fabien Potencier也进行了一系列介绍

There’s one drawback to this pattern, though: when the number of dependencies grows, many objects need to be created/configured before being passed into the dependent objects. We can end up with a pile of boilerplate code, and a long queue of parameters in our constructor methods. Enter Dependency Injection containers!

但是,这种模式有一个缺点:当依赖项数量增加时,需要先创建/配置许多对象,然后再将它们传递给依赖项对象。 我们可以在构造函数方法中得到一堆样板代码和一长串参数。 输入依赖注入容器!

A Dependency Injection container – or simply a DI container – is an object which knows exactly how to create a service and handle its dependencies.

依赖注入容器(或简称为DI容器)是一个对象,它确切地知道如何创建服务并处理其依赖关系。

In this article, we’ll demonstrate the concept further with a newcomer in this field: Disco.

在本文中,我们将与该领域的新手Disco一起进一步演示该概念。

For more information on dependency injection containers, see our other posts on the topic here.

有关依赖注入容器的更多信息,请参见此处有关该主题的其他文章。

As frameworks are great examples of deploying DI containers, we will finish the article by creating a basic HTTP-based framework with the help of Disco and some Symfony Components.

由于框架是部署DI容器的绝佳示例,因此我们将在Disco和一些Symfony Components的帮助下通过创建基于HTTP的基本框架来结束本文。

安装 (Installation)

To install Disco, we use Composer as usual:

要安装Disco,我们照常使用Composer:

composer require bitexpert/disco

To test the code, we’ll use PHP’s built-in web server:

为了测试代码,我们将使用PHP的内置Web服务器

php -S localhost:8000 -t web

As a result, the application will be accessible under http://localhost:8000 from the browser. The last parameter -t option defines the document root – where the index.php file resides.

结果,可以从浏览器通过http://localhost:8000访问该应用程序。 最后一个参数-t选项定义文档根目录– index.php文件所在的位置。

入门 (Getting Started)

Disco is a container_interop compatible DI container. Somewhat controversially, Disco is an annotation-based DI container.

Disco是container_interop兼容的DI容器。 有争议的是,Disco是基于注释的DI容器。

Note that the package container_interop consists of a set of interfaces to standardize features of container objects. To learn more about how that works, see the tutorial in which we build our own, SitePoint Dependency Injection Container, also based on container-interop.

请注意,包container_interop由一组接口组成,这些接口用于标准化容器对象的功能。 要了解有关其工作原理的更多信息,请参阅我们在其中基于容器互操作构建自己的SitePoint依赖注入容器的教程

To add services to the container, we need to create a configuration class. This class should be marked with the @Configuration annotation:

为了向容器添加服务,我们需要创建一个配置类 。 此类应使用@Configuration批注标记:

<?php
/**
 * @Configuration
 */
 class Services {
    // ...
 }

Each container service should be defined as a public or protected method inside the configuration class. Disco calls each service a Bean, which originates from the Java culture.

每个容器服务都应在配置类内定义为公共受保护的方法。 Disco将每个服务都称为Bean ,它起源于Java文化。

Inside each method, we define how a service should be created. Each method must be marked with @Bean which implies that this a service, and @return annotations which notes the type of the returned object.

在每种方法中,我们定义了如何创建服务。 每个方法都必须@Bean标记(表示这是一项服务),并用@return注释(说明返回的对象的类型)进行标记。

This is a simple example of a Disco configuration class with one “Bean”:

这是带有一个“ Bean”的Disco配置类的简单示例:

<?php
/**
 * @Configuration
 */
class Configuration {

    /**
     * @Bean
     * @return SampleService
     */
    public function getSampleService()
    {
        // Instantiation
        $service  = new SampleService();

        // Configuration
        $service->setParameter('key', 'value');
        return $service; 
    }
}

The @Bean annotation accepts a few configuration parameters to specify the nature of a service. Whether it should be a singleton object, lazy loaded (if the object is resource-hungry), or even its state persisted during the session’s lifetime is specified by these parameters.

@Bean批注接受一些配置参数来指定服务的性质 。 这些参数指定它是否应该是单例对象,是否是延迟加载的 (如果对象很耗资源),甚至是在会话的生存期内保持不变的状态。

By default, all the services are defined as singleton services.

默认情况下,所有服务都定义为单例服务。

For example, the following Bean creates a singleton lazy-loaded service:

例如,以下Bean创建一个单例延迟加载服务:

<?php

// ...

/**
 * @Bean({"singleton"=true, "lazy"=true})
 * @return \Acme\SampleService
 */
 public function getSampleService()
 {
     return new SampleService();
 }

// ...

Disco uses ProxyManager to do the lazy-loading of the services. It also uses it to inject additional behaviors (defined by the annotations) into the methods of the configuration class.

Disco使用ProxyManager进行服务的延迟加载。 它还使用它将其他行为(由批注定义)注入到配置类的方法中。

After we create the configuration class, we need to create an instance of AnnotationBeanFactory, passing the configuration class to it. This will be our container.

创建配置类之后,我们需要创建AnnotationBeanFactory实例,并将配置类传递给它。 这将是我们的容器。

Finally, we register the container with BeanFactoryRegistry:

最后,我们使用BeanFactoryRegistry注册容器:

<?php

// ...

use \bitExpert\Disco\AnnotationBeanFactory;
use \bitExpert\Disco\BeanFactoryRegistry;

// ...

// Setting up the container
$container = new AnnotationBeanFactory(Services::class, $config);
BeanFactoryRegistry::register($container);

如何从容器中获得服务 (How to Get a Service from the Container)

Since Disco is container/interop compatible, we can use get() and has() methods on the container object:

由于Disco与container/interop兼容,因此我们可以在容器对象上使用get()has()方法:

// ...

$sampleService = $container->get('sampleService');
$sampleService->callSomeMethod();

服务范围 (Service Scope)

HTTP is a stateless protocol, meaning on each request the whole application is bootstrapped and all objects are recreated. We can, however, influence the lifetime of a service by passing the proper parameters to the @Bean annotation. One of these parameters is scope. The scope can be either request or session.

HTTP是一种无状态协议,这意味着在每个请求上引导整个应用程序,并重新创建所有对象。 但是,我们可以通过将适当的参数传递给@Bean批注来影响服务的寿命。 这些参数之一是scope 。 范围可以是requestsession

If the scope is session, the service state will persist during the session lifetime. In other words, on subsequent HTTP requests, the last state of the object is retrieved from the session.

如果范围是session ,则服务状态将在会话生存期内保持不变。 换句话说,在后续的HTTP请求中,将从会话中检索对象的最后状态。

Let’s clarify this with an example. Consider the following class:

让我们用一个例子来阐明这一点。 考虑以下类别:

<?php

class sample {

    public $counter = 0;

    public function add()
    {
        $this->counter++;
        return $this;
    } 
}

In the above class, the value of $counter is incremented each time the add() method is called; now, let’s add this to the container, with scope set to session:

在上面的类中,每次调用add()方法时, $counter的值都会增加; 现在,让我们将其添加到容器中,范围设置为session

// ...
/**
 * @Bean({"scope"="session"})
 * @return Sample
 */
public function getSample()
{
    return new Sample();
}
// ...

And if we use it like this:

如果我们这样使用它:

// ...
$sample = $container->get('getSample');
$sample->add()
       ->add()
       ->add();

echo $sample->counter; // output: 3
// ...

In the first run, the output will be three. If we run the script again (to make another request), the value will be six (instead of three). This is how object state is persisted across requests.

在第一次运行中,输出将为三。 如果我们再次运行脚本(发出另一个请求 ),则该值为6(而不是3)。 这就是对象状态在请求之间持久化的方式。

If the scope is set to request, the value will be always three in subsequent HTTP requests.

如果将范围设置为request ,则在后续的HTTP请求中,该值始终为3。

容器参数 (Container Parameters)

Containers usually accept parameters from the outside world. With Disco, we can pass the parameters into the container as an associative array like this:

容器通常接受来自外界的参数。 使用Disco,我们可以将参数作为关联数组传递到容器中,如下所示:

// ...
$parameters = [

    // Database configuration
    'database' => [        
        'dbms' => 'mysql',
        'host' => 'localhost',
        'user' => 'username',
        'pass' => 'password',
    ],
];

// Setting up the container
$container = new AnnotationBeanFactory(Services::class, $parameters);
BeanFactoryRegistry::register($container);

To use these values inside each method of the configuration class, we use @Parameters and @parameter annotations:

要在配置类的每个方法中使用这些值,我们使用@Parameters@parameter批注:

<?php
// ...

/**
 * @Bean
 * @Parameters({
 *    @parameter({"name"= "database"})
 * })
 *
*/
public function sampleService($database = null)
{
    // ...
}

迪斯科行动 (Disco in Action)

In this section, we’re going to create a basic HTTP-based framework. The framework will create a response based on the information received from the request.

在本节中,我们将创建一个基于HTTP的基本框架。 框架将基于从请求中收到的信息来创建响应

To build our framework’s core, we’ll use some Symfony Components.

为了构建框架的核心,我们将使用一些Symfony组件

HTTP Kernel

HTTP内核

The heart of our framework. Provides the request / response basics.

我们框架的核心。 提供请求/响应的基础知识。

Http Foundation

Http基金会

A nice object-oriented layer around PHP’s HTTP super globals.

PHP的HTTP超级全局变量周围的漂亮的面向对象层。

Router

路由器

According to the official website: maps an HTTP request to a set of configuration variables – more on this below.

根据官方网站的说法:将HTTP请求映射到一组配置变量-有关更多信息,请参见下文。

Event Dispatcher

事件调度程序

This library provides a way to hook into different phases of a request / response lifecycle, using listeners and subscribers.

该库提供了一种使用侦听器和订户连接到请求/响应生命周期的不同阶段的方法。

To install all these components:

要安装所有这些组件:

composer require symfony/http-foundation symfony/routing symfony/http-kernel symfony/event-dispatcher

As a convention, we’ll keep the framework’s code under the Framework namespace.

按照惯例,我们将框架的代码保留在Framework名称空间下。

Let’s also register a PSR-4 autoloader. To do this, we add the following namespace-to-path mapping under the psr-4 key in composer.json:

我们还要注册一个PSR-4自动装带器 。 为此,我们在composer.jsonpsr-4键下添加以下名称空间到路径的映射:

// ...
 "autoload": {
        "psr-4": {
            "": "src/"
        }
    }
// ...

As a result, all namespaces will be looked for within the src/ directory. Now, we run composer dump-autoload for this change to take effect.

结果,将在src/目录中查找所有名称空间。 现在,我们运行composer dump-autoload来使更改生效。

Throughout the rest of the article, we’ll write our framework’s code along with code snippets to make some concepts clear.

在本文的其余部分中,我们将编写框架代码以及代码片段,以使一些概念清晰明了。

内核 (The Kernel)

The foundation of any framework is its kernel. This is where a request is processed into a response.

任何框架的基础都是其内核。 这是将请求处理为响应的地方

We’re not going to create a Kernel from scratch here. Instead, we’ll extend the Kernel class of the HttpKernel component we just installed.

这里我们不会从头开始创建内核。 相反,我们将扩展刚刚安装的HttpKernel组件的Kernel类。

<?php
// src/Framework/Kernel.php

namespace Framework;

use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class Kernel extends HttpKernel implements HttpKernelInterface {
}

Since the base implementation works just fine for us, we won’t reimplement any methods, and will instead just rely on the inherited implementation.

由于基本实现对我们来说很好,因此我们不会重新实现任何方法,而是仅依赖继承的实现。

路由 (Routing)

A Route object contains a path and a callback, which is called (by the Controller Resolver) every time the route is matched (by the URL Matcher).

Route对象包含一个路径和一个回调 ,每次路由(由URL Matcher )匹配时,就会调用该回调 (由Controller Resolver调用)。

The URL matcher is a class which accepts a collection of routes (we’ll discuss this shortly) and an instance of RequestContext to find the active route.

URL匹配器是一个类,它接受路由集合(我们将在稍后讨论)和一个RequestContext实例以查找活动路由。

A request context object contains information about the current request.

请求上下文对象包含有关当前请求的信息。

Here’s how routes are defined by using the Routing component:

使用路由组件定义路由的方法如下:

<?php

// ...

use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$routes = new RouteCollection();

$routes->add('route_alias', new Route('path/to/match', ['_controller' => function(){
    // Do something here...
}]
));

To create routes, we need to create an instance of RouteCollection (which is also a part of the Routing component), then add our routes to it.

要创建路由,我们需要创建RouteCollection的实例(它也是Routing组件的一部分),然后向其中添加路由。

To make the routing syntax more expressive, we’ll create a route builder class around RouteCollection.

为了使路由语法更具表现力,我们将围绕RouteCollection创建一个路由构建器类。

<?php
// src/Framework/RouteBuilder.php

namespace Framework;

use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

class RouteBuilder {

    protected $routes;

    public function __construct(RouteCollection $routes)
    {
        $this->routes = $routes;
    }

    public function get($name, $path, $controller)
    {
        return $this->add($name, $path, $controller, 'GET');
    }

    public function post($name, $path, $controller)
    {
        return $this->add($name, $path, $controller, 'POST');
    }

    public function put($name, $path, $controller)
    {
        return $this->add($name, $path, $controller, 'PUT');
    }

    public function delete($name, $path, $controller)
    {
        return $this->add($name, $path, $controller, 'DELETE');
    }

    protected function add($name, $path, $controller, $method)
    {
        $this->routes->add($name, new Route($path, ['_controller' => $controller], ['_method' => $method]));

        return $this;
    }

}

This class holds an instance of RouteCollection. In RouteBuilder, for each HTTP verb, there is a method which calls add(). We’ll keep our route definitions in the src/routes.php file:

此类包含RouteCollection一个实例。 在RouteBuilder ,对于每个HTTP动词,都有一个方法调用add() 。 我们将路由定义保存在src/routes.php文件中:

<?php
// src/routes.php

use Symfony\Component\Routing\RouteCollection;
use Framework\RouteBuilder;

$routeBuilder = new RouteBuilder(new RouteCollection());

$routeBuilder

->get('home', '/', function() {
            return new Response('It Works!');
})

->get('welcome', '/welcome', function() {
            return new Response('Welcome!');
});

前端控制器 (The Front Controller)

The entry point of any modern web application is its front controller. It is a PHP file, usually named index.php. This is where the class autoloader is included, and the application is bootstrapped.

任何现代Web应用程序的入口点都是其前端控制器。 它是一个PHP文件,通常名为index.php 。 这是包含类自动装入器的位置,并且应用程序已引导。

All the requests go through this file, and are from here dispatched to the proper controllers. Since this is the only file we’re going to expose to the public, we put it inside our web root directory, keeping the rest of the code outside.

所有请求都通过该文件,并从此处分派到适当的控制器。 由于这是我们要向公众公开的唯一文件,因此我们将其放在我们的Web根目录中,并将其余代码保留在外部。

<?php
//web/index.php

require_once __DIR__ . '/../vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;

// Create a request object from PHP's global variables
$request = Request::createFromGlobals();

$routes = include __DIR__.'/../src/routes.php';
$UrlMatcher = new Routing\Matcher\UrlMatcher($routes, new Routing\RequestContext());

// Event dispatcher & subscribers
$dispatcher = new EventDispatcher();
// Add a subscriber for matching the correct route. We pass UrlMatcher to this class
$dispatcher->addSubscriber(new RouterListener($UrlMatcher, new RequestStack()));


$kernel = new Framework\Kernel($dispatcher, new ControllerResolver());
$response  = $kernel->handle($request);

// Sending the response
$response->send();

In the above code, we instantiate a Request object based on PHP’s global variables.

在上面的代码中,我们基于PHP的全局变量实例化一个Request对象。

<?php
// ...
$request = Request::createFromGlobals();
// ...

Next, we load the routes.php file into $routes. Detecting the right route is the responsibility of the UrlMatcher class, so we create it, passing the route collection along with a RequestContext object.

接下来,我们将routes.php文件加载到$routes 。 检测正确的路由是UrlMatcher类的职责,因此我们创建了该路由,并将路由集合与RequestContext对象一起传递。

<?php
// ...
$routes = include __DIR__.'/../src/routes.php';
$UrlMatcher = new Routing\Matcher\UrlMatcher($routes, new Routing\RequestContext());
// ...

To use the UrlMatcher instance, we pass it to the RouteListener event subscriber.

要使用UrlMatcher实例,我们将其传递给RouteListener事件订阅者。

<?php
// ...
// Event dispatcher & subscribers
$dispatcher = new EventDispatcher();
// Add a subscriber for matching the correct route. We pass UrlMatcher to this class
$dispatcher->addSubscriber(new RouterListener($UrlMatcher, new RequestStack()));
// ...

Any time a request hits the application, the event is triggered and the respective listener is called, which in turn detects the proper route by using the UrlMatcher passed to it.

每当请求到达应用程序时,都会触发该事件并调用相应的侦听器,后者随后使用传递给它的UrlMatcher来检测正确的路由。

Finally, we instantiate the kernel, passing in the Dispatcher and an instance of Controller Resolver – via its constructor:

最后,我们实例化内核,并通过其构造函数传入DispatcherController Resolver实例:

<?php
// ...

$kernel    = new Framework\Kernel($dispatcher, new ControllerResolver());
$response  = $kernel->handle($request);

// Sending the response
$response->send();
// ...

迪斯科时间 (Disco Time)

So far we had to do plenty of instantiations (and configurations) in the front controller, from creating the request context object, the URL matcher, the event dispatcher and its subscribers, and of course the kernel itself.

到目前为止,我们必须在前端控制器中进行大量实例化(和配置),从创建请求上下文对象,URL匹配器,事件分派器及其订阅者,当然还有内核本身。

It is now time to let Disco wire all these pieces together for us.

现在是时候让Disco将所有这些零件连接在一起。

As before, we install it using Composer:

和以前一样,我们使用Composer安装它:

composer require bitexpert/Disco;

Then, we create the configuration class, and define the services we’ll need in the front controller:

然后,我们创建配置类,并在前端控制器中定义我们需要的服务:

<?php
// src/Framework/Services.php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;
use bitExpert\Disco\Annotations\Parameters;
use bitExpert\Disco\Annotations\Parameter;

/**
 * @Configuration
 */
class Services {

    /**
     * @Bean
     * @return \Symfony\Component\Routing\RequestContext 
     */
    public function context()
    {
        return new \Symfony\Component\Routing\RequestContext();
    }

    /**
     * @Bean
     *
     * @return \Symfony\Component\Routing\Matcher\UrlMatcher
     */
    public function matcher()
    {
        return new \Symfony\Component\Routing\Matcher\UrlMatcher($this->routeCollection(), $this->context());
    }

    /**
     * @Bean
     * @return \Symfony\Component\HttpFoundation\RequestStack
     */
    public function requestStack()
    {
        return new \Symfony\Component\HttpFoundation\RequestStack();
    }

    /**
     * @Bean
     * @return \Symfony\Component\Routing\RouteCollection
     */
    public function routeCollection()
    {
        return new \Symfony\Component\Routing\RouteCollection();
    }

    /**
     * @Bean
     * @return \Framework\RouteBuilder
     */
    public function routeBuilder()
    {
        return new \Framework\RouteBuilder($this->routeCollection());
    }

    /**
     * @Bean
     * @return \Symfony\Component\HttpKernel\Controller\ControllerResolver
     */
    public function resolver()
    {
        return new \Symfony\Component\HttpKernel\Controller\ControllerResolver();
    }


    /**
     * @Bean
     * @return \Symfony\Component\HttpKernel\EventListener\RouterListener
     */
    protected function listenerRouter()
    {
        return new \Symfony\Component\HttpKernel\EventListener\RouterListener(
            $this->matcher(),
            $this->requestStack()
        );
    }

    /**
     * @Bean
     * @return \Symfony\Component\EventDispatcher\EventDispatcher
     */
    public function dispatcher()
    {
        $dispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher();

        $dispatcher->addSubscriber($this->listenerRouter());

        return $dispatcher;
    }

    /**
     * @Bean
     * @return Kernel
     */
    public function framework()
    {
        return new Kernel($this->dispatcher(), $this->resolver());
    }

}

Seems like a lot of code; but in fact, it’s the same code that resided in the front controller previously.

似乎很多代码; 但是实际上,它与前面的前端控制器中的代码相同。

Before using the class, we need to make sure it has been autoloaded by adding it under the files key in our composer.json file:

在使用该类之前,我们需要通过将其添加到composer.json文件中的files键下来确保已自动加载该类:

// ...
 "autoload": {
        "psr-4": {
            "": "src/"
        },
        "files": [
            "src/Services.php"
        ]
    }
// ...

And now onto our front controller.

现在到我们的前端控制器。

<?php

//web/index.php

require_once __DIR__ . '/../vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;

$request   = Request::createFromGlobals();

$container = new \bitExpert\Disco\AnnotationBeanFactory(Services::class);
\bitExpert\Disco\BeanFactoryRegistry::register($container);

$routes   = include __DIR__.'/../src/routes.php';

$kernel   = $container->get('framework')
$response = $kernel->handle($request);
$response->send();

Now our front controller can actually breathe! All the instantiations are done by Disco when we request a service.

现在我们的前端控制器实际上可以呼吸了! 当我们请求服务时,所有实例化都由Disco完成。

但是配置如何? (But How About the Configuration?)

As explained earlier, we can pass in parameters as an associative array to the AnnotationBeanFactory class.

如前所述,我们可以将参数作为关联数组传递给AnnotationBeanFactory类。

To manage configuration in our framework, we create two configuration files, one for development and one for the production environment.

为了在我们的框架中管理配置,我们创建了两个配置文件,一个用于开发 ,一个用于生产环境。

Each file returns an associative array, which we can be loaded into a variable.

每个文件都返回一个关联数组,我们可以将其加载到变量中。

Let’s keep them inside Config directory:

让我们将它们保留在Config目录中:

// Config/dev.php

return [
    'debug' => true;
];

And for production:

并用于生产:

// Config/prod.php

return [
    'debug' => false;
];

To detect the environment, we’ll specify the environment in a special plain-text file, just like we define an environment variable:

为了检测环境,我们将在特殊的纯文本文件中指定环境,就像我们定义环境变量一样:

ENV=dev

To parse the file, we use PHP dotenv, a package which loads environment variables from a file (by default the filename is .env) into PHP’s $_ENV super global. This means we can get the values by using PHP’s getenv() function.

为了解析文件,我们使用PHP dotenv ,这是一个将文件中的环境变量(默认文件名为.env ) .env到PHP的$_ENV超全局变量中的软件包。 这意味着我们可以使用PHP的getenv()函数获取值。

To install the package:

要安装软件包:

composer require vlucas/phpdotenv

Next, we create our .env file inside the Config/ directory.

接下来,我们在Config/目录中创建.env文件。

Config/.env

配置/.env

ENV=dev

In the front controller, we load the environment variables using PHP dotenv:

在前端控制器中,我们使用PHP dotenv加载环境变量:

<?php
//web/index.php

// ...

// Loading environment variables stored .env into $_ENV
$dotenv = new Dotenv\Dotenv(__DIR__ . '/../Config');
$dotenv->load();

// Load the proper configuration file based on the environment
$parameters = require __DIR__ . '/../config/' . getenv('ENV') . '.php';

$container = new \bitExpert\Disco\AnnotationBeanFactory(Services::class, $parameters);       \bitExpert\Disco\BeanFactoryRegistry::register($container);

// ...

In the preceding code, we first specify the directory in which our .env file resides, then we call load() to load the environment variables into $_ENV. Finally, we use getenv() to get the proper configuration filename.

在前面的代码中,我们首先指定.env文件所在的目录,然后调用load()将环境变量加载到$_ENV 。 最后,我们使用getenv()获取正确的配置文件名。

创建一个容器生成器 (Creating a Container Builder)

There’s still one problem with the code in its current state: whenever we want to create a new application we have to instantiate AnnotationBeanFactory in our front controller (index.php). As a solution, we can create a factory which creates the container, whenever needed.

当前状态下的代码仍然存在一个问题:每当我们要创建一个新应用程序时,都必须在前端控制器( index.php )中实例化AnnotationBeanFactory 。 作为解决方案,我们可以创建一个工厂,该工厂在需要时创建容器。

<?php
// src/Factory.php

namespace Framework;

class Factory {

    /**
     * Create an instance of Disco container
     *
     * @param  array $parameters
     * @return \bitExpert\Disco\AnnotationBeanFactory
     */
    public static function buildContainer($parameters = [])
    {
        $container = new \bitExpert\Disco\AnnotationBeanFactory(Services::class, $parameters);
        \bitExpert\Disco\BeanFactoryRegistry::register($container);

        return $container;
    }

}

This factory has a static method named buildContainer(), which creates and registers a Disco container.

该工厂有一个名为buildContainer()的静态方法,该方法创建并注册一个Disco容器。

This is how it improves our front controller:

这就是改进我们的前端控制器的方式:

<?php
//web/index.php

require_once __DIR__ . '/../vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;

// Getting the environment
$dotenv = new Dotenv\Dotenv(__DIR__ . '/../config');
$dotenv->load();

// Load the proper configuration file based on the environment
$parameters = require __DIR__ . '/../config/' . getenv('ENV') . '.php';

$request   = Request::createFromGlobals();
$container = Framework\Factory::buildContainer($parameters);
$routes    = include __DIR__.'/../src/routes.php';

$kernel   = $container->get('framework')
$response = $kernel->handle($request);
$response->send();

It looks much neater now, doesn’t it?

现在看起来更整洁了,不是吗?

应用类别 (Application Class)

We can take things one step further in terms of usability, and abstract the remaining operations (in the front controller) into another class. Let’s call this class Application:

就可用性而言,我们可以使事情更进一步,并将剩余的操作(在前端控制器中)抽象到另一个类中。 我们将此类称为Application

<?php

namespace Framework;

use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\Request;

class Application {

    protected $kernel;

    public function __construct(HttpKernelInterface $kernel)
    {
        $this->kernel = $kernel;
    }

    public function run()
    {
        $request = Request::createFromGlobals();

        $response = $this->kernel->handle($request);
        $response->send();
    }
}

Application is dependent on the kernel, and works as a wrapper around it. We create a method named run(), which populates the request object, and passes it to the kernel to get the response.

Application依赖于内核,并充当内核的包装器。 我们创建了一个名为run()的方法,该方法填充了请求对象,并将其传递给内核以获取响应。

To make it even cooler, let’s add this class to the container as well:

为了使其更加凉爽,我们也将此类添加到容器中:

<?php

// src/Framework/Services.php

// ...

    /**
     * @Bean
     * @return \Framework\Application
     */
    public function application()
    {
        return new \Framework\Application($this->kernel());
    }

// ...

And this is the new look of our front controller:

这是我们的前端控制器的新外观:

<?php

require_once __DIR__ . '/../vendor/autoload.php';

// Getting the environment
$dotenv = new Dotenv\Dotenv(__DIR__ . '/../config');
$dotenv->load();

// Load the proper configuration file based on the environment
$parameters = require __DIR__ . '/../config/' . getenv('ENV') . '.php';

// Build a Disco container using the Factory class
$container = Framework\Factory::buildContainer($parameters);

// Including the routes
require __DIR__ . '/../src/routes.php';

// Running the application to handle the response
$app = $container->get('application')
          ->run();

创建响应侦听器 (Creating a Response Listener)

We can use the framework now, but there is still room for improvement. Currently, we have to return an instance of Response in each controller, otherwise, an exception is thrown by the Kernel:

我们现在可以使用该框架,但是仍有改进的空间。 当前,我们必须在每个控制器中返回Response实例 ,否则,内核将引发异常:

<?php

// ...

$routeBuilder

->get('home', '/', function() {
            return new Response('It Works!');
});

->get('welcome', '/welcome', function() {
            return new Response('Welcome!');
});

// ...

However, we can make it optional and allow for sending back pure strings, too. To do this, we create a special subscriber class, which automatically creates a Response object if the returned value is a string.

但是,我们可以将其设为可选,并允许发送回纯字符串。 为此,我们创建一个特殊的订户类,如果返回的值是一个字符串,它将自动创建一个Response对象。

Subscribers must implement the Symfony\Component\EventDispatcher\EventSubscriberInterface interface. They should implement the getSubscribedMethods() method in which we define the events we’re interested in subscribing to, and their event listeners.

订阅者必须实现Symfony\Component\EventDispatcher\EventSubscriberInterface接口。 他们应该实现getSubscribedMethods()方法,在其中定义我们感兴趣的订阅事件及其事件侦听器。

In our case, we’re interested in the KernelEvents::VIEW event. The event happens when a response is to be returned.

在我们的例子中,我们对KernelEvents::VIEW事件感兴趣。 该事件在返回响应时发生。

Here’s our subscriber class:

这是我们的订户类:

<?php
// src/Framework/StringResponseListener
namespace Framework;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class StringResponseListener implements EventSubscriberInterface
{
    public function onView(GetResponseForControllerResultEvent $event)
    {
        $response = $event->getControllerResult();

        if (is_string($response)) {
            $event->setResponse(new Response($response));
        }
    }

    public static function getSubscribedEvents()
    {
        return array(KernelEvents::VIEW => 'onView');
    }
}

Inside the listener method onView, we first check if the response is a string (and not already a Response object), then create a response object if required.

在侦听器方法onView ,我们首先检查响应是否为字符串(并且还不是Response对象),然后根据需要创建响应对象。

To use the subscriber, we need to add it to the container as a protected service:

要使用订阅服务器,我们需要将其作为受保护的服务添加到容器中:

<?php

// ...

/**
 * @Bean
 * @return \Framework\StringResponseListener
 */
protected function ListenerStringResponse()
{
    return new \Framework\StringResponseListener();
}

// ...

Then, we add it to the dispatcher service:

然后,将其添加到调度程序服务中:

<?php

// ...

/**
 * @Bean
 * @return \Symfony\Component\EventDispatcher\EventDispatcher
 */
public function dispatcher()
{
    $dispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher();

    $dispatcher->addSubscriber($this->listenerRouter());
    $dispatcher->addSubscriber($this->ListenerStringResponse());

    return $dispatcher;
}

// ...

From now on, we can simply return a string in our controller functions:

从现在开始,我们可以简单地在控制器函数中返回一个字符串:

<?php

// ...

$routeBuilder

->get('home', '/', function() {
            return 'It Works!';
})

->get('welcome', '/welcome', function() {
            return 'Welcome!';
});

// ...

The framework is ready now.

该框架现已准备就绪。

结论 (Conclusion)

We created a basic HTTP-based framework with the help of Symfony Components and Disco. This is just a basic Request/Response framework, lacking any other MVC concepts like models and views, but allows for the implementation of any additional architectural patterns we may desire.

我们在Symfony Components和Disco的帮助下创建了一个基于HTTP的基本框架。 这只是一个基本的请求/响应框架,缺少模型和视图之类的其他MVC概念,但是允许实现我们可能需要的任何其他架构模式。

The full code is available on Github.

完整的代码可以在Github上找到

Disco is a newcomer to the DI-container game and if compared to the older ones, it lacks a comprehensive documentation. This article was an attempt at providing a smooth start for those who might find this new kind of DI container interesting.

迪斯科是DI容器游戏的新成员,与旧版本相比,它缺乏全面​​的文档资料。 本文旨在为可能会发现这种新型DI容器有趣的人提供一个平稳的开始。

Do you glue together your app’s components with DI containers? If so, which ones? Have you given Disco a try? Let us know!

您是否将应用程序的组件与DI容器粘合在一起? 如果是这样,哪个? 你有试过迪斯科吗? 让我们知道!

翻译自: https://www.sitepoint.com/disco-with-frameworks-and-design-patterns-a-fresh-look-at-dependency-injection/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值