11 заметок с тегом

php

Способы хранения временных данных в PHP

PHP позволяет хранить временные данные в ОЗУ или файлах. Это реализовано с помощью потоков чтения-записи php://memory и php://temp, которые можно открыть через fopen() и управлять данными в файлоподобной обёртке до завершения скрипта. После окончания работы PHP уничтожит все временные данные, если их намеренно не сохранить. Куда записать временные данные:

Способ                 Описание
————————————————————————————————————————————————————————————————————————————————————
php://memory           Записывает и всегда хранит данные в ОЗУ. С помощью fopen()
                       можно работать как с обычным файлом. Сохранить данные можно
                       копированием в другой поток stream_copy_to_stream() или
                       передачей контента stream_get_contents() в другой обработчик.

php://temp             Создаёт файл во временной папке, когда размер данных
                       превышает 2 Мбайт. До этого все записанные данные
                       хранятся в ОЗУ. Временная папка определяется аналогично
                       функции sys_get_temp_dir().

php://temp/maxmemory:0 Обнуляет максимальный размер данных в байтах для хранения
                       в памяти и с первым байтом создаёт файл во временной папке.
                       Можно установить свой лимит стартового хранения данных в
                       ОЗУ путем добавления /maxmemory:NN, где NN — это
                       максимальный размер данных в байтах для хранения в памяти
                       перед использованием временного файла.

tmpfile()              Создаёт временный файл с уникальным именем в режиме
                       чтения и записи r+, а затем возвращает файловый указатель.
                       Ничем не отличается от php://temp/maxmemory:0, кроме
                       способа вызова. URI временного файла можно получить
                       с помощью функции stream_get_meta_data() и сохранить данные
                       через копирование файла copy() по URI.

Все временные файлы автоматически удаляются, а ОЗУ очищается после завершения скрипта. php://memory и php://temp нельзя будет переиспользовать, т. к. после закрытия потоков невозможно сослаться на них. Как правило, сохранность временных данных, по необходимости, достигается путём копирования. Ниже привожу два примера, которые демонстрируют работу со временными данными:

<?php

///////
// 1 //
///////

// Откроем поток php://memory
$temp_data = fopen('php://memory', 'w+');

// Запишем "Hello, world!"
fwrite($temp_data, 'Hello, world!');

// Извлечём метаданные из потока
stream_get_meta_data($temp_data);

// Array
// (
//     [timed_out]    => false
//     [blocked]      => true
//     [eof]          => false
//     [wrapper_type] => PHP
//     [stream_type]  => MEMORY
//     [mode]         => w+b
//     [unread_bytes] => 0
//     [seekable]     => true
//     [uri]          => php://memory
// )

// Сбросим курсор
rewind($temp_data);

// Получим "Hello, world!"
stream_get_contents($temp_data);

// Создадим новый файл data.txt
$fh = fopen('data.txt', 'w+');

// Сбросим курсор ещё раз
rewind($temp_data);

// Сохраним временные данные в новый файл
stream_copy_to_stream($temp_data, $fh);

// Закроем ресурс файла data.txt
fclose($fh);

// Закрывать поток php://memory необязательно, т. к. все данные
// будут автоматически уничтожены по завершению скрипта.
// fclose($temp_data);


///////
// 2 //
///////

// Создадим временный файл
$tmpfile = tmpfile();

// Запишем "Hello, world!"
fwrite($tmpfile, 'Hello, world!');

// Извлечём метаданные из потока
stream_get_meta_data($tmpfile);

// Array
// (
//     [timed_out]    => false
//     [blocked]      => true
//     [eof]          => false
//     [wrapper_type] => plainfile
//     [stream_type]  => STDIO
//     [mode]         => r+b
//     [unread_bytes] => 0
//     [seekable]     => true
//     [uri]          => home\user\temp\phpDC08.tmp
// )

// Получаем URI временного файла
$uri = stream_get_meta_data($tmpfile)['uri'];

// Сохраним "Hello, world!" в файл data.txt
copy($uri, 'data.txt');

// Временный файл будет автоматически удалён по завершению
// скрипта. Закрывать открытый дескриптор необязательно.
// fclose($tmpfile);
2017   php   tmpfile   данные   файл

Уменьшаем итерабельную сложность массива в PHP

Задача. Объедините массив, приведённый ниже, таким образом, чтобы по значениям результирующего массива работал поиск array_search() и in_array(). Массив должен быть проходимым за один цикл. Ключи сохранять не обязательно.
$data = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
];

