provisionesta/gitlab-api-client

Laravel 的 GitLab API 客户端

4.1 2024-03-22 13:15 UTC

This package is auto-updated.

Last update: 2024-09-22 14:31:53 UTC


README

[[目录]]

概述

GitLab API 客户端是一个开源的 Composer 包,用于在 Laravel 应用程序中连接到 GitLab SaaS 或自托管实例,以提供用户、组、项目和其他相关功能的配置和撤销配置。

该包由开源社区维护,不由任何公司维护。请在自己的风险下使用,并为您遇到的任何错误创建合并请求。

问题陈述

我们采取了更简单的方法,而不是为 API 文档中的每个端点提供 SDK 方法,而是提供了一个通用的 ApiClient,它可以执行对您在 GitLab API 文档 中找到的任何端点的 GETPOSTPUTDELETE 请求。

这基于 Laravel HTTP Client 的简单性,该客户端由 Guzzle HTTP 客户端 提供,为 GitLab API 响应提供“最后代码解析”,以改善开发者体验。

此 API 客户端的价值在于它为您处理 API 请求日志、响应分页、速率限制回退和 4xx/5xx 异常处理。

有关包含预构建 Laravel Actions 的综合 SDK,包括控制台命令、服务类方法、可调度作业和 API 端点,请参阅 provisionesta/gitlab-laravel-actions 包。

示例用法

use Provisionesta\Gitlab\ApiClient;

// Get a list of records (positional arguments)
// https://docs.gitlab.com/ee/api/projects.html#list-all-projects
$projects = ApiClient::get('/projects');

// Get list of records (named arguments)
$projects = ApiClient::get(
    uri: '/projects'
);

// Search for records
// https://docs.gitlab.com/ee/api/projects.html#list-all-projects
$projects = ApiClient::get(
    uri: '/projects',
    data: [
        'search' => 'my-project-name',
        'membership' => true
    ]
);

// Get a specific record (positional arguments)
// https://docs.gitlab.com/ee/api/projects.html#get-single-project
$project = ApiClient::get('/projects/123456789');

// Get a specific record with URL encoded path
$project = ApiClient::get('/projects/' . ApiClient::urlencode('group-name/child-group-name/project-name'));

// Create a project
// https://docs.gitlab.com/ee/api/projects.html#create-project
$group_id = '12345678';
$record = ApiClient::post(
    uri: '/projects',
    data: [
        'name' => 'My Cool Project',
        'path' => 'my-cool-project',
        'namespace_id' => $group_id
    ]
);

// Update a project
// https://docs.gitlab.com/ee/api/projects.html#edit-project
$project_id = '123456789';
$record = ApiClient::put(
    uri: '/projects/' . $project_id,
    data: [
        'description' => 'This is a cool project that we created for a demo.'
    ]
);

// Delete a project
// https://docs.gitlab.com/ee/api/projects.html#delete-project
$project_id = '123456789';
$record = ApiClient::delete(
    uri: '/projects/' . $project_id
);

问题跟踪和错误报告

我们没有维护功能请求路线图,但是我们邀请您做出贡献,我们将很高兴审查您的合并请求。

请创建一个 问题 来报告错误。

贡献

请参阅 CONTRIBUTING.md 了解更多有关如何贡献的信息。

维护者

姓名GitLab 处理电子邮件
Jeff Martin@jeffersonmartinprovisionesta [at] jeffersonmartin [dot] com

贡献者致谢

安装

要求

要求版本
PHP^8.0, ^8.1, ^8.2, ^8.3
Laravel^8.0, ^9.0, ^10.0, ^11.0

升级指南

请参阅 变更日志 了解发行说明。

仍在使用 glamstack/gitlab-sdk (v2.x) 吗?请参阅 v3.0 变更日志 了解升级说明。

仍在使用 gitlab-it/gitlab-sdk (v3.x) 吗?请参阅 v4.0 变更日志 了解升级说明。

添加 Composer 包

composer require provisionesta/gitlab-api-client:^4.0

如果您正在为此包做出贡献,请参阅 CONTRIBUTING.md 了解如何配置带有符号链接的本地 composer 包的说明。

发布配置文件

这是可选的。配置文件指定了 API 连接存储在哪些 .env 变量名称中。只有当您想重命名 GITLAB_API_* .env 变量名称时,才需要发布配置文件。

php artisan vendor:publish --tag=gitlab-api-client

连接凭证

环境变量

请将以下变量添加到您的 .env 文件中。您可以在文件的任何位置添加这些变量(每行一个),或者添加到文件底部(任选其一)。

GITLAB_API_URL="https://gitlab.com"
GITLAB_API_TOKEN=""

如果您将连接密钥存储在数据库或密钥管理器中,您可以覆盖 config/gitlab-api-client.php 的配置,或者在每个请求中提供连接数组。有关更多信息,请参阅 连接数组

URL

如果您使用的是 GitLab.com SaaS(您没有托管自己的 GitLab 实例),那么 URL 是 https://gitlab.com。如果您刚开始使用,建议在 GitLab.com 上注册一个免费账户。您可以使用 API 操作您个人命名空间中的项目,或者用于开源或组织组。

GITLAB_API_URL="https://gitlab.com"
GITLAB_API_TOKEN=""

