provisionesta/google-api-client

Google API Client for Laravel

4.1 2024-03-22 12:37 UTC

This package is auto-updated.

Last update: 2024-09-22 13:50:38 UTC


README

[[目录]]

概述

Google API Client 是一个开源的 Composer 包,用于在 Laravel 应用程序中连接到 Google 进行资源分配和撤销分配,尤其是在 Google Workspace (Admin SDK) 和 Google Cloud 中。

此项目由开源社区维护,不由任何公司维护。请自行承担风险,并在遇到任何错误时创建合并请求。

问题陈述

官方 PHP Google API 客户端难以使用,并且不使用 Laravel 生态系统中其他 Composer 包的通用约定或命名空间。这主要是因为它是使用另一种语言创建的,PHP 库是自动生成的,而不是人工生成的。由于 Google 产品和服务众多以及不同的 API,这导致使用体验不佳,文档难以查找,API 响应结果难以解析。本包的作者花费了数百小时来尝试使用不到 10 个端点,最终放弃并决定构建一些东西来满足我们自己的需求,并帮助其他 Laravel 开发者。

我们不是为 API 文档中的每个端点提供 SDK 方法,而是通过提供一个通用的 ApiClient 来执行更简单的操作,该客户端可以对您在 Google API 文档 中找到的任何端点执行 GETPOSTPUTDELETE 请求。

这基于 Laravel HTTP Client 的简单性,该客户端由 Guzzle HTTP 客户端 驱动,提供对 Google API 响应的“最后一行代码解析”,以改善开发人员体验。

此 API 客户端的优点是它为您处理 API 请求记录、响应分页和 4xx/5xx 异常处理。将抛出速率限制错误,但由于各种 Google API 服务之间的非标准化差异,无法提供速率限制回退。

示例用法

use Provisionesta\Google\ApiClient;

// Create a group
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/insert
$group = ApiClient::post(
    url: "https://admin.googleapis.com/admin/directory/v1/groups",
    scope: "https://www.googleapis.com/auth/admin.directory.group",
    form_data: [
        "name" => "Hack the Planet Engineers",
        "email" => "elite-engineers@example.com"
    ],
);

// {
//     +"data": {
//       +"id": "0a1b2c3d4e5f6g7",
//       +"email": "elite-engineers@example.com",
//       +"name": "Hack the Planet Engineers",
//       +"description": "",
//       +"adminCreated": true,
//     },
//     +"headers": [
//        ...
//     ],
//     +"status": {
//       +"code": 200,
//       +"ok": true,
//       +"successful": true,
//       +"failed": false,
//       +"serverError": false,
//       +"clientError": false,
//     },
//   }

// Get a list of records
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/list
$groups = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups',
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_data: [],
    query_keys: ['customer']
);

foreach($groups->data as $group) {
    dd($group);
}

// {
//     +"kind": "admin#directory#group",
//     +"id": "0a1b2c3d4e5f6g7",
//     +"email": "elite-engineers@example.com",
//     +"name": "Hack the Planet Engineers",
//     +"directMembersCount": "0",
//     +"description": "",
//     +"adminCreated": true,
//     +"nonEditableAliases": [
//         "elite-engineers@example.com.test-google-a.com",
//     ],
// },

// Get a specific record
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/get
// $group_id = '0a1b2c3d4e5f6g7';
$group_id = 'elite-engineers@example.com';
$existing_group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
);

dd($existing_group);

// {
//     +"data": {
//         +"id": "0a1b2c3d4e5f6g7",
//         +"email": "elite-engineers@example.com",
//         +"name": "Hack the Planet Engineers",
//         +"directMembersCount": "0",
//         +"description": "",
//         +"adminCreated": true,
//         +"nonEditableAliases": [
//             "elite-engineers@example.com.test-google-a.com",
//         ],
//     },
//     +"headers": [
//         ...
//     ],
//     +"status": {
//         +"code": 200,
//         +"ok": true,
//         +"successful": true,
//         +"failed": false,
//         +"serverError": false,
//         +"clientError": false,
//     },
// }

$group_name = $group->data->name;
// Hack the Planet Engineers

// Update a group (patch)
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/patch
// $group_id = '0a1b2c3d4e5f6g7';
$group_id = 'elite-engineers@example.com';
$response = ApiClient::patch(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    form_data: [
        'description' => 'This group contains engineers that have liberated the garbage files.'
    ],
);

// Delete a group
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/delete
// $group_id = '0a1b2c3d4e5f6g7';
$group_id = 'elite-engineers@example.com';
$response = ApiClient::delete(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
);

问题跟踪和错误报告

我们不维护功能请求路线图,但我们邀请您贡献,并将乐意审查您的合并请求。

请为错误报告创建 问题

贡献

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

维护者

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

贡献者信誉

安装

需求

需求版本
PHP^8.0
Laravel^8.0, ^9.0, ^10.0, ^11.0

升级指南

查看 变更日志 了解发布说明。

