Silex 微服务框架
在体验了 Laravel 可以为您提供的功能之后,您很可能不想听到极简微框架。 尽管如此,我们认为了解多个框架还是有好处的。 您可以了解不同的方法,变得更加多才多艺,每个人都会希望您加入他们的团队。
我们选择 Silex 是因为它是一个微框架,与 Laravel 有很大不同,而且还因为它是 Symfony 家族的一部分。 通过对 Silex 的介绍,您将学习如何使用第二个框架,它是一种完全不同的类型,并且您也将更接近了解 Symfony,它是重要的参与者之一。
微框架有什么好处? 好吧,它们提供了非常基础的功能,即路由器、简单的依赖注入器、请求帮助器等等,但这就是它的结束。 您有足够的空间来选择和构建您真正需要的内容,包括外部库甚至您自己的库。 这意味着您可以拥有专门为每个不同项目定制的框架。 事实上,Silex 提供了一些内置服务提供商,您可以轻松集成它们,从模板引擎到日志记录或安全性。
安装
这里没有新闻。与 Laravel 一样,Composer 会为你做好一切。在新项目根目录下的命令行中执行以下命令,将 Silex 加入到 composer.json 文件中:
$ composer require silex/silex
您可能需要更多依赖项,但让我们在需要时添加它们。
Project 设置
Silex 最重要的类是 Silex/Application
。该类由 Pimple(一种轻量级依赖注入器)扩展而来,几乎可以管理任何东西。你可以将它用作一个数组,因为它实现了 ArrayAccess
接口,也可以调用它的方法来添加依赖关系、注册服务等。首先要做的是在 public/index.php
文件中将其实例化,如下所示:
<?php
use Silex\Application;
require_once __DIR__ . '/../vendor/autoload.php';
$app = new Application();
管理配置
我们要做的第一件事就是加载配置。我们可以做一些非常简单的事情,比如加入一个包含 PHP 或 JSON 内容的文件,但我们还是要使用其中一个服务提供商 ConfigServiceProvider
。让我们通过以下一行在 Composer 中添加它:
$ composer require igorw/config-service-provider
这项服务允许我们拥有多个配置文件,每个环境一个。假设我们需要两个环境:prod 和 dev,这意味着我们需要两个文件:一个在 config/prod.json 中,另一个在 config/dev.json 中。config/dev.json 文件看起来与下面类似:
{
"debug": true,
"cache": false,
"database": {
"user": "dev",
"password": ""
}
}
config/prod.json 文件看起来类似于:
{
"debug": false,
"cache": true,
"database ": {
"user": "root",
"password": "fsd98na9nc"
}
}
要在开发环境中工作,需要通过运行以下命令为环境变量设置正确的值:
export APP_ENV=dev
APP_ENV
环境变量将告诉我们所处的环境。现在,是使用该服务提供商的时候了。为了通过读取当前环境的配置文件来注册,请在 index.php 文件中添加以下几行:
$env = getenv('APP_ENV') ?: 'prod';
$app->register(
new Igorw\Silex\ConfigServiceProvider(
__DIR__ . "/../config/$env.json"
)
);
我们首先要做的是从环境变量中获取环境。默认情况下,我们将其设置为 prod。然后,我们从 $app
对象中调用 register
,通过传递正确的配置文件路径来添加 ConfigServiceProvider
实例。从现在起,$app
"数组" 将包含三个条目:debug
、cache
和包含配置文件内容的 db
。只要我们能访问 $app
,我们就能访问它们,而这将主要体现在任何地方。
设置模板引擎
另一个方便的服务提供商是 Twig。大家可能还记得,Twig 是我们在自己的框架中使用的模板引擎,事实上,它与 Symfony 和 Silex 是同一家公司开发的。你也已经知道如何使用 Composer 添加依赖关系;只需运行以下命令即可:
$ composer require twig/twig
要注册服务,我们需要在 public/index.php 文件中添加以下几行:
$app->register(
new Silex\Provider\TwigServiceProvider(),
['twig.path' => __DIR__ . '/../views']
);
此外,创建 views/
目录,以后我们将在此存储模板。现在,只需访问 $app['twig']
,就能获得 Twig_Environment
实例。
添加日志器
我们现在要注册的最后一个服务提供商是日志记录器。这次要使用的库是 Monolog
,可以通过以下方式将其包含在内:
$ composer require monolog/monolog
注册服务的最快方法是提供日志文件的路径,具体步骤如下:
$app->register(
new Silex\Provider\MonologServiceProvider(),
['monolog.logfile' => __DIR__ . '/../app.log']
);
如果您想为该服务提供商添加更多信息,如要保存的日志级别、日志名称等,可以将它们与日志文件一起添加到数组中。有关可用参数的完整列表,请参阅 http://silex.sensiolabs.org/doc/providers/monolog.html 上的文档。
与模板引擎一样,从现在起,您可以通过访问 $app['monolog']
,从应用程序对象中访问 Monolog\Logger
实例。
添加第一个端点
是时候看看 Silex 中的路由器是如何工作的了。我们想为主页添加一个简单的端点。正如我们已经提到的,$app 实例几乎可以管理任何东西,包括路由。在 public/index.php 文件末尾添加以下代码:
$app->get('/', function(Application $app) {
return $app['twig']->render('home.twig');
});
这种添加路由的方法与 Laravel 遵循的方法类似。我们调用了 get 方法,因为它是一个 GET 端点,我们传递了路由字符串和 Application 实例。正如我们在这里提到的,$app 也是一个依赖注入器—事实上,它是从依赖注入器扩展而来的: Pimple 扩展而来,所以你几乎随处都能看到应用程序实例。匿名函数的结果将是我们发送给用户的响应—在本例中,是一个呈现的 Twig 模板。
现在,这还不能解决问题。为了让 Silex 知道你已经完成了应用程序的设置,你需要调用 public/index.php 文件末尾的 run 方法。请记住,如果您需要在该文件中添加其他内容,必须在这一行之前添加:
$app->run();
你已经使用过 Twig,所以我们不会花太多时间在这上面。 首先要添加的是 views/home.twig 模板:
{% extends "layout.twig" %}
{% block content %}
<h1>Hi visitor!</h1>
{% endblock %}
现在,您可能已经猜到了,我们将添加 views/layout.twig 模板,如下所示:
<html>
<head>
<title>Silex Example</title>
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>
尝试访问您的应用程序的主页;你应该得到以下结果:

