Импорт фотографий к себе на сайт из Facebook, Vkontakte и Instagram.

Сейчас рассмотрим, как импортировать фотографии из Facebook, Vkontakte и Instagram к себе на сайт, красиво и охуенно, используя API этих сервисов. Представленный код написан для Zend Framework 1.* приложения, но вы можете его использовать где угодно с небольшими изменениями. Посмотреть процесс в действии вы можете на Tuffle.com (на странице создания воспоминания). Facebook и Vkontakte группируют фотографии по альбомам, в Instagram же альбомов нету. Начнем с backend части, опишем абстрактный класс-родитель.

abstract class Social_Abstract
{
    /**
     * Возвращает массив с альбомами, которые включают в себя название, кол-во объектов и превью
     * @return array
     */
    abstract function getAlbums();

    /**
     * Возвращает массив фотографий из альбома, или фотографии Instagram-пользователя (в этом случае не передается $album)
     * @param int|null $album
     * @return array
     */
    abstract function getPhotos($album = null);

    /**
     * Возвращает URI страницу, на которой пользователь должен авторизовать приложение. Об этом подробней чуть позже
     * @return string
     */
    abstract function getPermissionsUri();

    /**
     * @param string $code - параметр, который вернет социальная сеть, при помощи которого получается access_token
     * @return string
     */
    abstract function getAccessToken($code);
}

Видите, всего лишь 2 метода, как просто. Начнем реализацию с Facebook, у них даже SDK есть для PHP, который мы будем использовать. Напомним, для каждой социальной сети нам нужно зарегистрировать приложения, сделать это просто, на сайтах есть пункт «Разработчикам», где это делает пару кликов. Вам будут выданы уникальные ключи (id приложения и secret-хэш).

class Social_Facebook extends Social_Abstract
{
    /**
     * @var Facebook
     */
    private $_api;

    public function __construct()
    {
        // Подключаем класс SDK (https://github.com/facebook/facebook-php-sdk)
        require_once 'facebook.php';

        // Данные приложения мы храним в конфиге
        $config = Zend_Registry::get('config')->facebook;

        $this->_api     = new Facebook(array(
            'appId'  => $config->app_id,
            'secret' => $config->secret
        ));
    }

    public function getPermissionsUri()
    {
        $args = array(
            // На этот адрес пользователь будет перенаправлен после успешной авторизации
            'redirect_uri' => ROOT_PATH . '/social/facebook-auth',
            // Приложение запрашивает права на получение фотографий пользователя
            'scope'        => 'user_photos'
        );

        return $this->_api->getLoginUrl($args);
    }

    public function getAccessToken($code)
    {
        return $this->_api->getAccessToken();
    }

    public function getAlbums()
    {
        $session = new Zend_Session_Namespace('social');

        $this->_api->setAccessToken($session->facebook_token);
        $albums = $this->_api->api('/me/albums');

        // Get cover photo URL
        foreach ($albums['data'] as &$album) {
            $album['cover_path'] = 'https://graph.facebook.com/' . $album['cover_photo'] . '/picture';
        }

        return $albums;
    }

    public function getPhotos($album)
    {
        $session = new Zend_Session_Namespace('social');

        $this->_api->setAccessToken($session->facebook_token);
        $photos = $this->_fb->api('/' . $album . '/photos');

        return $photos['data'];
    }
}

Теперь посмотрим, на что способен Vkontakte.. Да на то же самое, только SDK официального тут нам не найти, а сторонние использовать — подвергать систему риску.

class Social_Vkontakte extends Social_Abstract
{
    /**
     * @var string
     */
    private $_appId;

    /**
     * @var string
     */
    private $_secret;

    public function __construct()
    {
        $config = Zend_Registry::get('config')->vkontakte;
        $this->_appId  = $config->app_id;
        $this->_secret = $config->secret;
    }

    public function getPermissionsUri()
    {
        $url = ROOT_PATH . '/social/vkontakte-auth';

        // Перадаем обратный URL, необходимые права
        return 'https://oauth.vk.com/authorize?client_id=' . $this->_appId . '&redirect;_uri=' . urlencode($url) .
            '&display;=popup&scope;=photos&response;_type=code';
    }

    public function getAccessToken($code)
    {
        // Должен быть такой же, какой и отправлялся при запросе на авторизацию
        $url       = ROOT_PATH . '/social/vkontakte-auth';
        $response = file_get_contents('https://oauth.vk.com/access_token?client_id=' . $this->_appId . '&client;_secret=' . urlencode($this->_secret) . '&code;=' . urlencode($code) . '&redirect;_uri=' . urlencode($url));
        $data     = json_decode($resp);

        return $data->access_token;
    }

    public function getAlbums()
    {
        $session = new Zend_Session_Namespace('social');

        $albums = file_get_contents('https://api.vk.com/method/photos.getAlbums?need_covers=1&access;_token=' . urlencode($session->vkontakte_token));
        $albums = json_decode($albums, true);

        // Хочется одинаковую структуру возвращаемого массива
        $albums = $albums['response'];
        foreach ($albums as &$album) {
            $album['id']         = $album['aid'];
            $album['count']      = $album['size'];
            $album['name']       = $album['title'];
            $album['cover_path'] = $album['thumb_src'];
        }

        return $albums;
    }

