fractal csdn_PHP Fractal-始终使您的API的JSON变得漂亮!

fractal csdn

This article was peer reviewed by Viraj Khatavkar. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

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



If you’ve built an API before, I’ll bet you’re used to dumping data directly as a response. It may not be harmful if done right, but there are practical alternatives that can help solve this small problem.

如果您以前构建过API,那么我敢打赌,您习惯于直接将数据转储为响应。 如果做得正确,可能不会造成危害,但是有一些实用的替代方法可以帮助解决这个小问题。

One of the available solutions is Fractal. It allows us to create a new transformation layer for our models before returning them as a response. It’s very flexible and easy to integrate into any application or framework.

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

Image of fractals with PHP embedded in them

安装 (Installation)

We will be using a Laravel 5.3 app to build an example and integrate the Fractal package with it, so go ahead and create a new Laravel app using the installer or via Composer.

我们将使用Laravel 5.3应用程序来构建示例并将Fractal软件包与其集成在一起,因此请继续使用安装程序或Composer创建一个新的Laravel应用程序。

laravel new demo

or

要么

composer create-project laravel/laravel demo

Then, inside the folder, we require the Fractal package.

然后,在文件夹中,我们需要Fractal软件包。

composer require league/fractal

创建数据库 (Creating the Database)

Our database contains a users and roles table. Every user has a role, and each role has a list of permissions.

我们的数据库包含一个usersroles表。 每个用户都有一个角色,每个角色都有一个权限列表。

// 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);
    }
}

创建变形金刚 (Creating Transformers)

We’re going to create a transformer for each model. Our UserTransformer class looks like this:

我们将为每个模型创建一个变压器。 我们的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
        ];
    }
}

Yup, that’s all it takes to create a transformer! It just transforms the data in a way that can be managed by the developer, and not left to the ORM or the repository.

是的,这就是创建一个变压器所需的一切! 它只是以可由开发人员管理的方式转换数据,而不是留给ORM或存储库。

We extend the TransformerAbstract class and define the transform method that will be called with a User instance. The same thing goes for the RoleTransformer class.

我们扩展TransformerAbstract类并定义将与User实例一起调用的transform方法。 RoleTransformer类也是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
        ];
    }
}

创建控制器 (Creating Controllers)

Our controllers should transform the data before sending it back to the user. We’re going to work on the UsersController class and only define the index and show actions for the moment.

我们的控制器应先转换数据,然后再将其发送回用户。 我们将处理UsersController类,仅定义indexshow操作。

// 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(); // Get users from DB
        $users = new Collection($users, $this->userTransformer); // Create a resource collection transformer
        $users = $this->fractal->createData($users); // Transform data

        return $users->toArray(); // Get transformed array of data
    }
}

The index action will query all users from the database, create a resource collection with the list of users and the transformer, and then perform the actual transformation process.

索引操作将从数据库中查询所有用户,使用用户列表和转换器创建资源集合,然后执行实际的转换过程。

{
  "data": [
    {
      "name": "Nyasia Keeling",
      "email": "crooks.maurice@example.net"
    },
    {
      "name": "Laron Olson",
      "email": "helen55@example.com"
    },
    {
      "name": "Prof. Fanny Dach III",
      "email": "edgardo13@example.net"
    },
    {
      "name": "Athena Olson Sr.",
      "email": "halvorson.jules@example.com"
    }
    // ...
  ]
}

Of course, it doesn’t make sense to return all the users at once, and we should implement a paginator for this.

当然,一次返回所有用户是没有意义的,我们应该为此实现分页器。

分页 (Pagination)

Laravel tends to make things simple. We can accomplish pagination like this:

Laravel倾向于使事情变得简单。 我们可以像这样完成分页:

$users = User::paginate(10);

But to make this work with Fractal, we may need to add a little bit more code to transform our data before calling the paginator.

但是要使它与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); // Transform data

        return $users->toArray(); // Get transformed array of data
    }
}

The first step is to paginate data from the model. Next, we create a resource collection like before, and then we set the paginator on the collection.

第一步是对模型中的数据进行分页。 接下来,我们像以前一样创建资源集合,然后在集合上设置分页器。

Fractal provides a paginator adapter for Laravel to convert the LengthAwarePaginator class, and it also has one for Symfony and Zend.

Fractal为Laravel提供了一个分页器适配器,以转换LengthAwarePaginator类,它还为Symfony和Zend提供了一个分页器适配器。

{
    "data": [
        {
            "name": "Nyasia Keeling",
            "email": "crooks.maurice@example.net"
        },
        {
            "name": "Laron Olson",
            "email": "helen55@example.com"
        },
        // ...
    ],
    "meta": {
        "pagination": {
            "total": 50,
            "count": 10,
            "per_page": 10,
            "current_page": 1,
            "total_pages": 5,
            "links": {
                "next": "http://demo.vaprobash.dev/users?page=2"
            }
        }
    }

}

Notice that it adds extra fields for pagination details. You can read more about pagination in the documentation.

请注意,它为分页详细信息添加了额外的字段。 您可以在文档中阅读有关分页的更多信息。

包括子资源 (Including Sub-Resources)

Now that we’ve become familiar with Fractal, it’s time to learn how to include sub-resources (relations) with the response when it’s requested by the user.

