附录B:MVC 和 框架
我们在不同的章节中介绍了一些框架的名称,但没有对它们进行讨论。在当今世界,我们不会再次发明轮子;我们会利用已经构建、测试和广泛使用的工具。因此,作为最佳实践,如果没有可用的工具来满足需求,我们可以使用最适合需求的框架来构建。
我们将讨论以下主题:
-
MVC 设计模式
-
Laravel
-
Lumen
-
Apigility
MVC 设计模式
模型视图控制器(MVC)是一种广泛应用于不同编程语言的设计模式。大多数 PHP 框架都使用这种设计模式。这种模式将应用程序分为三层: 模型、视图和控制器。其中每一层都有独立的任务,并且相互关联。MVC 有不同的可视化表示法,但下图是一个整体而简单的表示法:
现在,我们来讨论 MVC 设计模式的各个部分。
Model
模型层是应用程序的支柱,负责处理数据逻辑。通常,人们认为模型负责数据库的 CRUD 操作,这可能是对的,也可能是错的。如前所述,模型负责数据逻辑,这意味着数据验证操作也可以在这里进行。简单地说,模型为数据提供了一个抽象。其余的应用层并不知道或不关心数据是如何来的,从哪里来的,或者如何对数据进行操作。模型有责任处理所有数据逻辑。
在当今复杂的框架结构中,整体 MVC 结构发生了变化,模型不仅要处理数据操作,还要处理其他所有应用逻辑。我们采用的方法是胖模型和瘦控制器,这意味着所有应用逻辑都放在模型中,而控制器则尽可能保持干净。
Views
视图是最终用户可见的东西。与该用户和公众相关的所有数据都显示在视图中,因此视图可称为模型的可视化表示。视图需要数据才能显示。它要求控制器提供一些特定的数据或操作。视图不知道也不想知道控制器从哪里获取这些数据,它只是要求控制器获取这些数据。控制器知道该向谁索取这些特定数据,并与特定模型进行通信。这意味着视图与模型没有任何直接联系。然而,在前面的图表中,我们直接将模型与视图联系起来。这是因为在现在的高级系统中,视图可以直接从模型获取数据。例如,Magento 控制器不能将数据发送回视图。为了获取数据(即直接从数据库获取数据)和/或与模型通信,视图需要与块和辅助类通信。在现代实践中,视图可以直接连接到模型。
Controllers
控制器响应用户在视图中执行的操作,并对视图做出响应。例如,用户填写表格并提交。在这里,控制器处于中间位置,开始对提交的表单采取行动。现在,控制器将首先检查用户是否被允许提出此请求。然后,控制器将采取适当的操作,如与模型通信或其他操作。打个简单的比方,控制器就是视图和模型之间的中间人。正如我们之前在模型部分提到的,控制器应该是纤细的。因此,控制器大多只用于处理请求以及与模型和视图通信。所有类型的数据操作都在模型中执行。
MVC 设计模式的唯一任务就是将应用程序中不同部分的职责分开。因此,模型用于管理应用程序数据。控制器用于对用户输入进行操作,而视图则负责数据的可视化表示。正如我们之前提到的,MVC 分离了各个部分的职责,因此从控制器还是视图访问模型并不重要;唯一重要的是,视图和控制器不应用于对数据执行操作,因为这是模型的职责,而控制器不应用于终端用户查看任何类型的数据,因为这是视图的职责。
Laravel
Laravel 是最流行的 PHP 框架之一,根据 Laravel 官方网站的介绍,它是一个面向网络工匠的框架。Laravel 美观大方、功能强大,拥有大量功能,可以帮助开发人员编写高效、高质量的代码。Laravel 的官方文档写得很好,非常容易理解。那么,让我们来玩玩 Laravel 吧。
安装
安装非常简单方便。让我们使用 Composer 来安装 Laravel。我们在附录 A 中讨论了 Composer。在终端发出以下命令,安装并创建一个 Laravel 项目:
composer create-project --prefer-dist laravel/laravel packt
如果系统中没有全局安装 Composer,请将 composer.phar 放在应安装 Laravel 的目录中,并在该目录根目录下的终端中发出以下命令:
php composer.phar create-project --prefer-dist laravel/laravel packt
现在,将下载 Laravel,并创建名为 packt 的新项目。同时,Composer 会下载并安装项目的所有依赖项。
打开浏览器,进入项目的 URL,我们会看到一个简洁的页面,上面写着 Laravel 5。
|
在编写本书时,Laravel 5.2.29 是最新版本。不过,如果使用 Composer,那么每次使用 |
功能
Laravel 提供了大量的功能,我们在这里只讨论其中的一些。
路由
Laravel 提供强大的路由功能。路由可以分组,路由组可以定义前缀、命名空间和中间件。此外,Laravel 支持所有 HTTP 方法,包括 POST、GET、DELETE、PUT、OPTIONS 和 PATCH。所有路由都在应用程序 app 文件夹中的 routes.php 文件中定义。请看下面的示例:
Route::group(['prefix' => 'customer', 'namespace' => 'Customer',
'middleware' => 'web'], function() {
Route::get('/', 'CustomerController@index');
Route::post('save', 'CustomerController@save');
Route::delete('delete/{id}', 'CustomerController@delete');
});
在前面的代码段中,我们创建了一个新的路由组。只有当 URL 的前缀是 customer 时,才会使用该组。例如,如果 URL 类似于 domain.com/customer 我们还使用了客户命名空间。命名空间允许我们使用标准的 PHP 命名空间,并将文件划分到子文件夹中。在前面的示例中,所有客户控制器都可以放在 Controllers 目录中的 Customer 子文件夹中,控制器的创建过程如下:
namespace App\Http\Controllers\Customer
use App\Http\{
Controllers\Controller,
Requests,
};
use Illuminate\Http\Request;
Class CustomerController extends Controller
{
…
…
}
因此,通过路由组的命名,我们可以将控制器文件放在子文件夹中,便于管理。此外,我们还使用了网络中间件。中间件提供了一种在请求进入应用程序之前对请求进行过滤的方法,这使我们能够使用它来检查用户是否登录、CSRF 保护,或者是否有其他可以在中间件中执行的操作需要在请求发送到应用程序之前执行。Laravel 自带了一些中间件,包括 web、api、auth 等。
如果路由被定义为 GET,就不能向该路由发送 POST 请求。这非常方便,让我们不必担心请求方法过滤的问题。然而,HTML 表单并不支持 DELETE、PATCH 和 PUT 等 HTTP 方法。为此,Laravel 提供了方法欺骗(method spoofing)功能,即使用 name_method 的隐藏表单字段和 HTTP 方法值来实现请求。例如,在路由组中,如果要删除路由,我们需要一个类似下面的表单:
<form action="/customer/delete" method="post">
{{ method_field('DELETE') }}
{{ csrf_field() }}
</form>
当提交前一个表单时,它将起作用,并使用删除路由。此外,我们还创建了一个 CSRF 隐藏字段,用于 CSRF 保护。
|
Laravel 路由非常有趣,也是一个大话题。更多详细信息,请访问 https://laravel.com/docs/5.2/routing。 |
Eloquent ORM
Eloquent ORM 提供与数据库交互的活动记录。要使用 Eloquent ORM,我们只需从 Eloquent 模型中扩展我们的模型。让我们来看看一个简单的用户模型,如下所示:
namespace App;
use Illuminate\Database\Eloquent\Model;
class user extends Model
{
//protected $table = 'customer';
//protected $primaryKey = 'id_customer';
…
…
}
就这样,我们有了一个可以处理所有 CRUD 操作的模型。请注意,我们注释了 $table 属性,同样也注释了 $primaryKey。这是因为 Laravel 使用类的复数名称来查找表,除非表是用受保护的 $table 属性定义的。在我们的例子中,Laravel 会查找表名 users 并使用它。不过,如果我们想使用名为 customers 的表,只需取消这行注释即可,如下所示:
protected $table = 'customers';
同样,Laravel 认为表的主键列名为 id。但是,如果需要另一列,我们可以覆盖默认的主键,如下所示:
protected $primaryKey = 'id_customer';
Eloquent 模型还能轻松处理时间戳。默认情况下,如果表有 created_at 和 updated_at 字段,那么这两个日期将自动生成并保存。如果不需要时间戳,可以禁用它们,如下所示:
protected $timestamps = false;
向表中保存数据很简单。因此,如果我们的客户表有 name、email、phone 等列,我们可以在路由部分提到的 customer 控制器中进行如下设置:
namespace App\Http\Controllers\Customer
use App\Http\{
Controllers\Controller,
Requests,
};
use Illuminate\Http\Request;
use App\Customer
Class CustomerController extends Controller
{
public function save(Request $request)
{
$customer = new Customer();
$customer->name = $request->name;
$customer->email = $request->email;
$customer->phone = $request->phone;
$customer->save();
}
}
在上例中,我们在控制器中添加了保存操作。现在,如果对表单数据进行 POST 或 GET 请求,Laravel 就会将表单提交的所有数据分配给一个请求对象(Request object),作为与表单字段名称相同的属性。然后,通过这个请求对象,我们可以使用 POST 或 GET 访问表单提交的所有数据。将所有数据赋值给模型属性(与表列的名称相同)后,我们就可以调用保存方法了。现在,我们的模型没有任何保存方法,但它的父类,也就是 Eloquent 模型,已经定义了这个方法。不过,我们可以在我们的模型类中覆盖这个 save 方法,以防我们需要这个方法的其他功能。
从 Eloquent 模型中获取数据也很容易。让我们举个例子。在客户控制器中添加一个新操作,如下所示:
public function index()
{
$customers = Customer::all();
}
我们在模型中使用了 all() 静态方法,该方法基本上是在 Eloquent 模型中定义的,而 Eloquent 模型又会获取 customers 表中的所有数据。现在,如果我们想通过主键获取单个客户,可以使用 find($id) 方法,如下所示:
$customer = Customer::find(3);
这将获取 ID 为 3 的客户。
更新很简单,并且使用相同的 save() 方法,如下所示:
$customer = Customer::find(3);
$customer->name = 'Altaf Hussain';
$customer->save();
首先,我们加载了 customer,然后为其属性分配了新数据,然后调用了相同的 save() 方法。删除模型既简单又容易,具体操作如下:
$customer = Customer::find(3);
$customer->delete();
我们首先加载 ID 为 3 的客户,然后调用 delete 方法,删除 ID 为 3 的客户。
Laravel 的 Eloquent 模型非常强大,提供了许多功能。这些功能在 https://laravel.com/docs/5.2/eloquent 文档中有详细说明。Laravel 数据库部分也值得一读,请访问 https://laravel.com/docs/5.2/database 。
Artisan CLI
Artisan 是 Laravel 提供的命令行界面,它有一些不错的命令,可用于快速操作。它有很多命令,使用以下命令可以查看完整列表:
php artisan list
这将列出所有可用的选项和命令。
|
|
一些基本命令如下:
-
make:controller:此命令在Controllers文件夹中创建一个新控制器。该命令的使用方式如下:php artisan make:controller MyController如果需要使用命名空间控制器,就像之前的客户命名空间一样,可以按以下方式操作:
php artisan make:controller Customer/CustomerController此命令将在
Customer文件夹中创建CustomerController。如果Customer文件夹不可用,它也会创建该文件夹。 -
make:model:这会在应用程序文件夹中创建一个新模型。语法与 make:controller 命令相同,如下:php artisan make:model Customer对于命名空间模型,可以按如下方式使用:
php artisan make:model Customer/Customer这将在 Customer 文件夹中创建 Customer 模型并为其使用 Customer 命名空间。
-
make:event:这将在
Events文件夹中创建一个新的event类。使用方法如下:php artisan make:event MyEvent -
make:listener:此命令为事件创建一个新的侦听器。可以按如下方式使用:
php artisan make:listener MyListener --event MyEvent上述命令将为
MyEvent事件创建一个新的监听器。我们必须使用--event选项提及需要创建监听器的事件。 -
make:migration:该命令将在数据库 /migrations 文件夹中创建一个新的迁移。
-
php artisan migrate:运行所有未执行的可用迁移。
-
php artisan optimize:该命令优化框架以提高性能。
-
php artisan down:将应用程序置于维护模式。
-
php artisan up:该命令将应用程序从维护模式下恢复运行。
-
php artisan cache:clear: 该命令清除应用程序缓存。
-
php artisan db:seed:此命令为数据库添加记录种子。
-
php artisan view:clear:清除所有已编译的视图文件。
|
有关 Artisan 控制台或 Artisan CLI 的更多详情,请参阅 https://laravel.com/docs/5.2/homestead 文档。 |
Migrations
迁移是 Laravel 的另一个强大功能。在迁移中,我们可以定义数据库模式—无论是创建表格、删除表格,还是添加/更新表格中的列。迁移在部署时非常方便,而且可以作为数据库的版本控制。让我们为数据库中还没有的客户表创建一个迁移。要创建迁移,请在终端中发出以下命令:
php artisan make:migration create_custmer_table
将在 database/migrations 文件夹中创建一个新文件,文件名为 create_customer_table,前缀为当前日期和唯一 ID。创建的类为 CreateCustomerTable。该类如下:
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCustomerTable extends Migrations
{
//Run the migrations
public function up()
{
//schemas defined here
}
public function down()
{
//Reverse migrations
}
}
该类将有两个公共方法:up() 和 down()。up() 方法应包含表的所有新模式。down() 方法负责逆转已执行的迁移。现在,让我们在 up() 方法中添加客户表模式,如下所示:
public function up()
{
Schema::create('customers', function (Blueprint $table)
{
$table->increments('id', 11);
$table->string('name', 250)
$table->string('email', 50);
$table->string('phone', 20);
$table->timestamps();
});
}
public function down()
{
Schema::drop('customers');
}
在 up() 方法中,我们定义了模式和表名。表的列是单独定义的,包括列的大小。增量() 方法定义了自动递增列,在我们的例子中就是 id 列。接下来,我们创建了三个字符串列,分别是姓名、电子邮件和电话。然后,我们使用 timestamps() 方法创建了 created_at 和 updated_at 时间戳列。在 down() 方法中,我们使用 Schema 类的 drop() 方法删除了客户表。现在,我们需要使用以下命令运行迁移:
php artisan migrate
前面的命令不仅会运行我们的迁移,还会运行所有尚未执行的迁移。当迁移被执行时,Laravel 会把迁移名称保存在一个叫做 migrations 的表中,Laravel 会在这个表中决定哪些迁移要执行,哪些要跳过。
现在,如果我们需要回滚最新执行的迁移,可以使用下面的命令:
php artisan migrate:rollback
这将回滚到上一批迁移。要回滚应用程序的所有迁移,我们可以使用重置命令,如下所示:
php artisan migrate:reset
这将回滚整个应用程序迁移。
迁移可以简化部署工作,因为我们不需要在每次创建表格或数据库新变更时上传数据库模式。我们只需创建迁移并上传所有文件,然后执行迁移命令,所有模式就会更新。
Blade 模板
Laravel 自带名为 Blade 的模板语言。此外,Blade 模板文件支持纯 PHP 代码。Blade 模板文件被编译成纯 PHP 文件并缓存起来,直到被修改。Blade 还支持布局。例如,下面是我们在 Blade 中的主页面布局,放置在 resources/views/layout 文件夹中,名称为 master.blade.php。请看下面的代码:
<!DOCTYPE html>
<html>
<head>
<title>@yield('title')</title>
</head>
<body>
@section('sidebar')
Our main sidebar
@show
<div class="contents">
@yield('content')
</div>
</body>
</html>
在前面的示例中,我们为侧边栏定义了一个内容部分。此外,我们还使用了 @yield 来显示部分的内容。现在,如果我们想使用这种布局,就需要在子模板文件中对其进行扩展。让我们在 resources/views/ 文件夹中创建 customers.blade.php 文件,并在其中添加以下代码:
@extend('layouts.master')
@section('title', 'All Customers')
@section('sidebar')
This will be our side bar contents
@endsection
@section('contents')
These will be our main contents of the page
@endsection
从前面的代码中可以看出,我们扩展了 master 布局,然后在 master 布局的每个部分都放置了内容。此外,还可以在另一个模板中包含不同的模板。例如,让我们在 resources/views/includes 文件夹中包含两个文件:sidebar.blade.php 和 menu.blade.php。然后,我们可以在任何模板中包含这些文件,如下所示:
@include(includes.menu)
@include(includes.sidebar)
我们使用 @include 来包含一个模板。点(.)表示文件夹分隔。我们可以轻松地从控制器或路由器向 Blade 模板或视图发送数据。我们只需将数据作为数组传递给视图,如下所示:
return view('customers', ['count => 5]);
现在,计数可在我们的客户视图文件中使用,并且可以按如下方式访问:
Total Number of Customers: {{ count }}
是的,Blade 使用双大括号来呼应变量。关于控制结构和循环,我们再举一个例子。让我们向客户视图发送数据,如下所示:
return view('customers', ['customers' => $allCustomers]);
现在,如果要显示所有客户数据,我们的客户视图文件将类似于下面这样:
…
…
@if (count($customers) > 0)
{{ count($customers) }} found. <br />
@foreach ($customers as $customer)
{{ $customer->name }} {{ $customer->email }}
{{ $customer->phone }} <br>
@endforeach
@else
Now customers found.
@endif;
…
前面的所有语法看起来都很熟悉,因为它们与纯 PHP 几乎相同。不过,要显示一个变量,我们必须使用双大括号 {{}}。
|
有关 Blade 模板的简明易读的文档,请访问 https://laravel.com/docs/5.2/blade 。 |
Lumen
Lumen 是 Laravel 提供的一个微型框架。Lumen 主要用于创建无状态应用程序接口(stateless API),拥有 Laravel 的最低限度功能。此外,Lumen 与 Laravel 兼容,这意味着如果我们将 Lumen 应用程序复制到 Laravel,它也能正常工作。安装很简单。只需使用下面的 Composer 命令创建一个 Lumen 项目,它就会下载包括 Lumen 在内的所有依赖项:
composer create-project --prefer-dist laravel/lumen api
前面的命令将下载 Lumen,然后创建我们的 API 应用程序。之后,将 .env.example 重命名为 .env。此外,创建一个 32 个字符长的应用程序密钥,并将其放入 .env 文件中。现在,基本应用程序已准备就绪,可以使用和创建 API。
|
Lumen 与 Laravel 几乎相同,但默认情况下不包含 Laravel 的某些功能。更多详情请访问 https://lumen.laravel.com/docs/5.2 。 |
Apigility
Apigility 由 Zend 在 Zend Framework 2 中构建和开发。Apigility 提供了一个易于使用的图形用户界面来创建和管理 API。它非常易于使用,而且能够创建复杂的 API。让我们先用 Composer 安装 Apigility。在终端中发出以下命令
composer create-project -sdev zfcampus/zf-apigility-skeleton packt
上述命令将下载 Apigility 及其依赖项(包括 Zend Framework 2),并建立名为 packt 的项目。现在,执行以下命令启用开发模式,以便我们可以访问图形用户界面:
php public/index.php development enable
现在,打开网址为 yourdomain.com/packt/public,我们将看到一个漂亮的图形用户界面,如下图所示:
现在,让我们创建第一个 API。我们将把这个 API 称为 "books",它将返回一个图书列表。单击 "新建 API" 按钮(如上图所示),弹出一个窗口。在文本框中输入书籍作为 API 名称,然后单击 "创建" 按钮;新的 API 将被创建。API 创建完成后,我们将看到如下界面:
Apigility 提供了为 API 设置其他属性的简单方法,例如版本控制和身份验证。现在,让我们点击左侧边栏的 "新建服务"(New Service)按钮,创建一个 RPC 服务。此外,我们还可以点击前面截图中 RPC 部分的 "创建一个新服务" 链接。我们将看到如下界面:
如前面的截图所示,我们在书籍 API 中创建了一个名为 get 的 RPC 服务。输入的路由 URI 是 /books/get,它将用于调用此 RPC 服务。点击 "创建服务" 按钮后,将显示 API 创建成功的消息,同时还将显示以下屏幕:
从前面的截图中可以看出,该服务允许使用的 HTTP 方法只有 GET。让我们保持原样,但可以选择所有或任意一种。此外,我们希望将内容协商选择器保留为 Json,这样我们的服务就会接受/接收所有 JSON 格式的内容。此外,我们还可以选择不同的媒体类型和内容类型。
接下来,我们应该为服务添加一些将使用的字段。单击 "字段" 选项卡,我们将看到 "字段" 屏幕。单击 "新建字段" 按钮,我们将看到以下弹出窗口:
从前面的截图中可以看到,我们可以设置字段的所有属性,如名称、描述、是否必填以及其他一些设置,包括验证失败时的错误信息。在创建了标题和作者两个字段后,我们将看到与下面类似的屏幕:
从上一屏幕可以看出,我们还可以为每个字段添加验证器和筛选器。
|
由于这只是 Apigility 的入门主题,因此我们不会在本书中涉及验证器、过滤器和其他主题。 |
下一个主题是文档。点击 "文档" 选项卡后,我们将看到如下界面:
在这里,我们将记录我们的服务,添加一些说明,还可以生成响应体以用于记录。这一点非常重要,因为它能让其他人更好地理解我们的应用程序接口和服务。
现在,我们需要从某个地方获取所有书籍。它可以来自数据库、其他服务或任何其他来源。不过,现在我们只使用图书数组进行测试。点击 "源" 选项卡,我们会发现服务代码位于 module/books/src/books/V1/Rpc/Get/GetController.php。Apigility 为我们的 API books 创建了一个模块,然后根据我们的 API 版本(默认为 V1)将该模块中的所有源代码放置在不同的文件夹中。我们可以为 API 添加更多版本,如 V2 和 V3。现在,如果我们打开 GetController 文件,就会根据路由 URI 找到一些代码和一个名为 getAction 的动作。代码如下,高亮显示的代码是我们添加的代码:
namespace books\V1\Rpc\Get;
use Zend\Mvc\Controller\AbstractActionController;
use ZF\ContentNegotiation\ViewModel;
class GetController extends AbstractActionController
{
public function getAction()
{
$books = [ 'success' => [
[
'title' => 'PHP 7 High Performance',
'author' => 'Altaf Hussain'
],
[
'title' => 'Magento 2',
'author' => 'Packt Publisher'
],
]
];
return new ViewModel($books);
}
}
在前面的代码中,我们使用了 ContentNegotiation/ViewModel,它负责以我们在服务设置中选择的格式(本例中为 JSON 格式)响应数据。然后,我们用为服务创建的字段名创建了一个简单的 $books 数组,并为其赋值。然后,我们使用 ViewModel 对象将其返回,该对象负责将响应数据转换为 JSON 格式。
现在,让我们测试一下我们的 API。由于我们的服务可以接受 GET 请求,我们只需在浏览器中输入带有 books/get URI 的 URL,就能看到 JSON 响应。最好使用 RestClient 或用于 Google Chrome 浏览器的 Postman 等工具来检查 API,它们提供了一个易于使用的界面,可以向 API 提出不同类型的请求。我们使用 Postman 进行了测试,得到了如下截图所示的响应:
另外请注意,我们将服务设置为只接受 GET 请求。因此,如果我们发送 GET 以外的请求,就会收到 HTTP 状态代码 405 methods not allowed 的错误信息。
Apigility 功能强大,提供了大量功能,如 RESTFul API、HTTP 身份验证、通过易于创建的 DB 连接器连接数据库的服务,以及为服务选择表。使用 Apigility 时,我们无需担心 API、服务结构安全性等问题,因为 Apigility 会为我们解决这些问题。我们只需专注于 API 和服务的业务逻辑。
|
本附录无法全面介绍 Apigility。Apigility 有很多功能可以在一本完整的书中介绍。Apigility 的官方文档 https://apigility.org/documentation 是入门和了解更多信息的好地方。 |