Integrare le Google Drive API con PHP - Step 2

- Pubblicato in coding di - Permalink

Google Workspace

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;
    }
→