На первый взгляд кажется, что можно скормить массив в array_merge() и получить желаемый результат, но array_merge() принимает как минимум два аргумента с массивами, а у нас один. Заметьте, вложенные массивы с данными обёрнуты в ещё один массив. Это усложняет доступ к этим данным. Можно, конечно, обойти весь массив двумя foreach() и извлечь все данные, но такой подход довольно-таки топорный:

<?php

$data = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
];

$flattened = [];

foreach ($data as $array) {
    foreach ($array as $key => $value) {
        $flattened[] = $value;
    }
}

var_dump($flattened);

// вернёт [1, 2, 3, 4, 5, 6, 7, 8, 9]

Существует более изящный способ объединить все массивы в один — скормить массив с данными в array_merge(), но через call_user_func_array(). Данная функция вызывает другую функцию с массивом параметров, где первым аргументом идёт название функции, а вторым — параметры в виде индексированного массива. Получается, что массив-обёртку съест call_user_func_array(), а остальные пойдут как аргументы в array_merge() и в итоге мы получим склеенный массив:

<?php

$data = [ // <- съест call_user_func_array()
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]; // <- съест call_user_func_array()

call_user_func_array('array_merge', $data);

// вернёт [1, 2, 3, 4, 5, 6, 7, 8, 9]

Начиная с версии 5.6 мы можем распаковать массив-обёртку, если перед передаваемым аргументом с массивом данных при вызове функции array_merge() подставим многоточие. Читайте подробнее про списки аргументов переменной длины. В нашем случае массив будет распакован и вложенные в него массивы объединятся:

<?php

$data = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
];

array_merge(...$data);

// вернёт [1, 2, 3, 4, 5, 6, 7, 8, 9]

Эти методы выполняют поставленную задачу, но только с двухуровневыми массивами, как на примерах выше. А что, если нужно склеить все значения многоуровневого массива в один? Разумеется необходимо рекурсивно пройти по каждому значению. Делается это с сохранением ключей и без, а также с учётом уровня вложенности. Ниже привожу все известные мне самописные функции:

<?php

///////
// 1 //
///////
if ( ! function_exists('array_flatten') ) {

    /**
     * Выравнивает массив без сохранения ключей
     *
     * @link https://gist.github.com/intedinmamma/246665
     *
     * @param array $data
     * @param array $flattened
     *
     * @return array
     */
    function array_flatten(array $data, array $flattened = [])
    {
        foreach($data as $value) {
            is_array($value)                               ?
            $flattened = array_flatten($value, $flattened) :
            $flattened[] = $value;
        }

        return $flattened;
    }
}

///////
// 2 //
///////
if ( ! function_exists('array_flatten') ) {

    /**
     * Выравнивает массив с сохранением ключей
     *
     * @link http://brandonwamboldt.github.io/utilphp/#array_flatten
     *
     * @param array $data
     * @param bool  $preserve_keys
     * @param array $flattened
     *
     * @return array
     */
    function array_flatten(
        array $data,
        bool $preserve_keys = true,
        array $flattened = []
    ) {
        array_walk_recursive($data, function ($value, $key) use (&$flattened, $preserve_keys) {
            $preserve_keys && !is_int($key) ?
            $flattened[$key] = $value       :
            $flattened[] = $value;
        });

        return $flattened;
    }
}

///////
// 3 //
///////
if ( ! function_exists('array_flatten') ) {

    /**
     * Выравнивает массив с помощью итератора с сохранением ключей или без
     *
     * @link http://stackoverflow.com/a/12205381
     *
     * @param array $data
     * @param bool  $use_keys
     *
     * @return array
     */
    function array_flatten(array $data, bool $use_keys = false)
    {
        $flattened = new \RecursiveIteratorIterator(
            new \RecursiveArrayIterator($data)
        );

        return iterator_to_array($flattened, $use_keys);
    }
}

///////
// 4 //
///////
if ( ! function_exists('array_flatten') ) {

    /**
     * Выравнивает массив по уровню вложенности
     *
     * @link https://github.com/spatie/array-functions#array_flatten
     *
     * @param array $array
     * @param int   $levels
     *
     * @return array
     */
    function array_flatten(array $array, int $levels = -1)
    {
        if ($levels === 0) {
            return $array;
        }

        $flattened = [];

        if ($levels !== -1) {
            --$levels;
        }

        foreach ($array as $value) {
            $flattened = array_merge(
                $flattened,
                is_array($value) ? array_flatten($value, $levels) : [$value]
            );
        }

        return $flattened;
    }
}


