PHP
Article

Commenting, Upvoting and Uploading Photos with the 500px API

By Younes Rafie

Building a Custom 500px Laravel App

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.

Logo

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.

Comments

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.

New comment

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>

Upload form

// 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.

New photo

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.

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in PHP, once a week, for free.