    public function getPhotos($album)
    {
        $session = new Zend_Session_Namespace('social');

        $photos = file_get_contents('https://api.vk.com/method/photos.get?aid=' . $album . '&need;_covers=1&access;_token=' . urlencode($session->vkontakte_token));
        $photos = json_decode($photos, true);

        $photos = $photos['response'];

        // Приводим к одному формату с Facebook
        foreach ($photos as &$photo) {
            $photo['picture'] = $photo['src'];
            $photo['source']  = $photo['src_big'];
        }

        return $photos;
    }
}

Такое чувство, что Instagram скопировал API у Vkontakte

class Social_Instagram extends Social_Abstract
{
    /**
     * @var string
     */
    private $_appId;

    /**
     * @var string
     */
    private $_secret;

    public function __construct()
    {
        $config = Zend_Registry::get('config')->instagram;
        $this->_appId  = $config->app_id;
        $this->_secret = $config->secret;
    }

    public function getPermissionsUri()
    {
        $url = ROOT_PATH . '/social/instagram-auth';

        return 'https://api.instagram.com/oauth/authorize/?client_id=' . $this->_appId . '&redirect;_uri=' . urlencode(url) . '&response;_type=code';
    }

    public function getAccessToken($code)
    {
        // Тут Instagram выебнулся и решил заставить программистов использовать CURL. Так что ваш PHP должен его поддерживать
        $url      = ROOT_PATH . '/social/instagram-auth';

        $apiData = array(
            'client_id'       => $this->_appId,
            'client_secret'   => $this->_secret,
            'grant_type'      => 'authorization_code',
            'redirect_uri'    => $url,
            'code'            => $code
        );

        $apiHost = 'https://api.instagram.com/oauth/access_token';

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $apiHost);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($apiData));
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: application/json'));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $jsonData = curl_exec($ch);
        curl_close($ch);

        $user = json_decode($jsonData);
        return $user->access_token;
    }

    // Заглушка
    public function getAlbums()
    {
        return array();
    }

    public function getPhotos()
    {
        $session = new Zend_Session_Namespace('social');

        // count=-1 показывает, что нужно вернуть все фото
        $response = file_get_contents('https://api.instagram.com/v1/users/self/media/recent?access_token=' . $session->instagram_token . '&count;=-1');
        $feed = json_decode($response, true);

        $photos = array();
        foreach ($feed['data'] as $item) {
            $photos[] = array(
                'picture' => $item['images']['low_resolution']['url'],
                'source'  => $item['images']['standard_resolution']['url']
            );
        };

        return $photos;
    }
}

Написали мы наши классы-модели. Теперь рассмотрим подробней алгоритм работы. Делать всё мы это хотим безе перезагрузки страницы, поэтому при запросе на импорт фотографий мы будем открывать окно с URL для авторизации (из метода getPermissionsUri). В этом окне мы будем авторизироваться в конкретной социальной сети и подтверждать участие. После авторизации нам вернут access_token, который мы временно сохраним в сессию, и сделаем ajax-запрос на получение альбомов/фотографий. Ниже представлен Javascript-код, отвечающий за авторизацию приложения в социальных сетях. Это view script.

//php
$facebook  = new Social_Facebook();
$vkontakte = new Social_Vkontakte();
$instagram = new Social_Instagram();
$(document).ready(function() {
    // Обрабатываем нажатие на кнопку импорта из Facebook
    $('#import_from_facebook').click(function() {
        // Размеры окна
        var left = (screen.width - 600)/ 2,
            top  = (screen.height - 400)/ 2;

        window.facebookAuthorized = 0;
        window.open('getPermissionsUri(); ?>',
            'vk', 'toolbar=0,status=0,width=600,height=400,left=' + left + ',top=' + top);

        // Таким хитрым образом мы отслеживаем, когда можно закрыть окно. facebookAuthorized установит страница, на которую нас перенаправил facebook
        var intervalFacebook = setInterval(function() {
            if (window.facebookAuthorized == 1) {
                clearInterval(intervalFacebook);
            }
        }, 250);
        return false;
    )};

    // Такая же логика для Vkontakte и Instagram
    $('#import_from_vkontakte').click(function() {
        var left = (screen.width - 600)/ 2,
            top  = (screen.height - 400)/ 2;

        window.vkontakteAuthorized = 0;
        window.open('getPermissionsUri(); ?>',
            'vk', 'toolbar=0,status=0,width=600,height=400,left=' + left + ',top=' + top);

        var intervalVkontakte = setInterval(function() {
            if (window.vkontakteAuthorized == 1) {
                clearInterval(intervalVkontakte);
            }
        }, 250);
        return false;
    )};

    $('#import_from_instagram').click(function() {
        var left = (screen.width - 600)/ 2,
            top  = (screen.height - 400)/ 2;

        window.instagramAuthorized = 0;
        window.open('getPermissionsUri(); ?>',
            'vk', 'toolbar=0,status=0,width=600,height=400,left=' + left + ',top=' + top);

        var intervalInstagram = setInterval(function() {
            if (window.instagramAuthorized == 1) {
                clearInterval(intervalInstagram);
            }
        }, 250);
        return false;
    )};
)};

