Лента фоток Instagram для сайта.


Всем привет!

Когда-то давно в Modstore существовал компонент InstagramWidget. Затем его не стало, по той простой причине, что инстаграм перестал поддерживать технологию, на основе которой этот компонент был написал, и он тупо перестал работать.

Когда пару лет назад мне в руки попала хотелка от клиента — запустить виджет инстаграма на сайте, — путем недолгого гугления я нашел вот это. Радует, что проект живой — последнее обновление три дня назад. И недолго помучившись, я его запихнул в сайт клиента.

Однако далеко не все меня там устраивало. Во-первых, пришлось аккуратно выпиливать все эти жесткие die() в случае ошибок. Во-вторых, там довольно кустарная система кеширования. И так далее.

Поэтому, когда недавно мне снова прилетела похожая хотелка, я поскрипел зубами, и вдохновившись этим решением написал свое — проще и чисто под Modx.


Состоит оно из:

1. класс php

<?php
/**
 * Class Insta
 *
 * TODO: move configs to system settings
 */
class Insta
{
    private $modx;
    private $config = array();
    private $pdo;
    public $images = array();

    function __construct(modX &$modx, array $config = array())
    {
        $this->modx = &$modx;
        $this->pdo = $this->modx->getService('pdoTools');
        $this->config = array_merge(array(
            'login' => '', // юзернейм нужного аккаунта
            'token' => '', // как получить токен - читаем на проекте inwidget.ru
            'count' => 12, // количество забираемых за раз фоток
            'assetsPath' => $this->modx->getOption('assets_path').'images/insta/',
            'assetsUrl' => $this->modx->getOption('assets_url').'images/insta/',
            'chunksPath' => $this->modx->getOption('core_path').'elements/chunks/',
        ), $config);
    }

    public function storeChunk() {
        $images = $this->getImages();
        $tpl = '@FILE chunks/instagram.template.tpl';
        $render = $this->pdo->getChunk($tpl, array('images' => $images));
        if (file_put_contents($this->config['chunksPath'].'instagram.rendered.tpl', $render)) {
            return true;
        } else {
            return false;
        }
    }

    public function getImages() {
        $response = $this->getResponse('https://api.instagram.com/v1/users/search?q='.$this->config['login'].'&access_token='.$this->config['token']);
        if ($response) {
            foreach ($response->data as $k => $row) {
                if ($row->username == $this->config['login']) {
                    $this->config['userId'] = $row->id;
                    break;
                }
            }
        }
        if (empty($this->config['userId'])) {
            $this->log('User not found');
            return false;
        }
        $response = $this->getResponse('https://api.instagram.com/v1/users/'.$this->config['userId'].'/media/recent/?access_token='.$this->config['token'].'&count='.$this->config['count']);
        if ($response and !empty($response->data)) {
            $images = array();
            foreach ($response->data as $k => $row) {
                $images[$k]['text'] = $row->caption->text;
                $images[$k]['link'] = $row->link;
                $images[$k]['large'] = $this->storeImage($row->images->low_resolution->url, $k.'-large');
                $images[$k]['fullsize'] = $this->storeImage($row->images->standard_resolution->url, $k.'-fullsize');
                $images[$k]['small'] = $this->storeImage($row->images->thumbnail->url, $k.'-small');
            }
            $this->images = $images;
        }
        return $this->images;
    }

    public function storeImage($url, $name) {
        $newPath = $this->config['assetsPath'].$name.'.'.pathinfo($url, PATHINFO_EXTENSION);
        $newUrl = $this->config['assetsUrl'].$name.'.'.pathinfo($url, PATHINFO_EXTENSION);
        if (copy($url, $newPath)) {
            $url = $newUrl.'?v='.date('Ymd-His'); // добавляем метку кеширования для браузеров
        }
        return $url;
    }

