正文
这是一篇社区协同翻译的文章,已完成翻译,更多信息请点击
协同翻译介绍
。
![file]()
我为你们准备了一个富有挑战性的事情。接下来你们将以
无
框架的方式开启一个项目之旅。 首先声明, 这篇并非又臭又长的反框架裹脚布文章。也不是推销
非原创
思想 。毕竟, 我们还将在接下来的开发之旅中使用其他框架开发者编写的辅助包。我对这个领域的创新也是持无可非议的态度。
这无关他人,而是关乎己身。作为一名开发者,它将有机会让你成长。
也许无框架开发令你受益匪浅的地方就是,可以从底层运作的层面中汲取丰富的知识。抛却依赖神奇的,帮你处理无法调试和无法真正理解的东西的框架,你将清楚的看到这一切是如何发生的。
很有可能下一份工作中,你并不能随心所以地选择框架开拓新项目。现实就是,在很多高价值,关键业务的 PHP 工作中均使用现有应用。 并且该应用程序是否构建在当前令人舒爽的 Laravel 或 Symfony 等流行框架中,亦或是陈旧过时的 CodeIgniter 或者 FuelPHP 中,更有甚者它可能广泛出现在令人沮丧的
“面向包含体系结构” 的传统的 PHP 应用
之中,所以
无
框架开发会在将来你所面临的
任何
PHP 项目中助你一臂之力。
上古时代, 因为
某些系统
不得不解释分发 HTTP 请求,发送 HTTP 响应,管理依赖关系,无框架开发就是痛苦的鏖战。缺乏行业标准必然意味着,框架中的这些组件高度耦合 。如果你从无框架开始,你终将难逃自建框架的命运。
时至今日,幸亏有
PHP-FIG
完成所有的自动加载和交互工作,无框架开发并非让你白手起家。各色供应商都有这么多优秀的可交互的软件包。把他们组合起来容易得超乎你的想象!
wilson_yang 翻译于 1天前
4
重译
由
Summer
审阅
PHP 是如何工作的?
在做其他事之前,搞清楚 PHP 如何与外界沟通是非常重要的。
PHP 以请求 / 响应为周期运行服务端应用程序。与你的应用程序的每一次交互——无论是来自浏览器,命令行还是 REST API ——都是作为请求进入应用程序的。 当接收到请求以后:
-
程序开始启动;
-
开始处理请求;
-
产生响应;
-
接着,响应返回给产生请求的相应客户端;
-
最后程序关闭。
每一个
请求都在重复以上的交互。
前端控制器
用这些知识把自己武装起来以后,就可以先从我们的前端控制器开始编写程序了。前端控制器是一个 PHP 文件,它处理程序的每一个请求。控制器是请求进入程序后遇到的第一个 PHP 文件,并且(本质上)也是响应走出你应用程序所经过的最后一个文件。
我们使用经典的
Hello, world!
作为例子来确保所有东西都正确连接上,这个例子由
PHP 的内置服务器
驱动。在你开始这样做之前,请确保你已经安装了
PHP7.1 或者更高版本。 创建一个含有
public
目录的项目,然后在该目录里面创建一个
index.php
文件,文件里面写入如下代码:
<?php
declare(strict_types=1);
echo 'Hello, world!';
注意,这里我们声明了使用严格模式 —— 作为最佳实践,你应该在应用程序的
每个 PHP 文件的开头
都这样做。因为对从你后面来的开发者来说类型提示对
调试和清晰的交流意图很重要
。
使用命令行(比如 macOS 的终端)切换到你的项目目录并启动 PHP 的内置服务器。
php -S localhost:8080 -t public/
现在,在浏览器中打开
http://localhost:8080/
。是不是成功地看到了 "Hello, world!" 输出?
很好。接下来我们可以开始进入正题了!
JiaZombie 翻译于 1天前
0
重译
由
Summer
审阅
自动加载与第三方包
当你第一次使用 PHP 时,你可能会在你的程序中使用
includes
或
requires
语句来从其他 PHP 文件导入功能和配置。 通常,我们会避免这么干,因为这会使得其他人更难以遵循你的代码路径和理解依赖在哪里。这让调试成为了一个
真正的
噩梦。 解决办法是使用自动加载(autoloading)。 自动加载的意思是:当你的程序需要使用一个类,
PHP 在调用该类的时候知道去哪里找到并加载它。虽然从 PHP 5 开始就可以使用这个特性了, 但是得益于
PSR-0
( 自动加载标准,后来被
PSR-4
取代),其使用率才开始有真正的提升。 我们可以编写自己的自动加载器来完成任务,但是由于我们将要使用的管理第三方依赖的
Composer
已经包含了一个完美的可用的自动加载器,那我们用它就行了。 确保你已经在你的系统上
安装
了 Composer。然后为此项目初始化 Composer:
composer init
这条命令通过交互式引导你创建
composer.json
配置文件。 一旦文件创建好了,我们就可以在编辑器中打开它然后向里面写入
autoload
字段,使他看起来像这个样子(这确保了自动加载器知道从哪里找到我们项目中的类):
{
"name": "kevinsmith/no-framework",
"description": "An example of a modern PHP application bootstrapped without a framework.",
"type": "project",
"require": {},
"autoload": {
"psr-4": {
"ExampleApp\\": "src/"
}
}
}
现在为此项目安装 composer,它引入了依赖(如果有的话),并为我们创建好了自动加载器:
composer install
更新
public/index.php
文件来引入自动加载器。在理想情况下,这将是你在程序当中使用的少数『包含』语句之一。
<?php
declare(strict_types=1);
require_once dirname(__DIR__) . '/vendor/autoload.php';
echo 'Hello, world!';
此时如果你刷新浏览器,你将不会看到任何变化。因为自动加载器没有修改或者输出任何数据,所以我们看到的是同样的内容。让我们把
Hello, world!
这个例子移动到一个已经自动加载的类里面看看它是如何运作的。
在项目根目录创建一个名为
src
的目录,然后在里面添加一个叫
HelloWorld.php
的文件,写入如下代码:
<?php
declare(strict_types=1);
namespace ExampleApp;
class HelloWorld
{
public function announce(): void
{
echo 'Hello, autoloaded world!';
}
}
现在到
public/index.php
里面用
HelloWorld
类的
announce
方法替换掉
echo
语句。
// ...
require_once dirname(__DIR__) . '/vendor/autoload.php';
$helloWorld = new \ExampleApp\HelloWorld();
$helloWorld->announce();
刷新浏览器查看新的信息!
JiaZombie 翻译于 1天前
0
重译
由
Summer
审阅
什么是依赖注入?
依赖注入是一种编程技术,每个依赖项都供给它需要的对象,而不是在对象外获得所需的信息或功能。
举个例子,假设应用中的类方法需要从数据库中读取。为此,你需要一个数据库连接。常用的技术就是创建一个全局可见的新连接。
class AwesomeClass
{
public function doSomethingAwesome()
{
$dbConnection = return new \PDO(
"{$_ENV['type']}:host={$_ENV['host']};dbname={$_ENV['name']}",
$_ENV['user'],
$_ENV['pass']
);
// Make magic happen with $dbConnection
}
}
但是这样做显得很乱,它把一个并非属于这里的职责置于此地---创建一个
数据库连接对象
,
检查凭证
,
还有
处理一些连接失败的问题---它会导致应用中出现
大量
重复代码。如果你尝试对这个类进行单元测试,会发现根本不可行。这个类和应用环境以及数据库高度耦合。
相反,为何不一开始就搞清楚你的类需要什么?我们只需要首先将 “PDO” 对象注入该类即可。
class AwesomeClass
{
private $dbConnection;
public function __construct(\PDO $dbConnection)
{
$this->dbConnection = $dbConnection;
}
public function doSomethingAwesome()
{
// Make magic happen with $this->dbConnection
}
}
这样更简洁清晰易懂,且更不易产生 Bug。通过类型提示和依赖注入,该方法可以清楚准确地声明它要做的事情,而无需依赖外部调用去获取。在做单元测试的时候,我们可以很好地模拟数据库连接,并将其传入使用。
依赖注入
容器
是一个工具,你可以围绕整个应用程序来处理创建和注入这些依赖关系。容器并不需要能够使用依赖注入技术,但随着应用程序的增长并变得更加复杂,它将大有裨益。
我们将使用 PHP 中最受欢迎的 DI 容器之一:名副其实的
PHP-DI
。
(值得推荐的是它文档中的
依赖注入另解
可能会对读者有所帮助)
wilson_yang 翻译于 1天前
0
重译
由
Summer
审阅
依赖注入容器
现在我们已经安装了 Composer ,那么安装 PHP-DI 就轻而易举了,我们继续回到命令行来搞定它。
composer require php-di/php-di
修改
public/index.php
用来配置和构建容器。
// ...
require_once dirname(__DIR__) . '/vendor/autoload.php';
$containerBuilder = new \DI\ContainerBuilder();
$containerBuilder->useAutowiring(false);
$containerBuilder->useAnnotations(false);
$containerBuilder->addDefinitions([
\ExampleApp\HelloWorld::class => \DI\create(\ExampleApp\HelloWorld::class)
]);
$container = $containerBuilder->build();
$helloWorld = $container->get(\ExampleApp\HelloWorld::class);
$helloWorld->announce();
没啥大不了的。它仍是一个单文件的简单示例,你很容易能看清它是怎么运行的。
迄今为止, 我们只是在
配置容器
,所以我们必须
显式地声明依赖关系
(而不是使用
自动装配
或
注解
),并且从容器中检索
HelloWorld
对象。
小贴士:自动装配在你开始构建应用程序的时候是一个很不错的特性,但是它隐藏了依赖关系,难以维护。 很有可能在接下里的岁月里, 另一个开发者在不知情的状况下引入了一个新库,然后就造就了多个库实现一个单接口的局面,这将会破坏自动装配,导致一系列让接手者很容易忽视的的不可见的问题。
尽量
引入命名空间
,可以增加代码的可读性。
<?php
declare(strict_types=1);
use DI\ContainerBuilder;
use ExampleApp\HelloWorld;
use function DI\create;
require_once dirname(__DIR__) . '/vendor/autoload.php';
$containerBuilder = new ContainerBuilder();
$containerBuilder->useAutowiring(false);
$containerBuilder->useAnnotations(false);
$containerBuilder->addDefinitions([
HelloWorld::class => create(HelloWorld::class)
]);
$container = $containerBuilder->build();
$helloWorld = $container->get(HelloWorld::class);
$helloWorld->announce();
现在看来,我们好像是把以前已经做过的事情再拿出来小题大做。
毋需烦心,当我们添加其他工具来帮助我们引导请求时,容器就有用武之地了。它会在适当的时机下按需加载正确的类。
wilson_yang 翻译于 1天前
0
重译
由
Summer
审阅
中间件
如果把你的应用想象成一个洋葱,请求从外部进入,到达洋葱中心,最后变成响应返回出去。那么中间件就是洋葱的每一层。它接收请求并且可以处理请求。要么把请求传递到更里层,要么向更外层返回一个响应(如果中间件正在检查请求不满足的特定条件,比如请求一个不存在的路由,则可能发生这种情况)。
如果请求通过了所有的层,那么程序就会开始处理它并把它转换为响应,中间件接收到响应的顺序与接收到请求的顺序相反,并且也能对响应做修改,然后再把它传递给下一个中间件。
下面是一些中间件用例的闪光点:
-
在开发环境中调试问题
-
在生产环境中优雅的处理异常
-
对传入的请求进行频率限制
-
对请求传入的不支持资源类型做出响应
-
处理跨域资源共享(CORS)
-
将请求路由到正确的处理类
那么中间件是实现这些功能的唯一方式吗?当然不是。但是中间件的实现使得你对请求 / 响应这个生命周期的理解更清晰。这也意味着你调试起来更简单,开发起来更快速。
我们将从上面列出的最后一条用例,也就是路由,当中获益。
JiaZombie 翻译于 1天前
0
重译
由
Summer
审阅
路由
路由依靠传入的请求信息来确定应当由哪个类来处理它。(例如 URI
/products/purple-dress/medium
应该被
ProductDetails::class
类接收处理,同时
purple-dress
和
medium
作为参数传入) 在范例应用中,我们将使用流行的
FastRoute
路由,基于
PSR-15兼容的中间件实现
。
中间件调度器
为了让我们的应用可以和 FastRoute 中间件---以及我们安装的其他中间件协同工作---我们需要一个中间件调度器。
PSR-15
是为中间件和调度器定义接口的中间件标准(在规范中又称“请求处理器”),它允许各式各样的中间件和调度器互相交互。我们只需选择兼容 PSR-15 的调度器,这样就可以确保它能和任何兼容 PSR-15 的中间件协同工作。 我们先安装一个
Relay
作为调度器。
composer require relay/relay:2.x@dev
而且根据 PSR-15 的中间件标准要求实现可传递
兼容 PSR-7 的 HTTP 消息
, 我们使用
Zend Diactoros
作为 PSR-7 的实现。
composer require zendframework/zend-diactoros
我们用 Relay 去接收中间件。
// ...
use DI\ContainerBuilder;
use ExampleApp\HelloWorld;
use Relay\Relay;
use Zend\Diactoros\ServerRequestFactory;
use function DI\create;
// ...
$container = $containerBuilder->build();
$middlewareQueue = [];
$requestHandler = new Relay($middlewareQueue);
$requestHandler->handle(ServerRequestFactory::fromGlobals());
我们在第 16 行使用
ServerRequestFactory::fromGlobals()
把
创建新请求的必要信息合并起来
然后把它传给 Relay。这正是
Request
进入我们中间件堆栈的起点。
现在我们继续添加 FastRoute 和请求处理器中间件。(FastRoute 确定请求是否合法,究竟能否被应用程序处理,然后请求处理器发送
Request
到路由配置表中已注册过的相应处理程序中)
composer require middlewares/fast-route middlewares/request-handler
然后我们给
Hello, world!
处理类定义一个路由。我们在此使用
/hello
路由来展示基本 URI 之外的路由。
// ...
use DI\ContainerBuilder;
use ExampleApp\HelloWorld;
use FastRoute\RouteCollector;
use Middlewares\FastRoute;
use Middlewares\RequestHandler;
use Relay\Relay;
use Zend\Diactoros\ServerRequestFactory;
use function DI\create;
use function FastRoute\simpleDispatcher;
// ...
$container = $containerBuilder->build();
$routes = simpleDispatcher(function (RouteCollector $r) {
$r->get('/hello', HelloWorld::class);
});
$middlewareQueue[] = new FastRoute($routes);
$middlewareQueue[] = new RequestHandler();
$requestHandler = new Relay($middlewareQueue);
$requestHandler->handle(ServerRequestFactory::fromGlobals());
为了能运行,你还需要修改
HelloWorld
使其成为一个可调用的类, 也就是说
这里类可以像函数一样被随意调用
。
// ...
class HelloWorld
{
public function __invoke(): void
{
echo 'Hello, autoloaded world!';
exit;
}
}
(注意在魔术方法
__invoke()
中加入
exit;
。 我们只需1秒钟就能搞定——只是不想让你遗漏这个事)
现在打开
http://localhost:8080/hello
,开香槟吧!
wilson_yang 翻译于 18小时前
0
重译
由
Summer
审阅
万能胶水
睿智的读者可能很快看出,虽然我们仍旧囿于配置和构建 DI 容器的藩篱之中,容器现在实际上对我们毫无用处。调度器和中间件在没有它的情况下也一样运作。
那它何时才能发挥威力?
嗯,如果---在实际应用程序中总是如此——
HelloWorld
类具有依赖关系呢?
我们来讲解一个简单的依赖关系,看看究竟发生了什么。
// ...
class HelloWorld
{
private $foo;
public function __construct(string $foo)
{
$this->foo = $foo;
}
public function __invoke(): void
{
echo "Hello, {$this->foo} world!";
exit;
}
}
刷新浏览器..
WOW!
看下这个
ArgumentCountError
. 发生这种情况是因为
HelloWorld
类在构造的时候需要注入一个字符串才能运行,在此之前它只能等着。
这
正是容器要帮你解决的痛点。
我们在容器中定义该依赖关系,然后将容器传给
RequestHandler
去
解决这个问题
。
// ...
use Zend\Diactoros\ServerRequestFactory;
use function DI\create;
use function DI\get;
use function FastRoute\simpleDispatcher;
// ...
$containerBuilder->addDefinitions([
HelloWorld::class => create(HelloWorld::class)
->constructor(get('Foo')),
'Foo' => 'bar'
]);
$container = $containerBuilder->build();
// ...
$middlewareQueue[] = new FastRoute($routes);
$middlewareQueue[] = new RequestHandler($container);
$requestHandler = new Relay($middlewareQueue);
$requestHandler->handle(ServerRequestFactory::fromGlobals());
嗟夫!当刷新浏览器的时候, "Hello, bar world!"将映入你的眼帘!
wilson_yang 翻译于 1天前
2
重译
由
Summer
审阅
正确地发送响应
是否还记得我之前提到过的位于
HelloWorld
类中的
exit
语句? 当我们构建代码时,它可以让我们简单粗暴的获得响应,但是它绝非输出到浏览器的最佳选择。这种粗暴的做法给
HelloWorld
附加了额外的响应工作——其实应该由其他类负责的——它会过于复杂的发送所有的头部信息和
状态码
,然后立刻退出了应用,使得
HelloWorld
之后
的中间件也无机会运行了。 记住,每个中间件都有机会在
Request
进入我们应用时修改它,然后 (以相反的顺序) 在响应输出时修改响应。 除了
Request
的通用接口, PSR-7 同样也定义了另外一种 HTTP 消息结构,以辅助我们在应用运行周期的后半部分之用:
Response
。(如果你想真正了解这些细节,请阅读
HTTP 消息以及为何 PSR-7 请求和响应标准这么碉堡
。)
修改
HelloWorld
返回一个
Response
。
// ...
namespace ExampleApp;
use Psr\Http\Message\ResponseInterface;
class HelloWorld
{
private $foo;
private $response;
public function __construct(
string $foo,
ResponseInterface $response
) {
$this->foo = $foo;
$this->response = $response;
}
public function __invoke(): ResponseInterface
{
$response = $this->response->withHeader('Content-Type', 'text/html');
$response->getBody()
->write("<html><head></head><body>Hello, {$this->foo} world!</body></html>");
return $response;
}
}
然后修改容器给
HelloWorld
提供一个新的
Response
对象。
// ...
use Middlewares\RequestHandler;
use Relay\Relay;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
use function DI\create;
// ...
$containerBuilder->addDefinitions([
HelloWorld::class => create(HelloWorld::class)
->constructor(get('Foo'), get('Response')),
'Foo' => 'bar',
'Response' => function() {
return new Response();
},
]);
$container = $containerBuilder->build();
// ...
如果你现在刷新页面,会发现一片空白。我们的应用正在从中间件调度器返回正确的
Response
对象,但是... 肿么回事?