使用Symfony组件构建自己PHP框架

You’ve probably met Symfony in your PHP career – or have at least heard of it. What you may not know is that Symfony is, at its core, composed of separate libraries called components, which can be reused in any PHP application.

您可能在您PHP职业生涯中认识过Symfony,或者至少听说过它。 您可能不知道的是,Symfony的核心是由称为components的独立库组成 ,可以在任何PHP应用程序中重用。

For example, the popular PHP framework Laravel was developed using several Symfony components we will also be using in this tutorial. The next version of the popular CMS Drupal is also being built on top of some of the main Symfony components.

例如,流行PHP框架Laravel是使用几个Symfony组件开发的,我们还将在本教程中使用它。 流行的CMS Drupal的下一版本也在一些主要的Symfony组件的基础上构建。

We’ll see how to build a minimal PHP framework using these components, and how they can interact to create a basic structure for any web application.

我们将看到如何使用这些组件构建最小PHP框架,以及它们如何进行交互以为任何Web应用程序创建基本结构。

alt

Note: This tutorial won’t cover every Symfony component and every feature of each one. We’ll see only the main things that we need to build a minimal functional framework. If you want to go deeper into Symfony components, I encourage you to read their excellent documentation.

注意:本教程不会涵盖每个 Symfony组件以及每个组件的每个功能。 我们将仅看到构建最小功能框架所需的主要内容。 如果您想更深入地研究Symfony组件,建议您阅读其出色的文档

创建项目 (Creating the project)

We’ll start from scratch with a simple index.php file at the root of our project directory, and use Composer to install the dependencies.

我们将从头开始,在项目目录的根目录下使用一个简单的index.php文件,并使用Composer安装依赖项。

For now, our file will only contain this simple piece of code:

现在,我们的文件将只包含以下简单代码:

switch($_SERVER['PATH_INFO']) {
        case '/':
            echo 'This is the home page';
            break;
        case '/about':
            echo 'This is the about page';
            break;   
        default:
            echo 'Not found!';
    }

This code just maps the requested URL (contained in $_SERVER['PATH_INFO']) to the right echo instruction. It’s a very, very primitive router.

此代码仅将请求的URL(包含在$_SERVER['PATH_INFO'] )映射到正确的echo指令。 这是一个非常非常原始的路由器。

HttpFoundation组件 (The HttpFoundation component)

HttpFoundation acts as a top-level layer for dealing with the HTTP flow. Its most important entrypoints are the two classes Request and Response.

HttpFoundation充当处理HTTP流的顶层。 它最重要的入口点是RequestResponse这两个类。

Request allows us to deal with the HTTP request information such as the requested URI or the client headers, abstracting default PHP globals ($_GET, $_POST, etc.). Response is used to send back response HTTP headers and data to the client, instead of using header or echo as we would in “classic” PHP.

Request允许我们处理HTTP请求信息,例如请求的URI或客户端标头,抽象出默认PHP全局变量( $_GET$_POST等)。 Response用于将响应HTTP标头和数据发送回客户端,而不是像在“经典” PHP中那样使用headerecho

Install it using composer :

使用composer安装它:

php composer.phar require symfony/http-foundation 2.5.*

This will place the library into the vendor directory. Now put the following into the index.php file:

这会将库放入vendor目录。 现在将以下内容放入index.php文件:

// Initializes the autoloader generated by composer
    $loader = require 'vendor/autoload.php';
    $loader->register();

    use Symfony\Component\HttpFoundation\Request;
    
    $request = Request::createFromGlobals();
    
    switch($request->getPathInfo()) {
        case '/':
            echo 'This is the home page';
            break;
        case '/about':
            echo 'This is the about page';
            break;   
        default:
            echo 'Not found!';
    }

What we did here is pretty straightforward:

我们在这里所做的非常简单:

  • Create a Request instance using the createFromGlobals static method. Instead of creating an empty object, this method populates a Request object using the current request information.

    使用createFromGlobals静态方法创建一个Request实例。 该方法不是创建一个空对象,而是使用当前请求信息填充一个Request对象。

  • Test the value returned by the getPathInfo method.

    测试getPathInfo方法返回的值。