    public function getResponse($url){
        $response = false;
        if (extension_loaded('curl')) {
            $curl = curl_init();
            curl_setopt_array($curl, array(
                CURLOPT_SSL_VERIFYPEER => false,
                CURLOPT_SSL_VERIFYHOST => false,
                CURLOPT_HEADER => false,
                CURLOPT_POST => false,
                CURLOPT_RETURNTRANSFER => 1,
                CURLOPT_TIMEOUT => 10,
                CURLOPT_URL => $url
            ));
            $response = curl_exec($curl);
            curl_close($curl);
        }
        elseif (ini_get('allow_url_fopen') and extension_loaded('openssl')) {
            $response = file_get_contents($url);
        }
        if ($response) {
            $response = json_decode($response);
            $response = $this->checkResponse($response);
        }
        return $response;
    }

    public function checkResponse($response) {
        if (!is_object($response)) return false;
        if ($response->meta->code == 200 and empty($response->data)) {
            $this->log('No data received');
            return false;
        }
        if ($response->meta->code != 200) {
            $this->log('ERROR '.$response->meta->code);
            return false;
        }
        return $response;
    }

    private function log($message) {
        if (empty($message)) return;
        if (is_array($message)) {
            $message = print_r($message, true);
        }
        $this->modx->log(modX::LOG_LEVEL_ERROR, $message);
    }
}

2. скрипт для cron

<?php
define('MODX_API_MODE', true);
require_once dirname(dirname(dirname(dirname(__FILE__)))).'/index.php';
$modx->getService('error','error.modError');
$modx->setLogLevel(modX::LOG_LEVEL_INFO);

if (!$insta = $modx->getService('Insta', 'Insta', MODX_CORE_PATH.'components/insta/', array())) die('Insta class not loaded');

if ($insta->storeChunk()) {
    exit(date('Y-m-d H:i:s').' - ok!');
} else {
    exit(date('Y-m-d H:i:s').' - fail :(');
}

4. чанк-шаблон instagram.template.tpl — естесствно, тут он примерный — какие переменные туда приходят, смотри в классе

<div class="insta-row row">
    {foreach $images as $image}
        <div class="insta-item col-xs-4 col-sm-3 col-md-2">
            <a rel="nofollow" target="_blank" href="{$image.link}"
               style="background-image: url({$image.large})"></a>
        </div>
    {/foreach}
</div>

3. чанк вывода instagram.rendered.tpl — перезаписывается динамически, так что писать его руками смысла нет

Как это работает
Для работы необходим pdoTools, fenom и файловые элементы. Хотя это все легко можно переделать, чтобы обходиться без перечисленного.

В назначенное время (у меня это каждый час) по крону запускается скрипт, который забирает с Instagram последние N фоток и кладет их в указанную папку сайта, попутно собирая данные о них в массив.

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

И на странице я делаю всего лишь {include 'file:chunks/instagram.rendered.tpl'}
Решение в определенной степени корявенькое, но простое, на мой взгляд довольно элегантное и не особо ресурсоемкое. Файлы перезаписываются с теми же именами и не копятся на сервере.

Вдруг кому пригодится)
29 января 2018, 04:04    Алексей Бгатов   
18    1071 +11