仍在使用 glamstack/*? 此包是 glamstack/google-auth-sdkglamstack/google-workspace-sdkglamstack/google-cloud-sdk 的替代品,但已完全重构,需要重新实现之前的使用。

添加 Composer 包

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

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

发布配置文件

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

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

环境变量

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

任何被注释掉的变量是可选的,如果未设置,将显示默认值。

# GOOGLE_API_CUSTOMER="my_customer"
# GOOGLE_API_DOMAIN=""
# GOOGLE_API_EXCEPTIONS=true
# GOOGLE_API_KEY_PATH=""
GOOGLE_API_SUBJECT_EMAIL=""

GOOGLE_API_CUSTOMER

API将运行的Google帐户的客户编号。这将需要与关联的服务帐户的客户编号匹配。Google提供了一个别名my_customer,它默认使用服务帐户的客户ID。

GOOGLE_API_DOMAIN

您希望过滤结果的Google Workspace组织中的域。这应该设置为您的首选电子邮件域名。当与Workspace组和工作用户一起工作时,可以将domain添加到ApiClient请求的query_params数组中,以仅过滤到此域名。您也可以为测试留空。

GOOGLE_API_EXCEPTIONS

是否在收到4xx或5xx错误时抛出异常。如果false,您可以根据每个响应中返回的status数组捕获和处理异常。

GOOGLE_API_KEY_PATH

您可以指定JSON密钥文件的完整操作系统路径。出于安全原因,除非您已在storage/keys或类似位置配置了适当的文件权限,否则应将其保存到Laravel目录之外。

除非您无法使用gcloud CLI附加服务帐户方法,否则不应存储和使用JSON密钥文件。如果您的架构支持,您应该在CI/CD变量或秘密库(例如Ansible Vault、AWS Parameter Store、GCP Secrets Manager、HashiCorp Vault等)中存储您的变量,而不是在本地服务器上。

GOOGLE_API_SUBJECT_EMAIL

运行Google Workspace API的电子邮件地址。这与权限或授权无关,只需是一个具有在Admin UI中执行相同操作权限的有效用户电子邮件即可。

API凭证

使用Google Cloud、Google Workspace和其他Google API的API身份验证很复杂,可能令人困惑。这些说明旨在尽快且安全地帮助您入门。有关更多信息,请参阅Google API身份验证文档

此包使用GOOGLE_APPLICATION_CREDENTIALS环境变量、JSON密钥文件或运行时传递到包含数据库值的connection array中的JSON字符串,并会自动生成用于调用API端点的OAuth2 JWT令牌。

已知限制:由于架构和技术发现挑战,您可能能够使用GCP实例级别的附加服务帐户,但我们尚未能够充分测试这一点。

在大多数情况下,这将是一个GCP项目服务帐户应用程序默认凭证

如果您已指定GOOGLE_APPLICATION_CREDENTIALS环境变量,请将GOOGLE_API_KEY_FILE变量注释掉。如果您已从GCP下载了JSON密钥,请参阅GOOGLE_API_KEY_PATH变量的说明。

如果您在数据库或密钥管理器中存储了连接密钥,您可以通过覆盖 config/google-api-client.php 配置或在每次请求时提供连接数组来重写它。有关更多信息,请参阅连接数组

API密钥优先级

每个API请求将按照以下顺序检查密钥是否存在,并使用找到的第一个值。

  1. key_string 密钥存在于传递给GET、POST、PATCH、PUT或DELETE请求的connection参数的数组中。
  2. key_path 密钥存在于传递给请求的connection参数的数组中。
  3. key_path 密钥已在.env文件中设置,或作为GOOGLE_API_KEY_PATH环境变量。
  4. GOOGLE_APPLICATION_CREDENTIALS 环境变量已在您的服务器上设置,或者使用gcloud auth application-default login命令。
  5. (未测试)GCP实例、集群、容器、CloudRun等的元数据服务器凭据。

如果找不到有效的JSON密钥,将创建一个日志消息,并抛出带有google.api.validate.error.empty事件类型的Provisionesta\Google\Exceptions\ConfigurationException异常。

安全最佳实践

不共享令牌

请勿使用已为其他应用程序或脚本创建的API服务帐户或密钥。您应该为每个用例生成新的服务帐户和密钥。

这在安全事件期间很有帮助,当时需要在一个受损害的系统上吊销密钥,并且您不希望使用相同用户或服务帐户的其他系统受到影响,因为这些系统使用的是不同的、未被吊销的密钥。

API令牌存储

最佳实践:除非您不能使用gcloud CLI附加服务帐户方法,否则您不应存储和使用JSON密钥文件。如果您的架构支持,您应该将变量存储在CI/CD变量或秘密保险库(例如Ansible Vault、AWS Parameter Store、GCP Secrets Manager、HashiCorp Vault等)中,而不是在服务器上本地存储。

请勿将您的API密钥添加到任何config/*.php文件中,以避免将其提交到您的存储库(秘密泄露)。

所有JSON API密钥通常应保存为文件系统中的安全位置的.json文件,并由GOOGLE_API_KEY_PATH .env变量引用。

您还可以使用GOOGLE_APPLICATION_CREDENTIALS环境变量,该变量在.env文件中未设置值时使用。

有关更多信息,请参阅Google文档中的其他最佳实践。

创建JSON密钥

有几种方法可以静态或动态地使用JSON密钥凭据。这与使用AWS IAM访问密钥和秘密(JSON凭证文件)或IAM角色假定类似。

按照适用于您环境的步骤进行操作。

这里没有涵盖与密钥管理器相关的其他高级用例。无论哪种方法,API客户端只需要设置GOOGLE_APPLICATION_CREDENTIALS环境变量、GOOGLE_API_KEY_PATH环境变量或.env变量值。

使用gcloud CLI的本地开发环境

这是推荐的新手入门方法。您可能需要不时重新认证,尤其是如果您积极使用 gcloud 来管理多个 GCP 项目或资源时。只需使用所需的 gcloud auth login 和/或 gcloud auth application-default login 命令。

  1. (先决条件) 在您的计算机上 安装初始化 gcloud SDK。
  2. 运行 gcloud auth login认证 您的 Google Cloud 组织。
  3. 运行 gcloud auth application-default login自动配置 您计算机上的 GOOGLE_APPLICATION_CREDENTIALS 环境变量。
  4. 您可以在 ~/.config/gcloud/application_default_credentials.json 中看到配置的密钥。请不要将其复制到 Laravel 仓库或其他位置。> 尽管此密钥的格式看起来与您下载的 JSON 密钥略有不同,但 Google 仍然可以在后台生成令牌时解析它。
  5. 您已经设置好了!您不需要触摸或移动任何 JSON 密钥或更新 .env 变量。所有 API 调用都使用您电子邮件地址分配的角色和权限。> 请记住,如果未设置 GOOGLE_API_KEY_PATHGOOGLE_API_KEY_STRING 值,API 客户端将自动使用 GOOGLE_APPLICATION_CREDENTIALS 环境变量。

如果您需要更多帮助,请参阅 Google 文档

具有附加服务帐户 IAM 认证的 GCP 基础设施

已知限制:由于架构和技术探索挑战,您可能能够使用 GCP 实例级附加服务帐户,但我们尚未能够充分测试这一点。如果需要,请回退到 GCP 项目服务帐户密钥 和设置 GOOGLE_APPLICATION_CREDENTIALS 环境变量。

如果您的应用程序在 Google Cloud 基础设施或托管服务上运行,则可以在机器/资源级别而不是应用程序级别使用 IAM 认证。许多 Google Cloud 服务(例如,Compute Engine 虚拟机、Google Kubernetes Engine 集群、Cloud Run 部署、AppEngine 等)允许您附加一个服务帐户,该帐户可用于提供访问 Google Cloud API 的凭据。如果 ADC 在 GOOGLE_APPLICATION_CREDENTIALS 环境变量或 Google 帐户凭据的已知位置找不到可用的凭据,则它将使用元数据服务器获取运行代码的服务凭据。

如果您的 Laravel 应用程序在 Google Cloud 上运行,则使用附加服务帐户的凭据是 首选方法

请参阅 Google 文档和最佳实践以了解更多信息。

GCP 项目服务帐户密钥

这是手动方法,除非您无法使用 gcloud CLI附加服务帐户 方法,否则不建议使用。

创建密钥
  1. (先决条件) 创建一个 GCP 项目或选择一个可以创建服务帐户用户的现有项目。
  2. 导航到 https://console.cloud.google.com/iam-admin/serviceaccounts
  3. 从左上角的下拉菜单中选择您的项目。
  4. 点击页面顶部的 创建服务帐户 按钮。
  5. 使用以下推荐值或选择自己的值。
    • 服务帐户名称:Laravel Google API Client
    • 服务帐户 ID:laravel-app
    • 描述:(空白)
    • 项目角色:(见服务帐户项目角色说明)
    • 授予用户:(空白)
  6. 点击完成按钮。
  7. 在表中点击您新服务帐户的链接名称。如果您在这个GCP项目中有很多服务帐户,您可以使用搜索栏(例如:laravel-app)。
  8. 点击页面顶部的密钥选项卡。
  9. 点击添加密钥 > 创建新密钥并选择JSON类型。
  10. 密钥将自动下载。
在Mac上存储您的密钥

最佳实践:如果可能,您应该使用JSON密钥内容设置GOOGLE_APPLICATION_CREDENTIALS环境变量,而不是将JSON密钥文件保存到本地磁盘。

以下所有步骤都是推荐的新手入门说明。您可以将JSON密钥存储在您喜欢的任何地方,只要它位于一个安全的位置,并具有适当的权限(例如,0600),并且不要意外将其提交到您的代码存储库。

  1. 打开您的Terminal或iTerm。
  2. 运行mkdir -p ~/.config/gcloud/service-accounts创建服务帐户目录。
  3. 运行cd ~/.config/gcloud/service-accounts切换到新目录。
  4. 运行cp ~/Downloads/{project-name}-###########.json .将您的服务帐户移动到新(当前)目录。 > 您可以使用tab完成(或可选地重命名文件)。
  5. 运行realpath {project-name}-###########.json获取此文件的完整路径。
更新您的Laravel环境变量
  1. 使用您的IDE(例如,VS Code)打开您的Laravel应用程序存储库。
  2. 打开并编辑.env文件。
  3. 取消注释GOOGLE_API_KEY_PATH并将值设置为从Terminal的文件路径。
GOOGLE_API_KEY_PATH="/Users/dmurphy/.config/gcloud/service-accounts/{project-name}-###########.json"
授予密钥权限

根据您将要使用的API端点,您需要添加Google Workspace权限和/或添加Google Cloud权限

服务帐户项目角色

  • 如果您将要执行管理此GCP项目中资源的API调用,请从基本 > 编辑器开始设置,作为起点(或一个或多个细粒度的GCP角色)。
  • 如果此服务帐户将用于GCP组织级管理、子项目文件夹级管理或不同的GCP项目资源,请留空并继续不分配角色。
  • 如果此服务帐户将用于Google Workspace管理,请留空并继续不分配角色。

为Google Workspace添加权限

要使用为组和使用Google Workspace(Admin API)端点,您需要将服务帐户的OAuth客户端ID添加到域范围委派,并为您要调用的端点设置相应的范围。还有将服务帐户添加到管理员角色的替代方法,但它并不直观,并且不一定按预期工作。

请参阅Google文档中的域范围委派分步说明。有关应授予服务帐户哪些范围的详细信息,请参阅API范围

请参阅在Google Cloud中启用API的步骤,以启用Admin SDK API和您可能需要的任何其他API。

为Google Cloud添加权限

对于Google Cloud,您需要在组织级别、文件夹级别或项目级别(或根据需要多个级别)将gcloud用户电子邮件地址(例如 dmurphy@example.com)或服务帐户电子邮件地址(例如 laravel-app@{project}.iam.gserviceaccount.com)添加为IAM主体,并分配一个自定义角色或一个预定义角色来管理任何给定组织、文件夹或项目中的资源。

请参阅Google文档以了解更多关于授予、更改和撤销对GCP组织、文件夹和项目访问权限的信息。

在Google Cloud中启用API

如果您正在使用服务帐户(未使用 gcloud auth),则需要在该服务帐户创建的GCP项目中启用相应的服务API(例如 @{project}.iam.gserviceaccount.com)。这同样适用于Google Workspace服务帐户。

  1. 导航至https://console.cloud.google.com/apis/library
  2. 搜索API名称(以下是一些常见示例)。
    • Admin SDK API(用于Google Workspace群组和用户)
    • Google Drive API
    • Google Sheets API
    • Cloud Resource Manager API
    • 身份和访问管理(IAM)API
  3. 点击启用按钮。这可能需要几分钟才能完成。>如果可见管理按钮,则API已启用,无需采取任何操作。

API令牌权限和作用域

Google使用OAUTH作用域来管理Google Workspace内的权限。

您需要指定该特定端点(您的API密钥已被授权使用)的作用域之一。您将在调用特定端点的REST API文档中找到一个作用域列表。

以下是一些您可能会用到的常见作用域,以帮助您开始

  • https://www.googleapis.com/auth/admin.directory.group
  • https://www.googleapis.com/auth/admin.directory.user
  • https://www.googleapis.com/auth/cloud-identity
  • https://www.googleapis.com/auth/cloud-platform

您可以在Google文档中查看作用域的完整列表。

连接数组

您在.env文件中定义的变量默认使用,除非您使用包含所需参数的数组设置连接参数。

如果您将Google服务帐户JSON密钥存储在数据库中(例如多租户架构或具有细粒度权限的隔离服务帐户),则建议使用这种方法。

可以使用key_string参数将JSON数组作为字符串传递。

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

use App\Models\GoogleServiceAccount;
use Provisionesta\Google\ApiClient;

class MyClass
{
    private array $connection;

    public function __construct($service_account_id)
    {
        $service_account = GoogleServiceAccount::findOrFail($service_account_id);

        $this->connection = [
            'customer' => 'my_customer',
            'exceptions' => true,
            'key_string' => $service_account->json_key,
            'subject_email' => $service_account->subject_email
        ];
    }

    public function listGroups($group_key)
    {
        return ApiClient::get(
            url: 'https://admin.googleapis.com/admin/directory/v1/groups',
            scope: 'https://www.googleapis.com/auth/admin.directory.group',
            query_keys: ['customer'],
            connection: $this->connection,
        )->data;
    }
}

API请求

您可以对Google API文档中的任何资源端点进行API请求。

刚开始?探索Google Workspace群组Google Workspace用户Cloud Identity GroupsGoogle Cloud Compute Engine端点。

依赖注入

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

use Provisionesta\Google\ApiClient;

class MyClass
{
    public function getGroup($group_id)
    {
        return ApiClient::get(
            url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
            scope: 'https://www.googleapis.com/auth/admin.directory.group'
        )->data;
    }
}

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

class MyClass
{
    public function getGroup($group_id)
    {
        return \Provisionesta\Google\ApiClient::get(
            url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
            scope: 'https://www.googleapis.com/auth/admin.directory.group'
        )->data;
    }
}

类实例化

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

use Provisionesta\Google\ApiClient;

ApiClient::get(...);
ApiClient::post(...);
ApiClient::patch(...);
ApiClient::put(...);
ApiClient::delete(...);

命名参数与位置参数

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

建议使用命名参数。

在 PHP 文档中了解更多有关 函数参数命名参数 以及这篇有用的 博客文章

use Provisionesta\Google\ApiClient;

// Named Arguments
ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups',
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_keys: ['customer'],
)->data;

// Positional Arguments
ApiClient::get(
    'https://admin.googleapis.com/admin/directory/v1/groups',
    'https://www.googleapis.com/auth/admin.directory.group',
    [],
    ['customer'],
    []
)->data;

GET 请求

由于 Google 的 API 分布在多个域名和子域名中,您需要在 url 命名参数/参数中提供 API 端点的完整 URL。

查看 API 令牌权限和范围 以了解如何为每个端点查找范围。

GET 方法参数

参数类型描述
url字符串API 端点的完整 URL
scope字符串API 端点所需的权限之一,您的 API 密钥已授权使用。
query_data数组(可选)应用于请求的查询数据的数组
query_keys数组(可选)连接 配置键 的数组(例如:customerdomainsubject_email),应包含在特定端点(例如:Google Workspace 目录 API)的 API 请求中以合并到 query_data。
connection数组(可选)包含 API 连接变量的数组。如果没有设置,config('google-api-client') 将使用您 .env 文件中的 GOOGLEAPI* 变量。

GET 示例用法

use Provisionesta\Google\ApiClient;

// Get a list of records
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/list
$groups = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups',
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_data: [],
    query_keys: ['customer']
);

您还可以使用变量或数据库模型来获取构建端点所需的数据。某些端点需要 ID,而其他端点则允许使用人类友好的别名(例如:资源名称或电子邮件地址)。

use Provisionesta\Google\ApiClient;

// Get a specific record using a variable
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/get
// $group_id = '0a1b2c3d4e5f6g7';
$group_id = 'elite-engineers@example.com';
$existing_group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_data: [],
    query_keys: ['customer']
);

// Get a specific record using a database value
// This assumes that you have a database column named `api_group_id` that
// contains the string with the Google ID `0a1b2c3d4e5f6g7`.
$database_group = \App\Models\GoogleGroup::where('id', $id)->firstOrFail();
$api_group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $google_group->api_group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_data: [],
    query_keys: ['customer']
);

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

get() 方法的第三个位置参数或 query_data 命名参数是可选的参数数组,由 API 客户端和 Laravel HTTP 客户端 解析,并以 ?& 自动添加的方式渲染为查询字符串。

查询键

许多端点将需要 customerdomain 和/或 subject_email 在查询字符串中传递。为了提高您的开发体验,您无需添加这些值或手动合并查询字符串数组。您只需在 query_keys 数组中提供端点需要的键。值将从您的连接配置(或 .env 文件)中提取,并将键值对自动合并到 query_data 数组中,形成查询字符串。

查看 ApiClient.php 中的 getConnectionQueryParams() 方法以了解更多信息。

use Provisionesta\Google\ApiClient;

$group = ApiClient::post(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups',
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_data: [],
    query_keys: ['customer']
    // query_keys: ['customer', 'domain', 'subject_email']
);
https://admin.googleapis.com/admin/directory/v1/groups?customer=my_customer
API 请求过滤

Google API 使用子数组来表示多个资源。在搜索值时,您使用点表示法(例如:profile.name)来访问这些属性。

API 响应过滤

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

请参阅 使用 Laravel 集合 中的 API 响应 文档。

POST 请求

post() 方法几乎与 get() 请求相同,只是增加了 form_data 数组参数。这是行业标准,并非特定于 API 客户端。

由于许多Google端点需要其中一个query_keys,一些端点还使用额外的查询字符串参数,所以我们不能仅仅使用一个data参数进行整合。

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

use Provisionesta\Google\ApiClient;

// Create a group
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/insert
$group = ApiClient::post(
    url: "https://admin.googleapis.com/admin/directory/v1/groups",
    scope: "https://www.googleapis.com/auth/admin.directory.group",
    form_data: [
        "name" => "Hack the Planet Engineers",
        "email" => "elite-engineers@example.com"
    ],
    query_keys: ["customer"]
);
参数类型描述
url字符串API 端点的完整 URL
scope字符串API 端点所需的权限之一,您的 API 密钥已授权使用。
form_data数组(可选)将转换为JSON的表单数据数组
query_data数组(可选)应用于请求的查询数据的数组
query_keys数组(可选)连接 配置键 的数组(例如:customerdomainsubject_email),应包含在特定端点(例如:Google Workspace 目录 API)的 API 请求中以合并到 query_data。
connection数组(可选)包含 API 连接变量的数组。如果没有设置,config('google-api-client') 将使用您 .env 文件中的 GOOGLEAPI* 变量。

PATCH请求

patch()方法用于更新现有记录的一个或多个属性。PATCH用于部分更新。如果您想更新和替换现有记录的所有属性,应使用put()方法

您需要确保提供的记录ID在URL中。在大多数应用程序中,这将是从数据库或其他位置获取的变量,而不是硬编码的。

PATCH方法参数

参数类型描述
url字符串API 端点的完整 URL
scope字符串API 端点所需的权限之一,您的 API 密钥已授权使用。
form_data数组(可选)将转换为JSON的表单数据数组
query_data数组(可选)应用于请求的查询数据的数组
query_keys数组(可选)连接 配置键 的数组(例如:customerdomainsubject_email),应包含在特定端点(例如:Google Workspace 目录 API)的 API 请求中以合并到 query_data。
connection数组(可选)包含 API 连接变量的数组。如果没有设置,config('google-api-client') 将使用您 .env 文件中的 GOOGLEAPI* 变量。

PATCH示例用法

use Provisionesta\Google\ApiClient;

// Update a group (patch)
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/patch
// $group_id = '0a1b2c3d4e5f6g7';
$group_id = 'elite-engineers@example.com';
$response = ApiClient::patch(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    form_data: [
        'description' => 'This group contains engineers that have liberated the garbage files.'
    ],
    query_keys: ['customer']
);

PUT请求

put()方法用于更新和替换现有记录的所有属性。如果您想更新一个或多个属性而不更新整个现有记录,应使用patch()方法。对于大多数用例,您将希望使用patch()方法来更新记录。

这可能需要获取记录,覆盖数组中特定键的值,然后将整个数组传递回API客户端的form_data参数。

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

PUT方法参数

参数类型描述
url字符串API 端点的完整 URL
scope字符串API 端点所需的权限之一,您的 API 密钥已授权使用。
form_data数组(可选)将转换为JSON的表单数据数组
query_data数组(可选)应用于请求的查询数据的数组
query_keys数组(可选)连接 配置键 的数组(例如:customerdomainsubject_email),应包含在特定端点(例如:Google Workspace 目录 API)的 API 请求中以合并到 query_data。
connection数组(可选)包含 API 连接变量的数组。如果没有设置,config('google-api-client') 将使用您 .env 文件中的 GOOGLEAPI* 变量。

PUT示例用法

use Provisionesta\Google\ApiClient;

// Get a specific record using a variable
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/get
// $group_id = '0a1b2c3d4e5f6g7';
$group_id = 'elite-engineers@example.com';
$existing_group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
);

// {
//     +"data": {
//         +"id": "0a1b2c3d4e5f6g7",
//         +"email": "elite-engineers@example.com",
//         +"name": "Hack the Planet Engineers",
//         +"directMembersCount": "0",
//         +"description": "",
//         +"adminCreated": true,
//         +"nonEditableAliases": [
//             "elite-engineers@example.com.test-google-a.com",
//         ],
//     },
//     +"headers": [
//         ...
//     ],
//     +"status": {
//         +"code": 200,
//         +"ok": true,
//         +"successful": true,
//         +"failed": false,
//         +"serverError": false,
//         +"clientError": false,
//     },
// }

$existing_group->data->description = 'This group contains engineers that have revealed to the world their elite skills.';

// Update a group (put)
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/update
// $group_id = '0a1b2c3d4e5f6g7';
$group_id = 'elite-engineers@example.com';
$response = ApiClient::patch(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    form_data: $existing_group->data->description
);

DELETE请求

delete()方法用于根据您提供的ID销毁资源。

请注意,delete()方法将根据供应商返回不同的状态码(例如,200、201、202、204等)。Google的API将为成功删除的资源返回204状态码。您应使用$response->status->successful布尔值来检查结果。

DELETE方法参数

参数类型描述
url字符串API 端点的完整 URL
scope字符串API 端点所需的权限之一,您的 API 密钥已授权使用。
form_data数组(可选)将转换为JSON的表单数据数组
query_data数组(可选)应用于请求的查询数据的数组
query_keys数组(可选)连接 配置键 的数组(例如:customerdomainsubject_email),应包含在特定端点(例如:Google Workspace 目录 API)的 API 请求中以合并到 query_data。
connection数组(可选)包含 API 连接变量的数组。如果没有设置,config('google-api-client') 将使用您 .env 文件中的 GOOGLEAPI* 变量。

DELETE示例用法

use Provisionesta\Google\ApiClient;

// Delete a group
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/delete
// $group_id = '0a1b2c3d4e5f6g7';
$group_id = 'elite-engineers@example.com';
$response = ApiClient::delete(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group'
);

类方法

上面的示例显示了适用于大多数用例的基本内联用法。如果您喜欢使用类和构造函数,下面的示例将会有所帮助。

<?php

use Provisionesta\Google\ApiClient;
use Provisionesta\Google\Exceptions\NotFoundException;

class GoogleGroupService
{
    private $connection;

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

    public function listGroups($query = [])
    {
        $groups = ApiClient::get(
            url: 'https://admin.googleapis.com/admin/directory/v1/groups',
            scope: 'https://www.googleapis.com/auth/admin.directory.group',
            query_data: $query,
            query_keys: ['customer'],
            connection: $this->connection
        );

        return $groups->data;
    }

    public function getGroup($id, $query = [])
    {
        try {
            $existing_group = ApiClient::get(
                url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $id,
                scope: 'https://www.googleapis.com/auth/admin.directory.group',
                query_data: $query,
                connection: $this->connection
            );
        } 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 $group->data;
    }

    public function storeGroup($request_data)
    {
        $group = ApiClient::post(
            url: "https://admin.googleapis.com/admin/directory/v1/groups",
            scope: "https://www.googleapis.com/auth/admin.directory.group",
            form_data: $request_data,
            connection: $this->connection
        );

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

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

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

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

        // To throw an exception if the request fails
        // throw_if(!$group->status->successful, new \Exception($group->error->message, $group->status->code));

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

    public function updateGroup($id, $request_data)
    {
        try {
            $group = ApiClient::patch(
                url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $id,
                scope: 'https://www.googleapis.com/auth/admin.directory.group',
                form_data: $request_data,
                connection: $this->connection
            );
        } 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 group
        return $group->data;

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

    public function deleteGroup($id)
    {
        try {
            $group = ApiClient::delete(
                url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
                scope: 'https://www.googleapis.com/auth/admin.directory.group',
                connection: $this->connection
            );
        } 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 $group->status->successful;
    }
}

速率限制

由于Google API端点中提供的服务数量庞大,此API客户端无法提供接近检测和退避的速率限制。如果超过速率限制,将记录异常并抛出,请求将失败。

有关您调用的特定服务的速率限制信息,请参阅速率限制文档,或考虑在需要时添加sleep(#)助手。

API响应

此API客户端使用Provisionesta标准进行API响应格式化。

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

// 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_id = 'elite-engineers@example.com';
$group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group'
);

$group->data;
{
    +"id": "0a1b2c3d4e5f6g7",
    +"email": "elite-engineers@example.com",
    +"name": "Hack the Planet Engineers",
    +"directMembersCount": "0",
    +"description": "This group contains engineers that have liberated the garbage files.",
    +"adminCreated": true,
    +"nonEditableAliases": [
        "elite-engineers@example.com.test-google-a.com",
    ],
},

访问单个记录值

您可以使用对象表示法访问这些变量。这是处理API响应的最常见用例。

$group_id = 'elite-engineers@example.com';
$group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group'
)->data;

$group_name = $group->name;
// Hack the Planet Engineers

遍历记录

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

$groups = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups',
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_data: [],
    query_keys: ['customer']
)->data;

foreach($groups as $group) {
    dd($group->name);
    // Hack the Planet Engineers
}

缓存响应

API客户端不使用缓存,以避免任何限制您控制哪些端点进行缓存。

在调用API时,您可以将端点包装在缓存外观中。您可以在Laravel缓存文档中了解详细信息。

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

$groups = Cache::remember('google_groups', now()->addHours(2), function () {
    return ApiClient::get(
        url: 'https://admin.googleapis.com/admin/directory/v1/groups',
        scope: 'https://www.googleapis.com/auth/admin.directory.group',
        query_keys: ['customer']
    )->data;
});

foreach($groups as $group) {
    dd($group->name);
    // Hack the Planet Engineers
}

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

$group_id = '0a1b2c3d4e5f6g7';
// $group_id = 'elite-engineers@example.com';

$groups = Cache::remember('google_group_' . $group_id, now()->addHours(12), function () use ($group_id) {
    return ApiClient::get(
        url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
        scope: 'https://www.googleapis.com/auth/admin.directory.group'
    )->data;
});

日期格式化

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

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

使用Laravel集合

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

响应头

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

$group_id = 'elite-engineers@example.com';
$group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group'
)->data;

$group->headers;
[
    "ETag" => "vd5VuYRc7EabgWBin1OmNozuzPe13OUXakGXQzUmnHA/AKpK7DnSvYzi2eNow9bdQ2H0TcE",
    "Content-Type" => "application/json; charset=UTF-8",
    "Vary" => [
        "Origin",
        "X-Origin",
        "Referer",
    ],
    "Date" => "Tue, 27 Feb 2024 03:30:20 GMT",
    "Server" => "ESF",
    "Content-Length" => "400",
    "X-XSS-Protection" => "0",
    "X-Frame-Options" => "SAMEORIGIN",
    "X-Content-Type-Options" => "nosniff",
    "Alt-Svc" => "h3=":443"; ma=2592000,h3-29=":443"; ma=2592000",
],

获取头值

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

响应状态

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

$group_id = 'elite-engineers@example.com';
$group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group'
)->data;

$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_id = 'elite-engineers@example.com';
$group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_data: [],
    query_keys: ['customer']
)->data;

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

错误响应

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

异常

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

捕获异常

您可以捕获任何您想静默处理的异常。任何未捕获的异常将出现在用户面前,并导致500错误,这些错误将在您的监控软件中显示。

use Provisionesta\Google\Exceptions\NotFoundException;

try {
    $group_id = 'elite-engineers@example.com';
    $group = ApiClient::get(
        url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
        scope: 'https://www.googleapis.com/auth/admin.directory.group',
        query_data: [],
        query_keys: ['customer']
    )->data;
} catch (NotFoundException $e) {
    // Group is not found. You can create a log entry, throw an exception, or handle it another way.
    Log::error('Google group could not be found', ['google_group_id' => $group_id]);
}

禁用异常

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

GOOGLE_API_EXCEPTIONS=false

日志示例

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

事件类型

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

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

成功的请求

GET请求日志

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiToken::sendAuthRequest Success {"event_type":"google.api.auth.success","method":"App\\Actions\\Connections\\Google\\ApiToken::sendAuthRequest"}

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Success {"event_type":"google.api.get.success","method":"App\\Actions\\Connections\\Google\\ApiClient::get","count_records":5,"event_ms":810,"event_ms_per_record":162,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Success {"event_type":"google.api.get.success","method":"App\\Actions\\Connections\\Google\\ApiClient::get","count_records":29,"event_ms":334,"event_ms_per_record":11,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users/0a1b2c3d4e5f6g7?customer=my_customer"}}

GET分页请求日志

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiToken::sendAuthRequest Success {"event_type":"google.api.auth.success","method":"App\\Actions\\Connections\\Google\\ApiToken::sendAuthRequest"}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Success {"event_type":"google.api.get.success","method":"App\\Actions\\Connections\\Google\\ApiClient::get","count_records":100,"event_ms":966,"event_ms_per_record":9,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Paginated Results Process Started {"event_type":"google.api.get.process.pagination.started","method":"App\\Actions\\Connections\\Google\\ApiClient::get","metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::getPaginatedResults Success {"event_type":"google.api.getPaginatedResults.success","method":"App\\Actions\\Connections\\Google\\ApiClient::getPaginatedResults","count_records":100,"event_ms":613,"event_ms_per_record":6,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer&pageToken=REDACTED"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::getPaginatedResults Success {"event_type":"google.api.getPaginatedResults.success","method":"App\\Actions\\Connections\\Google\\ApiClient::getPaginatedResults","count_records":100,"event_ms":1352,"event_ms_per_record":13,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer&pageToken=REDACTED"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::getPaginatedResults Success {"event_type":"google.api.getPaginatedResults.success","method":"App\\Actions\\Connections\\Google\\ApiClient::getPaginatedResults","count_records":100,"event_ms":2151,"event_ms_per_record":21,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer&pageToken=REDACTED"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::getPaginatedResults Success {"event_type":"google.api.getPaginatedResults.success","method":"App\\Actions\\Connections\\Google\\ApiClient::getPaginatedResults","count_records":100,"event_ms":2835,"event_ms_per_record":28,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer&pageToken=REDACTED"}}
...
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Paginated Results Process Complete {"event_type":"google.api.get.process.pagination.finished","method":"App\\Actions\\Connections\\Google\\ApiClient::get","count_records":3820,"duration_ms":29423,"duration_ms_per_record":7,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users"}}

POST请求日志

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::post Success {"event_type":"google.api.post.success","method":"App\\Actions\\Connections\\Google\\ApiClient::post","count_records":5,"event_ms":1054,"event_ms_per_record":210,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/groups?customer=my_customer"}}

PATCH请求日志

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::patch Success {"event_type":"google.api.patch.success","method":"App\\Actions\\Connections\\Google\\ApiClient::patch","count_records":6,"event_ms":775,"event_ms_per_record":128,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/groups/0a1b2c3d4e5f6g7?customer=my_customer"}}

PUT成功日志

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::put Success {"event_type":"google.api.put.success","method":"App\\Actions\\Connections\\Google\\ApiClient::put","count_records":6,"event_ms":623,"event_ms_per_record":103,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/groups/0a1b2c3d4e5f6g7?customer=my_customer"}}

DELETE成功日志

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::delete Success {"event_type":"google.api.delete.success","method":"App\\Actions\\Connections\\Google\\ApiClient::delete","event_ms":584,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/groups/0a1b2c3d4e5f6g7?customer=my_customer"}}

错误

缺少认证作用域错误

[YYYY-MM-DD HH:II:SS] local.CRITICAL: ApiToken::sendAuthRequest Error {"event_type":"google.api.auth.error","method":"Provisionesta\\Google\\ApiToken::sendAuthRequest","errors":["Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested."]}

缺少客户ID错误

您最常看到的错误是神秘通用的 400 Bad Request,这是由于在请求中未添加 query_keys: ['customer'] 所导致的,尤其是在调用与 Google Workspace 相关的 API 时。这种情况在执行返回大量结果的 get() 请求时尤其常见。

// Before
$groups = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups',
    scope: 'https://www.googleapis.com/auth/admin.directory.group'
);

// After
$groups = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups',
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_keys: ['customer'],
);

400 Bad Request

请参阅缺失客户ID错误以进行初步故障排除。

[YYYY-MM-DD HH:II:SS] local.WARNING: ApiClient::get Client Error {"event_type":"google.api.get.warning.bad-request","method":"App\\Actions\\Connections\\Google\\ApiClient::get","event_ms":468,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/groups"}}

404 Not Found

[YYYY-MM-DD HH:II:SS] local.WARNING: ApiClient::get Client Error {"event_type":"google.api.get.warning.not-found","method":"App\\Actions\\Connections\\Google\\ApiClient::get","event_ms":354,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users/user@example.com?customer=my_customer"}}

429 速率限制异常

[YYYY-MM-DD HH:II:SS] local.CRITICAL: ApiClient::get Client Error {"event_type":"google.api.get.critical.rate-limit","method":"Provisionesta\\Google\\ApiClient::get","errors":{"message":"Quota exceeded for quota metric 'Read requests' and limit 'Read requests per minute per user' of service 'sheets.googleapis.com' for consumer 'project_number:123456789012'."},"event_ms":129,"metadata":{"url":"https://sheets.googleapis.com/v4/spreadsheets/REDACTED"}}