We can also replace the different echo commands by using a Response instance to hold our content, and send it to the client (which basically outputs the response headers and content).

我们还可以使用Response实例替换不同的echo命令,以保存我们的内容,并将其send给客户端(基本上输出响应标头和内容)。

$loader = require 'vendor/autoload.php';
    $loader->register();

    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    
    $request = Request::createFromGlobals();
    $response = new Response();
    
    switch ($request->getPathInfo()) {
    case '/':
    	$response->setContent('This is the website home');
    	break;
    
    	case '/about':
    		$response->setContent('This is the about page');
    		break;
    
    	default:
    		$response->setContent('Not found !');
    	$response->setStatusCode(Response::HTTP_NOT_FOUND);
    }
    
    $response->send();

使用HttpKernel包装框架核心 (Use HttpKernel to wrap the framework core)

php composer.phar require symfony/http-kernel 2.5.*

For now, as simple as it is, the framework logic is still located in our front controller, the index.php file. If we wanted to add more code, it would be better to wrap it into another class, which would become the “core” of our framework.

就目前而言,尽管如此简单,但框架逻辑仍位于我们的前端控制器index.php文件中。 如果我们想添加更多的代码,最好将其包装到另一个类中,这将成为我们框架的“核心”。

The HttpKernel component was conceived with that goal in mind. It is intended to work with HttpFoundation to convert the Request instance to a Response one, and provides several classes for us to achieve this. The only one we will use, for the moment, is the HttpKernelInterface interface. This interface defines only one method: handle.

HttpKernel组件的构想就是为了实现这一目标。 它旨在与HttpFoundation一起将Request实例转换为Response实例,并为我们提供了几个类来实现此目的。 目前,我们唯一使用的是HttpKernelInterface接口。 该接口仅定义一种方法: handle

This method takes a Request instance as an argument, and is supposed to return a Response. So, each class implementing this interface is able to process a Request and return the appropriate Response object.

此方法将Request实例作为参数,并应返回Response 。 因此,实现此接口的每个类都可以处理Request并返回适当的Response对象。

Let’s create the class Core of our framework that implements the HttpKernelInterface. Now create the Core.php file under the lib/Framework directory:

让我们创建实现HttpKernelInterface框架类Core 。 现在在lib/Framework目录下创建Core.php文件:

<?php
    
    namespace Framework;

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

    class Core implements HttpKernelInterface
    {
    	public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
    	{
    		switch ($request->getPathInfo()) {
    			case '/':
    				$response = new Response('This is the website home');
    				break;
    
    			case '/about':
    				$response = new Response('This is the about page');
    				break;
    
    			default:
    				$response = new Response('Not found !', Response::HTTP_NOT_FOUND);
    		}
    
    		return $response;
    	}
    }

Note: The handle method takes two more optional arguments: the request type, and a boolean indicating if the kernel should throw an exception in case of error. We won’t use them in this tutorial, but we need to implement the exact same method defined by HttpKernelInterface, otherwise PHP will throw an error.

注意: handle方法带有两个可选参数:请求类型和一个布尔值,指示在发生错误时内核是否应引发异常。 在本教程中我们不会使用它们,但是我们需要实现与HttpKernelInterface定义的方法完全相同的方法,否则PHP将抛出错误。

The only thing we did here is move the existing code into the handle method. Now we can get rid of this code in index.php and use our freshly created class instead:

我们在这里所做的唯一一件事就是将现有代码移到handle方法中。 现在,我们可以在index.php删除此代码,而使用我们新创建的类:

require 'lib/Framework/Core.php';
  
    $request = Request::createFromGlobals();
    
    // Our framework is now handling itself the request
    $app = new Framework\Core();
    
    $response = $app->handle($request);
    $response->send();

更好的路由系统 (A better routing system)

There is still a problem with our class: it is holding the routing logic of our application. If we wanted to add more URLs to match, we would have to modify the code inside our framework – which is clearly not a good idea. Moreover, this would mean adding a case block for each new route. No, we definitely don’t want to go down that dirty road.