Комментарии ()

  1. Александр 29 января 2018, 09:45 # 0
    Есть ещё такой вариант без крона и с кэшированием: https://modx.com/extras/package/getlatestpostsfrominstagram
    1. Олег 29 января 2018, 10:51 # 0
      И без токена, что порой важно. Но даже при обновлении версии с github, не работает: с
      1. Александр 29 января 2018, 11:04 # 0
        Да, токен не нужен.
        У меня работает. Но пришлось повозиться. Например, кэш удаляется только удалением /core/cache/instagram_latest_posts/latest_posts.cache.php
        И fenom не поддерживается.
        1. Алексей Бгатов 29 января 2018, 12:52 # 0
          а у меня фотки локальные, а не со стороннего ресурса)
        2. Игорь Сухинин 29 января 2018, 16:18 # +1
          Да как же не работает :) Работает, только на самом деле надо брать актуальную версию с github. Я только что проверил на сайте, который, собственно, и был источником идеи этой разработки.
          1. Игорь Терентьев 02 февраля 2018, 13:20 # 0
            Да, вот здесь рабочая версия: github.com/igorsuhinin/modx-instagram-latest-posts
            Делал недавно на основе этой версии сниппет, который по крону загружает изображения из инстаграма в Gallery.
            1. Игорь Сухинин 02 февраля 2018, 14:08 # 0
              Да, вот здесь рабочая версия: github.com/igorsuhinin/modx-instagram-latest-posts
              О, спасибо, а то вдруг я мог забыть, куда же это я выкладывал ее :D
              1. Игорь Терентьев 02 февраля 2018, 17:18 # 0
                Это я для тех, кто комменты будет читать:) А то ссылки то нет нигде.
                А вам спасибо за сниппет!
                1. Алексей Бгатов 02 февраля 2018, 17:34 # 0
                  все здорово, но класс в сниппете?..
                  1. Игорь Сухинин 02 февраля 2018, 23:38 # 0
                    Вы так это говорите, как будто это что-то плохое :) А что не так?
                    1. Алексей Бгатов 02 февраля 2018, 23:53 # 0
                      никанон))
                      1. Игорь Сухинин 03 февраля 2018, 00:00 # 0
                        Ой, беда. Ну что ж, тогда придется удалить из репозитория :)
                      2. Василий Наумкин 03 февраля 2018, 13:39 # 0
                        Ну хотя-бы
                        if (!class_exists('InstagramLatestPosts')) {
                        	class InstagramLatestPosts {
                        		...
                        	}
                        }
                        нужно добавить.

                        А то при 2х вызовах сниппета на одной странице будет
                        PHP Fatal error:  Cannot declare class InstagramLatestPosts, because the name is already in use ...
                        1. Игорь Сухинин 03 февраля 2018, 18:42 # 0
                          Спасибо, учту. Как-то не приходило в голову, что этот сниппет могут вызывать дважды на одной странице.
          2. Сергей Шлоков 29 января 2018, 10:41 # 0
            3. чанк вывода instagram.rendered.tpl — перезаписывается динамически, так что писать его руками смысла нет
            Очень странный подход. Обычно результат компиляции кладут в кэш.
            А достать его можно и на странице.
            {cache('instagram')}
            Плюс, название чанка шаблона я бы посоветовал вынести в настройки, а не указывать его жестко в классе.

            П.С. Ты уверен, что здесь нужен and, а не &&?
            if ($response->meta->code == 200 and empty($response->data)) {
            1. Василий Наумкин 29 января 2018, 11:03 # +1
              and, а не &&
              Это одно и то же — разницы нет.
              1. Алексей Бгатов 29 января 2018, 12:56 # +1
                про and — мне так больше нравится) разница только — у and ниже приоритет, он позже чем «=« обрабатывается, что позволяет избавиться от лишних скобок, если, например, в условии создается и сразу проверяется объект.

                про кэш — не умею пока.

                про название чанка — писал для себя, поэтому в системные настройки вообще ричего не выносил.
                1. Сергей Шлоков 29 января 2018, 13:23 # 0
                  Я знаю про приоритеты. Просто в условии видеть «and» непривычно. За всю свою программерскую жизнь видел только один раз. С && и || глаз на автомате делит условие по частям. Поэтому и спросил. Обычно эти операторы используют так
                  $res = $object->method() or die('Some message');
                  

                  По кэшированию начни с простого — modHelpers.
                  Записывай в кэш
                  // Кэш на час
                  cache(['instagram' => $render], 3600);
                  
                  На странице
                  {cache('instagram')}
                  
                  Можно добавить условия.
                  1. Воеводский Михаил 29 января 2018, 13:26 # +3
                    Мб, я старомоден, но везде в PHP использую только and и or.
              2. Олег 29 января 2018, 10:56 # 0
                Самый большой минус в том, что нужен токен, и решение все равно не самое быстрое.
                Но благо апи instagram обновилось и появились более простые методы.
                Вот мое решение (немного странный код), на js. Получение фото без токена и без кеширования (а нужно ли оно вообще), посредством парсинга json страницы.
                <script type="text/javascript">
                            var name = "[[*instagram]]";
                            $.get(name + "?__a=1", function (data, status) {
                                //console.log('IG_NODES', data.user.media.nodes);
                                $.each(data.user.media.nodes, function (n, item) {
                                    //console.log('ITEMS', item.display_src);
                                    $('.test').append(
                                        "<a href='https://www.instagram.com/p/"+ item.code +"' target='_blank'><div class='block3-1' style='background: url(" + item.display_src + ") center no-repeat; background-size: cover;'></div></a>"
                                    );
                                    return n<2;
                                });
                            });
                		</script>
                
                [[*instagram]] — тв с содержимым: www.instagram.com/ник/
                return n<2; — ограничивает количество фото, до последних 3х.
                Все фотографии рендерятся внутри дива с классом test.
                ?__a=1 — параметр выдает json страницу instagram аккаунта.
                1. Алексей Бгатов 29 января 2018, 13:03 # +1
                  во-первых, не знал что токен не нужен. На момент когда я нашел inwidget год назад, без токена вообще ничего не получить было. Во-вторых, меня устраивает то что картинки локальные и страница без задержек отдается
                  1. Олег 29 января 2018, 13:07 # 0
                    на js тоже задержек нет, я думаю cdn instagram более производителен веб сервера :)
                    единственная нагрузка на браузер — парсинг json
                    1. Алексей Бгатов 29 января 2018, 13:18 # +1
                      это ты думаешь, что он более производителен, а гугл пейджспид так не думает. И сеошники не любят внешних ресурсов ни в каком виде. Да и я не люблю
                      1. Олег 29 января 2018, 13:23 # 0
                        google pagespeed ничего не скажет, так как рендер идет после загрузки страницы.
                        seo тут никак не зависит, генерируется html шаблон (теги можно указать любые, вроде alt, title) и внешние ссылки на cdn не влияют на вес страницы. Даже более того, внешние источники с последними апдейтами не убирают вес в выдаче.
                        Я просто показал альтернативное взаимодействие, прямо говорю — это костыль. Но городить непонятно что, не надо, лишь бы написать :)
                2. Иван Климчук 29 января 2018, 11:03 # +3
                  А в чем сакоальный смысл крона? Я не к тому, что это плохо, но это дополнительная настройка, без которой можно обойтись. Можно ведь в кеш положить значение с ttl в один час (или другое, настраиваемое) и просто в при вызове снипета проверять это значение и если нужно, запрашивать новые данные перед отображением.
                  1. Олег 29 января 2018, 12:57 # 0
                    Еще проще записывать файлы кеша с unixtimestamp и сравнивать. Операция никаких ресурсов особо не кушает.
                    1. Иван Климчук 29 января 2018, 13:04 # +2
                      Зачем городить собственный велосипед с файлами, и гадить в папку с кешем, если встроенный cacheManager успешно и так это делает при указании ttl 3-м параметром в методе set().
                    2. Алексей Бгатов 29 января 2018, 12:58 # +2
                      можно. научусь — буду так и делать
                      1. Alex Lenk 30 января 2018, 16:07 # 0
                        Если, что-то доделаете или переделайте, то обновите в текучем посте, дабы не собирать по крупицам.
                        Очень полезное решение, спасибо вам!
                      2. Алексей Бгатов 29 января 2018, 13:21 # 0
                        вообще надо было сделать быстро и не запариваясь. На бегете крон-задание настраивается за две секунды. Ну и плюс пользователь всегда получает готовый код, а не ждет когда кеш перегенерируется, если ему повезло быть первым за час. там кстати нормально так ждать приходится
                      3. Pavel Zarubin 08 марта 2018, 02:46 # 0
                        Спасибо, работает, оформи в пакет и будет вообще круто! :)
                        Вы должны авторизоваться, чтобы оставлять комментарии.