Integrare le Google Drive API con PHP - Step 2

Quando eseguiremo la procedura OAuth, Google ci restituirà (se la procedura e le credenziali sono corrette) il token di accesso con la sua scadenza e il codice dedicato alla sua rigenerazione.
È consigliabile memorizzarlo, nella modalità a voi più comoda; in questa mini guida, provvederò a memorizzarlo all'interno di un database MySQL.
Ho provveduto a creare una tabella "oauthTokens" con il codice SQL sotto riportato:
CREATE TABLE `oauthTokens` (
`tknId` int(11) NOT NULL AUTO_INCREMENT,
`tknValue` text NOT NULL,
PRIMARY KEY (`tknId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Il token di accesso, come già anticipato, ha una sua scadenza quindi deve essere rigenerato (preferibilmente in background) e le nuove informazioni devono essere aggiornate nel database.
Il codice necessario ad eseguire queste operazioni, può essere integrato all'interno di una classe per maggiore comodità.
private const CONFIG_PATH = __DIR__ . 'private/drivedata.json';
private const LOG_FILE = __DIR__ . 'logs/drivedebug.log';
private const APP_NAME = 'Google Drive Manager';
private string $driveEmail = '';
Ho rinominato in "drivedata.json" il file JSON scaricato dalla Google Developer Console (vedi prima parte) e l'ho salvato in una cartella denominata "private".
Ho previsto un file per salvare i log delle operazioni (drivedebug.log) e ho dichiarato una variabile $driveEmail che conterrà l'indirizzo e-mail utilizzato per creare i settings nella Console.
Le funzioni e i metodi che andrò ad esporre ed utilizzare, si interfacceranno alle Google's PHP API Client, una libreria client PHP sviluppata da Google per l'accesso alle sue API.
Per l'installazione è possibile scaricare tutto il contenuto della release di Github o utilizzare Composer.
Io ho utilizzato Composer creando un file "composer.json" con le seguenti istruzioni:
{
"require": {
"php": "^8.0",
"google/apiclient": "^2.15.0"
},
"scripts": {
"pre-autoload-dump": "Google\\Task\\Composer::cleanup"
},
"extra": {
"google/apiclient-services": [
"Drive"
]
}
}
Per evitare di includere dipendenze nel codice che non andremo ad utilizzare (esistono oltre 200 servizi API di Google), ho specificato nelle istruzioni di composer, Google\Task\Composer::cleanup; questo comando rimuoverà tutti i servizi tranne “Drive” quando viene eseguito un “composer update” o una nuova installazione di Composer.
Apriamo un terminale / prompt dei comandi, ci portiamo nella root del nostro progetto o comunque dove abbiamo salvato il nostro file per composer ed eseguiamo composer install. Questo comando installerà automaticamente i pacchetti necessari alle nostre operazioni.
Vediamo ora il codice per utilizzare, salvare, generare il nostro token OAuth:
private function getAccessToken()
{
$query = $this->db->query("SELECT tknValue FROM oauthTokens ORDER BY tknId DESC LIMIT 1");
if ($query->num_rows > 0) {
$result = $query->fetch_assoc();
return json_decode($result['tknValue'], true);
}
return null;
}
private function parseToken(array $arr_token)
{
$expiresAt = isset($arr_token['expires_in']) ? time() + $arr_token['expires_in'] : 0;
return [
'access_token' => $arr_token['access_token'],
'expires_in' => $arr_token['expires_in'],
'expires_at' => $expiresAt,
'refresh_token' => $arr_token['refresh_token'] ?? "",
'scope' => $arr_token['scope'] ?? "",
'token_type' => $arr_token['token_type']
];
}
private function validateRefreshToken(string $refreshToken)
{
try {
$client = new Google_Client();
$client->setAuthConfig(self::CONFIG_PATH);
$token = $client->fetchAccessTokenWithRefreshToken($refreshToken);
if (isset($token['error'])) {
throw new Exception("Il refresh token è stato revocato o è scaduto.");
}
return true;
} catch (Exception $e) {
throw $e;
}
}
private function validateToken(array $token)
{
if (empty($token['access_token'])) {
throw new Exception("Token non valido: 'access_token' mancante o vuoto.");
}
if (empty($token['expires_in']) || !is_numeric($token['expires_in']) || intval($token['expires_in']) <= 0) {
throw new Exception("Token non valido: 'expires_in' mancante o non valido.");
}
if (isset($token['error'])) {
$error = $token['error'];
switch ($error) {
case 'invalid_grant':
throw new Exception("Errore: 'invalid_grant'. Il token o il refresh token potrebbe non essere più valido.");
case 'invalid_request':
throw new Exception("Errore: 'invalid_request'. La richiesta non è corretta.");
default:
throw new Exception("Errore sconosciuto: " . $error);
}
}
}
private function getAccessTokenFromSource()
{
$arr_token = $this->getAccessToken();
if ($arr_token && is_array($arr_token)) {
$this->validateToken($arr_token);
return $this->parseToken($arr_token);
}
return null;
}
private function updateAccessToken(string $token)
{
$sql = sprintf("INSERT INTO oauthTokens(tknValue) VALUES('%s')", $this->db->real_escape_string($token));
$this->db->query($sql);
}
private function saveAccessToken(array $accessToken)
{
$jsonToken = json_encode($accessToken);
$this->updateAccessToken($jsonToken);
}
private function refreshOrGetNewToken(Google_Client $client)
{
try {
if ($client->getRefreshToken()) {
$accessToken = $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
if (!array_key_exists('error', $accessToken)) {
$this->saveAccessToken($accessToken);
return $accessToken;
} else {
if ($accessToken['error'] == 'invalid_grant') {
} else {
throw new Exception("Errore nel tentativo di rinnovo del token: " . json_encode($accessToken));
}
}
} else {
$authUrl = $client->createAuthUrl();
header('Location: ' . $authUrl);
exit();
}
} catch (Google\Exception $e) {
throw new Exception("Errore durante il tentativo di rinnovo del token: " . $e->getMessage());
}
return null;
}
public function oauthCallback(string $authCode = '')
{
$client = new Google_Client();
$client->setApplicationName(self::APP_NAME);
$client->setAuthConfig(self::CONFIG_PATH);
$client->setScopes(Google\Service\Drive::DRIVE);
$client->setAccessType('offline');
$client->setApprovalPrompt('force');
$accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
if (array_key_exists('error', $accessToken)) {
throw new Exception("Errore con il codice di autorizzazione: " . json_encode($accessToken));
}
$this->saveAccessToken($accessToken);
if (isset($_SESSION['redirectUri']) && !empty($_SESSION['redirectUri']) {
$redirectUri = $_SESSION['redirectUri'];
unset($_SESSION['redirectUri']);
header("Location: " . $redirectUri);
} else {
throw new Exception("Non è stato possibile trovare la richiesta originale.");
}
}
public function manageDriveClient()
{
$client = new Google_Client();
$client->setApplicationName(self::APP_NAME);
$client->setAuthConfig(self::CONFIG_PATH);
$client->setScopes(Google\Service\Drive::DRIVE);
$client->setAccessType('offline');
$client->setApprovalPrompt('force');
$client->setPrompt('select_account consent');
$client->setLoginHint($this->driveEmail);
$redirectUri = current_url();
$accessToken = $this->getAccessTokenFromSource();
if (isset($accessToken)) {
try {
$client->setAccessToken($accessToken);
} catch (Google\Exception $e) {
}
}
if ($client->isAccessTokenExpired()) {
$_SESSION['redirectUri'] = $redirectUri;
$accessToken = $this->refreshOrGetNewToken($client);
if ($accessToken) {
try {
$client->setAccessToken($accessToken);
} catch (Google\Exception $e) {
throw new Exception("Errore con l'accesso a Google Drive: " . $e->getMessage());
}
}
}
return $client;
}