访问数据库
在本节中,我们将编写一个端点,用于为我们的食谱创建食谱。运行以下 MySQL 查询,以设置食谱数据库并创建空食谱表:
mysql> CREATE SCHEMA cookbook;
Query OK, 1 row affected (0.00 sec)
mysql> USE cookbook;
Database changed
mysql> CREATE TABLE recipes(
-> id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
-> name VARCHAR(255) NOT NULL,
-> ingredients TEXT NOT NULL,
-> instructions TEXT NOT NULL,
-> time INT UNSIGNED NOT NULL);
Query OK, 0 rows affected (0.01 sec)
Silex 没有集成任何 ORM,因此您需要手工编写 SQL 查询。不过,有一个 Doctrine 服务提供商可以提供比 PDO 更简单的接口,因此我们可以尝试集成它。要安装它,请运行以下命令:
$ composer require "doctrine/dbal:~2.2"
现在,我们可以注册服务提供商了。与其他服务一样,在路由定义之前将以下代码添加到 public/index.php 中:
$app->register(new Silex\Provider\DoctrineServiceProvider(), [
'dbs.options' => [
[
'driver' => 'pdo_mysql',
'host' => '127.0.0.1',
'dbname' => 'cookbook',
'user' => $app['database']['user'],
'password' => $app['database']['password']
]
]
]);
注册时,需要提供数据库连接的选项。其中有些选项在任何环境下都是相同的,如驱动程序甚至主机,但有些选项则来自配置文件,如 $app['database']['user']
。从现在起,你可以通过 $app['db']
访问数据库连接。
建立数据库后,我们来添加路由,以便添加和获取食谱。与 Laravel 一样,你可以指定匿名函数(我们已经这样做了),也可以指定控制器和方法来执行。用以下三个路由替换当前路由:
$app->get(
'/',
'CookBook\\Controllers\\RecipesController::getAll'
);
$app->post(
'/recipes',
'CookBook\\Controllers\\RecipesController::create'
);
$app->get(
'/recipes',
'CookBook\\Controllers\\RecipesController::getNewForm'
);
正如您所观察到的,将有一个新的控制器 CookBook\Controllers\ RecipesController
放置在 src/Controllers/RecipesController.php
中。这意味着你需要更改 Composer 中的自动加载器。用以下内容编辑你的 composer.json
文件:
"autoload": {
"psr-4": {"CookBook\\": "src/"}
}
现在,让我们添加控制器类,如下所示:
<?php
namespace CookBook\Controllers;
class Recipes {
}
我们要添加的第一个方法是 getNewForm
方法,该方法只会呈现添加新菜谱页面。 该方法看起来类似于:
public function getNewForm(Application $app): string {
return $app['twig']->render('new_recipe.twig');
}
该方法只会呈现 new_recipe.twig。该模板的示例如下:
{% extends "layout.twig" %}
{% block content %}
<h1>Add recipe</h1>
<form method="post">
<div>
<label for="name">Name</label>
<input type="text" name="name" value="{{ name is defined ? name : "" }}" />
</div>
<div>
<label for="ingredients">Ingredients</label>
<textarea name="ingredients">
{{ ingredients is defined ? ingredients : "" }}
</textarea>
</div>
<div>
<label for="instructions">Instructions</label>
<textarea name="instructions">
{{ instructions is defined ? instructions : "" }}
</textarea>
</div>
<div>
<label for="time">Time (minutes)</label>
<input type="number" name="time" value="{{ time is defined ? time : "" }}" />
</div>
<div>
<button type="submit">Save</button>
</div>
</form>
{% endblock %}
该模板会发送菜名、配料、说明以及准备这道菜所需的时间。获取该表单的端点需要获取响应对象,以便提取这些信息。就像我们可以获取应用程序实例作为参数一样,如果在方法定义中指定请求实例,我们也可以获取请求实例。访问 POST 参数非常简单,只需通过发送参数名称调用 get
方法,或调用 $request->request->all()
以数组形式获取所有参数即可。添加以下方法,检查所有数据是否有效,如果无效则重新渲染表单,并发送已提交的数据和错误信息:
public function create(Application $app, Request $request): string {
$params = $request->request->all();
$errors = [];
if (empty($params['name'])) {
$errors[] = 'Name cannot be empty.';
}
if (empty($params['ingredients'])) {
$errors[] = 'Ingredients cannot be empty.';
}
if (empty($params['instructions'])) {
$errors[] = 'Instructions cannot be empty.';
}
if ($params['time'] <= 0) {
$errors[] = 'Time has to be a positive number.';
}
if (!empty($errors)) {
$params = array_merge($params, ['errors' => $errors]);
return $app['twig']->render('new_recipe.twig', $params);
}
}
为了显示返回的错误,layout.twig 模板也需要进行编辑。我们可以通过执行以下命令来做到这一点:
{# ... #}
{% if errors is defined %}
<p>Something went wrong!</p>
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
{% block content %}
{# ... #}
此时,您已经可以尝试访问 http://localhost/recipes ,填写表格并留空,然后提交,并返回错误的表单。它看起来应该与下面类似(有一些额外的 CSS 样式):

控制器的延续应允许我们将正确的数据作为新菜谱存储到数据库中。要做到这一点,最好创建一个单独的类,如 CookBook\Models\RecipeModel 类;不过,为了加快速度,让我们在控制器中添加以下几行模型。请记住,我们有 Doctrine 服务提供商,因此无需直接使用 PDO:
$sql = 'INSERT INTO recipes (name, ingredients, instructions, time) ' . 'VALUES(:name, :ingredients, :instructions, :time)';
$result = $app['db']->executeUpdate($sql, $params);
if (!$result) {
$params = array_merge($params, ['errors' => $errors]);
return $app['twig']->render('new_recipe.twig', $params);
}
return $app['twig']->render('home.twig');
在获取数据时,Doctrine 也能提供帮助。请查看第三个也是最后一个方法,在这个方法中,我们将获取所有食谱,以便向用户展示:
public function getAll(Application $app): string {
$recipes = $app['db']->fetchAll('SELECT * FROM recipes');
return $app['twig']->render(
'home.twig',
['recipes' => $recipes]
);
}
只需一行,我们就可以执行查询。虽然不如 Laravel 的 Eloquent ORM 那样简洁,但至少比使用原始 PDO 少了很多啰嗦。最后,你可以用以下内容更新 home.twig 模板,以显示我们刚刚从数据库中获取的食谱:
{% extends "layout.twig" %}
{% block content %}
<h1>Hi visitor!</h1>
<p>Check our recipes!</p>
<table>
<th>Name</th>
<th>Time</th>
<th>Ingredients</th>
<th>Instructions</th>
{% for recipe in recipes %}
<tr>
<td>{{ recipe.name }}</td>
<td>{{ recipe.time }}</td>
<td>{{ recipe.ingredients }}</td>
<td>{{ recipe.instructions }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}