518 lines
15 KiB
PHP
518 lines
15 KiB
PHP
<?php
|
|
namespace App\Libraries;
|
|
|
|
use Google_Client;
|
|
use Google_Service_Drive;
|
|
use Google_Service_Sheets;
|
|
use Google_Service_Sheets_BatchUpdateSpreadsheetRequest;
|
|
use Google_Service_Sheets_ValueRange;
|
|
|
|
class GoogleSheetLib
|
|
{
|
|
protected Google_Client $client;
|
|
protected Google_Service_Sheets $sheets;
|
|
protected Google_Service_Drive $drive;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->client = new Google_Client();
|
|
|
|
// Service account JSON
|
|
$this->client->setAuthConfig(
|
|
ROOTPATH . 'nhance-ee8d1-e3c5269b1ec7.json'
|
|
);
|
|
|
|
// IMPORTANT for service account
|
|
$this->client->useApplicationDefaultCredentials();
|
|
|
|
// Required scopes
|
|
$this->client->addScope([
|
|
Google_Service_Drive::DRIVE,
|
|
Google_Service_Sheets::SPREADSHEETS,
|
|
]);
|
|
|
|
// Init services
|
|
$this->sheets = new Google_Service_Sheets($this->client);
|
|
$this->drive = new Google_Service_Drive($this->client);
|
|
|
|
}
|
|
|
|
/* ===================== READ ===================== */
|
|
|
|
public function read(string $spreadsheetId, string $range = 'RFQ Page')
|
|
{
|
|
$response = $this->sheets
|
|
->spreadsheets_values
|
|
->get($spreadsheetId, $range);
|
|
|
|
return $response->getValues() ?? [];
|
|
}
|
|
|
|
/* ===================== WRITE ===================== */
|
|
|
|
public function write(string $spreadsheetId, array $values, string $range = 'Sheet1')
|
|
{
|
|
$body = new Google_Service_Sheets_ValueRange([
|
|
'values' => $values,
|
|
]);
|
|
|
|
$this->sheets
|
|
->spreadsheets_values
|
|
->update(
|
|
$spreadsheetId,
|
|
$range,
|
|
$body,
|
|
['valueInputOption' => 'RAW']
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
/* ===================== DOWNLOAD ===================== */
|
|
|
|
public function downloadExcel(string $spreadsheetId)
|
|
{
|
|
$response = $this->drive->files->export(
|
|
$spreadsheetId,
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
['alt' => 'media']
|
|
);
|
|
|
|
return $response->getBody()->getContents();
|
|
}
|
|
|
|
/* ================= LIST FOLDER SHEETS ===================== */
|
|
|
|
/**
|
|
* Lists files inside a Google Drive folder and returns JSON with
|
|
* filename and sheet id (Drive file id / spreadsheet id).
|
|
*/
|
|
public function listFolderSheetIdsAsJson(string $folderId = ''): string
|
|
{
|
|
if ($folderId === '') {
|
|
$folderId = '19uySI-PSFQfFpZvMSnBtirXbwfCTXFBL';
|
|
}
|
|
|
|
$results = [];
|
|
$pageToken = null;
|
|
$folderIdEsc = str_replace("'", "\\'", $folderId);
|
|
$query = "'{$folderIdEsc}' in parents and trashed = false";
|
|
$listParams = [
|
|
'q' => $query,
|
|
'fields' => 'nextPageToken, files(id,name,mimeType)',
|
|
'pageSize' => 1000,
|
|
'supportsAllDrives' => true,
|
|
'includeItemsFromAllDrives' => true,
|
|
];
|
|
|
|
do {
|
|
if ($pageToken) {
|
|
$listParams['pageToken'] = $pageToken;
|
|
} else {
|
|
unset($listParams['pageToken']);
|
|
}
|
|
|
|
$response = $this->drive->files->listFiles($listParams);
|
|
|
|
foreach (($response->getFiles() ?? []) as $file) {
|
|
// "sheetId" here is the Drive file id (spreadsheetId for Google Sheets)
|
|
$results[] = [
|
|
'name' => (string) $file->getName(),
|
|
'sheetId' => (string) $file->getId(),
|
|
];
|
|
}
|
|
|
|
$pageToken = $response->getNextPageToken();
|
|
} while (! empty($pageToken));
|
|
|
|
return json_encode($results, JSON_UNESCAPED_SLASHES);
|
|
}
|
|
|
|
/* ================= COPY TEMPLATE ================= */
|
|
|
|
public function copyTemplate(string $templateId, string $name, string $folderId): string
|
|
{
|
|
$file = $this->drive->files->copy(
|
|
$templateId,
|
|
new \Google_Service_Drive_DriveFile([
|
|
'name' => $name,
|
|
'parents' => [$folderId],
|
|
|
|
]), [
|
|
'supportsAllDrives' => true,
|
|
'fields' => 'id, name, parents',
|
|
]
|
|
);
|
|
|
|
return $file->id;
|
|
}
|
|
|
|
/**
|
|
* Find/replace text across all sheets in the spreadsheet (batchUpdate).
|
|
*
|
|
* @param array<string, string> $replacements placeholder => replacement (empty string allowed)
|
|
*/
|
|
public function findAndReplaceAllSheets(string $spreadsheetId, array $replacements): void
|
|
{
|
|
if ($replacements === []) {
|
|
return;
|
|
}
|
|
|
|
$requests = [];
|
|
|
|
foreach ($replacements as $find => $replacement) {
|
|
if ($find === '') {
|
|
continue;
|
|
}
|
|
$requests[] = [
|
|
'findReplace' => [
|
|
'find' => (string) $find,
|
|
'replacement' => (string) $replacement,
|
|
'allSheets' => true,
|
|
'matchCase' => false,
|
|
'matchEntireCell' => false,
|
|
'includeFormulas' => true,
|
|
],
|
|
];
|
|
}
|
|
|
|
if ($requests === []) {
|
|
return;
|
|
}
|
|
|
|
$this->sheets->spreadsheets->batchUpdate(
|
|
$spreadsheetId,
|
|
new Google_Service_Sheets_BatchUpdateSpreadsheetRequest([
|
|
'requests' => $requests,
|
|
])
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Quote a sheet title for A1 notation ranges.
|
|
*/
|
|
private function quoteSheetTitleForRange(string $title): string
|
|
{
|
|
return "'" . str_replace("'", "''", $title) . "'";
|
|
}
|
|
|
|
/**
|
|
* 1-based column index to Excel column letters (A, B, …, Z, AA, …).
|
|
*/
|
|
private function columnNumberToLetters(int $n): string
|
|
{
|
|
$s = '';
|
|
|
|
while ($n > 0) {
|
|
$m = ($n - 1) % 26;
|
|
$s = chr(65 + $m) . $s;
|
|
$n = intdiv($n - 1, 26);
|
|
}
|
|
|
|
return $s;
|
|
}
|
|
|
|
/**
|
|
* Sheet title from an A1 range like 'RFQ Page'!A1:ZZ3000 or Sheet1!A1.
|
|
*/
|
|
private function parseSheetTitleFromBatchRange(string $rangeStr): string
|
|
{
|
|
$parts = explode('!', $rangeStr, 2);
|
|
$sheet = $parts[0] ?? '';
|
|
|
|
if ($sheet === '') {
|
|
return '';
|
|
}
|
|
|
|
if (str_starts_with($sheet, "'")) {
|
|
return str_replace("''", "'", trim($sheet, "'"));
|
|
}
|
|
|
|
return $sheet;
|
|
}
|
|
|
|
/**
|
|
* Locate a placeholder in any sheet and write a 2D table starting at that cell.
|
|
* First row of $tableValues is treated as headers.
|
|
*
|
|
* @param list<list<string|int|float>> $tableValues
|
|
*/
|
|
public function writeTableAtPlaceholder(string $spreadsheetId, string $placeholder, array $tableValues): bool
|
|
{
|
|
if ($placeholder === '' || $tableValues === []) {
|
|
return false;
|
|
}
|
|
|
|
$meta = $this->sheets->spreadsheets->get(
|
|
$spreadsheetId,
|
|
['fields' => 'sheets(properties(title))']
|
|
);
|
|
|
|
$titles = [];
|
|
|
|
foreach ($meta->getSheets() ?? [] as $sheet) {
|
|
$props = $sheet->getProperties();
|
|
|
|
if ($props !== null) {
|
|
$titles[] = (string) $props->getTitle();
|
|
}
|
|
}
|
|
|
|
if ($titles === []) {
|
|
return false;
|
|
}
|
|
|
|
$ranges = [];
|
|
|
|
// Use row-based range so API returns row 1 == index 0 (leading empty rows are not dropped).
|
|
foreach ($titles as $title) {
|
|
$ranges[] = $this->quoteSheetTitleForRange($title) . '!1:3000';
|
|
}
|
|
|
|
$batch = $this->sheets->spreadsheets_values->batchGet($spreadsheetId, [
|
|
'ranges' => $ranges,
|
|
'majorDimension' => 'ROWS',
|
|
]);
|
|
|
|
$valueRanges = $batch->getValueRanges() ?? [];
|
|
|
|
foreach ($valueRanges as $vr) {
|
|
$title = $this->parseSheetTitleFromBatchRange((string) ($vr->getRange() ?? ''));
|
|
|
|
if ($title === '') {
|
|
continue;
|
|
}
|
|
|
|
$values = $vr->getValues() ?? [];
|
|
|
|
foreach ($values as $r => $row) {
|
|
if (! is_array($row)) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($row as $c => $cell) {
|
|
if (! is_string($cell) && ! is_numeric($cell)) {
|
|
continue;
|
|
}
|
|
|
|
$text = (string) $cell;
|
|
|
|
if (! str_contains($text, $placeholder)) {
|
|
continue;
|
|
}
|
|
|
|
$startCol = $this->columnNumberToLetters((int) $c + 1);
|
|
$startRow = (int) $r + 1;
|
|
$q = $this->quoteSheetTitleForRange($title);
|
|
$range = "{$q}!{$startCol}{$startRow}";
|
|
|
|
$body = new Google_Service_Sheets_ValueRange([
|
|
'values' => $tableValues,
|
|
]);
|
|
|
|
$this->sheets->spreadsheets_values->update(
|
|
$spreadsheetId,
|
|
$range,
|
|
$body,
|
|
['valueInputOption' => 'USER_ENTERED']
|
|
);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/* ================= PERMISSIONS ================= */
|
|
|
|
public function applyPermissions(string $fileId, array $permissions)
|
|
{
|
|
foreach ($permissions['editors'] as $email) {
|
|
$this->createPermission($fileId, $email, 'writer');
|
|
}
|
|
|
|
foreach ($permissions['viewers'] as $email) {
|
|
$this->createPermission($fileId, $email, 'reader');
|
|
}
|
|
}
|
|
|
|
private function createPermission(string $fileId, string $email, string $role)
|
|
{
|
|
$type = str_starts_with($email, 'group:') ? 'group' : 'user';
|
|
$email = str_replace('group:', '', $email);
|
|
|
|
$this->drive->permissions->create(
|
|
$fileId,
|
|
new \Google_Service_Drive_Permission([
|
|
'type' => $type,
|
|
'role' => $role,
|
|
'emailAddress' => $email,
|
|
]),
|
|
['sendNotificationEmail' => false, 'supportsAllDrives' => true]
|
|
);
|
|
}
|
|
|
|
/* ================= PROTECTIONS ================= */
|
|
|
|
public function applyProtectionsold(string $spreadsheetId, array $ranges)
|
|
{
|
|
$spreadsheet = $this->sheets->spreadsheets->get($spreadsheetId);
|
|
$sheetId = $spreadsheet->getSheets()[0]->getProperties()->getSheetId();
|
|
|
|
$requests = [];
|
|
|
|
foreach ($ranges as $range) {
|
|
[$sheetName, $a1] = explode('!', $range);
|
|
|
|
$requests[] = [
|
|
'addProtectedRange' => [
|
|
'protectedRange' => [
|
|
'range' => [
|
|
'sheetId' => $sheetId,
|
|
],
|
|
'warningOnly' => false,
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
$this->sheets->spreadsheets->batchUpdate(
|
|
$spreadsheetId,
|
|
new Google_Service_Sheets_BatchUpdateSpreadsheetRequest([
|
|
'requests' => $requests,
|
|
])
|
|
);
|
|
}
|
|
|
|
public function applyProtections(string $spreadsheetId, array $protections)
|
|
{
|
|
// Fetch spreadsheet metadata
|
|
$spreadsheet = $this->sheets->spreadsheets->get(
|
|
$spreadsheetId,
|
|
['fields' => 'sheets(properties(sheetId,title,gridProperties))']
|
|
);
|
|
|
|
// Map sheet names
|
|
$sheetMap = [];
|
|
foreach ($spreadsheet->getSheets() as $sheet) {
|
|
$props = $sheet->getProperties();
|
|
$sheetMap[$props->getTitle()] = [
|
|
'sheetId' => $props->getSheetId(),
|
|
'rowCount' => $props->getGridProperties()->getRowCount(),
|
|
'colCount' => $props->getGridProperties()->getColumnCount(),
|
|
];
|
|
}
|
|
|
|
$requests = [];
|
|
|
|
foreach ($protections as $protection) {
|
|
|
|
$rangeStr = $protection['range'];
|
|
|
|
if (! str_contains($rangeStr, '!')) {
|
|
throw new \Exception("Invalid range format: {$rangeStr}");
|
|
}
|
|
|
|
[$sheetName, $a1] = explode('!', $rangeStr, 2);
|
|
|
|
if (! isset($sheetMap[$sheetName])) {
|
|
throw new \Exception("Sheet not found: {$sheetName}");
|
|
}
|
|
|
|
$sheetMeta = $sheetMap[$sheetName];
|
|
|
|
$gridRange = $this->convertA1ToGridRange(
|
|
$a1,
|
|
$sheetMeta['sheetId'],
|
|
$sheetMeta['rowCount'],
|
|
$sheetMeta['colCount']
|
|
);
|
|
|
|
$protectedRange = [
|
|
'range' => $gridRange,
|
|
'description' => 'RFQ Protected Area',
|
|
'warningOnly' => false,
|
|
'editors' => [
|
|
'users' => $protection['users'] ?? [],
|
|
'groups' => $protection['groups'] ?? [],
|
|
],
|
|
];
|
|
|
|
$requests[] = [
|
|
'addProtectedRange' => [
|
|
'protectedRange' => $protectedRange,
|
|
],
|
|
];
|
|
}
|
|
|
|
if (! empty($requests)) {
|
|
$batch = new \Google_Service_Sheets_BatchUpdateSpreadsheetRequest([
|
|
'requests' => $requests,
|
|
]);
|
|
|
|
$this->sheets->spreadsheets->batchUpdate($spreadsheetId, $batch);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* ================= FILE NAME ================= */
|
|
|
|
public function getFileName(string $fileId): string
|
|
{
|
|
$file = $this->drive->files->get($fileId, [
|
|
'supportsAllDrives' => true,
|
|
'fields' => 'name',
|
|
]);
|
|
|
|
return $file->getName() ?? '';
|
|
}
|
|
|
|
/* ================= URL ================= */
|
|
|
|
public function sheetUrl(string $sheetId): string
|
|
{
|
|
return "https://docs.google.com/spreadsheets/d/{$sheetId}/edit";
|
|
}
|
|
|
|
private function convertA1ToGridRange($a1, $sheetId, $maxRows, $maxCols)
|
|
{
|
|
if (preg_match('/^([A-Z]+)(\d+)(?::([A-Z]+)(\d+))?$/i', $a1, $m)) {
|
|
|
|
$startCol = $this->colToIndex($m[1]);
|
|
$startRow = intval($m[2]) - 1;
|
|
|
|
if (! empty($m[3])) {
|
|
$endCol = $this->colToIndex($m[3]) + 1;
|
|
$endRow = intval($m[4]);
|
|
} else {
|
|
$endCol = $startCol + 1;
|
|
$endRow = $startRow + 1;
|
|
}
|
|
|
|
return [
|
|
'sheetId' => $sheetId,
|
|
'startRowIndex' => $startRow,
|
|
'endRowIndex' => $endRow,
|
|
'startColumnIndex' => $startCol,
|
|
'endColumnIndex' => $endCol,
|
|
];
|
|
}
|
|
|
|
throw new \Exception("Unsupported A1 format: {$a1}");
|
|
}
|
|
|
|
private function colToIndex($letters)
|
|
{
|
|
$letters = strtoupper($letters);
|
|
$index = 0;
|
|
for ($i = 0; $i < strlen($letters); $i++) {
|
|
$index = $index * 26 + (ord($letters[$i]) - 64);
|
|
}
|
|
return $index - 1;
|
|
}
|
|
|
|
}
|