globalvisionmedia/oauth2-harvest

为PHP League OAuth2-Client提供的Harvest OAuth 2.0客户端 - 更新了Nile Suan的Harvest API v2提供者

1.2 2023-02-10 08:59 UTC

This package is auto-updated.

Last update: 2024-09-08 15:19:26 UTC


README

对PHP League的OAuth 2.0客户端提供Harvest OAuth 2.0支持,感谢Nile Suan提供的原始harvest提供者。此包更新以支持Harvest API的V2版本

安装

composer require globalvisionmedia/oauth2-harvest

获取Harvest访问密钥

  1. 要获取密钥,您需要在Harvest开发者区域创建OAuth2应用程序:(https://id.getharvest.com/developers

  2. 单击创建新的Auth2应用程序按钮来创建密钥

  3. 为您的应用程序选择一个名称

  4. 重定向URL必须与下面的redirectUri完全相同(包括http://或https://),并且是您的应用程序的URL。访问 - 选择我需要访问一个账户 产品 - 选择我想要访问Harvest。

  5. 记下客户端ID和客户端密钥

  6. 您需要在Harvest上知道您的账户ID。创建OAuth2应用程序时不会显示它,但如果您创建一个临时的“个人访问令牌”,则会显示您的账户ID。记下来,然后您可以删除临时的个人访问令牌。

使用方法

使用方法与The League的OAuth客户端相同,使用\GlobalVisionMedia\OAuth2\MYOBClient\Provider\MYOB作为提供者,除了以下内容

  1. Harvest要求您在Guzzle中设置一个用户代理(见以下示例)

  2. 您需要向Harvest API提供账户ID(见上文)

  3. Harvest的API是受限制的 - 文档中规定的限制为每100秒15次调用(最简单的选项是每秒限制为6次) - 请见以下示例

实例化

$provider = new \GlobalVisionMedia\OAuth2\HarvestClient\Provider\Harvest([
    'clientId'                => 'yourId',                // The Client ID assigned to you by Harvest
    'clientSecret'            => 'yourSecret',            // The Client Secret assigned to you by Harvest
    'redirectUri'             => 'yourRedirectUri',       // The Redirect URL you specified for your app on Harvest
    'accountId'               => 'yourAccountId,          // See point 6 above
    'userAgent'               => 'My User Agent'          // This can be any string but ideally should identify your application
]);

提示(也适用于其他提供者)

When you instantiate your provider, you can also pass a second parameter containing a collaborator for your httpClient.
Doing that means you can define your own Guzzle client and do things such as:

  1. Setting Guzzle into debug mode, or
  2. Adding a rate limiter mildeware (composer require spatie/guzzle-rate-limiter-middleware)
  

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use Spatie\GuzzleRateLimiterMiddleware\RateLimiterMiddleware;

define('CALLBACK_URI','https://xxx.yyy/zzzzzzzz.php');
define('HARVEST_CLIENT_ID','xxxxxxxxxxxxxxxxxxxxxxxx');
define('HARVEST_CLIENT_SECRET','xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
define('HARVEST_USER_AGENT','My Harvest Integration');
define('HARVEST_ACCOUNT_ID','xxxxxx');

// Add a rate limiter
$stack=HandlerStack::create();
$stack->push(RateLimiterMiddleware::perSecond(6));
$options=['debug' => $debug, 'exceptions' => false, 'handler' => $stack];
$httpClient = new Client($options);

$this->provider = \GlobalVisionMedia\OAuth2\HarvestClient\Provider\Harvest([
    'redirectUri'       => CALLBACK_URI,
    'clientId'          => HARVEST_CLIENT_ID,
    'clientSecret'      => HARVEST_CLIENT_SECRET,
    'accountId'         => HARVEST_ACCOUNT_ID,
    'userAgent'         => HARVEST_USER_AGENT
  ],
  ['httpClient'         => $httpClient]);

示例应用程序

<?php
require __DIR__ . '/vendor/autoload.php';

// This is a prebuilt rate limiter for guzzle - unfortunately MYOB does not seem to work as documented any you may need to add additional sleep() calls.

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use Spatie\GuzzleRateLimiterMiddleware\RateLimiterMiddleware;

define('CALLBACK_URI','https://xxx.yyy/zzzzzzzz.php');
define('HARVEST_CLIENT_ID','xxxxxxxxxxxxxxxxxxxxxxxx');
define('HARVEST_CLIENT_SECRET','xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
define('HARVEST_USER_AGENT','My Harvest Integration');
define('HARVEST_ACCOUNT_ID','xxxxxx');

define('CACHEDIR','/tmp/');                                      // a writeable area for storing tokens


class myHarvest {

  public function __construct($debug=false) {
    $this->cache=CACHEDIR.'_API_TOKEN_CACHE_'.md5(__FILE__.get_class($this));

    // Add the rate limiter
    $stack=HandlerStack::create();
    $stack->push(RateLimiterMiddleware::perSecond(6));
    $options=['debug' => $debug, 'exceptions' => false, 'handler' => $stack];
    $httpClient = new Client($options);

    $this->provider = \GlobalVisionMedia\OAuth2\HarvestClient\Provider\Harvest([
        'redirectUri'       => CALLBACK_URI,
        'clientId'          => HARVEST_CLIENT_ID,
        'clientSecret'      => HARVEST_CLIENT_SECRET,
        'username'          => HARVEST_ACCOUNT_ID,
        'password'          => HARVEST_USER_AGENT
      ],
      ['httpClient'         => $httpClient]);

    // First check our cache to see if we have an existing token. This sppeds the application by avoiding the need to re-authenticate.
    if (file_exists($this->cache)) {
      $this->accessToken=unserialize(file_get_contents($this->cache));
      if ($this->accessToken->hasExpired()) {
        $this->accessToken=$this->provider->getAccessToken('refresh_token', ['refresh_token'=>8]);
      }
    } elseif (!isset($_GET['code'])) {
      // If we don't have an authorization code then get one
      $authUrl = $this->provider->getAuthorizationUrl();
      $_SESSION['oauth2state'] = $this->provider->getState();

      header('Location: '.$authUrl);
      exit;

      // Check given state against previously stored one to mitigate CSRF attack
    } elseif (empty($_GET['state']) ||
              (isset($_SESSION['oauth2state']) && $_GET['state'] !== $_SESSION['oauth2state'])) {
      if (isset($_SESSION['oauth2state'])) unset($_SESSION['oauth2state']);
      exit('Invalid state');

      // Try to get an access token using the authorisation code grant.
    } else try {
      $this->accessToken = $this->provider->getAccessToken('authorization_code', [ 'code' => $_GET['code'] ]);

      // Cache the token
      file_put_contents($this->cache,serialize($this->accessToken));

    } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
      // Failed to get the access token or user details.
      exit($e->getMessage());
    }
  }

  public function getItemsAsArray($result) {
    $keys=array_keys($result);
    //find whichever key is not in the following list - it must be our data.
    $dataKey=array_diff($keys,['page','total_pages','total_entries','next_page','previous_page','links']);
    return $result[$dataKey[0]];
  }

  public function apiCall($method, $url, $pageSize=100) {
    if (strpos($url,"https://")===false) {  // is this a nextpage link? if so leave url unchanged
      $url="https://api.harvestapp.com/v2$url?per_page=$pageSize";
    }
    $request=$this->provider->AuthenticatedRequest($method, $url, $this->accessToken);
    return $this->provider->getParsedResponse($request);
  }

  // This function retrieves paginated data as a single array
  public function fetchAll($method, $url, $pageSize=100) {
    $allResults=array();
    do {
      $result=$this->apiCall($method,$url,$pageSize);
      $allResults=array_merge($allResults,$this->getItemsAsArray($result));
      $url = $result['links']['next'];
    } while (!empty($url));
    return $allResults;
  }

}

session_start();
$myHarvest=new myHarvest();
print_r($myHarvest->fetchAll('GET', '/contacts'));