Лента фоток 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, 01:04
21
1 731
+11

Комментарии: 37

Александр
29 января 2018, 09:45
1
0
Есть ещё такой вариант без крона и с кэшированием: https://modx.com/extras/package/getlatestpostsfrominstagram
    Олег
    29 января 2018, 10:51
    0
    И без токена, что порой важно. Но даже при обновлении версии с github, не работает: с
      Александр
      29 января 2018, 11:04
      0
      Да, токен не нужен.
      У меня работает. Но пришлось повозиться. Например, кэш удаляется только удалением /core/cache/instagram_latest_posts/latest_posts.cache.php
      И fenom не поддерживается.
        Алексей Бгатов
        29 января 2018, 12:52
        0
        а у меня фотки локальные, а не со стороннего ресурса)
      Игорь Сухинин
      29 января 2018, 16:18
      +1
      Да как же не работает :) Работает, только на самом деле надо брать актуальную версию с github. Я только что проверил на сайте, который, собственно, и был источником идеи этой разработки.
        Игорь Терентьев
        02 февраля 2018, 13:20
        1
        0
        Да, вот здесь рабочая версия: github.com/igorsuhinin/modx-instagram-latest-posts
        Делал недавно на основе этой версии сниппет, который по крону загружает изображения из инстаграма в Gallery.
          Игорь Сухинин
          02 февраля 2018, 14:08
          0
          Да, вот здесь рабочая версия: github.com/igorsuhinin/modx-instagram-latest-posts
          О, спасибо, а то вдруг я мог забыть, куда же это я выкладывал ее :D
            Игорь Терентьев
            02 февраля 2018, 17:18
            0
            Это я для тех, кто комменты будет читать:) А то ссылки то нет нигде.
            А вам спасибо за сниппет!
            Алексей Бгатов
            02 февраля 2018, 17:34
            0
            все здорово, но класс в сниппете?..
              Игорь Сухинин
              02 февраля 2018, 23:38
              0
              Вы так это говорите, как будто это что-то плохое :) А что не так?
                Алексей Бгатов
                02 февраля 2018, 23:53
                0
                никанон))
                  Игорь Сухинин
                  03 февраля 2018, 00:00
                  0
                  Ой, беда. Ну что ж, тогда придется удалить из репозитория :)
                Василий Наумкин
                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 ...
                  Игорь Сухинин
                  03 февраля 2018, 18:42
                  0
                  Спасибо, учту. Как-то не приходило в голову, что этот сниппет могут вызывать дважды на одной странице.
        Виталий Барышников
        01 июля 2018, 11:52
        0
        Приветствую. Вы своё дополнение уже не будете актуализировать? А то вижу везде последние комментарии ещё в марте были. Использовал на своих сайтах, пока Instagram не поменялся и стал ошибку выдавать.
          Алексей Бгатов
          01 июля 2018, 13:28
          0
          там нужно заново токен получить и все заработает. а вообще надо на новое апи переписывать, т. к. это к 20-му году закроют
            Виталий Барышников
            01 июля 2018, 15:40
            0
            Я про InstagramLatestPost, он без токена работал. Я так понимаю версия из шапки сейчас рабочая, но нужен токен, что не пугает. Подскажите, где этот класс нужно вставить в modx, не особо в архитектуре modx силён, а с остальным разберусь.
              Алексей Бгатов
              01 июля 2018, 16:35
              0
              вопрос был про «мое дополнение», я к InstagramLatestPost отношения не имею
Сергей Шлоков
29 января 2018, 10:41
0
3. чанк вывода instagram.rendered.tpl — перезаписывается динамически, так что писать его руками смысла нет
Очень странный подход. Обычно результат компиляции кладут в кэш.
А достать его можно и на странице.
{cache('instagram')}
Плюс, название чанка шаблона я бы посоветовал вынести в настройки, а не указывать его жестко в классе.

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

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

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

      По кэшированию начни с простого — modHelpers.
      Записывай в кэш
      // Кэш на час
      cache(['instagram' => $render], 3600);
      На странице
      {cache('instagram')}
      Можно добавить условия.
Олег
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 аккаунта.
    Алексей Бгатов
    29 января 2018, 13:03
    +1
    во-первых, не знал что токен не нужен. На момент когда я нашел inwidget год назад, без токена вообще ничего не получить было. Во-вторых, меня устраивает то что картинки локальные и страница без задержек отдается
      Олег
      29 января 2018, 13:07
      0
      на js тоже задержек нет, я думаю cdn instagram более производителен веб сервера :)
      единственная нагрузка на браузер — парсинг json
        Алексей Бгатов
        29 января 2018, 13:18
        +1
        это ты думаешь, что он более производителен, а гугл пейджспид так не думает. И сеошники не любят внешних ресурсов ни в каком виде. Да и я не люблю
          Олег
          29 января 2018, 13:23
          0
          google pagespeed ничего не скажет, так как рендер идет после загрузки страницы.
          seo тут никак не зависит, генерируется html шаблон (теги можно указать любые, вроде alt, title) и внешние ссылки на cdn не влияют на вес страницы. Даже более того, внешние источники с последними апдейтами не убирают вес в выдаче.
          Я просто показал альтернативное взаимодействие, прямо говорю — это костыль. Но городить непонятно что, не надо, лишь бы написать :)
Иван Климчук
29 января 2018, 11:03
+3
А в чем сакоальный смысл крона? Я не к тому, что это плохо, но это дополнительная настройка, без которой можно обойтись. Можно ведь в кеш положить значение с ttl в один час (или другое, настраиваемое) и просто в при вызове снипета проверять это значение и если нужно, запрашивать новые данные перед отображением.
    Олег
    29 января 2018, 12:57
    0
    Еще проще записывать файлы кеша с unixtimestamp и сравнивать. Операция никаких ресурсов особо не кушает.
      Иван Климчук
      29 января 2018, 13:04
      +2
      Зачем городить собственный велосипед с файлами, и гадить в папку с кешем, если встроенный cacheManager успешно и так это делает при указании ttl 3-м параметром в методе set().
    Алексей Бгатов
    29 января 2018, 12:58
    +2
    можно. научусь — буду так и делать
      Alex Lenk
      30 января 2018, 16:07
      0
      Если, что-то доделаете или переделайте, то обновите в текучем посте, дабы не собирать по крупицам.
      Очень полезное решение, спасибо вам!
    Алексей Бгатов
    29 января 2018, 13:21
    0
    вообще надо было сделать быстро и не запариваясь. На бегете крон-задание настраивается за две секунды. Ну и плюс пользователь всегда получает готовый код, а не ждет когда кеш перегенерируется, если ему повезло быть первым за час. там кстати нормально так ждать приходится
Pavel Zarubin
08 марта 2018, 02:46
0
Спасибо, работает, оформи в пакет и будет вообще круто! :)
Игорь Улькин
24 мая 2018, 10:06
0
Господа, есть ли идеи, что делать если у сниппета modx-instagram-latest-posts обновлял код с github, но на странице выводит:

Error: The remote loading of JSON content failed. Please check if your account name is correct.

Код сниппета в шаблоне:
[[!InstagramLatestPosts?
	&accountName=`sharks.project`
	&limit=`8`
	&showVideo=`1`
	&innerTpl=`Instagram-Inner`
	&outerTpl=`Instagram-Outer`
	&errorTpl=`Instagram-Error`
	&cacheEnabled=`1`
	&cacheExpTime=`1800`
]]
    Bor
    Bor
    01 июля 2018, 14:24
    0
    Тоже самое( видать инстаграмм api обновил.