本文经Viraj Khatavkar同行评审。感谢所有SitePoint的同行评审员,使SitePoint的内容达到最佳状态!
如果您之前构建过API,我敢打赌您习惯于直接将数据作为响应输出。如果操作正确,这可能不会造成危害,但有一些实际的替代方案可以帮助解决这个问题。
其中一个可用的解决方案是Fractal。它允许我们在将模型作为响应返回之前,为模型创建一个新的转换层。它非常灵活,易于集成到任何应用程序或框架中。
我们将使用Laravel 5.3应用程序来构建示例并将Fractal包与其集成,因此请继续使用安装程序或通过Composer创建一个新的Laravel应用程序。
laravel new demo
或
composer create-project laravel/laravel demo
然后,在文件夹内,我们需要Fractal包。
composer require league/fractal
我们的数据库包含users和roles表。每个用户都有一个角色,每个角色都有一个权限列表。
// app/User.php
class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
'password',
'role_id',
];
protected $hidden = [
'password', 'remember_token',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function role()
{
return $this->belongsTo(Role::class);
}
}
// app/Role.php
class Role extends Model
{
protected $fillable = [
'name',
'slug',
'permissions'
];
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function users()
{
return $this->hasMany(User::class);
}
}
我们将为每个模型创建一个Transformer。我们的UserTransformer类如下所示:
// app/Transformers/UserTransformer.php
namespace App\Transformers;
use App\User;
use League\Fractal\TransformerAbstract;
class UserTransformer extends TransformerAbstract
{
public function transform(User $user)
{
return [
'name' => $user->name,
'email' => $user->email
];
}
}
是的,创建Transformer就这么简单!它只是以开发人员可以管理的方式转换数据,而不是留给ORM或存储库。
我们扩展TransformerAbstract类并定义transform方法,该方法将使用User实例调用。RoleTransformer类也是如此。
namespace App\Transformers;
use App\Role;
use League\Fractal\TransformerAbstract;
class RoleTransformer extends TransformerAbstract
{
public function transform(Role $role)
{
return [
'name' => $role->name,
'slug' => $role->slug,
'permissions' => $role->permissions
];
}
}
我们的控制器应该在将数据发送回用户之前转换数据。我们现在将处理UsersController类,暂时只定义index和show操作。
// app/Http/Controllers/UsersController.php
class UsersController extends Controller
{
/**
* @var Manager
*/
private $fractal;
/**
* @var UserTransformer
*/
private $userTransformer;
function __construct(Manager $fractal, UserTransformer $userTransformer)
{
$this->fractal = $fractal;
$this->userTransformer = $userTransformer;
}
public function index(Request $request)
{
$users = User::all(); // 从数据库获取用户
$users = new Collection($users, $this->userTransformer); // 创建资源集合转换器
$users = $this->fractal->createData($users); // 转换数据
return $users->toArray(); // 获取转换后的数据数组
}
}
index操作将从数据库查询所有用户,使用用户列表和转换器创建一个资源集合,然后执行实际的转换过程。
{
"data": [
{
"name": "Nyasia Keeling",
"email": "[email protected]"
},
{
"name": "Laron Olson",
"email": "[email protected]"
},
{
"name": "Prof. Fanny Dach III",
"email": "[email protected]"
},
{
"name": "Athena Olson Sr.",
"email": "[email protected]"
}
// ...
]
}
当然,一次返回所有用户是没有意义的,我们应该为此实现分页器。
Laravel倾向于简化事情。我们可以像这样实现分页:
$users = User::paginate(10);
但是为了使这与Fractal一起工作,我们可能需要添加一些代码来转换数据,然后再调用分页器。
// app/Http/Controllers/UsersController.php
class UsersController extends Controller
{
// ...
public function index(Request $request)
{
$usersPaginator = User::paginate(10);
$users = new Collection($usersPaginator->items(), $this->userTransformer);
$users->setPaginator(new IlluminatePaginatorAdapter($usersPaginator));
$users = $this->fractal->createData($users); // 转换数据
return $users->toArray(); // 获取转换后的数据数组
}
}
第一步是从模型分页数据。接下来,我们像以前一样创建一个资源集合,然后在集合上设置分页器。
Fractal为Laravel提供了一个分页器适配器来转换LengthAwarePaginator类,它还为Symfony和Zend提供了一个适配器。
{
"data": [
{
"name": "Nyasia Keeling",
"email": "[email protected]"
},
{
"name": "Laron Olson",
"email": "[email protected]"
},
// ...
],
"meta": {
"pagination": {
"total": 50,
"count": 10,
"per_page": 10,
"current_page": 1,
"total_pages": 5,
"links": {
"next": "http://demo.vaprobash.dev/users?page=2"
}
}
}
}
请注意,它为分页详细信息添加了额外的字段。您可以在文档中阅读更多关于分页的信息。
现在我们已经熟悉了Fractal,是时候学习如何在用户请求时包含子资源(关系)到响应中了。
我们可以请求包含额外资源到响应中,例如http://demo.vaprobash.dev/users?include=role。我们的转换器可以自动检测正在请求的内容并解析include参数。
// app/Transformers/UserTransformer.php
class UserTransformer extends TransformerAbstract
{
protected $availableIncludes = [
'role'
];
public function transform(User $user)
{
return [
'name' => $user->name,
'email' => $user->email
];
}
public function includeRole(User $user)
{
return $this->item($user->role, App::make(RoleTransformer::class));
}
}
$availableIncludes属性告诉转换器我们可能需要包含一些额外的数据到响应中。如果include查询参数请求用户角色,它将调用includeRole方法。
// app/Http/Controllers/UsersController.php
class UsersController extends Controller
{
// ...
public function index(Request $request)
{
$usersPaginator = User::paginate(10);
$users = new Collection($usersPaginator->items(), $this->userTransformer);
$users->setPaginator(new IlluminatePaginatorAdapter($usersPaginator));
$this->fractal->parseIncludes($request->get('include', '')); // 解析includes
$users = $this->fractal->createData($users); // 转换数据
return $users->toArray(); // 获取转换后的数据数组
}
}
$this->fractal->parseIncludes行负责解析include查询参数。如果我们请求用户列表,我们应该看到类似这样的内容:
{
"data": [
{
"name": "Nyasia Keeling",
"email": "[email protected]",
"role": {
"data": {
"name": "User",
"slug": "user",
"permissions": [ ]
}
}
},
{
"name": "Laron Olson",
"email": "[email protected]",
"role": {
"data": {
"name": "User",
"slug": "user",
"permissions": [ ]
}
}
},
// ...
],
"meta": {
"pagination": {
"total": 50,
"count": 10,
"per_page": 10,
"current_page": 1,
"total_pages": 5,
"links": {
"next": "http://demo.vaprobash.dev/users?page=2"
}
}
}
}
如果每个用户都有一个角色列表,我们可以将转换器更改为如下所示:
// app/Transformers/UserTransformer.php
class UserTransformer extends TransformerAbstract
{
protected $availableIncludes = [
'roles'
];
public function transform(User $user)
{
return [
'name' => $user->name,
'email' => $user->email
];
}
public function includeRoles(User $user)
{
return $this->collection($user->roles, App::make(RoleTransformer::class));
}
}
包含子资源时,我们可以使用点表示法来嵌套关系。假设每个角色都有一个存储在单独表中的权限列表,并且我们想列出具有其角色和权限的用户。我们可以这样做include=role.permissions。
有时,我们需要默认包含一些必要的关联,例如地址关联。我们可以通过在转换器中使用$defaultIncludes属性来实现。
class UserTransformer extends TransformerAbstract
{
// ...
protected $defaultIncludes = [
'address'
];
// ...
}
我最喜欢Fractal包的一件事是能够将参数传递给include参数。文档中的一个很好的例子是按顺序排列。我们可以将其应用到我们的示例中,如下所示:
// app/Transformers/RoleTransformer.php
use App\Role;
use Illuminate\Support\Facades\App;
use League\Fractal\ParamBag;
use League\Fractal\TransformerAbstract;
class RoleTransformer extends TransformerAbstract
{
protected $availableIncludes = [
'users'
];
public function transform(Role $role)
{
return [
'name' => $role->name,
'slug' => $role->slug,
'permissions' => $role->permissions
];
}
public function includeUsers(Role $role, ParamBag $paramBag)
{
list($orderCol, $orderBy) = $paramBag->get('order') ?: ['created_at', 'desc'];
$users = $role->users()->orderBy($orderCol, $orderBy)->get();
return $this->collection($users, App::make(UserTransformer::class));
}
}
这里重要的部分是list($orderCol, $orderBy) = $paramBag->get('order') ?: ['created_at', 'desc'];,这将尝试从用户include获取order参数,并将其应用于查询构建器。
我们现在可以通过传递参数来按顺序排列包含的用户列表(/roles?include=users:order(name|asc))。您可以在文档中阅读更多关于包含资源的信息。
但是,如果用户没有任何关联的角色会怎样?它将停止并出现错误,因为它期望的是有效数据而不是null。让我们从响应中删除该关系,而不是显示其null值。
// app/Transformers/UserTransformer.php
class UserTransformer extends TransformerAbstract
{
protected $availableIncludes = [
'roles'
];
public function transform(User $user)
{
return [
'name' => $user->name,
'email' => $user->email
];
}
public function includeRoles(User $user)
{
if (!$user->role) {
return null;
}
return $this->collection($user->roles, App::make(RoleTransformer::class));
}
}
因为Eloquent在访问模型时会延迟加载模型,所以我们可能会遇到n 1问题。这可以通过一次性急切加载关系来解决,以优化查询。
class UsersController extends Controller
{
// ...
public function index(Request $request)
{
$this->fractal->parseIncludes($request->get('include', '')); // 解析includes
$usersQueryBuilder = User::query();
$usersQueryBuilder = $this->eagerLoadIncludes($request, $usersQueryBuilder);
$usersPaginator = $usersQueryBuilder->paginate(10);
$users = new Collection($usersPaginator->items(), $this->userTransformer);
$users->setPaginator(new IlluminatePaginatorAdapter($usersPaginator));
$users = $this->fractal->createData($users); // 转换数据
return $users->toArray(); // 获取转换后的数据数组
}
protected function eagerLoadIncludes(Request $request, Builder $query)
{
$requestedIncludes = $this->fractal->getRequestedIncludes();
if (in_array('role', $requestedIncludes)) {
$query->with('role');
}
return $query;
}
}
这样,在访问模型关系时,我们将不会有任何额外的查询。
我在阅读Phil Sturgeon撰写的《构建你不会讨厌的API》时偶然发现了Fractal,这是一本很棒且内容丰富的读物,我强烈推荐。
您在构建API时是否使用过转换器?您是否有任何首选的执行相同工作的包,或者您只是使用json_encode?请在下面的评论部分告诉我们!
PHP Fractal是一个强大的工具,有助于为API呈现和转换数据。它很重要,因为它提供了一种标准化的方法来输出复杂、嵌套的数据结构,确保API的数据输出一致、结构良好且易于理解。这使得开发人员更容易使用您的API,并减少了出错的可能性。
PHP Fractal的工作原理是获取复杂的数据结构并将其转换为更易于使用的格式。它通过两个主要组件来实现:Transformer和Serializer。Transformer负责将复杂的数据转换为更简单的格式,而Serializer则格式化最终输出。
PHP Fractal中的Transformer是定义应用程序数据应如何在API响应中输出的类。它们获取复杂的数据结构并将它们转换为更简单、更易于使用的格式。这允许您精确控制API响应中包含哪些数据以及数据的结构。
PHP Fractal中的Serializer负责格式化API的最终输出。它们获取已由Transformer转换的数据,并将其格式化为特定的结构。这允许您确保API的输出一致且易于理解。
在项目中实现PHP Fractal包括通过Composer安装Fractal库,为数据创建Transformer,然后使用Fractal类使用Transformer转换数据。然后,您可以使用Fractal的Serializer之一输出转换后的数据。
是的,PHP Fractal是一个独立的库,可以与任何PHP项目一起使用。它不依赖于任何特定的框架或平台,这使得它成为任何PHP开发人员的通用工具。
使用PHP Fractal提供了许多好处。它确保API的输出一致且结构良好,使开发人员更容易使用。它还提供了一种标准化的方法来转换复杂的数据结构,减少了出错的可能性,并使代码更容易维护。
PHP Fractal以其简单性和灵活性而脱颖而出。它提供了一种直接的方法来转换复杂的数据结构,并且它使用Transformer和Serializer允许高度定制。这使得它成为任何使用API的开发人员的强大工具。
是的,PHP Fractal是高度可定制的。您可以创建自定义Transformer来精确控制数据的转换方式,并且您可以使用不同的Serializer以不同的方式格式化输出。这允许您根据您的特定需求调整API的输出。
有很多资源可以帮助您了解更多关于PHP Fractal的信息。官方PHP League网站提供了全面的文档,并且网上有许多教程和博文。此外,PHP Fractal GitHub存储库是一个探索代码并查看其使用方法示例的好地方。
Descargo de responsabilidad: Todos los recursos proporcionados provienen en parte de Internet. Si existe alguna infracción de sus derechos de autor u otros derechos e intereses, explique los motivos detallados y proporcione pruebas de los derechos de autor o derechos e intereses y luego envíelos al correo electrónico: [email protected]. Lo manejaremos por usted lo antes posible.
Copyright© 2022 湘ICP备2022001581号-3