PHP code example of goletter / hyperf-docs

1. Go to this page and download the library: Download goletter/hyperf-docs library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

goletter / hyperf-docs example snippets


return [
    'default' => env('DOCS_PLATFORM', 'google'),

    // file(默认)| database(读库,缺项回落 file)
    'config_source' => env('DOCS_CONFIG_SOURCE', 'file'),

    'database' => [
        'connection' => env('DOCS_DB_CONNECTION'), // null = 默认库
        'table' => env('DOCS_CONFIG_TABLE', 'docs_platform_configs'),
        'columns' => [
            'platform' => 'platform',
            'tenant_id' => 'tenant_id',
            'payload' => 'config', // 可选 JSON 列;无列/为空则读 attributes
        ],
        'attributes' => [
            'client_id',
            'client_secret',
            'redirect_uri',
            'app_id',
            'app_secret',
            'api_key',
            'drive_root_folder',
            'scopes',
        ],
    ],

    // 自定义平台 class 映射(工厂可扩展 / 覆盖)
    'platforms_map' => [
        // 'my_docs' => \App\Docs\Platform\MyDocsPlatform::class,
    ],

    'platforms' => [
        'google' => [
            'client_id' => env('GOOGLE_CLIENT_ID'),
            'client_secret' => env('GOOGLE_CLIENT_SECRET'),
            'redirect_uri' => env('GOOGLE_REDIRECT_URI'),
            'api_key' => env('GOOGLE_API_KEY'),
            'drive_root_folder' => env('GOOGLE_DRIVE_ROOT_FOLDER_NAME', 'Goletter'),
            'scopes' => [
                'https://www.googleapis.com/auth/documents',
                'https://www.googleapis.com/auth/drive.file',
                'https://www.googleapis.com/auth/spreadsheets',
            ],
        ],
        'tencent' => [
            'client_id' => env('TENCENT_DOCS_CLIENT_ID'),
            'client_secret' => env('TENCENT_DOCS_CLIENT_SECRET'),
            'redirect_uri' => env('TENCENT_DOCS_REDIRECT_URI'),
            'drive_root_folder' => env('TENCENT_DOCS_DRIVE_ROOT_FOLDER_NAME', 'Goletter'),
            // OAuth 文档要求 scope 固定为 all;实际权限以开放平台已开通权限为准
            'scopes' => env('TENCENT_DOCS_SCOPES', 'all'),
        ],
        'feishu' => [
            'app_id' => env('FEISHU_APP_ID'),
            'app_secret' => env('FEISHU_APP_SECRET'),
            'redirect_uri' => env('FEISHU_REDIRECT_URI'),
            'drive_root_folder' => env('FEISHU_DRIVE_ROOT_FOLDER_NAME', 'Goletter'),
            'scopes' => env('FEISHU_SCOPES', 'sheets:spreadsheet drive:drive offline_access'),
        ],
    ],
];

use Goletter\Docs\DocsManager;

// 授权 URL / 换 token:凭证从库(或回落文件)读取
$url = $docs->auth('google')->getAuthUrl($state);
$token = $docs->auth('google')->fetchToken($code);

// 表格:用户 token 仍要自己传
$docs->sheets('google')->createSpreadsheet(
    ['access_token' => $userAccessToken],
    '月报'
);

use Goletter\Docs\DocsContext;

DocsContext::setTenantId((string) $tenantId);

// 中间件示例:
// DocsContext::setTenantId((string) $request->getHeaderLine('X-Tenant-Id'));

use Goletter\Docs\DocsManager;
use Goletter\Docs\Contract\PlatformFactoryInterface;
use Hyperf\Di\Annotation\Inject;

#[Inject]
protected DocsManager $docs;

#[Inject]
protected PlatformFactoryInterface $factory;

$google = $this->docs->platform('google');
$tencent = $this->docs->platform('tencent');
$feishu = $this->docs->platform('feishu');
$authUrl = $this->docs->auth('google')->getAuthUrl();

$file = $this->docs->sheets('google')->createSpreadsheet(
    ['access_token' => $googleToken['access_token']],
    '报表'
);

$file = $this->docs->sheets('tencent')->createSpreadsheet(
    [
        'access_token' => $tencentToken['access_token'],
        'open_id' => $tencentToken['open_id'],
    ],
    '报表'
);

$file = $this->docs->sheets('feishu')->createSpreadsheet(
    ['access_token' => $feishuToken['access_token']],
    '报表'
);

$platform = $this->docs->make('google', [
    'client_id' => $row['client_id'],
    'client_secret' => $row['client_secret'],
    'redirect_uri' => $row['redirect_uri'],
    'drive_root_folder' => 'Goletter',
]);

