最近刚好在用Slim框架,看文档的时候,中文文档中缺了这一篇没有翻译,于是我就把它翻译过来了。本人英语水平有限,如有错误请告知。
如果你在寻找创建一款非常简单的 Slim 应用程序的流程,来这里算是找对地方了(在这个应用程序中,没有用到 Twig,但用到了 MonoLog 和一款 PDO 数据库连接器)。你可以在此教程中学习如何构建这个示例应用程序,也可以按自己的需要去改写某个步骤。
在你开始之前:这里有一个skeleton项目可以帮助你快速创建一个应用程序样板,你可以使用它直接开始创建你的应用程序,而不用再纠结应用程序的组织结构了。
此教程贯穿构建示例应用程序的全流程。如果有需求,可以在 Github 上浏览它的项目源代码。
开始
首先我们为项目创建一个文件夹,就叫它project
好了(因为起名字这种事实在是太难了,我们偷下懒)。我喜欢将那些不是代码的文件放在项目的顶层目录中,然后将源代码单独放在一个目录里,然后在将webroot
目录放置在代码目录中,所以我的初始结构如下:
.
├── project
│ └── src
│ └── public
安装Slim框架
Composer是安装Slim框架的最好方式。如果你还没有安装Composer,你可参照这个文档来安装Composer,在我的项目中,我刚将composer.phar
文件下载到/src目录中,所以我的地一个命令如下:
php composer.phar require slim/slim
这里做了两件事:
- 添加Slim框架依赖到composer.json
(在我目前的情况下,它会自动为我创建composer.json
文件,因为我目录中之前并没有这个文件,如果你已经有了这个文件,它会将依赖追加到文件中)
- 执行composer install
操作,将依赖的下载到项目中
如果现在你查看project
目录,你会发现在src
目录中出现vendor/
目录。同时还有两个新的文件composer.json
和composer.lock
。这也是设置版本控制系统的最好时机:在使用composer
时,我们总是将vendor/
目录排除,但是composer.json
和composer.lock
应该包含在版本管理中。由于我使用了composer.phar
,因此我把它也加入到版本管理中,这样可以在所有需要它的电脑上安装composer命令。
创建一个名为src/.gitignore
文件并把下面的内容放在文件中:
vendor/*
现在git不会再提示你将vendor/
目录添加到版本控制系统中了(我们只需要使用composer来管理这些依赖项即可,不需要添加到版本管理中)。
创建应用程序
Slim 主页上的代码是一个优秀的例子,所以我们使用它来做我们的起点。将下面这些代码添加到src/public/index.php
文件中:
<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;
require '../vendor/autoload.php';
$app = new \Slim\App;
$app->get('/hello/{name}', function (Request $request, Response $response) {
$name = $request->getAttribute('name');
$response->getBody()->write("Hello, $name");
return $response;
});
$app->run();
我们只是粘贴了一段代码,让我们看看它的功能。
顶部的use
语句将ServerRequestInterface
和ResponseInterface
引入我们的代码中,并为它们起了一个更短的别名。Slim框架支持PSR-7,它是HTTP消息传递的PHP标准,所以在构建应用程序时注意到Request和Response对象是你经常看到的东西。只是编写Web应用程序的一种现代的、优秀的方法。
接下来我们引入了vendor/autoload.php
文件,这个文件是Composer
创建的,并允许我们引用先前安装的Slim和其它相关依赖项。如果你使用和我一样的文件结构,那么vendor/
目录比index.php
高一级别,所以需要像上面一样调整路径。
最后,我们创建了$app
对象作为Slim良好的开始。$app->get()
调用的是我们的第一个路由-当我们向/hello/someone
发出GET请求时,代码会对其作出响应。不要忘记最后的$app->run()
来告诉Slim我们已经完成了配置,现在是时候开始做主要事情了。
现在我们已经有一个应用程序了,我们需要运行它。我将介绍两个方法:PHP内置服务器和Apache。
使用PHP内置服务器运行你的应用程序
这是我首选的”快速启动”选项,因为它不依赖其它的任何东西(当然,你必须安装了PHP)。进入src/public
目录执行下面的命令:
php -S localhost:8000
上面的命令使你可以通过http://localhost:8000
访问你的应用程序(如果你已经使用了8000端口,会收到一个警告,只需要换一个未被使用的端口就好,PHP并不关注你将8000端口给了什么应用)。
注意,你访问上面的URL会得到一个”Page Not Found”的错误,但是这个错误是Slim发出的,因此这是可预期的错误。试试http://localhost:8000/hello/collin
通过Apache运行你的项目
为了在标准的LAMP
上运行项目,还需要一些其它的东西:一些虚拟主机的配置和一个重写规则。
虚拟机配置应当相当简单,我们不需要任何特别的配置。复制你现有的默认虚拟机配置,并将ServerName
设置为访问的地址就好了。例如下面这种形式:
ServerName slimproject.dev
然后,再将DocumentRoot
设置为项目的public/
目录即可,如下所示(编辑现有的行):
DocumentRoot /home/collin/projects/slim/project/src/public/
修改配置后,一定要重启Apache来重新载入配置文件。
我的src/public
目录下有个.htaccess
文件,需要启用Apache
的重写模块,见打的将所有的web请求转到index.php
文件上,以便Slim为我们处理所有的路由。下面就是.htaccess
文件的内容:
RewriteEngine on
RewriteCond %{REQUEST_FILENAME}% ! -d
RewriteCond %{REQUEST_FILENAEE}% ! -f
RewriteRule . index.php [L]
需要记住的是,我们需要使用http://slimproject.dev
来代替http://localhost:8000
。当然,如果你直接访问http://slimproject.dev
你还是会得到一个Slim的错误页面,你使用http://slimproject.dev/hello/collin
会有好的事情发生。
配置和自动加载器
现在我们已经建立了平台,我们可以开始在应用程序本身中获取所需要的一切了。
添加配置到你的应用程序中
最初的例子使用了Slim所有的默认值,但是我们创建它时,我们可以轻松的将配置添加到我们的应用程序。有几种配置的方式,但是在这里,我们使用配置数组来告诉Slim。
首先是配置本身:
$config['displayErrorDetails'] = true;
$config['db']['host'] = 'localhost';
$config['db']['user'] = 'user';
$config['db']['pass'] = 'password';
$config['db']['dbname'] = 'exampleapp';
第一行配置是非常重要的!在开发模式下打开此项获取有关错误的详细信息(没有开启的话,Slim只会记录最少的错误信息,如果你使用的是php内置的web服务器,那么将在控制台看到这些错误信息,这个功能对于调试来说很有用)。这里其它配置不是特定的键/值对,只是我们稍后能访问的一些数据。
现在将他们加入到Slim中,我们需要改变我们创建Slim/App
对象的代码,看起来像这样:
$app = new \Slim\App(['settings' => $config]);
稍后我们在应用程序的任何位置都能够访问$config
数组中的任何值。
为自己的类设置自动加载
我们已经添加了Composer
的自动加载文件,但是我们编写的其它代码并不在Composer
中呢?一种很好的解决办法是使用Composer
的自动加载规则,但是如果你愿意的话,也可以添加自己的自动加载器。
我的设置很简单,因为我只有一些额外的类,它们都在全局命名空间中,并且它们都在src/classes/
目录中。所以为了添加自动加载器,我在引入vendor/autoload.php
后面添加了下面这些代码:
spl_autoload_register(function ($classname) {
require ('../classes/' . $classname . '.php');
});
sql_autoload_register
中的回调方法中,会将还没有引入的类引入到代码中。
添加依赖
很多应用程序都有一些依赖,而Slim使用建立在Pimple上的DIC(依赖注入容器)很好的处理它们。这个例子将同时使用Monolog
和一个到MySQL
的PDO连接。依赖注入容器的想法是,你将容器配置为在需要时加载应用程序需要的依赖项。一旦DIC创建/组装了依赖关系,它就会存储它们并在需要时再次提供它们。
为了获得容器,我们可以添加下面的代码到我们创建$app
对象后,注册路由前:
$container = $app->getContainer();
现在我们有了Slim\Container
对象,我们可以添加我们的服务到容器中。
在你的应用中使用Monolog
如果你对Monolog
不熟悉,这是一个优秀的php应用程序日志框架,这也是我为什么使用它的原因。首先,通过Composer获得Monolog库:
php composer.phar require monolog/monolog
依赖项被命名为logger,添加它的代码如下所示:
$container['logger'] = function($c) {
$logger = new \Monolog\Logger('my_logger');
$file_handler = new \Monolog\Handler\StreamHandler('../logs/app.log');
$logger->pushHandler($file_handler);
return $logger;
};
我们添加一个元素到容器中,它本身是一个匿名函数(传入的$c
是容器本身,因此如果需要可以访问其它依赖项)。这将在我们第一次尝试访问这个依赖时被调用。这里的代码完成依赖关系的设置。下次我们尝试访问相同的依赖项时,会使用第一次创建的对象。
我的Monolog配置是相当简单的,仅仅只是设置应用程序将所有的错误信息存储在/logs/app.log
文件中(记住这个路径是从脚本运行的角度来看的,即index.php)。有了这个记录器,我可以在我的路由代码中使用这样一行代码。
$this->logger->addInfo("Something interesting happend");
对于所有应用程序来说,拥有一个优秀的日志组建是个重要的基础,所以我总是建议将这样的东西放在适当的位置。这使您可以根据需要添加尽可能多的或者尽可能少的调试信息,并通过对每条消息使用适当的日志级别,你可以尽可能多或尽可能少的细节,以适应你在任何时候所做的事情。
添加数据库连接
php有很多可用的数据库,但是这个例子使用PDO-这在php中是可用的,因此它可能在每个项目中都很有用,或者可以通过调整下面的示例来使用自己i的库。
就像我们向DIC添加Monolog一样,我们是使用匿名函数来添加依赖,在这里叫做db
:
$container['db'] = function ($c) {
$db = $c['settings']['db'];
$pdo = new PDO("mysql:host=" . $db['host'] . ";dbname=" . $db['dbname'], $db['user'], $db['pass']);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTr_DEFAULT_FETCH_MODE, PDO_FETCH_ASSOC);
return $pdo;
}
还记得我们之前添加到应用程序中的配置吗?那么,这里就是使用它的地方,容器知道如何访问我们的设置,所以我们可以很容易的从这里获得我们的配置。通过配置,我们创建了PDO对象(记住,如果它失败,将抛出一个PDOException,并且你可能想在此处理它们),以便我们可以链接到数据库。我使用了两个非必要的setAttribute()调用,但是我发现这两个设置使得PDO本身更容易使用,所以我将他们留在了案例中,你也可以使用它们。最后,我们返回我们的连接对象。
我们可以通过$this->
来访问我们的依赖。然后我们想要的依赖名称是$this->db
,所以我的代码看起来像这样:
$mapper = new TicketMapper($this->db);
这将从DIC中获取数据库依赖,并且在必要时创建数据库依赖项,并且在此实例中允许我将PDO对象直接传入我的映射器类。
创建路由
“路由”是我们将描述并附加功能的URL模式。Slim不使用任何自动映射或URL公式,所以你可以将任何喜欢的URL映射到任何你喜欢的功能上,这非常的灵活。路由可以连接到特定的HTTP请求方式上(如GET或者POST)或多个请求方式上。
作为第一个案例,下面是在我的bug跟踪应用程序中用GET请求/tickets
获取票据列表的代码。它只是输出变量,因为我们尚未向应用程序添加任何视图:
$app->get('/tickets', function (Request $request, Response $response) {
$this->logger()->addInfo('Ticket list');
$mapper = new TicketMapper($this->db);
$tickets = $mapper->getTickets();
$response->getBody()->write(var_export($tickets, true));
return $response;
});
这里使用$app->get()
的意思是这个路由只处理GET请求。相当于$app->post()
路由,它采用POST请求。这里还有其它的请求方式-还有map()
函数用多个请求方式同一段代码。
Slim路由按照他们的声明顺序进行匹配,所以如果你有一个路由与其他路由重叠,则需要首先声明最具体的那个。如果出现问题,Slim将抛出异常,例如在此应用中,我同时拥有/ticket/new
和ticket/{id}
两个路由,并按照顺序进行声明,否则路由会认为”new”也是一个ID!
在这个实例应用程序中,所有的路由都放置在index.php
文件中,实际上可能会造成index.php
文件过于庞大和臃肿的。重构应用程序将路由放入不同的文件中,或仅注册一组实际在其它地方声明回调的路由即可。
所有的路由回调函数接受三个参数(第三个参数是可选参数):
- Request: 包含传入的请求头、请求参数等的所有信息
- Response: 我们可以为此添加请求体和请求头,一旦完成,它就会变成客户端收到的HTTP响应。
- Arguments: URL中的占位符,这是可选的,如果没有,通常会被忽略。
对于Request和Response的强调说明了Slim3基于PSR-7 HTTP消息传递标准。使用Request和Response对象也使得应用程序更具可测性,因为不需要作出实际的请求和响应,我们可以根据需要设置对象。
带占位符的路由
有时候,我们的应用程序需要使用URL中的变量。在我的BUG跟踪示例程序中,我希望可以使用/ticket/42
这样的URL来引用ticket - 而Slim可以轻易的解析出”42”,并使其在代码中易于使用。下面的路由就是这样:
$app->get('/ticket/{id}', function (Request $request, Response $response, $args) {
$ticket_id = (int)$args['id'];
$mapper = new TicketMapper($this->db);
$ticket = $mapper->getTicketById($ticket_id);
$response->getBody()->write(var_export($ticket, true));
return $response;
})
看看路由本身的定义: 我们把它写成/ticket/{id}
,当我们这样做时,路由将从声明{id}
的地方获取URL的部分,并在回调中作为$args['id']
提供。
使用GET参数
由于GET和POST以不同的方式发送数据,那么我们从Request对象获取数据的方式在Slim中有很大的不同。
可以通过执行$request->getQueryParams()
来获取请求中所有查询参数,这将返回一个关联数组。所以从/tickets?sort=date&order=desc
我们可以得到一个如下所示的关联数组:
["sort" => "date", "order" => "desc"]
这些可以在回调中使用。
使用POST数据
处理传入数据时,我们可以在请求正文中找到它们。我们已经知道了可以通过执行$request->getQueryParams()
来解析URL中的数据以及如何获取GET变量,但POST数据又如何处理呢?POST请求数据可以在请求的正文中找到,并且Slim有一些很好的helpers帮助我们更容易的获取到请求数据。
对于来自Web表单的数据,Slim会将其转换为数组。我的票据实例程序有一个创建新的票据的Form表单,它只有两个字段:”title”和”description”。只是接收这些数据的路由的第一部分,请注意,对于POST路由,使用$app->post()
而不是$app->get()
:
$app->post('/ticket/new', function (Request $request, Response $response){
$data = $request->getParsedBody();
$ticket_data = [];
$ticket_data['title'] = filter_var($data['title'], FILTER_SANITIZE_string);
$ticket_data['description'] = filter_var($data['description'], FILTER_SANITIZE_STRING);
...
});
对$request->getParsedBody()
的调用要求Slim查看该请求的Content-Type标头,然后在body中做一些聪明而有用的事情。在这个例子中,它只是一个简单的表单,所以得到的$data
数组看起来与我们期望的$_POST
非常相似-并且我们可以使用过滤器扩展来在使用它之前检查值是否可用。使用内置的Slim方法的一个巨大优势是我们可以通过注入不同的请求对象来测试这件事情-如果我们直接使用$_POST
,我们无法做到这一点。
例如,如果你正在构建API或者编写AJAX,那么使用POST数据格式非常容易,但它们不是Web表单。只要Content-Type头设置正确,Slim就会将Json有效解析为一个数组,并且可以用完全相同的方式访问它:通过使用$request->getParsedBody()
。
视图和模板
Slim对你使用视图没啥看法,尽管它已经准备好了一些选项。你最好的选择是Twig或者老的PHP.这两个选项都有优点和缺点:如果你已经熟悉Twig,那么它提供了很多优秀的给你功能,比如布局-但是如果你还没有使用过Twig,那么它可能会认为它的学习曲线有点麻烦。如果你正在寻找一些简单的东西,那么PHP视图可能适合你!我为这个项目选择了PHP视图,如果你熟悉Twig,那么使用它也是一样的。
由于我们使用php视图,所以我们需要使用composer为我们的项目添加依赖。执行下面的命令:
php composer.phar require slim/php-view
为了能够呈现视图,我们首先需要创建一个视图并将其提供给我们的应用程序;我们通过将其添加到DIC中来实现这一点。
$container['view'] = new \Slim\Views\PhpRenderer("../templates/");
现在我们有一个view
元素在DIC中,默认情况下它会在src/templates
目录中查找它的模板。我们可以使用它来在我们的action中呈现模板-这里是票据的列表路由,包括将数据传递到模板并将其呈现的调用:
$app->get('/tickets', function (Request $request, Response $response) {
$this->logger()->addInfo("Ticket list");
$mapper = new TicketMapper($this->db);
$tickets = $mapper->getTickets();
$response = $this->view->render($response, "tickets.phtml", ["tickets" => $tickets]);
return $response;
});
这里唯一不一样的地方是我们设置 response变量的倒数第二行。现在该视图位于DIC中,我们可以将其视为‘ r e s p o n s e 变 量 的 倒 数 第 二 行 。 现 在 该 视 图 位 于 D I C 中 , 我 们 可 以 将 其 视 为 ‘ this->view`。调用`render()`需要我们提供三个参数: response对象,模板文件,以及要传入的数据。 r e s p o n s e 对 象 , 模 板 文 件 , 以 及 要 传 入 的 数 据 。 response对象是不可变的,这意味着对render()的调用并不会更新 response对象。但是它会给我们返回一个新的对象,所以我们需要给 r e s p o n s e 对 象 。 但 是 它 会 给 我 们 返 回 一 个 新 的 对 象 , 所 以 我 们 需 要 给 response重新赋值。
将数据传递给模板时,你可以添加尽可能多的元素到数组中,以便在模板中使用。数组的key在模板中就是变量名。
作为一个例子,下面是模板中的一个片段,它显示了票据列表(即来自src/templates/tickets.phtml的代码-它使用Pure.css帮我解决前台技能的缺乏):
<h1>All Tickets</h1>
<p><a href="/ticket/new">Add new ticket</a></p>
<table class="pure-table">
<tr>
<th>Title</th>
<th>Component</th>
<th>Description</th>
<th>Actions</th>
</tr>
<?php foreach($data['tickets'] as $ticket): ?>
<tr>
<td><?=$ticket->getTitle() ?></td>
<td><?=$ticket->getComponent() ?></td>
<td><?=$ticket->getShortDescription() ?> ...</td>
<td>
<a href="<?=$router->pathFor('ticket-detail', ['id' => $ticket->getId()])?>">view</a>
</td>
</tr>
<?php endforeach; ?>
</table>
在这种情况下, $tickets实际上是一个带getter和setter的TicketEntity类,但是如果你传入一个数组,你可以在这里使用数组而不是对象表示法来访问它。
你是否注意到在示例结尾的$router->pathFor()
右边有啥好玩的东西?下面我们来聊聊命名路由:)
使用命名路由轻松构建URL
当我们创建一个路由时,我们可以通过在路由对象上调用->setName()
来给它命名。在这个案例中,我将名字添加到允许我查看单个票据的路由中,以便通过路由名称快速创建正确的URL,所以我的代码看起来像这样:
$app->get('/ticket/{id}', function (Request $request, Response $response, $args) {
// ...
})->setName("ticket-detail");
要在我的模板中使用它,我需要在要创建此URL的模板中使路由器可用,因此我修改了tickets/
路由以将路由器传递到模板:
$response = $this->view->render($response, "tickets.phtml", ["tickets" => $tickets, "router" => $this->router]);
通过具有友好名称的/tickets/{id}
路由以及我们的模板现在可用的路由器,这就是我们模板中pathFor()调用的作用。通过提供这个id,它将被用作URL模式中的一个命名占位符,并创建链接到具有这些值的该路由的正确URL。此功能对于可读的模板URL非常有用,如果您因任何原因需要更改URL格式,则此功能会更好 - 无需使用grep模板查看其使用位置。这种方法绝对是推荐的,特别是对于你会使用很多的链接。
下一步
本文介绍了如何使用自己的简单应用程序进行设置,希望能够让您快速启动,查看一些正在运行的示例,并构建出令人敬畏的内容。 从这里,我建议你看一下项目文档的其他部分,以了解您尚未涉及的任何内容,或者您想查看其他示例。接下来的一个好的步骤是查看中间件部分 - 这种技术是我们如何对应用程序进行分层并添加可应用于多个路由的身份验证等功能。