diff --git a/README.md b/README.md index d8d5a2a..21f3aeb 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,90 @@ # Meetup API -This a very simple, one-file, PHP client for accessing most of the [Meetup API](http://www.meetup.com/meetup_api/). +This a very simple, one-file, PHP client for accessing most of the [Meetup API](http://www.meetup.com/meetup_api/). Some parameters are included behind the scenes so you don't have to using array_merge when the parameters have fixed values like signed or response_type depending on the nature of the request. + +The code is documented to include more information along with small code snippets in the documentation where applicable. This library supports OATH, api key, get/put/delete calls, and has several useful stub methods for accessing API functionality from meetup. There's documentation and comments in the code and a detailed README to help you get started easily using the library. + +#Exceptions +Exceptions are thrown and can be caught when there's any errors interacting with the API, they are standard exceptions. + +```php +try +{ + $meetup = new Meetup(''); + $meetup->getEvents(); +} +catch(Exception $e) +{ + echo $e->getMessage(); +} +``` + +#Hardcoded parameters +Underneath there's parameters (depending on the request being made) that get injected using array_merge because these values aren't variable they're fixed. This way you don't have to know about them or worry about implementing them and if they're duplicated by you it won't matter. You can just focus on including the core information and using the stub methods to handle the heavy lifting. ## Quick Start * Get your [API key](http://www.meetup.com/meetup_api/key/). +* Limited information returned on GET requests (ie may not get private event or member details unless authorized) * Require the library, create a Meetup object and set your key: ```php require 'meetup.php'; $meetup = new Meetup(array( - 'key' => 'YOUR_API_KEY' + 'key' => '' )); + +$response = $meetup->getEvents(); //somewhat restricted +``` + +* Get your [Consumer details](https://secure.meetup.com/meetup_api/oauth_consumers/). +* Use authorized access to get ALL information on GET requests and perform POST/DELETE requests also +* Require the library, create a Meetup object and set your consumer details and gain access: + +```php +if( !isset($_GET['code']) ) +{ + //authorize and go back to URI w/ code + $meetup = new Meetup(); + $meetup->authorize( + 'client_id' => '', + 'redirect_uri' => '' + ); +} +else +{ + //assuming we came back here... + $meetup = new Meetup( + array( + "client_id" => '', + "client_secret" => '', + "redirect_uri" => '', + "code" => $_GET['code'] //passed back to us from meetup + ) + ); + + //get an access token + $response = $meetup->access(); + + //now we can re-use this object for several requests using our access + //token + $meetup = new Meetup( + array( + "access_token" => $response->access_token, + ) + ); + + //store details for later in case we need to do requests elsewhere + //or refresh token + $_SESSION['access_token'] = $response->access_token; + $_SESSION['refresh_token'] = $response->refresh_token; + $_SESSION['expires'] = time() + intval($response->expires_in); //use if >= intval($_SESSION['expires']) to check + + //get all groups for this member, to get your own use array('member_id' => 'self') + $response = $meetup->getGroups(array('member_id' => '')); + + //get all events for this member, to get your own use array('member_id' => 'self') + $response = $meetup->getEvents(array('member_id' => '')); +} ``` * Retrieve some events: @@ -21,32 +95,50 @@ $response = $meetup->getEvents(array( )); // total number of items matching the get request -$total_count = $response->meta_total_count; - -$events = $response->results; +$total_count = $response->meta->total_count; -foreach ($events as $event) { +foreach ($response->results as $event) +{ echo $event->name . ' at ' . date('Y-m-d H:i', $event->time / 1000) . PHP_EOL; } ``` -Many of the get requests will match more entries than the API will return in one request. A convenience method has been provided to return the next -page of entries after you have performed a successful get request: +Many of the get requests will match more entries than the API will return in one request. A convenience method has been provided to return the next page of entries after you have performed a successful get request: -$response = $meetup->getNext($response); - -$events = $response->results; +```php +//can check if there's more by using $response->hasNext(). Keep processing +//events if they're available and make subsequent calls to the API +while( ($response = $meetup->getNext()) !== null) +{ + foreach($response->results as $event) + { + //process event + } +} +``` ... ## Constructing the client The class constructors takes one optional argument. This `(array)` will be stored in the object and used as default parameters for any request you make. -I would suggest passing the `key` when you construct the client, but you could do just `$meetup = new Meetup;` and then pass the `key` parameter in every request you make. +I would suggest passing the `key` or `consumer details` when you construct the client, but you could do just `$meetup = new Meetup;` and then pass parameters in every request you make. These requests are somewhat restricted on the information passed back, you have to use OATH 2 for full access otherwise you may not get back some information. + +Using OATH 2 there's additional steps required to get an access token and pass it on subsequent requests. Your access token is only good for 1 hour and you'll have to refresh it if you plan on making subsequent calls to the service after that. -## Doing GET requests -You can call any [Meetup API GET-method](http://www.meetup.com/meetup_api/docs/) using `get()`. +## What's OATH +To keep it short and sweet it's a way to authenticate against the system and gain full privileged access, without it you don't have full access using only an API key. You get consumer details from meetup, authorize yourself, meetup sends a code to your redirect uri, you read in the code and get an access token with it, using that access token you gain authenticated access to meetup. You only have an hour but you can use your refresh token to get a new access token and repeat the process always using the newest access/refresh token you get back. + +```php +$response = $meetup->refresh(); + +//new details passed back +//$response->access_token, $response->refresh_token, $response->expires_in +``` + +## Doing GET/POST/DELETE requests +You can call any [Meetup API method](http://www.meetup.com/meetup_api/docs/) using `get()` or `post()` or `delete()` or `put()`. There's several stub functions already for the more common ones and new ones will be added down the road. You just have to supply the path relative from meetup (don't include the base path) and the parameters you want. Use place holders in your path : and make sure to include the parameter in your parameters exactly as it appears in the placeholder. ### Arguments -The method takes two arguments, of which the second one is optional: +The method `get()`,`put()`,`post()`,`delete()` takes two arguments, of which the second one is optional: 1. `(string)` Meetup API method (e.g. `/2/events`) 2. `(array)` Meetup API method paramaters (e.g. `array('group_urlname' => 'your-meetup-group')`) @@ -67,22 +159,28 @@ Feel free to fork the code and add more! |Client method |API method | |---------------------|-----------------------------------| | getEvents | /2/events | +| getGroups | /2/groups | | getMembers | /2/members | | getPhotos | /2/photos | | getDiscussionBoards | /:urlname/boards | | getDiscussions | /:urlname/boards/:bid/discussions | - +| postEvent | /event/:id | +| deleteEvent | /event/:id | ## Roadmap -* Implement `POST` and `DELETE` methods. * Add more short-hands. * Have some meetups... +* Update Meetup object to be have member variables and store data internally for important information + like access tokens and etc. and don't just use arrays for everything ## Alternatives Before starting this client, I checked out the following [existing clients](http://www.meetup.com/meetup_api/clients/): * [wizonesolutions/meetup_api](https://github.com/wizonesolutions/meetup_api): Huge library, hasn't been updated for 3 years. * [blobaugh](https://github.com/blobaugh/Meetup-API-client-for-PHP): Huge library, documentation quick start doesn't get you started. +* [FokkeZB](https://github.com/FokkeZB/Meetup): Great simple library, missing OATH and post/delete. +* +This is a more simplified library for access and interactions covering OATH and post/delete using the Meetup API! ## License diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1725664 --- /dev/null +++ b/composer.json @@ -0,0 +1,21 @@ +{ + "name": "user3581488/Meetup", + "type": "library", + "description": "PHP client for accessing most of the Meetup API", + "keywords": ["meetup"], + "homepage": "https://github.com/user3581488/Meetup", + "license": "Apache-2.0", + "require": { + "php": ">=5.3.3" + }, + "autoload": { + "classmap": [ + "/" + ] + }, + "extra": { + "branch-alias": { + "dev-master": "1.0" + } + } +} diff --git a/meetup.php b/meetup.php index 317f45e..e716492 100644 --- a/meetup.php +++ b/meetup.php @@ -1,128 +1,479 @@ 'true', - ); - - public function __construct(array $parameters = array()) { - $this->_parameters = array_merge($this->_parameters, $parameters); - } - - public function getEvents(array $parameters = array()) { - return $this->get('/2/events', $parameters)->results; - } - - public function getPhotos(array $parameters = array()) { - return $this->get('/2/photos', $parameters)->results; - } - - public function getDiscussionBoards(array $parameters = array()) { - return $this->get('/:urlname/boards', $parameters); - } - - public function getDiscussions(array $parameters = array()) { - return $this->get('/:urlname/boards/:bid/discussions', $parameters); - } - - public function getMembers(array $parameters = array()) { - return $this->get('/2/members', $parameters); - } - - public function getNext($response) { - if (!isset($response) || !isset($response->meta->next)) - { - throw new Exception("Invalid response object."); - } - return $this->get_url($response->meta->next); - } - - public function get($path, array $parameters = array()) { - $parameters = array_merge($this->_parameters, $parameters); - - if (preg_match_all('/:([a-z]+)/', $path, $matches)) { - - foreach ($matches[0] as $i => $match) { - - if (isset($parameters[$matches[1][$i]])) { - $path = str_replace($match, $parameters[$matches[1][$i]], $path); - unset($parameters[$matches[1][$i]]); - } else { - throw new Exception("Missing parameter '" . $matches[1][$i] . "' for path '" . $path . "'."); - } - } - } - - $url = self::BASE . $path . '?' . http_build_query($parameters); - - return $this->get_url($url); - } - - protected function get_url($url) { - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HTTPHEADER, array("Accept-Charset: utf-8")); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); - $content = curl_exec($ch); - - if (curl_errno($ch)) { - $error = curl_error($ch); - curl_close($ch); - - throw new Exception("Failed retrieving '" . $url . "' because of ' " . $error . "'."); - } - - $response = json_decode($content); - $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); - - curl_close($ch); - - if ($status != 200) { - - if (isset($response->errors[0]->message)) { - $error = $response->errors[0]->message; - } else { - $error = 'Status ' . $status; - } - - throw new Exception("Failed retrieving '" . $url . "' because of ' " . $error . "'."); - } - - if (isset($response) == false) { - - switch (json_last_error()) { - case JSON_ERROR_NONE: - $error = 'No errors'; - break; - case JSON_ERROR_DEPTH: - $error = 'Maximum stack depth exceeded'; - break; - case JSON_ERROR_STATE_MISMATCH: - $error = ' Underflow or the modes mismatch'; - break; - case JSON_ERROR_CTRL_CHAR: - $error = 'Unexpected control character found'; - break; - case JSON_ERROR_SYNTAX: - $error = 'Syntax error, malformed JSON'; - break; - case JSON_ERROR_UTF8: - $error = 'Malformed UTF-8 characters, possibly incorrectly encoded'; - break; - default: - $error = 'Unknown error'; - break; - } - - throw new Exception("Cannot read response by '" . $url . "' because of: '" . $error . "'."); - } - - return $response; - } +/** + * @package Meetup + * @license http://www.gnu.org/licenses/gpl.html GNU/GPL + */ +class Meetup +{ + /** + * Base meetup api url + * @const + */ + const BASE = 'https://api.meetup.com'; + /** + * Base meetup api url + * @const + */ + const AUTHORIZE = 'https://secure.meetup.com/oauth2/authorize'; + /** + * ACCESS meetup api url + * @const + */ + const ACCESS = 'https://secure.meetup.com/oauth2/access'; + /** + * GET request + * @const + */ + const GET = 1; + /** + * POST request + * @const + */ + const POST = 2; + /** + * PUT request + * @const + */ + const PUT = 3; + /** + * DELETE request + * @const + */ + const DELETE = 4; + /** + * Parameters for requests + * @var array + */ + protected $_parameters = array(); + /** + * The response object from the request + * @var mixed + */ + protected $_response = null; + /** + * Constructor + * @param array $parameters The parameters passed during construction + */ + public function __construct(array $parameters = array()) + { + $this->_parameters = array_merge($this->_parameters, $parameters); + $this->_next = $this->_response = null; + } + /** + * Stub for fetching events + * + * @param array $parameters The parameters passed for this request + * @return mixed A json object containing response data + * @throws Exception if anything goes wrong + */ + public function getEvents(array $parameters = array()) + { + return $this->get('/2/events', $parameters); + } + /** + * Stub for fetching groups + * + * @param array $parameters The parameters passed for this request + * @return mixed A json object containing response data + * @throws Exception if anything goes wrong + */ + public function getGroups(array $parameters = array()) + { + return $this->get('/2/groups', $parameters); + } + /** + * Stub for fetching photos + * + * @param array $parameters The parameters passed for this request + * @return mixed A json object containing response data + * @throws Exception if anything goes wrong + */ + public function getPhotos(array $parameters = array()) + { + return $this->get('/2/photos', $parameters); + } + /** + * Stub for fetching discussion boards + * + * @param array $parameters The parameters passed for this request + * @return mixed A json object containing response data + * @throws Exception if anything goes wrong + */ + public function getDiscussionBoards(array $parameters = array()) + { + return $this->get('/:urlname/boards', $parameters); + } + /** + * Stub for fetching discussions + * + * @param array $parameters The parameters passed for this request + * @return mixed A json object containing response data + * @throws Exception if anything goes wrong + */ + public function getDiscussions(array $parameters = array()) + { + return $this->get('/:urlname/boards/:bid/discussions', $parameters); + } + /** + * Stub for fetching member + * + * @param array $parameters The parameters passed for this request + * @return mixed A json object containing response data + * @throws Exception if anything goes wrong + */ + public function getMembers(array $parameters = array()) + { + return $this->get('/2/members', $parameters); + } + /** + * Stub for grabbing the next response data if it's available in the meta information + * of a response. Normally if there's too many results it won't return them all. + * + * @return mixed A json object containing response data + */ + public function getNext() + { + return $this->hasNext() ? $this->api($this->_response->meta->next, array(), self::GET) : null; + } + /** + * Is there more data to retrieve? + * + * @return boolean True if there's more results to process + */ + public function hasNext() + { + $next = null; + if( isset($this->_response->meta) && isset($this->_response->meta->next) ) + { + $next = $this->_response->meta->next; + if( strlen($next) ) + { + return true; + } + } + + return false; + } + /** + * Stub for adding an event + * + * @param array $parameters The parameters passed for this request + * @return mixed A json object containing response data + * @throws Exception if anything goes wrong + */ + public function postEvent(array $parameters = array()) + { + return $this->post('/2/event', $parameters); + } + /** + * Stub for updating an event + * + * @param array $parameters The parameters passed for this request + * @return mixed A json object containing response data + * @throws Exception if anything goes wrong + */ + public function updateEvent(array $parameters = array()) + { + return $this->post('/2/event/:id', $parameters); + } + /** + * Stub for deleting an event + * + * @param array $parameters The parameters passed for this request + * @return mixed A json object containing response data + * @throws Exception if anything goes wrong + */ + public function deleteEvent(array $parameters = array()) + { + return $this->delete('/2/event/:id', $parameters); + } + /** + * Perform a get on any url supported by meetup, use : to specify parameters that use + * placeholders and pass that exact parameter name as a parameter. + * + * @param array $parameters The parameters passed for this request + * @return mixed A json object containing response data + * @throws Exception if anything goes wrong + * + * @code + * $meetup->get('/2/event/:id', array('id'=>10)); + * $meetup->get('/2/members', array('group_urlname'=>'foobar')); + * @endcode + */ + public function get($path, array $parameters = array()) + { + list($url, $params) = $this->params($path, $parameters); + + return $this->api(self::BASE . $url, $params, self::GET); + } + /** + * Perform a post on any url supported by meetup, use : to specify parameters that use + * placeholders and pass that exact parameter name as a parameter. + * + * @param array $parameters The parameters passed for this request + * @return mixed A json object containing response data + * @throws Exception if anything goes wrong + * + * @code + * $meetup->post('/2/member/:id', array('id'=>10)); + * @endcode + */ + public function post($path, array $parameters = array()) + { + list($url, $params) = $this->params($path, $parameters); + + return $this->api(self::BASE . $url, $params, self::POST); + } + /** + * Perform a put on any url supported by meetup, use : to specify parameters that use + * placeholders and pass that exact parameter name as a parameter. + * + * @param array $parameters The parameters passed for this request + * @return mixed A json object containing response data + * @throws Exception if anything goes wrong + * + * @note There isn't any PUT supported events at the moment + */ + public function put($path, array $parameters = array()) + { + list($url, $params) = $this->params($path, $parameters); + + return $this->api(self::BASE . $url, $params, self::PUT); + } + /** + * Perform a delete on any url supported by meetup, use : to specify parameters that use + * placeholders and pass that exact parameter name as a parameter. + * + * @param array $parameters The parameters passed for this request + * @return mixed A json object containing response data + * @throws Exception if anything goes wrong + * + * @code + * $meetup->delete('/2/member/:id', array('id'=>10)); + * @endcode + */ + public function delete($path, array $parameters = array()) + { + list($url, $params) = $this->params($path, $parameters); + + return $this->api(self::BASE . $url, $params, self::DELETE); + } + /** + * Utility function for swapping place holders with parameters if any are found in + * the request url. The place holder parameter gets swapped out and the array gets + * the parameter removed, otherwise the request is left un-altered. + * + * @param string $path The relative path of the request from meetup (not including base path) + * @param array $parameters The parameters passed for this request + * @return array An array of the path and parameters modified or un-altered + * @throws Exception if anything goes wrong + */ + protected function params($path, array $parameters = array()) + { + $url = $path; + $params = $parameters; + if (preg_match_all('/:([a-z]+)/', $url, $matches)) + { + foreach ($matches[0] as $i => $match) + { + if (isset($params[$matches[1][$i]])) + { + $url = str_replace($match, $params[$matches[1][$i]], $url); + unset($params[$matches[1][$i]]); + } + else + { + throw new Exception("Missing parameter '" . $matches[1][$i] . "' for path '" . $path . "'."); + } + } + } + + return array($url, $params); + } + /** + * Utility function for authorizing ourselves with meetup. Visit this url + * https://secure.meetup.com/meetup_api/oauth_consumers/ to learn about OATH and the + * consumer details required for authorized access. + * + * @param array $parameters The parameters passed for this request + * @note You're sent to meetup and they will either have an error or a page requiring you to authorize, they'll send + * you back to the redirect uri specified in your consumer details + * @note The parameter 'response_type' is automatically included with value 'code' + */ + public function authorize(array $parameters = array()) + { + $location = self::AUTHORIZE . '?' . http_build_query(array_merge($this->_parameters,$parameters, array('response_type'=>'code'))); + header("Location: " . $location); + } + /** + * Utility function for getting an access token from meetup with the code they passed back in + * the authorization step. Visit this url https://secure.meetup.com/meetup_api/oauth_consumers/ + * to learn about OATH and the consumer details required for authorized access. + * + * @param array $parameters The parameters passed for this request + * @throws Exception if anything goes wrong + * @note The parameter 'grant_type' is automatically included with value 'authorization_code' + */ + public function access(array $parameters = array()) + { + return $this->api(self::ACCESS, array_merge($parameters, array('grant_type'=>'authorization_code')), self::POST); + } + /** + * Utility function for getting an refresh token from meetup to avoid authorization from expiring. + * Visit this url https://secure.meetup.com/meetup_api/oauth_consumers/ to learn about OATH and the + * consumer details required for authorized access. + * + * @param array $parameters The parameters passed for this request + * @throws Exception if anything goes wrong + * @note The parameter 'grant_type' is automatically included with value 'refresh_token' + */ + public function refresh(array $parameters = array()) + { + return $this->api(self::ACCESS, array_merge($parameters, array('grant_type'=>'refresh_token')), self::POST); + } + /** + * Main routine that all requests go through which handles the CURL call to the server and + * prepares the request accordingly. + * + * @param array $parameters The parameters passed for this request + * @throws Exception if anything goes wrong + * @note The parameter 'sign' is automatically included with value 'true' if using an api key + */ + protected function api($url, $parameters, $action=self::GET) + { + //merge parameters + $params = array_merge($parameters, $this->_parameters); + + //make sure 'sign' is included when using api key only + if(in_array('key', $params) && $url!=self::ACCESS && $url!=self::AUTHORIZE) + { + //api request (any) - include sign parameters + $params = array_merge( array('sign', 'true'), $params ); + } + + //init curl + $ch = curl_init(); + + $headers = array("Accept-Charset: utf-8"); + + //set options for connection + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 120); + curl_setopt($ch, CURLOPT_TIMEOUT, 120); + curl_setopt($ch, CURLOPT_HEADER, false); + curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']); + + //either GET/POST/PUT/DELETE against api + if($action==self::GET || $action==self::DELETE) + { + //GET + DELETE + + //include headers as specified by manual + if( $url == self::ACCESS ) + { + array_push($headers, 'Content-Type: application/x-www-form-urlencoded'); + } + else if( strpos($url, self::BASE) === 0 && in_array('access_token', $params) ) + { + array_merge($params, array('token_type'=>'bearer')); + } + + curl_setopt($ch, CURLOPT_URL, $url . (!empty($params) ? ('?' . http_build_query($params)) : '')); + } + else + { + //POST + PUT + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, count($params)); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); + } + + //need custom types for PUT/DELETE + switch($action) + { + case self::DELETE: + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + break; + case self::PUT: + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); + break; + } + + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + //fetch content + $content = curl_exec($ch); + + //was there an error on the connection? + if (curl_errno($ch)) + { + $error = curl_error($ch); + curl_close($ch); + + throw new Exception("Failed retrieving '" . $url . "' because of connection issue: ' " . $error . "'."); + } + + //retrieve json and store it internally + $this->_response = json_decode($content); + $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + curl_close($ch); + + if (!is_null($this->_response) && ($status < 200 || $status >= 300)) + { + //tell them what went wrong or just relay the status + if( isset($this->_response->error) && isset($this->_response->error_description) ) + { + //what we see against Oath + $error = $this->_response->error . ' - ' . $this->_response->error_description; + } + else if( isset($this->_response->details) && isset($this->_response->problem) && isset($this->_response->code) ) + { + //what we see against regular access + $error = $this->_response->code . ' - ' . $this->_response->problem . ' - ' . $this->_response->details; + } + else + { + $error = 'Status ' . $status; + } + + throw new Exception("Failed retrieving '" . $url . "' because of ' " . $error . "'."); + } + else if (is_null($this->_response)) + { + //did we have any parsing issues for the response? + switch (json_last_error()) + { + case JSON_ERROR_NONE: + $error = 'No errors'; + break; + case JSON_ERROR_DEPTH: + $error = 'Maximum stack depth exceeded'; + break; + case JSON_ERROR_STATE_MISMATCH: + $error = ' Underflow or the modes mismatch'; + break; + case JSON_ERROR_CTRL_CHAR: + $error = 'Unexpected control character found'; + break; + case JSON_ERROR_SYNTAX: + $error = 'Syntax error, malformed JSON'; + break; + case JSON_ERROR_UTF8: + $error = 'Malformed UTF-8 characters, possibly incorrectly encoded'; + break; + default: + $error = 'Unknown error'; + break; + } + + throw new Exception("Cannot read response by '" . $url . "' because of: '" . $error . "'."); + } + + return $this->_response; + } } - +?>