我们的类仍然有一个问题:它持有我们应用程序的路由逻辑。 如果我们想添加更多的URL进行匹配,则必须在框架内修改代码-这显然不是一个好主意。 此外,这将意味着增加一个case块为每一个新的路线。 不,我们绝对不想走这条肮脏的路。

The solution is to add a routing system to our framework. We can do this by creating a map method that binds a URI to a PHP callback that will be executed if the right URI is matched:

解决方案是在我们的框架中添加一个路由系统。 为此,我们可以创建一个map方法,将URI绑定到PHP回调,如果正确的URI被匹配,该回调将被执行:

class Core implements HttpKernelInterface
	{
		protected $routes = array();
	
		public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
		{
			$path = $request->getPathInfo();
			
			// Does this URL match a route?
			if (array_key_exists($path, $this->routes)) {
			    // execute the callback
				$controller = $routes[$path];
				$response = $controller();
			} else {
			    // no route matched, this is a not found.
				$response = new Response('Not found!', Response::HTTP_NOT_FOUND);
			}
	
			return $response;
		}
		
		// Associates an URL with a callback function
		public function map($path, $controller) {
			$this->routes[$path] = $controller;
		}
	}

Now application routes can be set directly in the front controller:

现在可以直接在前端控制器中设置应用程序路由:

$app->map('/', function () {
		return new Response('This is the home page');
	});
	
	$app->map('/about', function () {
		return new Response('This is the about page');
	});
	
	$response = $app->handle($request);

This tiny routing system is working well, but it has major flaws: what if we wanted to match dynamic URLs that hold parameters? We could imagine a URL like posts/:id where :id is a variable parameter that could map to a post ID in a database.

这个小型的路由系统运行良好,但存在主要缺陷:如果我们想匹配包含参数的动态URL,该怎么办? 我们可以想象一个像posts/:id这样的URL,其中:id是一个可变参数,可以映射到数据库中的post ID。

We need a more flexible and powerful system: that’s why we’ll use the Symfony Routing component.

我们需要一个更加灵活和强大的系统:这就是为什么我们将使用Symfony Routing组件。

php composer.phar require symfony/routing 2.5.*

Using the Routing component allows us to load Route objects into a UrlMatcher that will map the requested URI to a matching route. This Route object can contain any attributes that can help us execute the right part of the application. In our case, such an object will contain the PHP callback to execute if the route matches. Also, any dynamic parameters contained in the URL will be present in the route attributes.

使用路由组件使我们能够将Route对象加载到UrlMatcher ,该UrlMatcher会将请求的URI映射到匹配的路由。 这个Route对象可以包含任何可以帮助我们执行应用程序正确部分的属性。 在我们的例子中,如果路由匹配,则此类对象将包含要执行PHP回调。 同样,URL中包含的任何动态参数都将出现在路由属性中。

In order to implement this, we need to do the following changes:

为了实现这一点,我们需要进行以下更改:

  • Replace the routes array with a RouteCollection instance to hold our routes.

    更换routes与阵列RouteCollection实例来保存我们的路线。

  • Change the map method so it registers a Route instance into this collection.

    更改map方法,以便将Route实例注册到此集合中。

  • Create a UrlMatcher instance and tell it how to match its routes against the requested URI by providing a context to it, using a RequestContext instance.

    创建UrlMatcher实例,并告诉它如何提供有利的环境给它,使用,以配合其对请求的URI路径RequestContext实例。