如果您托管自己的 GitLab 自托管实例,那么 URL 是您用来登录的实例的 FQDN(例如 https://gitlab.example.com)。

GITLAB_API_URL="https://gitlab.example.com"
GITLAB_API_TOKEN=""

如果您的 GitLab 实例位于防火墙后面,那么您需要与您的 IT 或基础设施团队合作,允许 Laravel 应用连接到 GitLab 实例。此配置取决于您的环境,且不提供支持。您可以通过使用 CURL 命令测试到 https://gitlab.example.com/api/v4/version 的初始连接性。

API 令牌

您需要在 GitLab 实例上生成一个访问令牌,并更新 .env 文件中的 GITLAB_API_TOKEN 变量。

GITLAB_API_TOKEN="glpat-S3cr3tK3yG03sH3r3"

在创建 API 令牌之前,请参阅 安全最佳实践

连接数组

您在 .env 文件中定义的变量默认使用,除非您设置包含 URL 和 API 令牌的数组作为连接参数。

安全警告:不要将硬编码的 API 令牌提交到代码库中。这应仅用于存储在数据库或密钥管理器中的动态变量。

$connection = [
    'url' => 'https://gitlab.com',
    'token' => 'glpat-S3cr3tK3yG03sH3r3'
];
use Provisionesta\Gitlab\ApiClient;

class MyClass
{
    private array $connection;

    public function __construct($connection)
    {
        $this->connection = $connection;
    }

    public function getGroup($group_id)
    {
        return ApiClient::get(
            connection: $this->connection,
            uri: 'groups/' . $group_id
        )->data;
    }
}

安全最佳实践

不共享令牌

不要使用您已经为其他目的创建的 API 令牌。您应该为每个用例生成一个新的 API 令牌。

这在安全事件期间很有用,当需要在受侵害的系统上撤销密钥时,您不希望使用相同用户或服务账户的其他系统受到影响,因为这些系统使用的是未被撤销的不同密钥。

API 令牌存储

不要将您的 API 令牌添加到任何 config/*.php 文件中,以避免将其提交到您的仓库(泄露秘密)。

所有 API 令牌都应定义在 .env 文件中,该文件包含在 .gitignore 中,且不会提交到您的仓库。

对于高级用例,您可以将变量存储在 CI/CD 变量或秘密保险库(例如 Ansible Vault、AWS Parameter Store、GCP Secrets Manager、HashiCorp Vault 等)中。

API 令牌权限

在为您的应用程序创建 API 令牌之前,我们建议您阅读有关 个人访问令牌组访问令牌项目访问令牌安全最佳实践 的更多信息。

所有 API 端点都需要 apiread_api 权限范围。如果您不执行任何读写操作,建议使用 read_api 权限范围作为主动安全措施。

如果您使用的是 个人访问令牌,则 API 令牌使用它所属用户的权限,因此对于生产应用程序用例,最佳实践是创建服务账户(机器人)用户。出于安全原因,大多数服务账户应该是 Regular 用户。如果您的用户账户具有 Administrator 权限,请格外小心。

GitLab 项目权限

每个API调用都对应一个或多个角色的特定权限。

除非你有需要此权限级别的特定API调用,否则不应为组访问令牌或项目访问令牌配置 所有者维护者 的过度许可角色。

https://docs.gitlab.com/ee/user/permissions.html#project-members-permissions

个人访问令牌

个人访问令牌可以访问您用户帐户有权限的所有组和项目。 这是一个广泛许可的令牌,应谨慎使用。 这仅建议用于执行管理员级别API调用或需要访问多个无法使用组访问令牌执行的操作的场景。

出于安全考虑,大多数服务帐户应该是 普通用户。 如果您的用户帐户有 管理员 访问,请务必小心。

https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html

组访问令牌

组访问令牌仅提供对特定GitLab组及其子组和GitLab项目的访问。 这是大多数场景下推荐使用的令牌类型。

https://docs.gitlab.com/ee/user/group/settings/group_access_tokens.html

项目访问令牌

项目访问令牌仅提供对其创建的项目及其关联的基于API密钥名称的bot用户的访问。 除非您仅使用此API客户端在单个项目中执行操作,否则建议使用组访问令牌或个人访问令牌。

https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html

API请求

您可以对GitLab REST API文档中的任何资源端点发起API请求。

刚开始使用? 探索用户项目问题端点。

端点API文档
users列出所有用户
users/{id}通过ID获取用户
users/{id}/projects获取用户是成员的项目
users创建用户
groups所有组的列表
groups/{id}/descendent_groups列出父组的所有子组
groups/{id}通过ID获取特定组
projects/{id}/members列出所有直接组成员
projects/{id}/members/all列出所有直接和继承的组成员
projects/{id}/members将成员添加到组
projects/{id}/members从组中移除成员
projects列出所有项目
projects/{id}通过ID获取特定项目
projects/{id}/members列出所有直接项目成员
projects/{id}/members/all列出所有直接和继承的项目成员
projects/{id}/members将成员添加到项目
projects/{id}/members从项目中移除成员
projects/{id}/issues列出项目问题
projects/{id}/issues/{id}获取特定项目问题
projects/{id}/issues创建新问题
projects/{id}/merge_requests列出项目合并请求
projects/{id}/repository/files/{urlencoded_path}从存储库获取文件元数据和内容

依赖注入

如果您在每个类的顶部包含完全限定的命名空间,您可以在发起API调用的方法中使用类名。

use Provisionesta\Gitlab\ApiClient;

class MyClass
{
    public function getGroup($group_id)
    {
        return ApiClient::get('groups/' . $group_id)->data;
    }
}

如果您不使用依赖注入,则在使用类时需要提供完全限定的命名空间。

class MyClass
{
    public function getGroup($group_id)
    {
        return \Provisionesta\Gitlab\ApiClient::get('groups/' . $group_id)->data;
    }
}

类实例化

我们在v4.0版本中过渡到使用静态方法,您不需要实例化ApiClient类。

ApiClient::get('groups');
ApiClient::post('groups', []);
ApiClient::get('groups/12345678');
ApiClient::put('groups/12345678', []);
ApiClient::delete('groups/12345678');

命名参数与位置参数

您可以使用命名参数/参数(PHP 8中引入)或位置函数参数/参数。

如果您正在指定请求数据或使用连接数组,建议使用命名参数。如果您只是指定URI,则可以使用位置参数。

有关函数参数命名参数以及这篇有用的博客文章,请查阅PHP文档。

// Named Arguments
ApiClient::get(
    uri: 'groups'
);

// Positional Arguments
ApiClient::get('groups');

GET请求

端点以或不以/开头,紧跟在/api/v4/之后。GitLab API文档提供了带有前导斜杠的端点,其中已隐含了/api/v4。是否包括端点的前导斜杠取决于您(仅为外观上的考虑)。API客户端会自动处理https://gitlab.com/api/v4/uri的字符串拼接。

使用API客户端,您可以使用get()方法,将端点groups作为uri参数。

ApiClient::get('groups');

您还可以使用变量或数据库模型来获取构建端点所需的数据。

// Get a list of records
$records = ApiClient::get('groups');

// Use variable for endpoint
$endpoint = 'groups';
$records = ApiClient::get($endpoint);

// Get a specific record
$group_id = '12345678';
$record = ApiClient::get('groups/' . $group_id);

// Get a specific record using a variable
// This assumes that you have a database column named `api_group_id` that
// contains the string with the GitLab Group ID `12345678`.
$gitlab_group = \App\Models\GitlabGroup::where('id', $id)->firstOrFail();
$record = ApiClient::get('groups/' . $gitlab_group->api_group_id);

带有查询字符串参数的GET请求

get()方法的第二个参数是一个可选的参数数组,该数组由API客户端和Laravel HTTP客户端解析,并以查询字符串的形式呈现,其中自动添加了?&

API请求过滤

一些API端点使用search查询字符串或其他参数来限制结果。请参阅列出用户列出所有项目API文档中的示例。每个端点都提供了不同的选项,您可以在使用每个端点时进行审查。

// Search for records
// https://docs.gitlab.com/ee/api/projects.html#list-all-projects
$records = ApiClient::get('projects', [
    'search' => 'my-project-name',
    'membership' => true
]);

// This will parse the array and render the query string
// https://gitlab.com/api/v4/projects?search=my-project-name&membership=true
API响应过滤

您还可以使用Laravel Collections来过滤和转换结果,无论是使用完整的数据集还是您通过API请求已经过滤的数据。

有关更多信息,请参阅使用Laravel Collections

POST请求

post()方法几乎与带有参数数组的get()请求相同,但是参数是以application/json内容类型作为表单数据传递的,而不是作为查询字符串放在URL中。这是行业标准,并不特定于API客户端。

您可以在Laravel HTTP客户端文档中了解更多有关请求数据的信息。

// Create a project
// https://docs.gitlab.com/ee/api/projects.html#create-project
$group_id = '12345678';
$record = ApiClient::post(
    uri: '/projects',
    data: [
        'name' => 'My Cool Project',
        'path' => 'my-cool-project',
        'namespace_id' => $group_id
    ]
);

PUT请求

put()方法用于更新现有记录的属性。

您需要确保在第一个参数(URI)中提供了您想要更新的记录的ID。在大多数应用程序中,这将是您从数据库或其他位置获取的变量,而不会是硬编码的。

// Update a project
// https://docs.gitlab.com/ee/api/projects.html#edit-project
$project_id = '123456789';
$record = ApiClient::put(
    uri: '/projects/' . $project_id,
    data: [
        'description' => 'This is a cool project that we created for a demo.'
    ]
);

DELETE请求

delete()方法用于基于您提供的ID销毁资源的操作。

请注意,delete()方法的返回状态码可能因供应商而异(例如,200、201、202、204等)。GitLab的API将为成功删除的资源返回204状态码,为已安排删除的资源返回202状态码。您应该使用$response->status->successful布尔值来检查结果。

// Delete a project
// https://docs.gitlab.com/ee/api/projects.html#delete-project
$project_id = '123456789';
$record = ApiClient::delete('projects/' . $project_id);

类方法

以上示例展示了基本的行内使用方法,适用于大多数场景。如果您希望使用类和构造函数,下面的示例将提供一个有用的示例。

<?php

use Provisionesta\Gitlab\ApiClient;
use Provisionesta\Gitlab\Exceptions\NotFoundException;

class GitlabProjectService
{
    private $connection;

    public function __construct(array $connection = [])
    {
        // If connection is null, use the environment variables
        $this->connection = !empty($connection) ? $connection : config('gitlab-api-client');
    }

    public function listProjects($query = [])
    {
        $projects = ApiClient::get(
            connection: $this->connection,
            uri: 'projects',
            data: $query
        );

        return $projects->data;
    }

    public function getProject($id, $query = [])
    {
        try {
            $project = ApiClient::get(
                connection: $this->connection,
                uri: 'projects/' . $id,
                data: $query
            );
        } catch (NotFoundException $e) {
            // Custom logic to handle a record not found. For example, you could
            // redirect to a page and flash an alert message.
        }

        return $project->data;
    }

    public function storeProject($request_data)
    {
        $project = ApiClient::post(
            connection: $this->connection,
            uri: 'projects',
            data: $request_data
        );

        // To return an object with the newly created project
        return $project->data;

        // To return the ID of the newly created project
        // return $project->data->id;

        // To return the status code of the form request
        // return $project->status->code;

        // To return a bool with the status of the form request
        // return $project->status->successful;

        // To return the entire API response with the data, headers, and status
        // return $project;
    }

    public function updateProject($id, $request_data)
    {
        try {
            $project = ApiClient::put(
                connection: $this->connection,
                uri: 'projects/' . $id,
                data: $request_data
            );
        } catch (NotFoundException $e) {
            // Custom logic to handle a record not found. For example, you could
            // redirect to a page and flash an alert message.
        }

        // To return an object with the updated created project
        return $project->data;

        // To return a bool with the status of the form request
        // return $project->status->successful;
    }

    public function deleteProject($id)
    {
        try {
            $project = ApiClient::delete(
                connection: $this->connection,
                uri: 'projects/' . $id
            );
        } catch (NotFoundException $e) {
            // Custom logic to handle a record not found. For example, you could
            // redirect to a page and flash an alert message.
        }

        return $project->status->successful;
    }
}

速率限制

在v4.0版本中,我们增加了当剩余20%速率限制时自动回退。这将通过在每个请求中实现sleep(10)来减慢请求速度。由于速率限制每60秒重置,这将减慢接下来的5-6个请求,直到速率限制重置。

如果GitLab的速率限制超出了端点,将抛出Provisionesta\Gitlab\Exceptions\RateLimitException

回退会减慢请求,但如果超出速率限制,请求将失败并终止。

API响应

此API客户端使用Provisionesta标准的API响应格式。

// API Request
$group = ApiClient::get('groups/80039310');

// API Response
$group->data; // object
$group->headers; // array
$group->status; // object
$group->status->code; // int (ex. 200)
$group->status->ok; // bool (is 200 status)
$group->status->successful; // bool (is 2xx status)
$group->status->failed; // bool (is 4xx/5xx status)
$group->status->clientError; // bool (is 4xx status)
$group->status->serverError; // bool (is 5xx status)

响应数据

data属性包含Laravel HTTP客户端object()方法的内容,已经解析,并且是任何分页结果的最终合并输出。

$group = ApiClient::get('groups/80039310');
$group->data;
{
    +"id": 80039310,
    +"web_url": "https://gitlab.com/groups/provisionesta",
    +"name": "provisionesta",
    +"path": "provisionesta",
    +"description": "Provisionesta is a library of open source packages, projects, and tools created by Jeff Martin mostly related to IAM/RBAC and REST API infrastructure and SaaS application provisioning.",
    +"visibility": "public",
    +"share_with_group_lock": false,
    +"require_two_factor_authentication": false,
    +"two_factor_grace_period": 48,
    +"project_creation_level": "developer",
    +"auto_devops_enabled": null,
    +"subgroup_creation_level": "maintainer",
    +"emails_disabled": false,
    +"emails_enabled": true,
    +"mentions_disabled": null,
    +"lfs_enabled": true,
    +"default_branch_protection": 2,
    +"default_branch_protection_defaults": {},
    +"avatar_url": "https://gitlab.com/uploads/-/system/group/avatar/80039310/121-automate.png",
    +"request_access_enabled": true,
    +"full_name": "provisionesta",
    +"full_path": "provisionesta",
    +"created_at": "2023-12-24T19:28:45.322Z",
    +"parent_id": null,
    +"organization_id": 1,
    +"shared_runners_setting": "enabled",
    +"ldap_cn": null,
    +"ldap_access": null,
    +"marked_for_deletion_on": null,
    +"wiki_access_level": "enabled",
    +"shared_with_groups": [],
    +"runners_token": "REDACTED",
    +"prevent_sharing_groups_outside_hierarchy": false,
    +"projects": [],
    +"shared_projects": [],
    +"shared_runners_minutes_limit": 50000,
    +"extra_shared_runners_minutes_limit": null,
    +"prevent_forking_outside_group": false,
    +"service_access_tokens_expiration_enforced": true,
    +"membership_lock": false,
    +"ip_restriction_ranges": null,
    +"unique_project_download_limit": 0,
    +"unique_project_download_limit_interval_in_seconds": 0,
    +"unique_project_download_limit_allowlist": [],
    +"unique_project_download_limit_alertlist": [
      4572001,
    ],
    +"auto_ban_user_on_excessive_projects_download": false,
}

访问单个记录值

您可以使用对象符号访问这些变量。这是处理API响应最常见的情况。

$group = ApiClient::get('groups/80039310')->data;

$group_name = $group->path;
// provisionesta

遍历记录

如果您有一个包含多个对象数组的记录,您可以遍历这些记录。API客户端会自动分页和合并记录数组,以改善开发者体验。

$groups = ApiClient::get('groups')->data;

foreach($groups as $group) {
    dd($group->path);
    // provisionesta
}

缓存响应

API客户端不使用缓存,以避免您在控制要缓存的端点时遇到任何限制。

当进行API调用时,您可以在端点周围包装一个缓存外观。您可以在Laravel Cache文档中了解更多信息。

use Illuminate\Support\Facades\Cache;
use Provisionesta\Gitlab\ApiClient;

$groups = Cache::remember('gitlab_groups', now()->addHours(12), function () {
    return ApiClient::get('groups')->data;
});

foreach($groups as $group) {
    dd($group->path);
    // provisionesta
}

在获取特定ID或传递额外参数时,请确保传递变量到use($var1, $var2)中。

$group_id = '12345678';

$groups = Cache::remember('gitlab_group_' . $group_id, now()->addHours(12), function () use ($group_id) {
    return ApiClient::get('groups/' . $group_id)->data;
});

日期格式化

您可以使用Carbon库来格式化日期和执行计算。

$created_date = Carbon::parse($group->data->created_at)->format('Y-m-d');
// 2023-01-01
$created_age_days = Carbon::parse($group->data->created_at)->diffInDays();
// 265

使用Laravel集合

您可以使用Laravel Collections,它是一组强大的数组辅助工具,类似于您可能已经熟悉的数组搜索和SQL查询。

有关更多信息,请参阅使用Laravel集合解析响应文档。

响应头

由于键使用连字符,会与访问键和值的语法冲突,因此返回数组而不是对象。

$group = ApiClient::get('groups/12345678');
$group->headers;
[
    +"Date": "Thu, 06 Jan 2022 21:40:18 GMT",
    +"Content-Type": "application/json",
    +"Transfer-Encoding": "chunked",
    +"Connection": "keep-alive",
    +"Cache-Control": "max-age=0, private, must-revalidate",
    +"Etag": "W/"ed65096017d349b25371385b9b96d102"",
    +"Vary": "Origin",
    +"X-Content-Type-Options": "nosniff",
    +"X-Frame-Options": "SAMEORIGIN",
    +"X-Request-Id": "01FRRNBQCPG9XKBY8211NEH285",
    +"X-Runtime": "0.100822",
    +"Strict-Transport-Security": "max-age=31536000",
    +"Referrer-Policy": "strict-origin-when-cross-origin",
    +"RateLimit-Observed": "6",
    +"RateLimit-Remaining": "1994",
    +"RateLimit-Reset": "1641505278",
    +"RateLimit-ResetTime": "Thu, 06 Jan 2022 21:41:18 GMT",
    +"RateLimit-Limit": "2000",
    +"GitLab-LB": "fe-12-lb-gprd",
    +"GitLab-SV": "localhost",
    +"CF-Cache-Status": "DYNAMIC",
    +"Expect-CT": "max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"",
    +"Report-To": "{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=LWJRP1mJdxCzclW3zKzqg40CbYJeUJ2mf2aRLBRfzxWvAgh15LrCQwpmqtk%2B4cJoDWsX3bx1yAkEB9HuokEMgKg%2FMkFXLoy2N8oE09KfHIH%2B8YWjBmX%2BdUD4hkg%3D"}],"group":"cf-nel","max_age":604800}",
    +"NEL": "{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}",
    +"Server": "cloudflare",
    +"CF-RAY": "6c981a9ba95639a7-SEA",
]

获取头值

$content_type = $group->headers['Content-Type'];
// application/json

响应状态

有关不同状态布尔值的更多信息,请参阅Laravel HTTP客户端文档

$group = ApiClient::get('groups/12345678');
$group->status;
{
  +"code": 200 // int (ex. 200)
  +"ok": true // bool (is 200 status)
  +"successful": true // bool (is 2xx status)
  +"failed": false // bool (is 4xx/5xx status)
  +"serverError": false // bool (is 4xx status)
  +"clientError": false // bool (is 5xx status)
}

API响应状态码

$group = ApiClient::get('groups/12345678');

$status_code = $group->status->code;
// 200

错误响应

对于任何4xx或5xx响应都会抛出异常。所有响应都会自动记录。

异常

代码异常类
N/AProvisionesta\Gitlab\Exceptions\ConfigurationException
400Provisionesta\Gitlab\Exceptions\BadRequestException
401Provisionesta\Gitlab\Exceptions\UnauthorizedException
403Provisionesta\Gitlab\Exceptions\ForbiddenException
404Provisionesta\Gitlab\Exceptions\NotFoundException
409Provisionesta\Gitlab\Exceptions\ConflictException
412Provisionesta\Gitlab\Exceptions\PreconditionFailedException
422Provisionesta\Gitlab\Exceptions\UnprocessableException
429Provisionesta\Gitlab\Exceptions\RateLimitException
500Provisionesta\Gitlab\Exceptions\ServerErrorException
503Provisionesta\Gitlab\Exceptions\ServiceUnavailableException

捕获异常

您可以捕获您想要静默处理的任何异常。任何未捕获的异常都会对用户可见,并导致在您的监控软件中出现的500错误。

use Provisionesta\Gitlab\Exceptions\NotFoundException;

try {
    $group = ApiClient::get('groups/12345678');
} catch (NotFoundException $e) {
    // Group is not found. You can create a log entry, throw an exception, or handle it another way.
    Log::error('GitLab group could not be found', ['gitlab_group_id' => $group_id]);
}

禁用异常

如果您不希望抛出异常,您可以为GitLab API客户端全局禁用异常,并自行处理每个请求的状态。只需在您的.env文件中设置GITLAB_API_EXCEPTIONS=false即可。

GITLAB_API_EXCEPTIONS=false

使用 Laravel 集合解析响应

您可以使用Laravel Collections,它是一组强大的数组辅助工具,类似于您可能已经熟悉的数组搜索和SQL查询。

$project_id = '12345678';

$issues = ApiClient::get('projects/' . $project_id . '/issues');

$issue_collection = collect($issues->data)->where('state', 'closed')->toArray();

// This will return an array of all issues that have been closed

为了语法规范和可读性,您可以轻松地将这些内容合并为一行。由于 ApiClient 自动处理任何 4xx 或 5xx 错误,因此您无需担心 try/catch 异常。

$users = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->toArray();

此方法可以使您获得与执行 SQL 查询相同的优势,并且在使用集合时感觉非常熟悉。

SELECT * FROM issues WHERE project_id='12345678' AND state='closed';

集合方法

以下是最常用的、用于过滤数据的常用方法

Laravel 文档使用示例
count使用示例
countBy使用示例
exceptN/A
filterN/A
flipN/A
groupBy使用示例
keyByN/A
onlyN/A
pluck使用示例
sort使用示例
sortBy使用示例
sortKeys使用示例
toArrayN/A
transform使用示例
unique使用示例
values使用示例
whereN/A
whereInN/A
whereNotInN/A

集合简化数组

Pluck 方法

您可以使用集合的 pluck 方法来获取特定的属性。

// Get an array of issue titles
$issue_titles = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->pluck('title')
    ->toArray();

// [
//     0 => 'Lorem ipsum dolor sit amet',
//     1 => 'Donec malesuada leo et efficitur imperdiet',
//     2 => 'Aliquam dignissim tortor faucibus',
//     3 => 'Sed convallis velit id massa',
//     4 => 'Vivamus congue quam eget nisl pharetra',
//     5 => 'Suspendisse finibus odio vitae',
// ]

您还可以使用 pluck 方法来获取两个属性,并将其中一个设置为数组键,另一个设置为数组值。

// Get an array with title keys and author array values
$issue_titles_authors = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->pluck('author', 'title')
    ->toArray();

// [
//     'Lorem ipsum dolor sit amet' => {
//         +"id": 123456,
//         +"username": "z3r0c00l.example",
//         +"name": "Dade Murphy",
//         +"state": "active",
//         +"locked": false,
//         +"web_url": "https://gitlab.com/z3r0c00l.example",
//     },
//     'Donec malesuada leo et efficitur imperdiet' => {
//         // truncated for docs
//      },
//     'Aliquam dignissim tortor faucibus' => {
//         // truncated for docs
//      },
//     'Sed convallis velit id massa' => {
//         // truncated for docs
//      },
//     'Vivamus congue quam eget nisl pharetra' => {
//         // truncated for docs
//      },
//     'Suspendisse finibus odio vitae' => {
//         // truncated for docs
//      },
// ]

使用点符号表示嵌套数组属性

如果只想返回一个字符串,则在使用 pluck 方法时可以使用点符号。您还可以在包括 wheregroupBy 方法在内的大多数其他集合方法中使用点符号。

$issue_titles_authors = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->pluck('author.name', 'title')
    ->toArray();

// [
//     'Lorem ipsum dolor sit amet' => "Dade Murphy",
//     'Donec malesuada leo et efficitur imperdiet' => "Kate Libby",
//     'Aliquam dignissim tortor faucibus' => "Kate Libby",
//     'Sed convallis velit id massa' => "Dade Murphy",
//     'Vivamus congue quam eget nisl pharetra' => "Paul Cook",
//     'Suspendisse finibus odio vitae' => "Joey Pardella",
// ]

变换数组

当处理 API 返回的记录时,您将有许多当前用例中不需要的数据。

您可以使用 transform 方法对每个记录执行 foreach 循环,并创建一个新的数组,其中包含您想要的特定字段。

您可以将 $item 变量视为具有特定记录所有元数据的 foreach($users as $item) { }

transform 方法使用函数(即闭包)返回应成为此特定数组键的新值的数组。

$issue_titles_authors = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->pluck('author', 'title')
    ->transform(function($item) {
        return [
            'id' => $item->id,
            'name' => $item->name
        ];
    })->toArray();

// [
//     'Lorem ipsum dolor sit amet' => {
//         +"id": 123456,
//         +"name": "Dade Murphy",
//     },
//     'Donec malesuada leo et efficitur imperdiet' => {
//         // truncated for docs
//      },
//     'Aliquam dignissim tortor faucibus' => {
//         // truncated for docs
//      },
//     'Sed convallis velit id massa' => {
//         // truncated for docs
//      },
//     'Vivamus congue quam eget nisl pharetra' => {
//         // truncated for docs
//      },
//     'Suspendisse finibus odio vitae' => {
//         // truncated for docs
//      },
// ]
箭头函数

如果您的所有转换都可以在数组内直接完成,而不需要定义额外的变量,则可以使用简写箭头函数。这仅是个人偏好,并非要求。

$users = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->pluck('author', 'title')
    ->transform(fn($item) => [
        'id' => $item->id,
        'name' => $item->name
    ])->toArray();

计算值

如果您想返回一个计算得到的数组或字符串,可以在 transform 方法中执行它们,就像使用具有输入和输出的正常函数一样。

$issue_titles_authors = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->pluck('author', 'title')
    ->transform(function($item) {
        // Disclaimer: Performing individual API calls on large data sets
        // may exhaust rate limits and is computationally intensive.
        $user = ApiClient::get('users/' . $item->id)->data;
        return $user->email;
    })->toArray();

// [
//     'Lorem ipsum dolor sit amet' => "dmurphy@example.com",
//     'Donec malesuada leo et efficitur imperdiet' => "klibby@example.com",
//     'Aliquam dignissim tortor faucibus' => "klibby@example.com",
//     'Sed convallis velit id massa' => "dmurphy@example.com",
//     'Vivamus congue quam eget nisl pharetra' => "pcook@example.com",
//     'Suspendisse finibus odio vitae' => "jpardella@example.com",
// ]

Unique 方法

您可以使用 unique 方法获取独特属性值列表(例如作者姓名)。

$unique_authors = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->unique('author.name')
    ->pluck('author.name')
    ->toArray();

// [
//     36 => 'Dade Murphy',
//     111 => 'Kate Libby',
//     238 => 'Paul Cook',
//     288 => 'Joey Pardella'
// ]

Values 方法

当使用 unique 方法时,它使用找到的第一个记录的键。您应该在最后添加 values 方法,以根据结果数量重置所有键整数。

$unique_authors = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->unique('author.name')
    ->pluck('author.name')
    ->values()
    ->toArray();

// [
//     0 => 'Dade Murphy',
//     1 => 'Kate Libby',
//     2 => 'Paul Cook',
//     3 => 'Joey Pardella'
// ]

排序方法

您可以通过属性值进行字母排序。只需将属性提供给 sortBy 方法即可(支持嵌套数组值)。如果您已经使用了 pluck 方法,并且数组值是字符串,则可以使用不接收参数的 sort 方法。

// Option 1
$unique_job_titles = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->sortBy('author.name')
    ->unique('author.name')
    ->pluck('author.name')
    ->values()
    ->toArray();

// Option 2
$unique_job_titles = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->unique('author.name')
    ->pluck('author.name')
    ->sort()
    ->values()
    ->toArray();

// [
//     0 => 'Dade Murphy',
//     1 => 'Joey Pardella'
//     2 => 'Kate Libby',
//     3 => 'Paul Cook',
// ]

如果您有数组键字符串,可以使用 sortKeys 方法按字母顺序对结果数组的键进行排序。

计数方法

您可以使用 count 方法获取所有方法应用后结果的总量。这作为对 toArray 的替代,这样您就得到一个整数值,而不是需要执行 count($collection_array)

// Get a count of issues
$unique_job_titles = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->count();

// 376

您可以使用 countBy 方法获取唯一属性值的计数。您应该使用 sortKeys 方法按字母顺序对结果数组的键进行排序。

// Get a count of unique job titles
$unique_job_titles = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->countBy('author.name')
    ->sortKeys()
    ->toArray();

// [
//     'Dade Murphy' => 2,
//     'Joey Pardella' => 1,
//     'Kate Libby' => 2,
//     'Paul Cook' => 1
// ]

分组方法

尽管您可以使用与原始响应一起的 groupBy 方法,但在数据分组后很难操作数据,因此建议在将数据转换后,将 groupBy('attribute_name') 添加到您的集合链的末尾。请注意,在转换数据时,您已重命名了数组值键(属性),因此您想使用新的数组键。

$users = collect(ApiClient::get('projects/' . $project_id . '/issues')->data)
    ->where('state', 'closed')
    ->transform(fn($item) => [
        'id' => $item->id,
        'title' => $item->title,
        'author_name' => $item->author->name,
    ])->sortBy('author_name')
    ->groupBy('author_name')
    ->toArray();

// "Dade Murphy" => [
//     [
//         "id" => "36",
//         "title" => "Lorem ipsum dolor sit amet",
//         "author_name" => "Dade Murphy",
//     ],
//     [
//         "id" => "368",
//         "title" => "Sed convallis velit id massa",
//         "author_name" => "Dade Murphy",
//     ]
// ],
// "Joey Pardella" => [
//     [
//         "id" => "288",
//         "title" => "Suspendisse finibus odio vitae",
//         "author_name" => "Joey Pardella",
//     ],
// ],
// "Kate Libby" => [
//     [
//         "id" => "111",
//         "title" => "Donec malesuada leo et efficitur imperdiet",
//         "author_name" => "Kate Libby",
//     ],
//     [
//         "id" => "219",
//         "title" => "Aliquam dignissim tortor faucibus",
//         "author_name" => "Kate Libby",
//     ]
// ],
// "Paul Cook" => [
//     [
//         "id" => "238",
//         "title" => "Vivamus congue quam eget nisl pharetra",
//         "author_name" => "Paul Cook",
//     ],
// ]

阅读更多内容

有关其他用法,请参阅 Laravel Collections 文档。有关更多实际示例,请参阅 provisionesta/gitlab-laravel-actions 包。

日志示例

此包使用 provisionesta/audit 包进行标准日志记录。

请求数据日志配置

为了提高日志的有用性,将随 POST 和 PUT 请求一起发送的 data 键/值对进行了记录。您可以选择在 .env 文件中禁用(排除)特定方法的 request_data

GITLAB_API_LOG_REQUEST_DATA_GET_ENABLED=true
GITLAB_API_LOG_REQUEST_DATA_POST_ENABLED=false # default is true
GITLAB_API_LOG_REQUEST_DATA_PUT_ENABLED=false # default is true
GITLAB_API_LOG_REQUEST_DATA_DELETE_ENABLED=true

如果您想排除特定的键,它们可以在您 发布配置文件 后在 config/gitlab-api-client.php 文件中设置。

默认情况下,keypassword 字段从 GET 请求中排除,而 contentPOSTPUT 请求中排除(例如,存储库文件的 base64 编码内容)。

事件类型

应使用 event_type 键进行任何分类和日志搜索。

  • 格式: gitlab.api.{method}.{result/log_level}.{reason?}
  • 方法: get|post|patch|put|delete
状态码事件类型日志级别
N/Agitlab.api.test.successDEBUG
N/Agitlab.api.test.errorCRITICAL
N/Agitlab.api.validate.errorCRITICAL
N/Agitlab.api.get.process.pagination.startedDEBUG
N/Agitlab.api.get.process.pagination.finishedDEBUG
N/Agitlab.api.rate-limit.approachingCRITICAL
N/Agitlab.api.rate-limit.exceeded(预异常)CRITICAL
N/Agitlab.api.{method}.error.http.exceptionERROR
200gitlab.api.{method}.successDEBUG
201gitlab.api.{method}.successDEBUG
202gitlab.api.{method}.successDEBUG
204gitlab.api.{method}.successDEBUG
400gitlab.api.{method}.warning.bad-requestWARNING
401gitlab.api.{method}.error.unauthorizedERROR
403gitlab.api.{method}.error.forbiddenERROR
404gitlab.api.{method}.warning.not-foundWARNING
405gitlab.api.{method}.error.method-not-allowedERROR
412gitlab.api.{method}.error.precondition-failedDEBUG
422gitlab.api.{method}.error.unprocessableDEBUG
429gitlab.api.{method}.critical.rate-limitCRITICAL
500gitlab.api.{method}.critical.server-errorCRITICAL
501gitlab.api.{method}.error.not-implementedERROR
503gitlab.api.{method}.critical.server-unavailableCRITICAL

测试连接

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Success {"event_type":"gitlab.api.get.success","method":"Provisionesta\\Gitlab\\ApiClient::get","event_ms":493,"metadata":{"url":"https://gitlab.example.com/api/v4/version"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::testConnection Success {"event_type":"gitlab.api.test.success","method":"Provisionesta\\Gitlab\\ApiClient::testConnection"}

成功请求

GET 请求日志

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Success {"event_type":"gitlab.api.get.success","method":"Provisionesta\\Gitlab\\ApiClient::get","event_ms":885,"metadata":{"url":"https://gitlab.example.com/api/v4/groups/25"}}

GET 分页请求日志

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Success {"event_type":"gitlab.api.get.success","method":"Provisionesta\\Gitlab\\ApiClient::get","count_records":100,"event_ms":986,"event_ms_per_record":9,"metadata":{"rate_limit_remaining":null,"url":"https://gitlab.example.com/api/v4/groups"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Paginated Results Process Started {"event_type":"gitlab.api.get.process.pagination.started","method":"Provisionesta\\Gitlab\\ApiClient::get","metadata":{"uri":"groups"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::getPaginatedResults Success {"event_type":"gitlab.api.getPaginatedResults.success","method":"Provisionesta\\Gitlab\\ApiClient::getPaginatedResults","count_records":100,"event_ms":904,"event_ms_per_record":9,"metadata":{"rate_limit_remaining":null,"url":"https://gitlab.example.com/api/v4/groups?order_by=name&owned=false&page=2&per_page=100&sort=asc&statistics=false&with_custom_attributes=false"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::getPaginatedResults Success {"event_type":"gitlab.api.getPaginatedResults.success","method":"Provisionesta\\Gitlab\\ApiClient::getPaginatedResults","count_records":20,"event_ms":391,"event_ms_per_record":19,"metadata":{"rate_limit_remaining":null,"url":"https://gitlab.example.com/api/v4/groups?order_by=name&owned=false&page=3&per_page=100&sort=asc&statistics=false&with_custom_attributes=false"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Paginated Results Process Complete {"event_type":"gitlab.api.get.process.pagination.finished","method":"Provisionesta\\Gitlab\\ApiClient::get","duration_ms":2287,"metadata":{"uri":"groups"}}

带有 URL 编码路径的 GET 请求

cool-group/my-cool-project

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Success {"event_type":"gitlab.api.get.success","method":"Provisionesta\\Gitlab\\ApiClient::get","event_ms":1160,"metadata":{"url":"https://gitlab.example.com/api/v4/projects/cool%2Dgroup%2Fmy%2Dcool%2Dproject"}}

POST 请求日志

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::post Success {"event_type":"gitlab.api.post.success","method":"Provisionesta\\Gitlab\\ApiClient::post","event_ms":1552,"metadata":{"url":"https://gitlab.example.com/api/v4/projects","request_data":{"name":"My Cool Project3","path":"my-cool-project3","namespace_id":"123"}}}

PUT 成功日志

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::put Success {"event_type":"gitlab.api.put.success","method":"Provisionesta\\Gitlab\\ApiClient::put","event_ms":423,"metadata":{"url":"https://gitlab.example.com/api/v4/projects/12345","request_data":{"description":"cool project description2"}}}

DELETE 成功日志

计划删除将返回 202 状态码而不是 204 状态码。

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::delete Success {"event_type":"gitlab.api.delete.success","method":"Provisionesta\\Gitlab\\ApiClient::delete","errors":{"message":"202 Accepted"},"event_ms":373,"metadata":{"url":"https://gitlab.example.com/api/v4/projects/12345"}}

错误

401 未授权

环境变量未设置
[YYYY-MM-DD HH:II:SS] local.CRITICAL: ApiClient::validateConnection Error {"event_type":"gitlab.api.validate.error","method":"Provisionesta\\Gitlab\\ApiClient::validateConnection","errors":["The url field is required.","The token field is required."]}

Provisionesta\Gitlab\Exceptions\ConfigurationException

Gitlab API configuration validation error. This occurred in Provisionesta\Gitlab\ApiClient::validateConnection. (Solution) The url field is required. The token field is required.
无效令牌
[YYYY-MM-DD HH:II:SS] local.ERROR: ApiClient::get Client Error {"event_type":"gitlab.api.get.error.unauthorized","method":"Provisionesta\\Gitlab\\ApiClient::get","errors":{"message":"401 Unauthorized"},"event_ms":225,"metadata":{"url":"https://gitlab.com/api/v4/projects/12345678","rate_limit_remaining":"1999"}}

Provisionesta\Gitlab\Exceptions\UnauthorizedException

The `GITLAB_API_TOKEN` has been configured but is invalid. (Reason) This usually happens if it does not exist, expired, or does not have permissions. (Solution) Please generate a new API Token and update the variable in your `.env` file.

404 未找到

[YYYY-MM-DD HH:II:SS] local.WARNING: ApiClient::get Client Error {"event_type":"gitlab.api.get.warning.not-found","method":"Provisionesta\\Gitlab\\ApiClient::get","errors":{"message":"404 Project Not Found"},"event_ms":253,"metadata":{"url":"https://gitlab.com/api/v4/projects/12345678"}}