Пример для любой из функций array_flatten():

<?php

$data = [
    [1, 2, 3],
    [4, [5, 6, 7], 8],
    [9, [10, 11], 12],
];

array_flatten($data);

// вернёт [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
2017   php   массив

Файл .htaccess и RewriteRule

В последнее время мне стало интересно изучать низкоуровневые подходы минуя тот функционал, который предлагают нам фреймворки. Таким образом, я открываю для себя новые знания в программировании. Когда-то я проходил все стадии написания «велосипедов», но чтобы быть нормальным проггером, нужно знать как там всё устроено, это нормально. Сегодня рассмотрим перенаправления с помощью файла .htaccess для веб-сервера Apache на небольшом примере.

Чтобы не томиться ожиданием, давайте сразу перейдём к задаче: нужно сделать перенаправление со страницы /pages/about.php на /about, т. е. убрать в адресе папку /pages и расширение .php в названии файла. Ещё нужно позаботиться о том, чтобы в конце URL не было закрывающего слеша. Вот такую файловую структуру содержит наш пример:

┌public_html/
├──pages/
│  ├──about.php
│  ├──contact.php
│  └──subscribe.php
├──index.php
└──.htaccess

В Apache для преобразования URL’ов есть специальный модуль mod_rewrite. Он отвечает за создание ЧПУ — создаёт красивые URL для PHP-скриптов на уровне веб-сервера. Его мы и задействуем для решения нашей задачи. Для начала создадим конфигурационный файл .htaccess в корне сайта со следующим содержимым, а затем я объясню как он отработает:

# Включаем mod_rewrite
RewriteEngine On

# Убираем последний слеш
RewriteRule ^(.*)/$ /$1 [L,R=301]

# Преобразуем /pages/about.php в /about
RewriteRule ^(\w+)$ /pages/$1.php [L,NC]

Командой RewriteEngine On мы включаем модуль mod_rewrite, а с помощью RewriteRule задаём правила преобразования адресов. RewriteRule просто преобразует входной URL по порядку в соответствии с регулярными выражениями. В конце каждого правила можно задать флаги для указания поведения mod_rewrite при обработке URL. Синтаксис прост как три копейки:

RewriteRule новый реальный [флаг1,флаг2,флаг3]

Давайте разберём первое правило из примера: ^(.*)/$ /$1 [L,R=301] — между маркерами ^ и $ мы указали начало строки с любого символа и любой длины (.*), где конец строки должен заканчиваться на слеш /. Всё, что заключено в круглые скобки (.*) образует группу символов, которую можно использовать через макрос $1.

Теперь все URL’ы, которые заканчиваются на слеш, будут переброшены на адрес без последнего слеша, где /$1 — все входные символы без последнего слеша. Флаг [L] останавливает чтение .htaccess, а [R=301] генерирует HTTP-код ответа сервера HTTP/1.1 301 Moved Permanently. После того, как правило отработало и Apache обрезал закрывающий слеш, файл .htaccess выполнится ещё раз и первое правило больше не отработает.

Второе правило: ^(\w+)$ /pages/$1.php [L,NC] — пользователь запросит адрес страницы, который имеет буквенное или цифровое содержимое от одного и больше символов (\w+). Это образует группу символов, которые мы подставим в адрес скрипта /pages/$1.php. По факту выполниться скрипт под другим URL. Флаг [L] останавливает .htaccess, а [NC] отменяет проверку регистра символов.

Все правила выполняются по порядку. Также стоит обратить внимание на значение флагов, которые могут менять поведение RewriteRule и веб-сервера. Ниже я собрал коллекцию флагов применительно для перенаправлений:

Флаг  Описание
———————————————————————————————————————————————————————————————————————————————
[C]   Chain — объединяет несколько правил в цепочку. Если первое правило
      цепочки не срабатывает, то вся цепочка игнорируется.

[F]   Forbidden — возвращает ошибку 403 Forbidden (запрещено).

[G]   Gone — возвращает ошибку 410 Gone (удалён).

[L]   Last — останавливает процесс преобразования, и текущая ссылка
      считается окончательной.

[N]   Next — запускает процесс преобразования с первого по порядку правила.