use Symfony\Component\Routing\Matcher\UrlMatcher;
		use Symfony\Component\Routing\RequestContext;
		use Symfony\Component\Routing\RouteCollection;
		use Symfony\Component\Routing\Route;
		use
		Symfony\Component\Routing\Exception\ResourceNotFoundException;

		class Core implements HttpKernelInterface
		{
			/** @var RouteCollection */
			protected $routes;

			public function __construct()
			{
				$this->routes = new RouteCollection();
			}
		
			public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
			{
			    // create a context using the current request
				$context = new RequestContext();
				$context->fromRequest($request);
				
				$matcher = new UrlMatcher($this->routes, $context);
		
				try {
					$attributes = $matcher->match($request->getPathInfo());
					$controller = $attributes['controller'];
					$response = $controller();
				} catch (ResourceNotFoundException $e) {
					$response = new Response('Not found!', Response::HTTP_NOT_FOUND);
				}
		
				return $response;
			}
		
			public function map($path, $controller) {
				$this->routes->add($path, new Route(
					$path,
					array('controller' => $controller)
				));
			}
		}

The match method tries to match the URL against a known route pattern, and returns the corresponding route attributes in case of success. Otherwise it throws a ResourceNotFoundException that we can catch to display a 404 page.

match方法尝试将URL与已知的路由模式进行匹配,并在成功的情况下返回相应的路由属性。 否则,它将引发ResourceNotFoundException ,我们可以捕获该异常来显示404页面。

We can now take advantage of the Routing component to retrieve any URL parameters. After getting rid of the controller attribute, we can call our callback function by passing other parameters as arguments (using the call_user_func_array function):

现在,我们可以利用路由组件来检索所有URL参数。 除去controller属性后,我们可以通过传递其他参数作为参数来调用回调函数(使用call_user_func_array函数):

try {
			$attributes = $matcher->match($request->getPathInfo());
			$controller = $attributes['controller'];
			unset($attributes['controller']);
			$response = call_user_func_array($controller, $attributes);
		} catch (ResourceNotFoundException $e) {
			$response = new Response('Not found!', Response::HTTP_NOT_FOUND);
		}

		return $response;
	}

We can now easily handle dynamic URLs like this:

现在,我们可以轻松地处理如下动态网址:

$app->map('/hello/{name}', function ($name) {
		return new Response('Hello '.$name);
	});

Note that this is very similar to what the Symfony full-stack framework is doing: we inject URL parameters into the right controller.

请注意,这与Symfony全栈框架的工作非常相似:我们将URL参数注入到正确的控制器中。

与框架挂钩 (Hooking into the framework)

The Symfony framework also provides various way to hook into the request lifecycle and to change it. A good example is the security layer intercepting a request which attempts to load an URL between a firewall.

Symfony框架还提供了多种连接请求生命周期并进行更改的方法。 一个很好的例子是安全层拦截尝试在防火墙之间加载URL的请求。

All of this is possible thanks to the EventDispatcher component, which allows different components of an application to communicate implementing the Observer pattern.

有了EventDispatcher组件,所有这些都是可能的,该组件允许应用程序的不同组件进行通信以实现Observer模式。

php composer.phar require symfony/event-dispatcher 2.5

At the core of it, there is the EventDispatcher class, which registers listeners of a particular event. When the dispatcher is notified of an event, all known listeners of this event are called. A listener can be any valid PHP callable function or method.

它的核心是EventDispatcher类,该类注册特定事件的侦听器。 当调度程序收到事件通知时,将调用该事件的所有已知侦听器。 侦听器可以是任何有效PHP可调用函数或方法。

We can implement this in our framework by adding a property dispatcher that will hold an EventDispatcher instance, and an on method, to bind an event to a PHP callback. We’ll use the dispatcher to register the callback, and to fire the event later in the framework.

我们可以在框架中通过添加将包含EventDispatcher实例的属性dispatcheron方法将事件绑定到PHP回调来实现此功能。 我们将使用分派器注册回调,并在稍后的框架中触发事件。

use Symfony\Component\Routing\Matcher\UrlMatcher;
		use Symfony\Component\Routing\RequestContext;
		use Symfony\Component\Routing\RouteCollection;
		use Symfony\Component\Routing\Route;
		use Symfony\Component\Routing\Exception\ResourceNotFoundException;
		use Symfony\Component\EventDispatcher\EventDispatcher

		class Core implements HttpKernelInterface
		{
			/** @var RouteCollection */
			protected $routes;

			public function __construct()
			{
				$this->routes = new RouteCollection();
				$this->dispatcher = new EventDispatcher();
			}
			
			// ... 

			public function on($event, $callback)
			{
				$this->dispatcher->addListener($event, $callback);
			}
		}