Время для контроллеров. КонтрОллеров, а не контролЁров. Это страницы, на которые нас перенаправят при успешной авторизации. Их мы указывали в параметрах redirect_url.

class SocialController extends Zend_Controller_Action
{
    // В этих методах мы сохраняем token в сессию
    public function facebookAuthAction()
    {
        $facebook = new Social_Facebook();
        $token    = $facebook->getAccessToken();

        $session = new Zend_Session_Namespace('social');
        $session->facebook_token = $token;
    }

    public function vkontakteAuthAction()
    {
        $vkontakte = new Social_Vkontakte();
        $token    = $vkontakte->getAccessToken();

        $session = new Zend_Session_Namespace('social');
        $session->vkontakte_token = $token;
    }

    public function instagramAuthAction()
    {
        $instagram = new Social_Instagram();
        $token    = $instagram->getAccessToken();

        $session = new Zend_Session_Namespace('social');
        $session->instagram_token = $token;
    }

    public function getFacebookAlbumsAction()
    {
        Zend_Layout::getMvcInstance()->disableLayout();
        $this->_helper->viewRenderer->setNoRender(true);

        $facebook = new Social_Facebook();
        $albums   = $facebook->getAlbums();

        $this->_helper->json($albums);
    }

    public function getVkontakteAlbumsAction()
    {
        Zend_Layout::getMvcInstance()->disableLayout();
        $this->_helper->viewRenderer->setNoRender(true);

        $vkontakte = new SocialVkontakte();
        $albums   = $vkontakte->getAlbums();

        $this->_helper->json($albums);
    }

    public function getFacebookPhotosAction()
    {
        Zend_Layout::getMvcInstance()->disableLayout();
        $this->_helper->viewRenderer->setNoRender(true);

        $albumId  = $this->_request->getPost('album');
        $facebook = new Social_Facebook();
        $photos   = $facebook->getPhotos($albumId);

        $this->_helper->json($photos);
    }

    public function getVkontaktePhotosAction()
    {
        Zend_Layout::getMvcInstance()->disableLayout();
        $this->_helper->viewRenderer->setNoRender(true);

        $albumId  = $this->_request->getPost('album');
        $vkontakte = new Social_Vkontakte();
        $photos   = $vkontakte->getPhotos($albumId);

        $this->_helper->json($photos);
    }

    public function getInstagramPhotosAction()
    {
        Zend_Layout::getMvcInstance()->disableLayout();
        $this->_helper->viewRenderer->setNoRender(true);

        $instagram = new Social_Instagram();
        $photos   = $instagram->getPhotos();

        $this->_helper->json($photos);
    }
}

Помните, мы проверяли переменные facebookAuthorized и другие? Это делается в view скриптах этого контроллера. facebook-auth.phtml

//script
window.opener.facebookAuthorized = 1;
    window.close();

vkontakte-auth.phtml

//script
window.opener.vkontakteAuthorized = 1;
    window.close();

instagram-auth.phtml

//script
window.opener.instagramAuthorized = 1;
    window.close();

Теперь у нас есть полное право на получение альбомов и фотографий. Делать всё это будем Ajax-ом, вызывая соответствующие backend-методы.

function getFacebookAlbums() {
    $.ajax({
        url:      '/social/get-facebook-albums',
        type:     'post',
        dataType: 'json',
        data:      {},
        success: function() {
            // Получили альбомы, выводим их
        }
    });
}
function getFVkontakteAlbums() {
    $.ajax({
        url:      '/social/get-vkontakte-albums',
        type:     'post',
        dataType: 'json',
        data:      {},
        success: function() {
            // Получили альбомы, выводим их
        }
    });
}
// При клике на альбом передаем его ID и получаем фотографии данного альбома
function getFacebookPhotos(albumId) {
    $.ajax({
        url:      '/social/get-facebook-photos',
        type:     'post',
        dataType: 'json',
        data:      {
            album: albumId
        },
        success: function() {
            // Получили фотографии, выводим их
        }
    });
}
function getVkontaktePhotos(albumId) {
    $.ajax({
        url:      '/social/get-vkontakte-photos',
        type:     'post',
        dataType: 'json',
        data:      {
            album: albumId
        },
        success: function() {
            // Получили фотографии, выводим их
        }
    });
}
function getInstagramPhotos() {
    $.ajax({
        url:      '/social/get-instagram-photos',
        type:     'post',
        dataType: 'json',
        data:      {}, // ID альбома передавать не нужно
        success: function() {
            // Получили фотографии, выводим их
        }
    });
}

Я намеренно упростил код, убрав обработчики ошибок, бизнес-логику, чтобы статья не вышла слышком большой.