[NS]  NoSubreq — разрешает срабатывание правила только для настоящих
      запросов, игнорируя подзапросы.

[NC]  NoCase — отключает проверку регистра символов.

[P]   Proxy — даёт команду Apache выполнить подзапрос к указанной странице
      с использованием программного модуля mod_proxy, при этом пользователь
      ничего не узнает об этом подзапросе. Если модуль mod_proxy отсутствует,
      то произойдет ошибка.

[PT]  PassThrough — останавливает процесс преобразования и передает
      полученную новую ссылку дальше по цепочке.
      
[QSA] Qsappend — добавляет исходные параметры запроса (Query String)
      к замене. Если замена не включает в себя новые параметры запроса,
      то исходные параметры запроса добавляются автоматически. Если же
      включает, то без флага QSA исходные параметры запроса будут утеряны.

[R]   Redirect — останавливает процесс преобразования и возвращает
      результат браузеру клиента как редирект на новую страницу.
      По умолчанию передаётся HTTP-код 302 Moved Temporarily (перемещенно
      временно), но его можно изменить путём присвоения нового статуса
      через знак равенства [R=301]. В этом случае будет передан HTTP-код
      301 Moved Permanently (перемещено навсегда).

[S]   Skip — пропускает следующее правило, если текущее правило сработало.
      Можно указать количество последующих игнорируемых правил [S=2].

Полезные ссылки:

    Остальные флаги в документации Apache
    Как на самом деле работает mod_rewrite
    Описание основных флагов mod_rewrite
    Волшебный файл .htaccess
    Коллекция сниппетов .htaccess

Учимся работать с API Яндекс.Метрики

Для извлечения статистических данных из Яндекс.Метрики необходимо воспользоваться специальным разделом API очётов. Сейчас актуальна версия v1. В качестве примеров, для получения статистики посещений всего сайта и конкретной страницы, мы выгрузим табличные данные в формате JSON, а затем преобразуем их и построим два линейных графика. Ниже привожу базовую структуру GET-запроса, с которым мы будем работать:

https://api-metrika.yandex.ru/stat/v1/data ? ids=<int>
                                           & oauth_token=<string>
                                           & metrics=<string>
                                           & dimensions=<string>
                                           & filters=<string>
                                           & date1=<string>
                                           & date2=<string>
                                           & sort=<string>

Яндекс.Метрика оперирует двумя базовыми сущностями: метрики (metrics) и группировки (dimensions). Метрики определяют числовые величины, которые могут быть рассчитаны на основе сессии или хита пользователя [количество посетителей, процент отказов, глубина просмотра, …]. Группировки предназначены для выборки данных по определённым признакам [страница входа, источник трафика, дата анализа, …].

Яндекс.Метрика работает отдельно с визитами (ym:s), где s — session, и хитами (ym:pv), где pv — pageview. В запросах к API эти характеристики нельзя использовать вместе в метриках и группировках. Список всех метрик и группировок можно посмотреть в документации.

Для детальных запросов существуют фильтры сегментации (filters). Они рассчитывают результат по отдельному сегменту данных. Например, когда нужно получить какие-либо данные только из поисковых систем или вытянуть статистику посещений по конкретной странице. С помощью параметров date1 и date2 можно задать отчётный период, а sort необходим для сортировки данных по признаку из dimensions. Небольшая шпаргалка ниже:

Параметр    Описание
——————————————————————————————————————————————————
ids         Номер счётчика.
oauth_token Токен авторизации.
metrics     Список метрик через запятую.
dimensions  Список группировок через запятую.
filters     Фильтры сегментации с условием.
date1       Дата начала периода выборки.
date2       Дата окончания периода выборки.
sort        Сортировка по метрикам и группировкам.

Для начала нужно зарегистрировать своё приложение на oauth.yandex.ru и получить oauth_token. В качестве ids выступает номер вашего счётчика (его можно узнать на главной странице Яндекс.Метрики). Параметры ids, oauth_token и metrics являются обязательными в простых запросах, но если применяется filters, то metrics и dimensions можно не указывать.

Предлагаю перейти в плоскость PHP и сформировать первый боевой запрос на примере моего блога, который получит количество визитов, просмотров и уникальных посетителей на всём сайте за последнюю неделю:

<?php

$url = 'https://api-metrika.yandex.ru/stat/v1/data';