现在我们已经熟悉了Fractal,是时候学习如何在用户请求时在响应中包括子资源(关系)了。

We can request extra resources to be included with the response like this http://demo.vaprobash.dev/users?include=role. Our transformer can automatically detect what’s being requested and parse the include parameter.

我们可以要求额外的资源包含在响应中,例如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));
    }
}

The $availableIncludes property tells the transformer that we may need to include some extra data with the response. It will call the includeRole method if the include query parameter is requesting the user roles.

$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', '')); // parse includes
        $users = $this->fractal->createData($users); // Transform data

        return $users->toArray(); // Get transformed array of data
    }
}

The $this->fractal->parseIncludes line is responsible for parsing the include query parameter. If we request the list of users we should see something like this:

$this->fractal->parseIncludes行负责解析include查询参数。 如果我们请求用户列表,我们应该看到类似以下内容:

{
    "data": [
        {
            "name": "Nyasia Keeling",
            "email": "crooks.maurice@example.net",
            "role": {
                "data": {
                    "name": "User",
                    "slug": "user",
                    "permissions": [ ]
                }
            }
        },
        {
            "name": "Laron Olson",
            "email": "helen55@example.com",
            "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"
            }
        }
    }
}

If every user had a list of roles, we could change the transformer to be like this:

如果每个用户都有一个角色列表,我们可以将转换器更改为:

// 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));
    }
}

When including a sub-resource, we can nest relations by using the a dot notation. Let’s say every role has a list of permissions stored in a separate table and we wanted to list users with their role and permissions. We can do it like this include=role.permissions.

当包含子资源时,我们可以使用点符号嵌套关系。 假设每个角色在单独的表中都有一个权限列表,我们想列出用户及其角色和权限。 我们可以这样执行include=role.permissions

Sometimes, we are required to include some necessary relations by default, like an address relation for example. We can do that by using the $defaultIncludes property inside the transformer.

有时,默认情况下,我们需要包括一些必要的关系,例如地址关系。 我们可以通过使用转换器内部的$defaultIncludes属性来实现。

class UserTransformer extends TransformerAbstract
{
    // ...

    protected $defaultIncludes = [
        'address'
    ];

    // ...
}

One of the things I really love about the Fractal package is the ability to pass parameters to include parameters. A good example from the documentation is order by. We can apply it to our example like so:

我真正喜欢Fractal软件包的一件事是能够传递参数以包含参数。 文档中的一个很好的例子是order by 。 我们可以将其应用于示例,如下所示:

// 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));
    }
}

The important part here is list($orderCol, $orderBy) = $paramBag->get('order') ?: ['created_at', 'desc'];, this will try to get the order parameter from the users include and will apply it to the query builder.

这里重要的部分是list($orderCol, $orderBy) = $paramBag->get('order') ?: ['created_at', 'desc']; ,这将尝试从用户include中获取订单参数,并将其应用于查询构建器。

We can now order our included users list by passing parameters (/roles?include=users:order(name|asc)). You can read more about including resources in the documentation.

现在,我们可以通过传递参数( /roles?include=users:order(name|asc) )来对包含的用户列表进行/roles?include=users:order(name|asc) 。 您可以阅读有关在文档中包含资源的更多信息。

But, what if the user didn’t have any roles attached? It will halt with an error because it was expecting valid data instead of null. Let’s make it delete the relation from the response instead of showing it with a null value.

但是,如果用户没有任何角色,该怎么办? 由于它期望有效的数据而不是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));
    }
}

渴望加载 (Eager Loading)

Because Eloquent will lazy load models when accessing them, we may encounter the n+1 problem. This can be solved by eager loading relations at once to optimize the query.

由于Eloquent在访问模型时会延迟加载模型,因此我们可能会遇到n + 1问题 。 这可以通过立即加载关系来优化查询来解决。

class UsersController extends Controller
{

    // ...

    public function index(Request $request)
    {
        $this->fractal->parseIncludes($request->get('include', '')); // parse 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); // Transform data

        return $users->toArray(); // Get transformed array of data
    }

    protected function eagerLoadIncludes(Request $request, Builder $query)
    {
        $requestedIncludes = $this->fractal->getRequestedIncludes();

        if (in_array('role', $requestedIncludes)) {
            $query->with('role');
        }

        return $query;
    }
}

This way, we won’t have any extra queries when accessing model relations.

这样,在访问模型关系时,我们不会有任何额外的查询。

结论 (Conclusion)

I came across Fractal while reading the Building APIs you won’t hate by Phil Sturgeon, which was a great and informative read that I wholeheartedly recommend.

在阅读Phil Sturgeon 不会讨厌Building API时,我遇到了Fractal,这是我衷心推荐的一本很棒的内容丰富的文章。

Do you use transformers when building your API? Do you have any preferred package that does the same job, or do you just dump the json_encodes? Let us know in the comments section below!

建立API时会使用变压器吗? 您是否有任何喜欢的软件包执行相同的工作,还是只是转储json_encode ? 让我们在下面的评论部分中知道!

翻译自: https://www.sitepoint.com/php-fractal-make-your-apis-json-pretty-always/

fractal csdn

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值