A few weeks ago I had to write a few lines of PHP to interact with MailChimp and Twitter. To do so without writing an ugly index.php
with an if-else-like structure, I ended up using Slim. It’s a micro Web framework for PHP that works like Sinatra. And to test my implementation, I used Postman, because it’s simpler than doing a curl or add some JS.
PHP has Composer to deal with packages management, so first I have to setup Composer on my project. It takes just one command: composer init
. The whole thing is interactive, so at some point, I just have to give it which library I want to use. In this case, it’s slim/slim. Alternatively, it can be installed with: composer require slim/slim
. Finally, I had my composer.json
file and my dependencies installed as well. You can find more about the composer.json
file in the documentation.
Then I just have to add some API wrappers for MailChimp and Twitter. After a diagonal reading of the Slim documentation and the corresponding wrappers API I had this pretty index.php
file:
require 'vendor/autoload.php';
use \DrewM\MailChimp\MailChimp;
use \Abraham\TwitterOAuth\TwitterOAuth;
$app = new Slim\App();
$app->post('/mailchimp', function ($request, $response, $args) {
$api_key = 'my-api-key';
$list_id = 'a list id';
$mailchimp_client = new MailChimp($api_key);
$data = $request->getParsedBody();
$result = $mailchimp_client->post('lists/$list_id/members', [
'email_address' => $data['email'],
'status' => 'subscribed'
]);
return $response->withJson($result);
});
$app->get('/twitter', function ($request, $response, $args) {
$twitter_client = new TwitterOAuth(
'consumer_key', 'consumer_secret',
'access_token', 'access_token_secret'
);
$tweets = $twitter_client->get('statuses/user_timeline', [
'screen_name' => 'user_name',
'count' => 5,
'exclude_replies' => true,
'include_rts' => false
]);
return $response->withJson($tweets);
});
$app->run();
As you can see, there are two routes for this little application. The first one is a POST on /mailchimp
and the second one is a GET on /twitter
. At this point, I have exactly what I want.
But according to the Twitter API documentation, there are some limitations to the Twitter API. The rate limit is pretty high, but it’s still a limit, and I don’t want to end with a 200 HTTP Status and an empty result from the API. To prevent this, I decided to implement a basic cache system that can work on a shared Web hosting service. Before writing anything, I had a look on packagist.org, but I found nothing as simple as I want. So I started writing my own package:
namespace Awea\TwitterFeed;
use Abraham\TwitterOAuth\TwitterOAuth;
class TwitterFeed
{
// @var Assoc array used to merge with constructor $opts
private $default_opts = [
'cache_folder' => 'api/cache',
'cache_expiration' => 3600
];
// @var String path to cache file
private $cache_file;
// @var Integer to define cache expiration
private $cache_expiration;
// @var String Twitter username
private $screen_name;
// @var Instance of TwitterOAuth
private $twitter_client;
/**
* Constructor
*
* @param assoc array $opts with keys 'screen_name', 'consumer_key',
* 'consumer_secret', 'access_token', 'access_token_secret',
* 'cache_folder', 'cache_expiration'
*/
public function __construct($opts)
{
$opts = $this->mergeDefaults($opts);
$this->cache_expiration = $opts['cache_expiration'];
$this->screen_name = $opts['screen_name'];
$this->twitter_client = new TwitterOAuth(
$opts['consumer_key'], $opts['consumer_secret'],
$opts['access_token'], $opts['access_token_secret']
);
$this->cache_folder = $this->getFullPath($opts['cache_folder']);
$this->cache_file = $this->cache_folder.$this->screen_name.'.json';
}
/**
* Get User Timeline
*
* @param integer $count specifies the number of Tweets to try and retrieve
* @param boolean $exclude_replies prevents replies from appearing
* @param boolean $include_rts strips any native retweets
*
* @return array
*/
public function getUserTL($count = 5, $exclude_replies = true, $include_rts = false) {
// Check if cache file doesn't exist or needs to be updated
if(!is_file($this->cache_file) || (date("U")-date("U", filemtime($this->cache_file))) > $this->cache_expiration) {
$response = $this->twitter_client->get('statuses/user_timeline', [
'screen_name' => $this->screen_name,
'count' => $count,
'exclude_replies' => $exclude_replies,
'include_rts' => $include_rts
]);
$this->updateCache($response);
}
return $this->readCache();
}
/**
* Merge constructor opts with default opts
*
* @param array $opts Assoc array of arguments (see default_opts)
* @return array merged with defaults
*/
private function mergeDefaults($opts){
return array_merge($this->default_opts, $opts);
}
/**
* Read cache
*
* @return array
*/
private function readCache() {
return json_decode(file_get_contents($this->cache_file));
}
/**
* Update Cache
*
* @return file_put_contents result (FALSE or number of bytes written)
*/
private function updateCache($response) {
if(is_dir($this->cache_folder)){
if(is_writable($this->cache_folder)){
return file_put_contents($this->cache_file, json_encode($response));
}
else {
throw new TwitterFeedException("Error: The folder you have specified is not writable.");
}
}
else {
throw new TwitterFeedException("Error: The folder you have specified does not exist.");
}
}
/**
* Return full path to cache folder
*
* @param string $cache_folder relative location of the cache folder
* @return string
*/
private function getFullPath($cache_folder){
$doc_root = $_SERVER['DOCUMENT_ROOT'];
if (substr($doc_root, -1) != '/'){
$doc_root = $doc_root.'/';
}
return $doc_root.$cache_folder . '/';
}
}
This package comes with a specific composer.json
to explain what it does, how it’s structured… It looks like this:
{
"name": "awea/twitter_feed",
"description": "Simple wrapper to get Twitter user timeline with cached results",
"keywords": ["twitter", "feed"],
"licence": "LGPL-3.0",
"require": {
"php": ">=5.3",
"abraham/twitteroauth": "^0.7.2"
},
"authors": [
{
"name": "awea",
"email": "aweaoftheworld@gmail.com"
}
],
"require-dev": {
},
"autoload": {
"psr-4" : {
"Awea\\TwitterFeed\\" : "src"
}
}
}
To test it before publication, I just linked this package to my application. To do so, I have to add a few lines in my composer.json
:
"repositories": [
{
"type": "path",
"url": "/home/awea/.workspace/twitter_feed"
}
]
And I have to run the following command: composer require "awea/twitter_feed @dev"
. Then I have to update my index.php
with my new homemade package:
use \Awea\TwitterFeed\TwitterFeed;
$app->get('/twitter', function ($request, $response, $args) {
$twitter_cache = new TwitterFeed([
'screen_name' => 'screen_name',
'consumer_key' => 'consumer_key',
'consumer_secret' => 'consumer_secret',
'access_token' => 'access_token',
'access_token_secret' => 'access_token_secret'
]);
$tweets = $twitter_cache->getUserTL();
return $response->withJson($tweets);
});
Then I wanted to share this little package of mine on Packagist, so I submitted the package by adding the link to the repository on Github. Then I added the corresponding service on my Github repository, to maintain it without thinking about the update process on Packagist. This will pull automatically any change I made.
Now my TwitterFeed package is available through composer, and I can update it anytime by just creating a new tag name on git.
David Authier
Développeur freelance