$params = [
    'ids'         => '14446750',
    'oauth_token' => 'Z4AAAAAYZjVkAAQP-FYZ6SJdSkhesN0MJ0dXOZo',
    'metrics'     => 'ym:s:visits,ym:s:pageviews,ym:s:users',
    'dimensions'  => 'ym:s:date',
    'date1'       => '7daysAgo',
    'date2'       => 'yesterday',
    'sort'        => 'ym:s:date',
];

echo file_get_contents( $url . '?' . http_build_query($params) );

Как видите, в metrics и dimensions мы указали значения для визитов (ym:s). Это значит, что данные будут сформированы в рамках сессии по каждому пользователю. Если посетитель перезагрузит одну и туже страницу сто раз, то ym:s:pageviews не будет меняться. В случае работы с хитами (ym:pv), значение ym:pv:pageviews увеличилось бы на сто. Хиты собирают статистику в рамках каждого просмотра страницы.

Дату для отчётного периода можно указать в формате YYYY-MM-DD или её относительное значение: today, yesterday, 7daysAgo (номер дня можно заменить на свой). В результате Яндекс.Метрика вернула следующие данные в формате JSON:

{
  "query" : {
    "ids" : [ 14446750 ],
    "dimensions" : [ "ym:s:date" ],
    "metrics" : [ "ym:s:visits", "ym:s:pageviews", "ym:s:users" ],
    "sort" : [ "ym:s:date" ],
    "date1" : "2017-02-14",
    "date2" : "2017-02-20",
    "limit" : 100,
    "offset" : 1,
    "group" : "Week",
    "auto_group_size" : "1",
    "quantile" : "50",
    "attribution" : "Last",
    "currency" : "RUB"
  },
  "data" : [ {
    "dimensions" : [ {
      "name" : "2017-02-14"
    } ],
    "metrics" : [ 437.0, 800.0, 376.0 ]
  }, {
    "dimensions" : [ {
      "name" : "2017-02-15"
    } ],
    "metrics" : [ 390.0, 559.0, 322.0 ]
  }, {
    "dimensions" : [ {
      "name" : "2017-02-16"
    } ],
    "metrics" : [ 243.0, 370.0, 196.0 ]
  }, {
    "dimensions" : [ {
      "name" : "2017-02-17"
    } ],
    "metrics" : [ 168.0, 274.0, 131.0 ]
  }, {
    "dimensions" : [ {
      "name" : "2017-02-18"
    } ],
    "metrics" : [ 111.0, 145.0, 75.0 ]
  }, {
    "dimensions" : [ {
      "name" : "2017-02-19"
    } ],
    "metrics" : [ 121.0, 150.0, 87.0 ]
  }, {
    "dimensions" : [ {
      "name" : "2017-02-20"
    } ],
    "metrics" : [ 132.0, 251.0, 103.0 ]
  } ],
  "total_rows" : 7,
  "total_rows_rounded" : false,
  "sampled" : false,
  "sample_share" : 1.0,
  "sample_size" : 1602,
  "sample_space" : 1602,
  "data_lag" : 74,
  "totals" : [ 1602.0, 2549.0, 988.0 ],
  "min" : [ 111.0, 145.0, 75.0 ],
  "max" : [ 437.0, 800.0, 376.0 ]
}

Секция query содержит параметры запроса, но нас больше интересует data, в которой содержатся наши статистические данные. Секция data представляет собой массив, где итеративно заполнены поля dimensions и metrics теми данными, которые были указаны в запросе. В нашем случае, поле dimensions содержит name — это название группировки ym:s:date. Поле metrics — массив с числовыми данными: ym:s:visits, ym:s:pageviews и ym:s:users.

Для обработки общих статистических данных можно использовать такие секции как: totals, min и max (в некоторых отчётах удобно анализировать сразу обобщённую информацию). Чтобы вывести данные из секции data на экран, скормим их Highcharts и построим линейный график:

Посмотреть процесс создания графика: PHP + Highcharts

Мы получили статистику по визитам, просмотрам и уникальным посетителям на всех страницах моего блога за последнюю неделю. Выглядит довольно таки неплохо. Для получения статистики конкретной страницы в Яндекс.Метрике нужно использовать filter. Вот так будет выглядеть запрос и построение графика на amCharts для просмотра главной страницы моего блога по хитам (ym:pv):

<?php

/////////////////////////////////////////////////////////
///// ПАРАМЕТРЫ GET-ЗАПРОСА ДЛЯ КОНКРЕТНОЙ СТРАНИЦЫ /////
/////////////////////////////////////////////////////////
$url = 'https://api-metrika.yandex.ru/stat/v1/data';

