├── .gitignore
├── phpunit.xml
├── .travis.yml
├── composer.json
├── LICENSE
├── CHANGELOG.md
├── tests
└── StravaApiTest.php
├── examples
└── oauth-flow.php
├── README.md
└── src
└── Iamstuartwilson
└── StravaApi.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.idea/
3 | /composer.lock
4 |
5 | /vendor/
6 |
7 | .phpunit.result.cache
8 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
'; 44 | print_r($_GET); 45 | echo ''; 46 | $code = $_GET['code']; 47 | $response = $api->tokenExchange($code); 48 | echo '
(after swapping the code from callback against tokens)
'; 50 | echo ''; 51 | print_r($response); 52 | echo ''; 53 | 54 | $_SESSION['strava_access_token'] = isset($response->access_token) ? $response->access_token : null; 55 | $_SESSION['strava_refresh_token'] = isset($response->refresh_token) ? $response->refresh_token : null; 56 | $_SESSION['strava_access_token_expires_at'] = isset($response->expires_at) ? $response->expires_at : null; 57 | 58 | echo '
'; 60 | print_r($_SESSION); 61 | echo ''; 62 | 63 | echo ''; 64 | echo ''; 65 | 66 | return; 67 | 68 | case 'refresh_token': 69 | echo '
'; 71 | print_r($_SESSION); 72 | echo ''; 73 | 74 | echo '
'; 77 | print_r($response); 78 | echo ''; 79 | 80 | $_SESSION['strava_access_token'] = isset($response->access_token) ? $response->access_token : null; 81 | $_SESSION['strava_refresh_token'] = isset($response->refresh_token) ? $response->refresh_token : null; 82 | $_SESSION['strava_access_token_expires_at'] = isset($response->expires_at) ? $response->expires_at : null; 83 | 84 | echo '
'; 86 | print_r($_SESSION); 87 | echo ''; 88 | 89 | return; 90 | 91 | case 'test_request': 92 | echo '
'; 94 | print_r($_SESSION); 95 | echo ''; 96 | 97 | $response = $api->get('/athlete'); 98 | echo '
'; 100 | print_r($response); 101 | echo ''; 102 | 103 | return; 104 | 105 | case 'default': 106 | default: 107 | echo '
Start authentication flow.
'; 108 | echo 'Start oAuth Authentication Flow (Strava oAuth URL: '
109 | . $api->authenticationUrl(CALLBACK_URL)
110 | . ')
'; 113 | print_r($_SESSION); 114 | echo ''; 115 | echo ''; 116 | } 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](https://travis-ci.org/iamstuartwilson/strava) 2 |  3 |  4 |  5 | 6 | # StravaApi 7 | 8 | The class simply houses methods to help send data to and receive data from the API. Please read the [API documentation](https://developers.strava.com/docs/reference/) to see what endpoints are available. 9 | 10 | *There is no file upload support at this time.* 11 | 12 | ## Installation 13 | 14 | ### With Composer 15 | 16 | ``` shell 17 | composer require iamstuartwilson/strava 18 | ``` 19 | 20 | Or add it manually to your `composer.json`: 21 | 22 | ``` json 23 | { 24 | "require" : { 25 | "iamstuartwilson/strava" : "^1.4" 26 | } 27 | } 28 | ``` 29 | 30 | ### Manually 31 | 32 | Copy `StravaApi.php` to your project and *require* it in your application as described in the next section. 33 | 34 | ## Getting Started 35 | 36 | Instantiate the class with your **client_id** and **client_secret** from your [registered app](https://www.strava.com/settings/api): 37 | 38 | ``` php 39 | require_once 'StravaApi.php'; 40 | 41 | $api = new Iamstuartwilson\StravaApi( 42 | $clientId, 43 | $clientSecret 44 | ); 45 | ``` 46 | 47 | If you're just testing endpoints/methods you can skip the authentication flow and just use the access token from your [settings page](https://www.strava.com/settings/api). 48 | 49 | You will then need to [authenticate](https://developers.strava.com/docs/authentication/) your strava account by requesting an access code. You can generate a URL for authentication using the following method: 50 | 51 | ``` php 52 | $api->authenticationUrl($redirect, $approvalPrompt = 'auto', $scope = null, $state = null); 53 | ``` 54 | 55 | When a code is returned you must then exchange it for an [access token and a refresh token](http://developers.strava.com/docs/authentication/#token-exchange) for the authenticated user: 56 | 57 | ``` php 58 | $result = $api->tokenExchange($code); 59 | ``` 60 | 61 | The token exchange result contains among other data the tokens. You can access them as attributes of the result object: 62 | 63 | ```php 64 | $accessToken = $result->access_token; 65 | $refreshToken = $result->refresh_token; 66 | $expiresAt = $result->expires_at; 67 | ``` 68 | 69 | Before making any requests you must set the access and refresh tokens as returned from your token exchange result or via your own private token from Strava: 70 | 71 | ``` php 72 | $api->setAccessToken($accessToken, $refreshToken, $expiresAt); 73 | ``` 74 | 75 | ## Example oAuth2 Authentication Flow 76 | 77 | `examples/oauth-flow.php` demonstrates how the oAuth2 authentication flow works. 78 | 79 | 1. Choose how to load the `StravaApi.php` – either via Composer autoloader or by manually *requiring* it. 80 | 2. Replace the three config values `CALLBACK_URL`, `STRAVA_API_ID`, and `STRAVA_API_SECRET` at the top of the file 81 | 3. Place the file on your server so that it's accessible at `CALLBACK_URL` 82 | 4. Point your browser to `CALLBACK_URL` and start the authentication flow. 83 | 84 | The scripts prints a lot of verbose information so you get an idea on how the Strava oAuth flow works. 85 | 86 | ## Example Requests 87 | 88 | Once successfully authenticated you're able to communicate with Strava's API. 89 | 90 | All actions that change Strava contents (`post`, `put`, `delete`) will need the **scope** set to *write* in the authentication flow. 91 | 92 | ### Get Athlete Stats 93 | 94 | ``` php 95 | $api->get('athletes/:id/stats'); 96 | ``` 97 | 98 | ### List Athlete Activities 99 | 100 | Some API endpoints support GET parameters: 101 | 102 | ``` php 103 | $api->get( 104 | 'athlete/activities', 105 | [ 106 | 'page' => 2, 107 | 'per_page' => 10, 108 | ] 109 | ); 110 | ``` 111 | 112 | ### Post a new activity 113 | 114 | ``` php 115 | $api->post( 116 | 'activities', 117 | [ 118 | 'name' => 'API Test', 119 | 'type' => 'Ride', 120 | 'start_date_local' => date('Y-m-d\TH:i:s\Z'), 121 | 'elapsed_time' => 3600, 122 | ] 123 | ); 124 | ``` 125 | 126 | ### Update a athlete's weight 127 | 128 | ``` php 129 | $api->put('athlete', ['weight' => 70]); 130 | ``` 131 | 132 | ## Releases 133 | 134 | See [CHANGELOG.md](https://github.com/iamstuartwilson/strava/blob/master/CHANGELOG.md). 135 | -------------------------------------------------------------------------------- /src/Iamstuartwilson/StravaApi.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * @link https://github.com/iamstuartwilson/strava 11 | */ 12 | 13 | class StravaApi 14 | { 15 | const BASE_URL = 'https://www.strava.com'; 16 | 17 | /** 18 | * If the access token expires in less than 3600 seconds, a refresh is required. 19 | */ 20 | const ACCESS_TOKEN_MINIMUM_VALIDITY = 3600; 21 | 22 | public $lastRequest; 23 | public $lastRequestData; 24 | public $lastRequestInfo; 25 | 26 | /** 27 | * Stores the HTTP headers from the last API response, e. g.: 28 | * 29 | * [ 30 | * 'Cache-Control' => 'max-age=0, private, must-revalidate', 31 | * 'X-RateLimit-Limit' => '600,30000', 32 | * 'X-RateLimit-Usage' => '4,25', 33 | * 'Content-Length' => '2031', 34 | * ... 35 | * ] 36 | * 37 | * Access with the `getResponseHeader()` or `getResponseHeaders()` methods. 38 | * 39 | * @var array 40 | */ 41 | protected $responseHeaders = array(); 42 | 43 | protected $apiUrl; 44 | protected $authUrl; 45 | protected $clientId; 46 | protected $clientSecret; 47 | 48 | private $accessToken; 49 | private $refreshToken; 50 | private $expiresAt; 51 | 52 | /** 53 | * Sets up the class with the $clientId and $clientSecret 54 | * 55 | * @param int $clientId 56 | * @param string $clientSecret 57 | */ 58 | public function __construct($clientId = 1, $clientSecret = '') 59 | { 60 | $this->clientId = $clientId; 61 | $this->clientSecret = $clientSecret; 62 | $this->apiUrl = self::BASE_URL . '/api/v3/'; 63 | $this->authUrl = self::BASE_URL . '/oauth/'; 64 | } 65 | 66 | /** 67 | * Returns the complete list of response headers. 68 | * 69 | * @return array 70 | */ 71 | public function getResponseHeaders() 72 | { 73 | return $this->responseHeaders; 74 | } 75 | 76 | /** 77 | * @param string $header 78 | * 79 | * @return string 80 | */ 81 | public function getResponseHeader($header) 82 | { 83 | if (! isset($this->responseHeaders[$header])) { 84 | throw new \InvalidArgumentException('Header does not exist'); 85 | } 86 | 87 | return $this->responseHeaders[$header]; 88 | } 89 | 90 | /** 91 | * Appends query array onto URL 92 | * 93 | * @param string $url 94 | * @param array $query 95 | * 96 | * @return string 97 | */ 98 | protected function parseGet($url, $query) 99 | { 100 | $append = strpos($url, '?') === false ? '?' : '&'; 101 | 102 | return $url . $append . http_build_query($query); 103 | } 104 | 105 | /** 106 | * Parses JSON as PHP object 107 | * 108 | * @param string $response 109 | * 110 | * @return object 111 | */ 112 | protected function parseResponse($response) 113 | { 114 | return json_decode($response); 115 | } 116 | 117 | /** 118 | * Makes HTTP Request to the API 119 | * 120 | * @param string $url 121 | * @param array $parameters 122 | * @param bool|string $request the request method, default is POST 123 | * 124 | * @return mixed 125 | * @throws \Exception 126 | */ 127 | protected function request($url, $parameters = array(), $request = false) 128 | { 129 | $this->lastRequest = $url; 130 | $this->lastRequestData = $parameters; 131 | $this->responseHeaders = array(); 132 | 133 | if (strpos($url, '/oauth/token') === false && $this->isTokenRefreshNeeded()) { 134 | throw new \RuntimeException('Strava access token needs to be refreshed'); 135 | } 136 | 137 | $curl = curl_init($url); 138 | 139 | $curlOptions = array( 140 | CURLOPT_SSL_VERIFYPEER => false, 141 | CURLOPT_REFERER => $url, 142 | CURLOPT_RETURNTRANSFER => true, 143 | CURLOPT_HEADERFUNCTION => array($this, 'parseHeader'), 144 | ); 145 | 146 | if (! empty($parameters) || ! empty($request)) { 147 | if (! empty($request)) { 148 | $curlOptions[ CURLOPT_CUSTOMREQUEST ] = $request; 149 | $parameters = http_build_query($parameters); 150 | } else { 151 | $curlOptions[ CURLOPT_POST ] = true; 152 | } 153 | 154 | $curlOptions[ CURLOPT_POSTFIELDS ] = $parameters; 155 | } 156 | 157 | curl_setopt_array($curl, $curlOptions); 158 | 159 | $response = curl_exec($curl); 160 | $error = curl_error($curl); 161 | 162 | $this->lastRequestInfo = curl_getinfo($curl); 163 | 164 | curl_close($curl); 165 | 166 | if (! empty($error)) { 167 | throw new \Exception($error); 168 | } 169 | 170 | return $this->parseResponse($response); 171 | } 172 | 173 | /** 174 | * Creates authentication URL for your app 175 | * 176 | * @param string $redirect 177 | * @param string $approvalPrompt 178 | * @param string $scope 179 | * @param string $state 180 | * 181 | * @link http://developers.strava.com/docs/authentication/ 182 | * 183 | * @return string 184 | */ 185 | public function authenticationUrl($redirect, $approvalPrompt = 'auto', $scope = null, $state = null) 186 | { 187 | $parameters = array( 188 | 'client_id' => $this->clientId, 189 | 'redirect_uri' => $redirect, 190 | 'response_type' => 'code', 191 | 'approval_prompt' => $approvalPrompt, 192 | 'state' => $state, 193 | ); 194 | 195 | if (! is_null($scope)) { 196 | $parameters['scope'] = $scope; 197 | } 198 | 199 | return $this->parseGet( 200 | $this->authUrl . 'authorize', 201 | $parameters 202 | ); 203 | } 204 | 205 | /** 206 | * Authenticates token returned from API 207 | * 208 | * @param string $code 209 | * 210 | * @link http://developers.strava.com/docs/authentication/#token-exchange 211 | * 212 | * @return string 213 | */ 214 | public function tokenExchange($code) 215 | { 216 | $parameters = array( 217 | 'client_id' => $this->clientId, 218 | 'client_secret' => $this->clientSecret, 219 | 'code' => $code, 220 | 'grant_type' => 'authorization_code' 221 | ); 222 | 223 | return $this->request( 224 | $this->authUrl . 'token', 225 | $parameters 226 | ); 227 | } 228 | 229 | /** 230 | * Refresh expired access tokens 231 | * 232 | * @link https://developers.strava.com/docs/authentication/#refresh-expired-access-tokens 233 | * 234 | * @return mixed 235 | */ 236 | public function tokenExchangeRefresh() 237 | { 238 | if (! isset($this->refreshToken)) { 239 | return null; 240 | } 241 | $parameters = array( 242 | 'client_id' => $this->clientId, 243 | 'client_secret' => $this->clientSecret, 244 | 'refresh_token' => $this->refreshToken, 245 | 'grant_type' => 'refresh_token' 246 | ); 247 | 248 | return $this->request( 249 | $this->authUrl . 'token', 250 | $parameters 251 | ); 252 | } 253 | 254 | /** 255 | * Deauthorises application 256 | * 257 | * @link http://strava.github.io/api/v3/oauth/#deauthorize 258 | * 259 | * @return string 260 | */ 261 | public function deauthorize() 262 | { 263 | return $this->request( 264 | $this->authUrl . 'deauthorize', 265 | $this->generateParameters(array()) 266 | ); 267 | } 268 | 269 | /** 270 | * Sets the access token used to authenticate API requests 271 | * 272 | * @param string $token 273 | * @param string $refreshToken 274 | * @param int $expiresAt 275 | * 276 | * @return string 277 | */ 278 | public function setAccessToken($token, $refreshToken = null, $expiresAt = null) 279 | { 280 | if (isset($refreshToken)) { 281 | $this->refreshToken = $refreshToken; 282 | } 283 | if (isset($expiresAt)) { 284 | $this->expiresAt = $expiresAt; 285 | if ($this->isTokenRefreshNeeded()) { 286 | throw new \RuntimeException('Strava access token needs to be refreshed'); 287 | } 288 | } 289 | 290 | return $this->accessToken = $token; 291 | } 292 | 293 | /** 294 | * Sends GET request to specified API endpoint 295 | * 296 | * @param string $request 297 | * @param array $parameters 298 | * 299 | * @example http://strava.github.io/api/v3/athlete/#koms 300 | * 301 | * @return string 302 | */ 303 | public function get($request, $parameters = array()) 304 | { 305 | $parameters = $this->generateParameters($parameters); 306 | $requestUrl = $this->parseGet($this->getAbsoluteUrl($request), $parameters); 307 | 308 | return $this->request($requestUrl); 309 | } 310 | 311 | /** 312 | * Sends PUT request to specified API endpoint 313 | * 314 | * @param string $request 315 | * @param array $parameters 316 | * 317 | * @example http://strava.github.io/api/v3/athlete/#update 318 | * 319 | * @return string 320 | */ 321 | public function put($request, $parameters = array()) 322 | { 323 | return $this->request( 324 | $this->getAbsoluteUrl($request), 325 | $this->generateParameters($parameters), 326 | 'PUT' 327 | ); 328 | } 329 | 330 | /** 331 | * Sends POST request to specified API endpoint 332 | * 333 | * @param string $request 334 | * @param array $parameters 335 | * 336 | * @example http://strava.github.io/api/v3/activities/#create 337 | * 338 | * @return string 339 | */ 340 | public function post($request, $parameters = array()) 341 | { 342 | return $this->request( 343 | $this->getAbsoluteUrl($request), 344 | $this->generateParameters($parameters) 345 | ); 346 | } 347 | 348 | /** 349 | * Sends DELETE request to specified API endpoint 350 | * 351 | * @param string $request 352 | * @param array $parameters 353 | * 354 | * @example http://strava.github.io/api/v3/activities/#delete 355 | * 356 | * @return string 357 | */ 358 | public function delete($request, $parameters = array()) 359 | { 360 | return $this->request( 361 | $this->getAbsoluteUrl($request), 362 | $this->generateParameters($parameters), 363 | 'DELETE' 364 | ); 365 | } 366 | 367 | /** 368 | * Adds access token to paramters sent to API 369 | * 370 | * @param array $parameters 371 | * 372 | * @return array 373 | */ 374 | protected function generateParameters($parameters) 375 | { 376 | return array_merge( 377 | $parameters, 378 | array( 'access_token' => $this->accessToken ) 379 | ); 380 | } 381 | 382 | /** 383 | * Parses the header lines into the $responseHeaders attribute 384 | * 385 | * Skips the first header line (HTTP response status) and the last header 386 | * line (empty). 387 | * 388 | * @param resource $curl 389 | * @param string $headerLine 390 | * 391 | * @return int length of the currently parsed header line in bytes 392 | */ 393 | protected function parseHeader($curl, $headerLine) 394 | { 395 | $size = strlen($headerLine); 396 | $trimmed = trim($headerLine); 397 | 398 | // skip empty line(s) 399 | if (empty($trimmed)) { 400 | return $size; 401 | } 402 | 403 | // skip first header line (HTTP status code) 404 | if (strpos($trimmed, 'HTTP/') === 0) { 405 | return $size; 406 | } 407 | 408 | $parts = explode(':', $headerLine); 409 | $key = array_shift($parts); 410 | $value = implode(':', $parts); 411 | 412 | $this->responseHeaders[$key] = trim($value); 413 | 414 | return $size; 415 | } 416 | 417 | /** 418 | * Checks the given request string and returns the absolute URL to make 419 | * the necessary API call 420 | * 421 | * @param string $request 422 | * 423 | * @return string 424 | */ 425 | protected function getAbsoluteUrl($request) 426 | { 427 | $request = ltrim($request); 428 | 429 | if (strpos($request, 'http') === 0) { 430 | return $request; 431 | } 432 | 433 | return $this->apiUrl . $request; 434 | } 435 | 436 | /** 437 | * @return bool 438 | */ 439 | public function isTokenRefreshNeeded() 440 | { 441 | if (empty($this->expiresAt)) { 442 | return false; 443 | } 444 | 445 | return $this->expiresAt - time() < self::ACCESS_TOKEN_MINIMUM_VALIDITY; 446 | } 447 | } 448 | --------------------------------------------------------------------------------