本文概述
去年, 我参加了三个大型项目。我的任务是摆脱基于PHP和服务器端HTML生成的旧体系结构, 并过渡到REST API。
使用旧方法时, 后端开发人员应该对应用程序的UI和视觉方面有更多的了解。因此, 他们必须注意应用程序的不同部分, 而不是专注于其主要目标。将后端API与用户界面严格分开, 使我们的开发人员可以专注于其代码的质量。
此外, 由于可以通过自动单元测试来验证REST API, 因此测试API服务要容易得多。
我在编写自己的框架方面有丰富的经验, 并且与Yii, CakePHP, CodeIgniter, Slim Framework, Symfony和其他一些开源框架一起工作。每次, 我都会遇到一些功能上的不足或笨拙的方法。
在决定选择它作为下一个项目的平台之前, 我使用了Laravel四个月。该项目本身取得了巨大的成功, 本文是这种经验的产物。现在, 我可以称自己为Laravel开发人员了。
为什么我选择Laravel
我已经概述了使用Laravel的一些原因和经验, 所以让我们仔细看看是什么使Laravel成为我最新项目的更好选择:
- 可以扩展的快速实用的核心
- 干净简单的路由
- 有效的ORM和数据库层
- 与第三方库(AWS, 导出库等)的轻松集成。你可以使用Composer或Packagist在你的项目中包含库
- 活跃且不断发展的社区可以提供快速的支持和解答
- 开箱即用的支持单元测试
- 异步队列和后台作业, 用于长时间运行的任务
Laravel核心和路由
Laravel内核托管在GitHub上。内核实现了一个IoC模式, 允许自定义和重写框架的任何部分(请求, 日志记录, 身份验证等)。
Laravel的设计人员并没有花太多时间来重新发明轮子。许多解决方案和实践都从其他框架转移而来。这种方法的一个很好的例子是名为Artisan的扩展Symfony控制台, 它是Laravel附带的命令行界面。
路由
Laravel中的路由很棒。它与Ruby on Rails(RoR)实现非常相似, 我对此非常喜欢。你可以轻松地对路由进行分组, 为CRUD页面创建资源, 附加过滤器并将模型自动绑定到请求参数。
嵌套路由是一个非常有用的功能:
Route::group(['prefix'=>'level0'], function(){
Route::get('/', array('uses' => '[email protected]'));
Route::group(['prefix'=>'/{level0}/level1'], function(){
Route::get('/', array('uses' => '[email protected]'));
Route::post('/{custom_variable}/custom_route', array('uses' => '[email protected]_route'));
});
});
可以使用” v1″前缀在所有嵌套路由的顶层将版本控制作为一个组来实现。当我们更改API版本时, 我们可以保留旧版本并使用” v2″前缀来开始使用新代码和逻辑(即对控制器和操作的新引用)来实现路由。
让我们逐步看一下此Laravel教程中使用的所有内容:
已定义的一组路由, 其路径级别0在我们的API的顶层。
如果我们有一个调用BASEURL / level0, 那么Laravel将解决它并调用TestController的level0()方法来处理请求。
我们有一个带有{level0} / level1模式的子组。要访问该组中的API资源, 我们应该使用父组(level0)的路径, 并匹配子组({level0} / level1)的模式。例如, level0 / 777 / level1是此API子组的正确路径。在这里, 我们有777作为变量level0, Laravel会将其传递到子组内路由的处理程序。
最后, 我们有两个示例路线:
BASEURL / level0 / 777 / level1-对此URI的GET请求将使用TestController的方法level1($ level0)处理, 其中第一个参数为{level0}, 并将其初始化为值777。
BASEURL / level0 / 777 / level1 / 888 / custom_variable-将使用CustomController的custom_route($ level0, $ custom_variable)方法处理对此URI的POST请求。
该方法中使用的参数来自路径变量。
最后, 我们可以将路线中的变量{level0}与模型(即LevelModel)相关联。在这种情况下, 框架会自动尝试查找现有的数据库记录, 并将其作为参数传递给控制器的方法。
它可以帮助我们减少代码编写量, 并且不需要在控制器中编写LevelModel :: find($ id)或LevelModel :: findOrFail($ id)。
Dingo API程序包使路由更进一步。
以下是Dingo向框架提供的一些功能:
- 变压器:用于响应自定义的特殊对象(例如, 类型转换整数和布尔值, 分页结果, 包含关系等)
- 保护路由并允许其他/自定义身份验证提供程序。 Controller Trait提供了自定义操作的保护, 而我们可以使用API :: user()轻松检索经过身份验证的用户信息。
- 通过使用内置或自定义限制来限制每个用户的请求数。
- 强大的内部请求机制, 允许在内部对API进行请求
Laravel使用雄辩的ORM
Laravel基于雄辩的ORM。我已经在PostgreSQL和MySQL中使用了它, 并且在两种情况下它都表现完美。
官方文档很全面, 因此没有必要在本文中重复说明:
查询范围–查询逻辑是作为具有特殊范围前缀的函数创建的。
以下示例显示了在Eloquent ORM中转换为查询功能的标准SQL查询:
SELECT * WHERE hub_id = 100 AND (name LIKE `%searchkey%` OR surname LIKE `%searchkey%`):
function scopeByFieldListLike($query, $fields, $value){
$query->where(function($query) use ($fields, $value){
foreach($fields as $field){
$query->orWhere($field , 'like', "%".$value."%");
}
});
return $query;
}
在代码中使用函数很简单。
$model = new User;
$model->byFieldListLike(['name', 'surname'], 'searchkey');
许多雄辩的方法返回QueryBuilder的实例。你几乎可以在模型上使用所有这些方法。
单元测试
编写单元测试通常很耗时, 但是, 绝对值得的, 所以请这样做。
Laravel依赖于TestCase基类来完成此任务。它创建应用程序的新实例, 解析路由并在其自己的沙箱中运行controller方法。但是, 它不运行应用程序筛选器(App :: before和App :: after)或更复杂的路由方案。为了在沙盒环境中启用这些过滤器, 我必须使用Route :: enableFilters()手动启用它们。
显然, 如果你知道如何使用Laravel, 就知道它可能会在单元测试部分使用更多的工作。我设置了一些功能来帮助我创建更高级的单元测试。随意使用它们并将其用于你的项目中。
为了执行更高级的现实生活测试并实现”类似curl”的请求, 我使用了kriswallsmith / buzz库。这为我提供了急需的测试功能集, 包括自定义标题和上传文件。
下面的代码显示了可以用来执行这种测试的函数:
public function browserRequest($method, $resource, $data = [], $headers = [], $files = [])
{
$host = $this->baseUrl();
if (count($files)){
// Use another form request for handling files uploading
$this->_request = new Buzz\Message\Form\FormRequest($method, $resource, $host);
if (isset($headers['Content-Type'])) {
// we don't need application/json, it should form/multipart-data
unset($headers['Content-Type']);
}
$this->_request->setHeaders($headers);
$this->_request->setFields($data);
foreach($files as $file) {
$upload = new Buzz\Message\Form\FormUpload($file['path']);
$upload->setName($file['name']);
$this->_request->setField($file['name'], $upload);
}
$response = new Buzz\Message\Response();
$client = new Buzz\Client\FileGetContents();
$client->setTimeout(60);//Set longer timout, default is 5
$client->send($this->_request, $response);
} else {
$this->_request = new Buzz\Message\Request($method, $resource, $host);
$this->_request->setContent(json_encode($data));
$this->_request->setHeaders($headers);
$response = new Buzz\Message\Response();
$client = new Buzz\Client\FileGetContents();
$client->setTimeout(60);//Set longer timout, default is 5
$client->send($this->_request, $response);
}
return $response;
}
我是一个非常懒惰的开发人员, 因此我添加了可选参数, 使我可以解码响应, 提取数据并检查响应代码。这导致了另一种方法, 其中包括默认头, 授权令牌和一些其他功能。
public function request($method, $path, $data = [], $autoCheckStatus = true, $autoDecode = true, $files = [])
{
if (!is_array($data)){
$data = array();
}
$servers = ['Content-Type' => 'application/json'];
if ($this->_token){
$servers['authorization'] = 'Token ' . $this->_token;
}
$this->_response = $this->browserRequest($method, $path, $data, $servers, $files);
if ($autoCheckStatus === true){
$this->assertTrue($this->_response->isOk());
} elseif(ctype_alnum($autoCheckStatus)){
$this->assertEquals($autoCheckStatus, $this->_response->getStatusCode());
}
if ($autoDecode){
$dataObject = json_decode($this->_response->getContent());
if (is_object($dataObject))
{
// we have object at response
return $this->_dataKeyAtResponse && property_exists($dataObject, $this->_dataKeyAtResponse) ? $dataObject->{$this->_dataKeyAtResponse} : $dataObject;
}
elseif (is_array($dataObject))
{
// perhaps we have a collection
return $this->_dataKeyAtResponse && array_key_exists($this->_dataKeyAtResponse, $dataObject) ? $dataObject[$this->_dataKeyAtResponse] : $dataObject;
}
else
{
// Uknown result
return $dataObject;
}
} else {
return $this->_response->getContent();
}
}
再一次, 我的懒惰习惯使我添加了旨在测试CRUD页面的包装器:
create($data = [], $autoCheckStatus = true, $autoDecode = true, $files = [])
show($id, $data = [], $autoCheckStatus = true, $autoDecode = true)
update($id, $data = [], $autoCheckStatus = true, $autoDecode = true, $files = [])
delete($id, $data = [], $autoCheckStatus = 204, $autoDecode = false)
index($data = [], $autoCheckStatus = true, $autoDecode = true)
一个简单的测试代码如下所示:
// send wrong request
// Validation error has code 422 in out specs
$response = $this->create(['title'=>'', 'intro'=>'Ha-ha-ha. We have validators'], 422);
// Try to create correct webshop
$dataObject = $this->create(
$data = [
'title'=>'Super webshop', 'intro'=>'I am webshop', ]
);
$this->assertGreaterThan(0, $dataObject->id);
$this->assertData($data, $dataObject);
// Try to request existing resource with method GET
$checkData = $this->show($dataObject->id);
// assertData is also a help method for validation objects with nested structure (not a part of PHPUnit).
$this->assertData($data, $checkData);
// Try to update with not valid data
$this->update($dataObject->id, ['title'=> $data['title'] = ''], 422);
// Try to update only title. Description should have old value. Title - changed
$this->update($dataObject->id, ['title'=> $data['title'] = 'Super-Super SHOP!!!']);
// Then check result with reading resource
$checkData = $this->show($dataObject->id);
$this->assertData($data, $checkData);
// Read all created resources
$list = $this->index();
$this->assertCount(1, $list);
// TODO:: add checking for each item in the collection
// Delete resoure
$this->delete($dataObject->id);
$this->show($dataObject->id, [], 404);
这是用于验证包括关系和嵌套数据的响应数据的另一个有用功能:
public function assertData($input, $checkData){
// it could be stdClass object after decoding json response
$checkData = (array)$checkData;
foreach($input as $k=>$v){
if (is_array($v) && (is_object($checkData[$k]) || is_array($checkData[$k]))) {
// checking nested data recursively only if it exists in both: response data($input) and expected($checkData)
$this->assertData($v, $checkData[$k]);
} else {
$this->assertEquals($v, $checkData[$k]);
}
}
}
排队
长时间运行的任务是Web应用程序中的常见瓶颈。一个简单的例子就是创建PDF报告并在组织内部分发它, 这会花费很多时间。
阻止用户执行此操作的请求不是解决此问题的理想方法。在Laravel中使用Queue是在后台处理和排队长时间运行的任务的好方法。
最后的问题之一是PDF的生成和通过电子邮件发送报告。我使用了Beanstalkd的默认队列驱动程序, 该软件包由pda / pheanstalk软件包支持。将任务添加到队列非常容易
例如, 可以这样生成和发送PDF:
\Queue::push('\FlexiCall\Queue\PdfSend', [....data for sending to job's handler..], 'default');
我们的PdfSend处理程序可以这样实现:
class PdfSend extends BaseQueue{
function fire($job, $data){
//.............................
//..... OUR IMPLEMENTATION.....
//.............................
}
}
Laravel规范建议使用–daemon选项跟踪队列, 但是我使用了主管守护程序来使其永久运行:
php artisan queue:listen --env=YOUR_ENV
队列非常适合保存数据之类的任务。它们可用于重复失败的作业, 在执行作业之前添加睡眠超时等。
放在一起
我在这篇Laravel评论中提到了与Laravel开发有关的几个关键点。如何将它们组合在一起并构建应用程序将取决于你的特定设置和项目。但是, 这是你可能要考虑的步骤的简要清单:
- 设置Homestead Vagrant框, 准备使用Laravel和已配置的本地环境进行开发。
- 添加端点(配置路由)。
- 添加身份验证层。
- 在应用程序执行之前/之后添加过滤器, 使你可以选择在应用程序执行之前和之后运行任何内容。此外, 你可以为任何路线定义过滤器。
我的默认”之前”过滤器将全局范围创建为单例并启动计时器以监视性能:
class AppBefore {
function filter($request) {
\App::singleton('scope', function() {
$scope = new AppScope();//My scope implementation
$scope->startTimer();
return $scope;
});
}
}
“之后”过滤器停止计时器, 将其记录到性能日志中, 并发送CORS标头。
class AppAfter{
function filter($request, $response){
$response->header('Access-Control-Allow-Origin', '*');
$response->header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
$response->header('Access-Control-Allow-Headers', 'Content-Type');
$response->header('Access-Control-Max-Age', '86400');
\App::make('scope')->endTimer();
\App::make('scope')->logTotalTime();
return $response;
}
}
开发每个模块可能需要执行以下步骤:
设置数据迁移, 并为默认数据设定种子。实施模型和关系, 转换器, 验证器和消毒器(如果需要)。编写单元测试。实施控制器并执行测试。
本文总结
我已经使用Laravel一年多了。与任何技术一样, 也存在一些棘手的问题, 但是我相信学习Laravel使我确信这是一个了不起的框架。在此Laravel教程中, 我还没有提到其他几件事, 但是如果你想了解更多信息, 则可以考虑以下几点:
- 订阅和侦听应用程序事件的强大模型。
- 支持Amazon SDK aws / aws-sdk-php。
- 正在缓存。
- 自己的模板引擎称为Blade, 如果你希望以”旧方式”构建应用程序而无需RESTful后端和UI分离。
- 配置简单。
- 内置条纹计费模块。
- 开箱即用的本地化。
- 捕获和处理大多数错误。
祝你好运, 敬请期待, Laravel是一个很有前途的框架, 我相信它将在未来几年内出现。
评论前必须登录!
注册