$params = [
    'ids'         => '14446750',
    'oauth_token' => 'Z4AAAAAYZjVkAAQP-FYZ6SJdSkhesN0MJ0dXOZo',
    'metrics'     => 'ym:pv:pageviews',
    'dimensions'  => 'ym:pv:date',
    'filters'     => rawurldecode("ym:pv:URL=='https://denisyuk.by/'"),
    'date1'       => '30daysAgo',
    'date2'       => 'yesterday',
    'sort'        => 'ym:pv:date',
];

/////////////////////////////////////////////////////////
///// ПОЛУЧИМ ДАННЫЕ В JSON И ПЕРЕВЕДЁМ ИХ В МАССИВ /////
/////////////////////////////////////////////////////////
$json = file_get_contents( $url . '?' . http_build_query($params) );
$data = json_decode($json, true)['data'];

/////////////////////////////////////////////////////////////
///// ПРЕОБРАЗУЕМ ДАННЫЕ ДЛЯ ЛИНЕЙНОГО ГРАФИКА AMCHARTS /////
/////////////////////////////////////////////////////////////
$tmpdata = [];

foreach ($data as $item) {
    $tmpdata[] = [
        'category' => $item['dimensions'][0]['name'],
        'column-1' => $item['metrics'][0],
    ];
}

////////////////////////////////////
///// ВЕРНЁМ JSON ДЛЯ AMCHARTS /////
////////////////////////////////////
$dataProvider = json_encode($tmpdata);

///////////////////////////////////////////////////////
///// ВЫВЕДЕМ HTML-КОД И ПОСТРОИМ ГРАФИК AMCHARTS /////
///////////////////////////////////////////////////////
echo <<<HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <script src="https://www.amcharts.com/lib/3/amcharts.js"></script>
  <script src="https://www.amcharts.com/lib/3/serial.js"></script>
  <script src="https://www.amcharts.com/lib/3/themes/patterns.js"></script>
  <script>
    AmCharts.makeChart("amcharts-container", {
      "type": "serial",
      "categoryField": "category",
      "startDuration": 1,
      "theme": "patterns",
      "categoryAxis": {
        "gridPosition": "start"
      },
      "graphs": [{
        "balloonText": "[[title]] за [[category]] — [[value]]",
        "bullet": "round",
        "title": "Просмотры",
        "id": "AmGraph-1",
        "type": "smoothedLine",
        "valueField": "column-1"
      }],
      "valueAxes": [{
        "title": "Количество"
      }],
      "titles": [{
        "text": "Просмотры https://denisyuk.by за последние 30 дней"
      }],
      "dataProvider": $dataProvider
    });
  </script>
</head>
<body>
  <div id="amcharts-container" style="max-width: 720px; height: 400px;"></div>
</body>
</html>
HTML;
//end

Обратите внимание, что значение filters мы обернули в функцию rawurldecode(). Это нужно делать, когда вы работаете со сложными фильтрами сегментации и кириллицей в запросах. Другие возможности фильтра доступны в документации: можно делать множественную выборку, использовать регулярные выражения, задействовать дополнительные операторы и др. В итоге мы получили график amCharts для статистики просмотров главной страницы по хитам:

Обновлено 23 Фев 2017: Решил добавить круговую диаграмму на Highcharts, чтобы разнообразить пост (под графиком сниппет кода для построения диаграммы). С помощью группировки ym:s:<attribution>TrafficSource и метрики ym:s:visits мы получили статистику по всем источникам трафика, а через фильтр сегментации, указав значение ym:s:visits>10, скрыли источники трафика меньше десяти, чтобы не портить график.

Посмотреть процесс создания круговой диаграммы: PHP + Highcharts

Мы рассмотрели базовые возможности API Яндекс.Метрики, которые могут пригодится для построения отчётов посещаемости своего сайта. Здесь важно понять, что все запросы формируются на основе metrics и dimensions, а через filters вычленяются конкретные данные по сайту. Если у вас остались вопросы, то задавайте их в комментариях, а я, по мере необходимости, буду редактировать или дополнять пост.

Продолжить обучение:

    Документация API очётов Яндекс.Метрики
    Становясь гуру API Яндекс.Метрики
    Получаем статистические данные, используя API Yandex Metrika
    Яндекс.Метрика для Laravel 5

