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应用程序创建基本结构。
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流的顶层。 它最重要的入口点是Request
和Response
这两个类。
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中那样使用header
或echo
。
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 thecreateFromGlobals
static method. Instead of creating an empty object, this method populates aRequest
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 aRouteCollection
instance to hold our routes.更换
routes
与阵列RouteCollection
实例来保存我们的路线。Change the
map
method so it registers aRoute
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 aRequestContext
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
实例的属性dispatcher
和on
方法将事件绑定到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/