Commenting, Upvoting and Uploading Photos with the 500px API
In the first part of this series we used the API to get the latest photos from 500px and filter through them, and we built a user profile with the list of associated photos. In this part, we are going to give the user the ability to vote, favorite and comment on photos, and finally, we will give the users the ability to upload their own photos. Let’s get started.
Authorization
When trying to access an API endpoint we need to check the authentication level required to access the resource. The API may require a consumer_key
or an OAuth.
In the first part we registered a new application on 500px and received our consumer_key
and consumer_secret
. We use the Grant app to get a valid token
and token_secret
for testing.
Authentication
After specifying your consumer_key
and consumer_secret
, you receive a token
and a token_secret
. These are going to give you the right to post and retrieve user content.
// bootstrap/start.php
App::singleton('pxoauth', function(){
$host = 'https://api.500px.com/v1/';
$consumer_key = 'YOUR CONSUMER KEY';
$consumer_secret = 'YOUR CONSUMER SECRET';
$token = 'GRANT TOKEN';
$token_secret = 'GRANT TOKEN SECRET';
$oauth = new PxOAuth($host, $consumer_key, $consumer_secret, $token, $token_secret);
return $oauth;
});
The PxOAuth
class wraps our communication with the 500px API.
// app/src/PxOAuth.php
class PxOAuth{
public $host;
public $client;
private $consumer_key;
private $consumer_secret;
private $token;
private $token_secret;
public function __construct($host, $consumer_key, $consumer_secret, $token, $token_secret){
$this->host = $host;
$this->consumer_key = $consumer_key;
$this->consumer_secret = $consumer_secret;
$this->token = $token;
$this->token_secret = $token_secret;
$params = [
'consumer_key' => $this->consumer_key,
'consumer_secret' => $this->consumer_secret,
'token' => $this->token,
'token_secret' => $this->token_secret
];
$oauth = new Oauth1($params);
$this->client = new Client([ 'base_url' => $this->host, 'defaults' => ['auth' => 'oauth']]);
$this->client->getEmitter()->attach($oauth);
// if app is in debug mode, we do logging
if( App::make('config')['app']['debug'] ) {
$this->addLogger();
}
}
private function addLogger(){
$log = new Logger('guzzle');
$log->pushHandler(new StreamHandler(__DIR__.'/../storage/logs/guzzle.log'));
$subscriber = new LogSubscriber($log, Formatter::DEBUG);
$this->client->getEmitter()->attach($subscriber);
}
}//class
When working with HTTP requests, it’s often useful to have some kind of logging. We add logging if the application is in debug mode.
Voting on Photos
If you used the 500px website, you could see that the user has the ability to vote on photos to increase their rating. We can retrieve the list of users who voted for a specific photo, and you can vote it up or down.
// app/routes.php
Route::post('/ajax/photo/vote', ['uses' => 'PXController@vote']);
// app/controllers/PXController.php
public function vote(){
$photoId = Input::get("pid");
$px = App::make('pxoauth');
$url = "photos/{$photoId}/vote";
try {
$result = $px->client->post($url, ["body" => ['vote' => '1']]);
}
catch(RequestException $e){
$response = $e->getResponse();
if($response->getStatusCode() === 403){
return (string) $response->getBody();
}
return ["status" => 500, "error" => "A serious bug occurred."];
}
return (string) $result->getBody();
}
// public/js/vote_favorite.js
$('body').on('click', '.thumb .vote', function(e){
e.preventDefault();
$this = $(this);
var pid = $this.parents(".thumb").data("photo-id");
$.ajax({
url: "/ajax/photo/vote",
type: "POST",
dataType: "json",
data: {
pid: pid
},
success: function(data){
if(data.hasOwnProperty("error")){
alert(data.error);
}
else{
$this.text(data.photo.votes_count);
alert("Photo voted successfully");
}
}
});
});
The best way to implement the voting action is through AJAX. We are going to send an AJAX post request with the photo ID, and get back a JSON response, which either contains an error message or a refreshed photo.
After retrieving the photo ID, we send a post request to the photos/photo_id/vote
endpoint along with a vote
parameter (0 or 1). If the request wasn’t successful we can catch the exception and test $response->getStatusCode()
. You can visit the docs to see the list of errors.
Marking Photos as Favorite
Marking photos as favorites is almost the same as upvoting them, except that you don’t pass any extra parameters in the body. You can see the docs for more details.
// app/routes.php
Route::post('/ajax/photo/favorite', ['uses' => 'PXController@favorite']);
// app/controllers/PXController.php
public function favorite(){
$photoId = Input::get("pid");
$px = App::make('pxoauth');
$url = "photos/{$photoId}/favorite";
try {
$result = $px->client->post($url);
}
catch(RequestException $e){
$response = $e->getResponse();
if($response->getStatusCode() === 403){
return (string) $response->getBody();
}
return ["status" => 500, "error" => "A serious bug occurred."];
}
return (string) $result->getBody();
}
// public/js/vote_favorite.js
$('body').on('click', '.thumb .favorite', function(e){
e.preventDefault();
$this = $(this);
var pid = $this.parents(".thumb").data("photo-id");
$.ajax({
url: "/ajax/photo/favorite",
type: "POST",
dataType: "json",
data: {
pid: pid
},
success: function(data){
if(data.hasOwnProperty("error")){
alert(data.error);
}
else{
$this.text(data.photo.favorites_count);
alert("Photo favourited successfully");
}
}
});
});
If you already voted or favorited a photo, you’ll get an error saying (Sorry, you have already voted for this Photo
). You can avoid that by disabling the link if the voted
or favorited
attribute inside a photo object is set to true
.
Comments
Commenting systems are mandatory for users to give their opinions. The 500px API provides endpoints for dealing with comments. We split the process into three main parts.
Single Photos
In the first part, when the user clicked on a photo we opened the link on the 500px website. Now, we are going to implement our own single photo page.
The photos/:photo_id
endpoint accepts a list of parameters, and returns the selected photo or a not found error.
// app/routes.php
Route::get('/photo/{id}', ['uses' => 'PXController@show']);
// app/controllers/PXController.php
public function show($id){
$px = App::make('pxoauth');
try {
$photo = $px->client->get("photos/{$id}?image_size=4")->json();
return View::make('single', ['photo' => $photo['photo']]);
}
catch(RequestException $e){
$response = $e->getResponse();
if($response->getStatusCode() === 404){
// handle 404: photo not found
}
}
}
// app/views/single.blade.php
...
<h1>{{ $photo['name'] }}</h1>
<p class="lead">
by <a href="/user/{{ $photo['user']['id'] }}">{{ $photo['user']['username'] }}</a>
</p>
<hr>
<p><span class="glyphicon glyphicon-time"></span> Posted on {{ $photo['created_at'] }}</p>
<hr>
<img class="img-responsive" src="{{ $photo['images'][0]['url'] }}" alt="">
<hr>
@if($photo['description'])
<p class="lead">{{ $photo['description'] }}</p>
@endif
<hr>
...
If the request was successful, we pass the photo object to the view, otherwise we catch the exception and handle the 404 error. You can visit the docs for more details.
Getting Comments
Retrieving comments can be done in two different ways. We can pass the comments
parameter to the single photo endpoint (photos/:id?comments
), or we can use the photos/:id/comments
endpoint.
The second way allows us to retrieve nested comments, which makes it easier for us to print them. The API limits the number of comments per request to 20 and you can use the page
parameter to get the complete list.
// app/controllers/PXController.php
public function show($id){
$px = App::make('pxoauth');
try {
$photo = $px->client->get("photos/{$id}?image_size=4")->json();
$comments = $px->client->get("photos/{$id}/comments?nested=true")->json();
return View::make('single', ['photo' => $photo['photo'], 'comments' => $comments['comments']]);
}
catch(RequestException $e){
$response = $e->getResponse();
if($response->getStatusCode() === 404){
// handle 404: photo not found
}
}
}
// app/views/single.blade.php
@foreach($comments as $comment)
<div class="media">
<a class="pull-left" href="#">
<img class="media-object" src="{{ $comment['user']['userpic_url'] }}" alt="">
</a>
<div class="media-body">
<h4 class="media-heading">{{ $comment['user']['username'] }}
<small>{{ $comment['created_at'] }}</small>
</h4>
{{ $comment['body'] }}
@if(count($comment['replies']))
@foreach($comment['replies'] as $ncomment)
<div class="media">
<a class="pull-left" href="#">
<img class="media-object" src="{{ $ncomment['user']['userpic_url'] }}" alt="">
</a>
<div class="media-body">
<h4 class="media-heading">{{ $ncomment['user']['username'] }}
<small>{{ $comment['created_at'] }}</small>
</h4>
{{ $comment['body'] }}
</div>
</div>
@endforeach
@endif
</div>
</div>
@endforeach
Nested comments are inserted on the replies
array, and we can paginate comments using the total_pages
attribute. You can visit the docs to see the list of available parameters.
New Comment
When the user is viewing a photo, he can comment on the photo to express his emotions and opinion. In this tutorial, we won’t allow replying on comments, we can only post a new comment, but it’s basically the same.
// app/views/single.blade.php
<div class="well">
<h4>Leave a Comment:</h4>
{{ Form::open(['url' => '/photo/comment', 'method' => 'post']) }}
<input type="hidden" name="pid" value="{{ $photo['id'] }}"/>
<div class="form-group">
<textarea name="comment" class="form-control" rows="3"></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
{{ Form::close() }}
</div>
// app/routes.php
Route::post('/photo/comment', ['uses' => 'PXController@comment']);
// app/controllers/PXController.php
public function comment(){
$photoId = Input::get('pid');
$comment = Input::get('comment');
$px = App::make('pxoauth');
$result = $px->client->post("photos/{$photoId}/comments", ['body' => ['body' => $comment]])->json();
if($result['status'] != 200){
// handle 400: Bad request.
}
return Redirect::back();
}
The method retrieves the photo ID and the comment body. We then post a request to the photos/:photo_id/comments
endpoint which only accepts those two parameters, and returns a 200 status code if the comment was successfully posted. Otherwise, we handle the 400 or 404 error. You can check the docs for more details.
Uploading New Photos
When using a package like Guzzle, uploading files become a straight forward process. The API accepts many parameters, but we are only going to use a name
, description
and a photo file. You can visit the docs for the full list of parameters.
// app/routes.php
Route::get('/upload', ['uses' => 'PagesController@photoUpload']);
Route::post('/photo/upload', ['as' => 'photo.upload', 'uses' => 'PXController@upload']);
// app/views/upload.blade.php
<div class="row">
{{ Form::open(['route' => 'photo.upload', 'files' => true]) }}
<div class="form-group">
{{ Form::label('name', 'Name: ') }}
{{ Form::text('name', null, ['class' => 'form-control']) }}
</div>
<div class="form-group">
{{ Form::label('description', 'Description: ') }}
{{ Form::textarea('description', null, ['class' => 'form-control']) }}
</div>
<div class="form-group">
{{ Form::label('photo', 'Upload a photo') }}
{{ Form::file('photo', null, ['class' => 'form-control']) }}
</div>
{{ Form::submit('Upload', ['class' => 'btn btn-primary']) }}
{{ Form::close() }}
</div>
// app/controllers/PXController.php
public function upload(){
try {
$px = App::make('pxoauth');
$result = $px->client->post('photos/upload', [
'body' => [
'name' => Input::get('name'),
'description' => Input::get('description'),
'file' => fopen(Input::file('photo')->getPathname(), 'r'),
]
])->json();
// you may want to pass a success message
return Redirect::to("/photo/{$result['photo']['id']}");
}
catch(RequestException $e){
$response = $e->getResponse();
if($response->getStatusCode() === 422){
// handle 422: Server error
}
}
}//upload
When you want to upload some data through HTTP you need to deal with multipart/form-data
headers and reading file content and adding it to the request. You can read more about sending files with Guzzle here.
The API returns a 200 status code along with the new photo on success, otherwise we get a 422 error if the file format is not supported, etc. We redirect the user to the new photo page.
Conclusion
The 500px API has more functionality than what we showed in this tutorial. You can check other API hack apps to get an idea of what is possible, and you can check the Github repository to test the final result. Let me know what you think in the comments below.