UploadedUrl — пишем класс для загрузки удалённых файлов на PHP

Задача. Написать класс для загрузки удалённых файлов по URL. Использовать для решения задачи пакет Symfony HttpFoundation. Класс должен иметь настройки и корректно обрабатывать ошибки. Объяснить устройство класса и показать примеры использования.

В Symfony HttpFoundation существует класс UploadedFile, который загружает файлы из $_FILES. Он умеет сохранять файл на диске и безопасно определять MIME-тип. Нам нужно создать класс с такой же функциональностью, но для загрузки удалённых файлов по URL. Свой класс мы назовём UploadedUrl, как бы расширяя Symfony HttpFoundation, и наследуем класс File, чтобы повторить все возможности UploadedFile. Структура нашего проекта будет выглядеть следующим образом:

┌src/
├──Exception/
│  └──UrlException.php
├──UploadedUrl.php
├──tmpfile.php
└──cacert.pem

Файл UploadedUrl.php будет содержать всю логику нашего класса, а остальные будут решать вспомогательные задачи: UrlException.php — класс для обработки исключений, tmpfile.php — класс для работы с временным файлом, cacert.pem — сертификат для безопасного соединения. Для тех, кто хочет сразу посмотреть результат, может скачать код с репозитория denisyukphp/uploaded-url или установить через Composer.

Для загрузки удалённого файла мы задействуем cURL, который скачает весь контент во временный файл, а затем передадим его URI в конструктор класса File. После завершения работы временный файл будет автоматически удалён. Подключить tmpfile.php можно через Composer с моего репозитория denisyukphp/tmpfile. Почему он лучше функции tmpfile() я рассказал на Хабре.

Перед началом написания класса нам необходимо определиться с интерфейсом инициализации. Предлагаю UploadedUrl принимать два аргумента: 1) http:// или https:// ссылку; 2) необязательный массив с настройками. В итоге вот так должен выглядеть запуск класса:

// обычная загрузка удалённого файла
new UploadedUrl('');

// загрузка файла с настройками по умолчанию
new UploadedUrl('', [
    'verify'     => true,
    'cert'       => 'cacert.pem',
    'maxsize'    => null,
    'timeout'    => null,
    'useragent'  => null,
    'buffersize' => 1024,
    'redirects'  => true,
    'maxredirs'  => 10,
    'curl'       => [],
]);

Подробнее о параметрах настроек поговорим чуть ниже, но этих будет вполне достаточно для полноценной работы. В целом схему работы класса можно представить в виде цепочки: получение URL и настроекинициализация и подготовка cURLзапрос и сохранения данных во временный файлобработка ошибок. Далее я буду небольшими блоками строить класс и по действиям в сниппетах кода давать разъяснения.

Посмотреть 258 строк кода UploadedUrl с комментариями
///////////////////////////////////
// 5 // Как пользоваться классом //
///////////////////////////////////

Для быстрого старта достаточно объявить класс new UploadedUrl('') с валидной ссылкой в первом аргументе. После чего с удалённым файлом можно работать на своём сервере. Файл будет находится во временной папке до конца работы PHP и автоматически удалится, если его не переместить в другое место. Ниже привожу примеры использования UploadedUrl с настройками и моими комментариями:

<?php

$file = new Denisyuk\UploadedUrl('http://i.imgur.com/JRorA8V.gif', [

    // Безопасное соединение. Если установить false, то
    // cURL отключит значения CURLOPT_SSL_VERIFYPEER и
    // CURLOPT_SSL_VERIFYHOST.
    'verify' => true,

    // Можно указать директорию с PEM-сертификатами или отдельный файл.
    // По умолчанию используется cacert.pem, свежию версию которого
    // можно скачать на https://curl.haxx.se/docs/caextract.html
    // или самостоятельно настроить автообновление свежего
    // сертификата по ссылке https://curl.haxx.se/ca/cacert.pem
    'cert' => __DIR__,

    // Зададим максимальный размер загружаемого файла
    // в байтах (по умолчанию без ограничений).
    'maxsize' => 1024 * 1024 * 32,

    // Максимальное время выполнения запроса.
    'timeout' => 60,

    // Информация о пользователе (если null, то будет
    // взято из $_SERVER['HTTP_USER_AGENT']).
    'useragent' => null,

    // Это количество байт, через которое cURL будет
    // сверять размер загружаемого файла. В нашем случае,
    // через каждый килобайт будет выполняться ф-я для
    // проверки размера скачанных данных. Для больших
    // размеров установите значение на 2% меньше
    // от разрешённого (для 1Гб установите 1024 * 1024 * 2).
    'buffersize' => 1024,

    // Разрешить следовать перенаправлениям (по умолчанию 10).
    // cURL будет переходит по ссылкам, где HTTP-ответ
    // имеет заголовок с Location.
    'redirects' => true,

    // Количество перенаправлений. Лучше установить
    // больше двух, т. к. возможны перенаправления
    // с HTTP на HTTPS.
    'maxredirs' => 5,

    // другие CURLOPT_* опции
    'curl' => [
        CURLOPT_AUTOREFERER    => true,
        CURLOPT_CONNECTTIMEOUT => 10,
        /* ... */
    ];
]);