$authUrl = $platform->auth()->getAuthUrl($state);
$token = $platform->auth()->fetchToken($code);

$file = $platform->sheets()->createSpreadsheet(
    ['access_token' => $token['access_token']],
    '报表'
);

// 飞书
$feishu = $this->docs->make('feishu', [
    'app_id' => $row['app_id'],
    'app_secret' => $row['app_secret'],
    'redirect_uri' => $row['redirect_uri'],
]);

// 等价
$this->factory->make('tencent', [
    'client_id' => '...',
    'client_secret' => '...',
    'redirect_uri' => '...',
]);

'docs' => [
    'handlers' => [
        [
            'class' => Monolog\Handler\RotatingFileHandler::class,
            'constructor' => [
                'filename' => BASE_PATH . '/runtime/logs/docs.log',
                'level' => Monolog\Level::Info,
            ],
        ],
    ],
],

// config/autoload/docs.php
'platforms_map' => [
    'my_docs' => \App\Docs\Platform\MyDocsPlatform::class,
],

// 或运行时注册
$this->factory->register('my_docs', MyDocsPlatform::class);
$this->factory->make('my_docs');

['id' => '...', 'title' => '...', 'url' => '...', 'raw' => /* 原始对象/数组 */]

use Goletter\Docs\DocsManager;
use Goletter\Docs\Platform\GooglePlatform;

/** @var GooglePlatform $platform */
$platform = $this->docs->platform('google');
$sheets = $platform->sheets();

$token = ['access_token' => $accessToken];
// 过期时:
// $token = $platform->refreshToken($refreshToken);

$spreadsheetId = '1f6KLBbJnsOMKMKUxkwH-Bfwm4YWkFA_RBibJbbLIq8M';
$gid = 649506568; // URL 里 #gid=

// 从分享链接解析 spreadsheetId / gid
// https://docs.google.com/spreadsheets/d/{id}/edit#gid=649506568
$parsed = $sheets->parseSpreadsheetUrl(
    'https://docs.google.com/spreadsheets/d/1f6KLBbJnsOMKMKUxkwH-Bfwm4YWkFA_RBibJbbLIq8M/edit#gid=649506568'
);
// → spreadsheet_id / gid / url
// 或:Goletter\Docs\Support\SheetUrl::parseGoogle($url)
// 反解:SheetUrl::buildGoogle($spreadsheetId, $gid)

$title = $platform->getSheetTitleByGid($token, $spreadsheetId, $gid);
$range = $platform->rangeForGid($token, $spreadsheetId, $gid, 'A1:Z100');

// 只返回有内容的行,并裁掉行尾空单元格
// 第 4 个参数:每行至少几个非空单元格才保留(默认 1)
$rows = $sheets->readCells($token, $spreadsheetId, "gid:{$gid}");
$rows = $sheets->readCells($token, $spreadsheetId, "gid:{$gid}", 3);

// 按 F 列(也可用 0-based 下标,A=0)匹配
$matches = $sheets->findRows($token, $spreadsheetId, "gid:{$gid}", 'F', '333333333');
/*
[
  [
    'row' => 12,
    'range' => "'Sheet'!A12:Z12",
    'values' => [..., '333333333', true],
  ],
]
*/
$one = $matches[0] ?? null;

// null = 跳过该格(不会把保护列放进请求 range)
// '' = 清空(仍需编辑权限)
// true/false = 勾选框
$sheets->writeCells($token, $spreadsheetId, "gid:{$gid}!A1", [
    [null, null, null, null, '名称', 'ID', null, true],
]);

// 更稳妥:直接写可编辑列
$sheets->writeCells($token, $spreadsheetId, "gid:{$gid}!F1", [['333333333']]);
$sheets->writeCells($token, $spreadsheetId, "gid:{$gid}!H1", [[true]]);

// 按行号编辑
$result = $sheets->updateRow(
    $token,
    $spreadsheetId,
    "gid:{$gid}",
    [null, null, null, null, '新名称', '333333333', null, true],
    row: 12,
);

// 按列内容定位第一行再编辑(找不到返回 null)
$result = $sheets->updateRow(
    $token,
    $spreadsheetId,
    "gid:{$gid}",
    [null, null, null, null, '新名称', '333333333', null, false],
    column: 'F',
    match: '333333333',
);
/*
[
  'row' => 12,
  'range' => "'Sheet'!A12:Z12",
  'values' => [..., '333333333', false],  // 单行一维
]
*/

