关键要点
本文由Thomas Punt同行评审。感谢所有SitePoint的同行评审员,使SitePoint的内容达到最佳状态!
几乎在每一次会议上,都会讨论到PHP异步编程这个话题。我很高兴它现在如此频繁地被提及。但是,这些演讲者没有透露一个秘密……
创建异步服务器、解析域名、与文件系统交互:这些都是简单的事情。创建你自己的异步库很难。而这正是你花费大部分时间的地方!
这些简单的事情之所以简单,是因为它们是概念验证——使异步PHP与NodeJS竞争。你可以看到它们的早期接口有多相似:
var http = require("http"); var server = http.createServer(); server.on("request", function(request, response) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Hello World"); }); server.listen(3000, "127.0.0.1");
此代码使用Node 7.3.0测试
require "vendor/autoload.php"; $loop = React\EventLoop\Factory::create(); $socket = new React\Socket\Server($loop); $server = new React\Http\Server($socket); $server->on("request", function($request, $response) { $response->writeHead(200, [ "Content-Type" => "text/plain" ]); $response->end("Hello world"); }); $socket->listen(3000, "127.0.0.1"); $loop->run();
此代码使用PHP 7.1和react/http:0.4.2测试
今天,我们将研究一些方法,使你的应用程序代码在异步架构中良好运行。别担心——你的代码仍然可以在同步架构中工作,因此你不必为了学习这项新技能而放弃任何东西。除了花费一些时间……
你可以在Github上找到本教程的代码。我已经用PHP 7.1和最新版本的ReactPHP和Amp测试过它了。
充满希望的理论
异步代码有一些常见的抽象。我们已经看到其中一个:回调。回调顾名思义,描述了它们如何处理缓慢或阻塞操作。同步代码充满了等待。请求某些东西,等待事情发生。
因此,异步框架和库可以使用回调。请求某些东西,当它发生时:框架或库将回调你的代码。
在HTTP服务器的情况下,我们不会抢先处理所有请求。我们也不会等待请求发生。我们只是描述应该调用的代码,如果请求发生的话。事件循环负责其余的工作。
第二个常见的抽象是Promise。回调是等待未来事件的钩子,而Promise是对未来值的引用。它们看起来像这样:
var http = require("http"); var server = http.createServer(); server.on("request", function(request, response) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Hello World"); }); server.listen(3000, "127.0.0.1");
这比单独使用回调多了一些代码,但这是一种有趣的方法。我们等待某些事情发生,然后做另一件事。如果出现问题,我们会捕获错误并做出合理的响应。这看起来很简单,但并没有被充分讨论。
我们仍在使用回调,但我们已经将它们包装在一个抽象中,这在其他方面对我们有帮助。一个好处是它们允许多个解析回调……
require "vendor/autoload.php"; $loop = React\EventLoop\Factory::create(); $socket = new React\Socket\Server($loop); $server = new React\Http\Server($socket); $server->on("request", function($request, $response) { $response->writeHead(200, [ "Content-Type" => "text/plain" ]); $response->end("Hello world"); }); $socket->listen(3000, "127.0.0.1"); $loop->run();
我还想让我们关注另一件事。那就是Promise提供了一种通用的语言——一种通用的抽象——来思考同步代码如何变成异步代码。
让我们获取一些应用程序代码并使其异步,使用Promise……
制作PDF文件
应用程序生成某种摘要文档是很常见的——无论是发票还是库存清单。假设你有一个电子商务应用程序,它通过Stripe处理付款。当客户购买商品时,你希望他们能够下载该交易的PDF收据。
你可以通过多种方式做到这一点,但一种非常简单的方法是使用HTML和CSS生成文档。你可以将其转换为PDF文档,并允许客户下载它。
我最近需要做类似的事情。我发现没有很多好的库支持这种操作。我找不到单个抽象可以让我在不同的HTML→PDF引擎之间切换。所以我开始自己构建一个。
我开始考虑我的抽象需要做什么。我选择了一个非常类似的接口:
readFile() ->then(function(string $content) { print "content: " . $content; }) ->catch(function(Exception $e) { print "error: " . $e->getMessage(); });
为了简单起见,我希望除了render方法之外的所有方法都能充当getter和setter。给定这组预期方法,接下来要做的是创建一个实现,使用一个可能的引擎。我将domPDF添加到我的项目中,并开始使用它:
$promise = readFile(); $promise->then(...)->catch(...); // ...让我们向现有代码添加日志记录 $promise->then(function(string $content) use ($logger) { $logger->info("file was read"); });
我不会详细介绍如何使用domPDF。我认为文档做得足够好,让我可以专注于此实现的异步部分。
我们稍后会查看data和parallel方法。关于此Driver实现的重要一点是它将数据(如果已设置,否则为默认值)和自定义选项收集在一起。它将这些传递给我们希望异步运行的回调。
domPDF不是异步库,将HTML转换为PDF是一个非常缓慢的过程。那么我们如何使其异步呢?好吧,我们可以编写一个完全异步的转换器,或者我们可以使用现有的同步转换器;但在并行线程或进程中运行它。
这就是我为parallel方法所做的:
var http = require("http"); var server = http.createServer(); server.on("request", function(request, response) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Hello World"); }); server.listen(3000, "127.0.0.1");
在这里,我实现了getter-setter方法,认为我可以将它们重用于下一个实现。data方法充当收集各种文档属性到数组中的快捷方式,使它们更容易传递给匿名函数。
parallel方法开始变得有趣:
require "vendor/autoload.php"; $loop = React\EventLoop\Factory::create(); $socket = new React\Socket\Server($loop); $server = new React\Http\Server($socket); $server->on("request", function($request, $response) { $response->writeHead(200, [ "Content-Type" => "text/plain" ]); $response->end("Hello world"); }); $socket->listen(3000, "127.0.0.1"); $loop->run();
我非常喜欢Amp项目。它是一组支持异步架构的库的集合,它们是async-interop项目的关键支持者。
他们的一个库称为amphp/parallel,它支持多线程和多进程代码(通过Pthreads和Process Control扩展)。这些spawn方法返回Amp的Promise实现。这意味着render方法可以像任何其他返回Promise的方法一样使用:
readFile() ->then(function(string $content) { print "content: " . $content; }) ->catch(function(Exception $e) { print "error: " . $e->getMessage(); });
这段代码有点复杂。Amp还提供了一个事件循环实现和所有辅助代码,以便能够将普通的PHP生成器转换为协程和Promise。你可以在我写的另一篇文章中阅读这甚至是如何可能的,以及它与PHP的生成器有什么关系。
返回的Promise也正在标准化。Amp返回Promise规范的实现。它与我上面显示的代码略有不同,但仍然执行相同的函数。
生成器的工作方式类似于具有协程的语言中的协程。协程是可以中断的函数,这意味着它们可以用于执行短时间的操作,然后在等待某些事情时暂停。暂停时,其他函数可以使用系统资源。
实际上,这看起来像这样:
$promise = readFile(); $promise->then(...)->catch(...); // ...让我们向现有代码添加日志记录 $promise->then(function(string $content) use ($logger) { $logger->info("file was read"); });
这看起来比一开始只编写同步代码复杂得多。但它允许的是,当我们等待funcReturnsPromise完成时,其他事情可以发生。
生成Promise正是我们所说的抽象。它为我们提供了一个框架,我们可以通过它来创建返回Promise的函数。代码可以以可预测和可理解的方式与这些Promise交互。
看看使用我们的驱动程序呈现PDF文档的样子:
interface Driver { public function html($html = null); public function size($size = null); public function orientation($orientation = null); public function dpi($dpi = null); public function render(); }
这不如在异步HTTP服务器中生成PDF那样有用。有一个名为Aerys的Amp库,它使创建这些类型的服务器更容易。使用Aerys,你可以创建以下HTTP服务器代码:
class DomDriver extends BaseDriver implements Driver { private $options; public function __construct(array $options = []) { $this->options = $options; } public function render() { $data = $this->data(); $custom = $this->options; return $this->parallel( function() use ($data, $custom) { $options = new Options(); $options->set( "isJavascriptEnabled", true ); $options->set( "isHtml5ParserEnabled", true ); $options->set("dpi", $data["dpi"]); foreach ($custom as $key => $value) { $options->set($key, $value); } $engine = new Dompdf($options); $engine->setPaper( $data["size"], $data["orientation"] ); $engine->loadHtml($data["html"]); $engine->render(); return $engine->output(); } ); } }
同样,我现在不会详细介绍Aerys。这是一个令人印象深刻的软件,非常值得拥有自己的文章。你不需要理解Aerys的工作原理就能看到我们的转换器代码在它旁边看起来有多自然。
我的老板说“不要用异步!”
如果你不确定你多久才能构建异步应用程序,为什么要费这么大的劲?编写此代码使我们能够深入了解新的编程范例。而且,仅仅因为我们正在将此代码编写为异步的,并不意味着它不能在同步环境中工作。
要在同步应用程序中使用此代码,我们只需要将一些异步代码移到内部:
abstract class BaseDriver implements Driver { protected $html = ""; protected $size = "A4"; protected $orientation = "portrait"; protected $dpi = 300; public function html($body = null) { return $this->access("html", $html); } private function access($key, $value = null) { if (is_null($value)) { return $this->$key; } $this->$key = $value; return $this; } public function size($size = null) { return $this->access("size", $size); } public function orientation($orientation = null) { return $this->access("orientation", $orientation); } public function dpi($dpi = null) { return $this->access("dpi", $dpi); } protected function data() { return [ "html" => $html, "size" => $this->size, "orientation" => $this->orientation, "dpi" => $this->dpi, ]; } protected function parallel(Closure $deferred) { // TODO } }
使用此装饰器,我们可以编写看起来像是同步代码的代码:
var http = require("http"); var server = http.createServer(); server.on("request", function(request, response) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Hello World"); }); server.listen(3000, "127.0.0.1");
它仍然异步运行代码(至少在后台),但所有这些都不会暴露给消费者。你可以在同步应用程序中使用它,并且永远不会知道幕后发生了什么。
支持其他框架
Amp具有一些特定的要求,使其不适合所有环境。例如,基本Amp(事件循环)库需要PHP 7.0。parallel库需要Pthreads扩展或Process Control扩展。
我不想强加这些限制给每个人,并且想知道我如何才能支持更广泛的系统。答案是将并行执行代码抽象到另一个驱动程序系统中:
require "vendor/autoload.php"; $loop = React\EventLoop\Factory::create(); $socket = new React\Socket\Server($loop); $server = new React\Http\Server($socket); $server->on("request", function($request, $response) { $response->writeHead(200, [ "Content-Type" => "text/plain" ]); $response->end("Hello world"); }); $socket->listen(3000, "127.0.0.1"); $loop->run();
我可以为Amp以及(限制较少,但更旧的)ReactPHP实现它:
readFile() ->then(function(string $content) { print "content: " . $content; }) ->catch(function(Exception $e) { print "error: " . $e->getMessage(); });
我习惯于将闭包传递给多线程和多进程工作程序,因为这就是Pthreads和Process Control的工作方式。使用ReactPHP Process对象完全不同,因为它们依赖于exec进行多进程执行。我决定实现我习惯使用的相同闭包功能。这对于异步代码来说不是必需的——这纯粹是品味的问题。
SuperClosure库序列化闭包及其绑定的变量。这里的大部分代码都是你期望在worker脚本中找到的代码。事实上,使用ReactPHP的子进程库的唯一方法(除了序列化闭包之外)是将任务发送到worker脚本。
现在,我们不再使用$this->parallel和Amp特定的代码加载我们的驱动程序,而是可以传递运行程序实现。作为异步代码,这类似于:
$promise = readFile(); $promise->then(...)->catch(...); // ...让我们向现有代码添加日志记录 $promise->then(function(string $content) use ($logger) { $logger->info("file was read"); });
不要被ReactPHP代码与Amp代码的不同之处所震惊。ReactPHP没有实现与Amp相同的协程基础。相反,ReactPHP更喜欢使用回调来处理大多数事情。这段代码仍然只是并行运行PDF转换,并返回生成的PDF数据。
通过抽象的运行程序,我们可以使用任何我们想要的异步框架,并且我们可以预期我们将使用的驱动程序将返回该框架的抽象。
我可以使用这个吗?
最初只是一个实验,变成了一个多驱动程序、多运行程序的HTML→PDF库;称为Paper。它就像HTML→PDF的Flysystem等效项,但它也是如何编写异步库的一个很好的例子。
当你尝试制作异步PHP应用程序时,你会发现库生态系统中的差距。不要被这些吓倒!相反,抓住机会思考一下你将如何使用ReactPHP和Amp提供的抽象来制作你自己的异步库。
你最近是否构建了一个有趣的异步PHP应用程序或库?请在评论中告诉我们。
关于异步转换HTML为PDF的常见问题解答(FAQ)
异步编程在将HTML转换为PDF方面起着至关重要的作用。它允许执行非阻塞操作,这意味着引擎在后台运行,允许你的其余代码在异步操作完成时继续执行。这导致更有效地利用资源并提高性能,尤其是在涉及大量I/O操作的应用程序中,例如将HTML转换为PDF。
ReactPHP是一个用于PHP中事件驱动编程的低级库。它为在PHP中创建异步库提供了核心基础设施。使用ReactPHP,你可以使用PHP熟悉的语法编写非阻塞代码,从而更容易创建高性能应用程序。
异步转换HTML为PDF的过程涉及几个步骤。首先,你需要设置一个HTML模板,该模板定义PDF的结构和内容。接下来,你使用ReactPHP之类的异步库来处理转换过程。这包括读取HTML文件,将其转换为PDF,然后保存生成的PDF文件。此过程的异步性质意味着你的应用程序可以在转换进行时继续执行其他任务。
是的,你可以使用其他语言进行异步编程。例如,Node.js由于其事件驱动的架构,是构建异步应用程序的流行选择。但是,如果你已经熟悉PHP,那么像ReactPHP这样的库可以让你轻松利用异步编程的优势,而无需学习新的语言。
错误处理是异步编程的一个重要方面。在ReactPHP中,你可以通过将错误事件处理程序附加到Promise对象来处理错误。如果在转换过程中发生错误,则将调用此处理程序,允许你记录错误或采取其他适当的操作。
将HTML转换为PDF有很多好处。它允许你创建一个静态的、可移植的网页版本,可以脱机查看、打印或轻松共享。PDF还保留了原始HTML的格式和布局,确保内容无论在什么设备或平台上查看都看起来相同。
有几种方法可以优化异步PHP应用程序的性能。一种方法是使用ReactPHP之类的库,它为事件驱动编程提供低级接口。这允许你编写非阻塞代码,这可以显著提高I/O密集型操作(如将HTML转换为PDF)的性能。
是的,可以同步转换HTML为PDF。但是,这种方法可能会阻塞你的应用程序的执行,直到转换过程完成,这可能会导致I/O密集型应用程序出现性能问题。另一方面,异步转换允许你的应用程序在转换进行时继续执行其他任务,从而获得更好的性能和资源利用率。
由于PHP的同步特性,PHP中的异步编程可能具有挑战性。但是,像ReactPHP这样的库提供了在PHP中编写非阻塞代码所需的架构。理解事件驱动编程模型并掌握Promise的使用也可能具有挑战性,但它们是利用异步编程优势的关键。
测试异步PHP应用程序的性能包括在不同的负载条件下测量关键指标,例如响应时间、内存使用率和CPU利用率。Apache JMeter或Siege之类的工具可用于模拟应用程序上的负载并收集性能数据。此外,Xdebug之类的分析工具可以帮助你识别代码中的瓶颈并优化其性能。
以上是编写异步库 - 让#x27; s将html转换为pdf的详细内容。更多信息请关注PHP中文网其他相关文章!