"Si un ouvrier veut bien faire son travail, il doit d'abord affûter ses outils." - Confucius, "Les Entretiens de Confucius. Lu Linggong"
Page de garde > La programmation > PHP FRACTAL - Faites de votre API JSON Pretty, toujours!

PHP FRACTAL - Faites de votre API JSON Pretty, toujours!

Publié le 2025-02-12
Parcourir:614

PHP Fractal - Make Your API's JSON Pretty, Always!

本文经Viraj Khatavkar同行评审。感谢所有SitePoint的同行评审员,使SitePoint的内容达到最佳状态!


如果您之前构建过API,我敢打赌您习惯于直接将数据作为响应输出。如果操作正确,这可能不会造成危害,但有一些实际的替代方案可以帮助解决这个问题。

其中一个可用的解决方案是Fractal。它允许我们在将模型作为响应返回之前,为模型创建一个新的转换层。它非常灵活,易于集成到任何应用程序或框架中。

PHP Fractal - Make Your API's JSON Pretty, Always!

关键要点

  • PHP Fractal是一种解决方案,允许开发人员在将模型作为响应返回之前为其模型创建新的转换层,从而使JSON数据更易于管理和保持一致性。
  • Fractal灵活且易于集成到任何应用程序或框架中。它的工作原理是使用Transformer将复杂的数据结构转换为更简单的格式,并使用Serializer来格式化最终输出。
  • Fractal还允许在用户请求时包含子资源(关系)到响应中,从而为数据呈现增加了另一层灵活性和控制。
  • 使用Fractal可以通过一次性急切加载关系来优化查询性能,从而解决了Eloquent延迟加载经常遇到的n 1问题。

安装

我们将使用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

我们将为每个模型创建一个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常见问题解答

什么是PHP Fractal,为什么它很重要?

PHP Fractal是一个强大的工具,有助于为API呈现和转换数据。它很重要,因为它提供了一种标准化的方法来输出复杂、嵌套的数据结构,确保API的数据输出一致、结构良好且易于理解。这使得开发人员更容易使用您的API,并减少了出错的可能性。

PHP Fractal是如何工作的?

PHP Fractal的工作原理是获取复杂的数据结构并将其转换为更易于使用的格式。它通过两个主要组件来实现:Transformer和Serializer。Transformer负责将复杂的数据转换为更简单的格式,而Serializer则格式化最终输出。

PHP Fractal中的Transformer是什么?

PHP Fractal中的Transformer是定义应用程序数据应如何在API响应中输出的类。它们获取复杂的数据结构并将它们转换为更简单、更易于使用的格式。这允许您精确控制API响应中包含哪些数据以及数据的结构。

PHP Fractal中的Serializer是什么?

PHP Fractal中的Serializer负责格式化API的最终输出。它们获取已由Transformer转换的数据,并将其格式化为特定的结构。这允许您确保API的输出一致且易于理解。

我如何在项目中实现PHP Fractal?

在项目中实现PHP Fractal包括通过Composer安装Fractal库,为数据创建Transformer,然后使用Fractal类使用Transformer转换数据。然后,您可以使用Fractal的Serializer之一输出转换后的数据。

我可以将PHP Fractal与任何PHP项目一起使用吗?

是的,PHP Fractal是一个独立的库,可以与任何PHP项目一起使用。它不依赖于任何特定的框架或平台,这使得它成为任何PHP开发人员的通用工具。

使用PHP Fractal的好处是什么?

使用PHP Fractal提供了许多好处。它确保API的输出一致且结构良好,使开发人员更容易使用。它还提供了一种标准化的方法来转换复杂的数据结构,减少了出错的可能性,并使代码更容易维护。

PHP Fractal与其他数据转换工具相比如何?

PHP Fractal以其简单性和灵活性而脱颖而出。它提供了一种直接的方法来转换复杂的数据结构,并且它使用Transformer和Serializer允许高度定制。这使得它成为任何使用API的开发人员的强大工具。

我可以自定义PHP Fractal的输出吗?

是的,PHP Fractal是高度可定制的。您可以创建自定义Transformer来精确控制数据的转换方式,并且您可以使用不同的Serializer以不同的方式格式化输出。这允许您根据您的特定需求调整API的输出。

我在哪里可以了解更多关于PHP Fractal的信息?

有很多资源可以帮助您了解更多关于PHP Fractal的信息。官方PHP League网站提供了全面的文档,并且网上有许多教程和博文。此外,PHP Fractal GitHub存储库是一个探索代码并查看其使用方法示例的好地方。

Dernier tutoriel Plus>

Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.

Copyright© 2022 湘ICP备2022001581号-3