// 也可以 findRows + writeCells
$hit = $sheets->findRows($token, $spreadsheetId, "gid:{$gid}", 'F', '333333333')[0] ?? null;
if ($hit) {
    $sheets->writeCells($token, $spreadsheetId, "gid:{$gid}!A{$hit['row']}", [
        [null, null, null, null, '新名称', '333333333', null, true],
    ]);
}

// 按行号删除
$result = $sheets->deleteRow($token, $spreadsheetId, "gid:{$gid}", row: 12);
// ['row' => 12]

// 按列内容定位第一行再删除(找不到返回 null)
$result = $sheets->deleteRow(
    $token,
    $spreadsheetId,
    "gid:{$gid}",
    column: 'F',
    match: '333333333',
);

// 单行(一维)
$result = $sheets->appendCells($token, $spreadsheetId, "gid:{$gid}", [
    null, null, null, null, '名称', '333333333', null, true,
]);
/*
[
  'row' => 12,
  'range' => "'Sheet'!A12:Z12",
  'values' => [..., '333333333', true],  // 单行返回一维
]
*/

// 多行(二维);values 仍为二维
$result = $sheets->appendCells($token, $spreadsheetId, "gid:{$gid}", [
    [null, null, null, null, '名称1', '111', null, true],
    [null, null, null, null, '名称2', '222', null, true],
]);

// 单行 / 多行均可;始终插在第 1 行表头下面
$result = $sheets->insertAfterHeader($token, $spreadsheetId, "gid:{$gid}", [
    [null, null, null, null, '名称1', '111', null, true],
    [null, null, null, null, '名称2', '222', null, true],
]);
/*
[
  'row' => 2,
  'range' => "'Sheet'!A2:Z3",
  'values' => [[...], [...]],
]
*/

// 单列:按 F 列匹配
$result = $sheets->upsertRows(
    $token,
    $spreadsheetId,
    "gid:{$gid}",
    [
        [null, null, null, null, '名称1', '111', null, true],
        [null, null, null, null, '名称2', '222', null, true],
    ],
    column: 'F',
);

// 多列 AND:E + F 都相等才算同一行
$result = $sheets->upsertRows(
    $token,
    $spreadsheetId,
    "gid:{$gid}",
    [
        [null, null, null, null, '名称1', '111', null, true],
        [null, null, null, null, '名称2', '222', null, true],
    ],
    column: ['E', 'F'],
);

// 表头占两行(第 1、2 行):数据从第 3 行起,新行插到第 3 行,第 1~2 行不参与匹配
$result = $sheets->upsertRows(
    $token,
    $spreadsheetId,
    "gid:{$gid}",
    [
        [null, null, null, null, '名称1', '111', null, true],
    ],
    column: 'F',
    dataStartRow: 3,
);
/*
[
  'updated' => [
    ['row' => 5, 'range' => "...", 'values' => [...]],
  ],
  'inserted' => [          // 没有新行时为 null
    'row' => 2,
    'range' => "...",
    'values' => [[...]],
  ],
]
*/

// 1) 表头下批量插入(默认 dataStartRow=2, position=prepend)
$sheets->insertBlock($token, $spreadsheetId, "gid:{$gid}!J:O", [
    ['2026/4/1', 2000, 1990, '水单', 1, '备注'],
    ['2026/4/2', 3000, 2980, '', 1, ''],
]);

// 2) 有则改、无则插到表头下(可多条件)
$sheets->upsertBlock(
    $token,
    $spreadsheetId,
    "gid:{$gid}!A:E",
    [
        ['2026/9/24', 10.5, 0.5, null, null],
        ['2026/9/23', 11, 0.6, null, null],
    ],
    column: 'A',          // 或多列 ['A', 'B'] / 相对下标 0
);

// 接到区块末尾(不插表头下)
$sheets->insertBlock(..., position: 'append');
$sheets->upsertBlock(..., column: 'P', position: 'append');

$sheets->upsertBlock(
    $token,
    $spreadsheetId,
    "gid:{$gid}!J:O",
    [['2026/4/1', 2000, 1990, '水单', 1, '备注']],
    column: 'J',
    dataStartRow: 4,      // 跳过第 1~3 行表头/合计
    position: 'prepend',  // 插到第 4 行,只挤 J:O
);

$file = $sheets->createSpreadsheet($token, '报表');
$sheets->batchWrite($token, $spreadsheetId, [
    ['range' => "gid:{$gid}!A1", 'values' => [['a', 'b']]],
]);
$sheets->addSheet($token, $spreadsheetId, 'Sheet2');
$sheets->moveSpreadsheetToDateFolder($token, $spreadsheetId);
$sheets->shareSpreadsheetForAnyoneReader($token, $spreadsheetId);