Поскольку UploadedUrl будет наследовать класс File, то он также вернёт набор методов класса SplFileInfo. Это расширяет возможности нашего класса. В общем мы имеем мощный инструмент для работы с файлом. Давайте посмотрим какая функциональность нам доступна:

// получить MIME-тип, который можно
// безопасно использовать для валидации
// определённых файлов
$file->getMimeType();

// получить расширение файла
$file->guessExtension();

// переместить файл в другую папку
$file->move(__DIR__ . '/data', 'foobar.jpg');

// получить размер файла
$file->getSize();

// Остальные доступные методы для SplFileInfo можно
// посмотреть на http://php.net/manual/ru/class.splfileinfo.php
/////////////////////////////////////////
// 6 // Выявляем и обрабатываем ошибки //
/////////////////////////////////////////

UploadedUrl сам не бросает исключение в случае ошибок при загрузке файла. Для отслеживания этого процесса можно использовать три метода: isValid() — проверит были ли ошибки в cURL; getErrorMessage() — вернёт сообщение об ошибке, которое можно показать пользователю; getErrorCode() — вернёт код ошибки cURL (можно использовать для написания сообщений об ошибках на своём языке). Как это всё работает показано ниже:

<?php

use Denisyuk\UploadedUrl;
use Denisyuk\Exception\UrlException;

try {
    // (1) ссылка на загружаемый файл
    $url = 'http://windows.php.net/downloads/releases/php-7.1.1-src.zip';

    // (2) загрузим свежую версию PHP, которая весит ~26 Мбайт
    $file = new UploadedUrl($url, [

        // (3) укажем максимальный размер файла 5 Мбайт
        'maxsize' => 1024 * 1024 * 5,
    ]);

    // (4) проверяем на ошибки
    if (!$file->isValid()) {

        // (5) бросаем исключение
        throw new UrlException(
            $file->getErrorMessage(),
            $file->getErrorCode()
        );

        // Здесь мы можем сформировать свои названия
        // ошибок исходя из getErrorCode(), т. к. это
        // код ошибки cURL. Например:
        //
        // $code = $file->getErrorCode();
        // $errors = [
        //     /*  1 */ CURLE_UNSUPPORTED_PROTOCOL => '',
        //     /*  6 */ CURLE_COULDNT_RESOLVE_HOST => '',
        //     /* 22 */ CURLE_HTTP_RETURNED_ERROR  => '',
        //     /* 42 */ CURLE_ABORTED_BY_CALLBACK  => '',
        // ];
        // $message = isset($errors[$code]) ? $errors[$code] : '';
        //
        // Также мы можем использовать свойства класса
        // $file->url и $file->maxsize для написания
        // более красивых названий своим ошибкам.
        //
        // throw new UrlException($message, $code);
    }

    // (6) работаем дальше, если нет ошибок
    echo $file->getMimeType();

// (7) ловим исключение
} catch(UrlException $e) {

    // (8) выводим сообщение об ошибке
    // выведет: 'Файл "php-7.1.1-src.zip" не должен превышать 5 Мбайт.'
    echo $e->getMessage();
}

Я дальше планирую поддерживать и развивать этот класс на denisyukphp/uploaded-url как дополнение к Symfony HttpFoundation. Если у вас есть какие-либо замечания или предложния, то можете отправить их мне на почту a@denisyuk.by или Вконтакте. Буду очень благодарен за любую обратную связь, которая поможет улучшить UploadedUrl. В ближайшее время намереваюсь нормально оформить проект на Гитхабе и добавить несколько косметических улучшений.

Ctrl + ↓ Ранее