We are now able to register listeners, which are just simple PHP callbacks. Let’s write now a fire method which will tell our dispatcher to notify all the listeners he knows when some event occurs.

现在,我们可以注册侦听器,它们只是简单PHP回调。 现在让我们编写一个fire方法,该方法将告诉我们的调度程序在发生某个事件时通知所有他知道的侦听器。

public function fire($event)
    {
	    return $this->dispatcher->dispatch($event);
	}

In less than ten lines of code, we just added a nice event listener system to our framework, thanks to the EventDispatcher component.

在不到十行的代码中,由于EventDispatcher组件,我们仅向框架中添加了一个不错的事件侦听器系统。

The dispatch method also takes a second argument, which is the dispatched event object. Every event inherits from the generic Event class, and is used to hold any information related to it.

dispatch方法还采用第二个参数,即已调度事件对象。 每个事件都继承自通用Event类,并用于保存与其相关的任何信息。

Let’s write a RequestEvent class, which will be immediately fired when a request is handled by the framework. Of course, this event must have access to the current request, using an attribute holding a Request instance.

让我们编写一个RequestEvent类,当框架处理请求时将立即将其触发。 当然,此事件必须使用持有Request实例的属性来访问当前请求。

namespace Framework\Event;
	
	use Symfony\Component\HttpFoundation\Request;
	use Symfony\Component\EventDispatcher\Event;

	class RequestEvent extends Event
	{
		protected $request;

		public function setRequest(Request $request)
		{
			$this->request = $request;
		}
	
		public function getRequest()
		{
			return $this->request;
		}
	}

We can now update the code in the handle method to fire a RequestEvent event to the dispatcher every time a request is received.

现在,我们可以更新handle方法中的代码,以在每次接收到请求时将RequestEvent事件发送到调度RequestEvent

public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
			{
				$event = new RequestEvent();
				$event->setRequest($request);
	
				$this->dispatcher->dispatch('request', $event);
				// ...
			}

This way, all called listeners will be able to access the RequestEvent object and also the current Request. For the moment, we wrote no such listener, but we could easily imagine one that would check if the requested URL has restricted access, before anything else happens.

这样,所有调用的侦听器都将能够访问RequestEvent对象以及当前的Request 。 目前,我们没有编写这样的侦听器,但是我们可以很容易地想象一个侦听器,该侦听器将在发生任何其他情况之前检查所请求的URL是否限制了访问。

$app->on('request', function (RequestEvent $event) {
		// let's assume a proper check here
		if ('admin' == $event->getRequest()->getPathInfo()) {
			echo 'Access Denied!';
			exit;
		}
	});

This is a very basic security system, but you could imagine implementing anything you want, because we now have the ability to hook into the framework at any moment, which makes it much more scalable.

这是一个非常基本的安全系统,但是您可以想象实现任何您想要的东西,因为我们现在可以随时挂接到框架中,从而使其更具可伸缩性。

结论 (Conclusion)

You’ve seen, by reading this tutorial, that Symfony components are great standalone libraries. Moreover, they can interact together to build a framework that fits your needs. There are many more of them which are really interesting, like the DependencyInjection component or the Security component.

通过阅读本教程,您已经了解到Symfony组件是出色的独立库。 此外,他们可以一起互动以建立适合您需求的框架。 其中有许多其他确实很有趣,例如DependencyInjection组件或Security组件。

Of course, full-stack frameworks such as Symfony itself or Laravel have pushed these components to their limits, to create the powerful tools we know today.

当然,像Symfony本身或Laravel这样的全栈框架将这些组件推到了极限,以创建我们今天所知道的强大工具。

翻译自: https://www.sitepoint.com/build-php-framework-symfony-components/

  • 1
    点赞
  • 0
    评论
  • 1
    收藏
  • 扫一扫,分享海报

表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
©️2022 CSDN 皮肤主题:编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值