use Goletter\Docs\Tencent\TencentAuth;
use Goletter\Docs\Tencent\TencentSheets;
use Hyperf\Di\Annotation\Inject;

#[Inject]
protected TencentAuth $auth;

#[Inject]
protected TencentSheets $sheets;

$authUrl = $this->auth->getAuthUrl(state: 'xyz'); // scope 固定为 all
$token = $this->auth->fetchToken($code);
// $token['access_token']、$token['refresh_token']、$token['open_id']

$file = $this->sheets->createSpreadsheet($accessToken, $openId, '报表');
$spreadsheetId = $file['ID'] ?? $file['id'];

// 从分享链接解析 spreadsheetId / sheetId(?tab=)
// https://docs.qq.com/sheet/DSEhEVGJuQ1dFQXhM?tab=BB08J2
// → spreadsheet_id=300000000$HHDTbnCWEAxL, sheet_id=BB08J2
$parsed = $this->docs->sheets('tencent')->parseSpreadsheetUrl(
    'https://docs.qq.com/sheet/DSEhEVGJuQ1dFQXhM?tab=BB08J2'
);
// 或:Goletter\Docs\Support\SheetUrl::parseTencent($url)

// 读:spreadsheet v3(需 scope.sheet / scope.sheet.readonly)
$values = $this->sheets->readCells($accessToken, $openId, $spreadsheetId, 'A1:Z1000');

// 写:sheetbook v2
$this->sheets->writeCells($accessToken, $openId, $spreadsheetId, 'A1', [['姓名', '部门']]);

// 追加 / 按列查找(门面 sheets('tencent') 同样支持)
$sheets = $this->docs->sheets('tencent');
$tokenPayload = [
    'access_token' => $accessToken,
    'open_id' => $openId,
];
$sheets->appendCells($tokenPayload, $spreadsheetId, 'A1', [
    '张三', '技术部',
]);
$sheets->findRows($tokenPayload, $spreadsheetId, 'A1:Z1000', 'A', '张三');
$sheets->updateRow($tokenPayload, $spreadsheetId, 'A1:Z1000', ['李四', '产品部'], column: 'A', match: '张三');
$sheets->deleteRow($tokenPayload, $spreadsheetId, 'A1:Z1000', column: 'A', match: '李四');

$this->sheets->moveSpreadsheetToDateFolder($accessToken, $openId, $spreadsheetId);
$this->sheets->shareSpreadsheetForAnyoneReader($accessToken, $openId, $spreadsheetId);

use Goletter\Docs\Feishu\FeishuAuth;
use Goletter\Docs\Feishu\FeishuSheets;
use Hyperf\Di\Annotation\Inject;

#[Inject]
protected FeishuAuth $auth;

#[Inject]
protected FeishuSheets $sheets;

$authUrl = $this->auth->getAuthUrl(state: 'xyz');
$token = $this->auth->fetchToken($code);
// $token['access_token']、$token['refresh_token'](须含 offline_access)
// 刷新后务必保存新的 refresh_token,旧值立即失效

$file = $this->docs->sheets('feishu')->createSpreadsheet(
    ['access_token' => $token['access_token']],
    '报表'
);
$spreadsheetToken = $file['id'];

// 从分享链接解析 spreadsheetToken / sheetId(?sheet=)
// https://xxx.feishu.cn/sheets/shtcnXXXX?sheet=0b12
$parsed = $this->docs->sheets('feishu')->parseSpreadsheetUrl($url);
// 或:Goletter\Docs\Support\SheetUrl::parseFeishu($url)

$sheets = $this->docs->sheets('feishu');
$tokenPayload = ['access_token' => $token['access_token']];

// range:默认第一张表;也可用 sheetId / 标题
$values = $sheets->readCells($tokenPayload, $spreadsheetToken, 'A1:Z1000');
$sheets->writeCells($tokenPayload, $spreadsheetToken, 'A1', [['姓名', '部门']]);
$sheets->appendCells($tokenPayload, $spreadsheetToken, 'A1', ['张三', '技术部']);
$sheets->insertAfterHeader($tokenPayload, $spreadsheetToken, $parsed['sheet_id'] ?: 'A1', [
    ['李四', '产品部'],
]);
$sheets->upsertRows($tokenPayload, $spreadsheetToken, 'A1:Z1000', [
    ['张三', '技术部'],
], column: 'A');

$sheets->moveSpreadsheetToDateFolder($tokenPayload, $spreadsheetToken);
$sheets->shareSpreadsheetForAnyoneReader($tokenPayload, $spreadsheetToken);
bash
php bin/hyperf.php vendor:publish goletter/hyperf-docs