Многие люди считают что php подходит только для разработки сайтиков, и никак не может быть использован, в других областях применения языков программирования, для создания программ… В этой статье я бы хотел осветить, применение php скриптов «не целевым» образом, а именно мы напишем скрипт который будет делать скрин, выгружать его на yandex диск и выводить адрес скриншота в консоль…

Рассмотрим структуру проекта, она очень проста и состоит из 3-х файлов:

1. screen.php — точка входа в приложение.
2. classes/autoload.php — автолоадер проекта.
3. classes/Request.php — класс реализующий запросы к api яндекса.

Далее расмотрим код screen.php:

Код screen.php
#!/usr/bin/php
<?php
require_once('classis/autoload.php');

$request = new Request();

if(isset($argv[1]) && $argv[1] == '--getToken') {
    echo $request->getOauthLink();die;
}

$home = $_SERVER['HOME'];
$config = include($home . '/.config/scrphp/config.php');
$nameScreenshot = date('Y_m_d_G_i_s_') . 'screen.png';

system('scrot -s /tmp/'.$nameScreenshot);

$result = $request
    ->setToken($config['token'])
    ->setFileNameOnDisk($nameScreenshot)
    ->setPathToFile('/tmp/'.$nameScreenshot)
    ->upload()
    ->publicateFile();

$url = $result['public_url'];

echo $url.PHP_EOL;



Как видите это точка входа в приложение, логика проста:
1. Формирование имени скриншота
2. Вызов системной программы scrot
3. Запрос api yandex.disk и выгрузка скриншота

Файл autoload.php тоже очень прост и состоит всего из трёх строк кода, я приведу его лишь для ознакомления, и мы не будем его рассматривать подробно.

spl_autoload_register(function($name){
    require_once __DIR__.'/'.$name.'.php';
});

Работа с yandex api довольно проста я написал небольшой класс Request.php, с набором неких методов, которые помогают мне в работе с ним…

Листинг Request.php
<?php
class Request
{
    private $_token = null;
    private $_href = null;
    private $_method = null;
    private $_filePath = null;
    private $_fileName = null;

    /**
     * get oauth link
     */
    public function getOauthLink()
    {
        /**
         * https://oauth.yandex.ru/authorize?
         * response_type=token
         * & client_id=<идентификатор приложения>
         * [& device_id=<идентификатор устройства>]
         * [& device_name=<имя устройства>]
         * [& display=popup]
         * [& login_hint=<имя пользователя или электронный адрес>]
         * [& force_confirm=yes]
         * [& state=<произвольная строка>]
         */
        $link = 'https://oauth.yandex.ru/authorize'
            .'?response_type=token'
            . '&client_id=8fc231e60575439fafcdb3b9281778a3';
        echo $link;
    }

    /**
     * set file path on disk
     * @param $filePath
     * @return $this
     */
    public function setFileNameOnDisk($name)
    {
        /**
         * https://cloud-api.yandex.net/v1/disk/resources/upload ?
         * path=<путь, по которому следует загрузить файл>
         */
        $link = 'https://cloud-api.yandex.net/v1/disk/resources/upload?path='.urlencode('/'.trim($name,'/'));
        $response = file_get_contents($link,false,$this->_context('GET'));
        $responseAsArray = json_decode($response,true);
        $this->_href = $responseAsArray['href'];
        $this->_method = $responseAsArray['method'];
        $this->_fileName = $name;
        return $this;
    }

    /**
     * get path to file on local disk
     * @param $path
     * @return $this
     */
    public function setPathToFile($path) {
        $this->_filePath = $path;
        return $this;
    }

    /**
     * upload file to disk
     */
    public function upload()
    {
        $ch = curl_init($this->_href);

        curl_setopt($ch,CURLOPT_HTTPHEADER,
            array(
                'Authorization',
                'OAuth '.$this->_token
            )
        );

        curl_setopt($ch,CURLOPT_INFILE,fopen($this->_filePath,"r"));
        curl_setopt($ch,CURLOPT_INFILESIZE,filesize($this->_filePath));
        curl_setopt($ch,CURLOPT_PUT,true);

        curl_exec($ch);
        curl_close($ch);
        return $this;
    }

    /**
     * public file and get public url for screenshot
     * @return mixed
     */
    public function publicateFile()
    {
        /**
         * https://cloud-api.yandex.net/v1/disk/resources/publish ?
         * path=<путь к публикуемому ресурсу>
         */
        $link = 'https://cloud-api.yandex.net/v1/disk/resources/publish?path='.urlencode('/'.trim($this->_fileName,'/'));
        $response = file_get_contents($link,false,$this->_context('PUT'));
        $responseAsArray = json_decode($response,true);
        $publicateFile = file_get_contents($responseAsArray['href'],false,$this->_context($responseAsArray['method']));
        $publicateFileAsArray = json_decode($publicateFile,true);
        return $publicateFileAsArray;
    }

    /**
     * set oauth token
     * @param $key
     * @return $this
     */
    public function setToken($token)
    {
        $this->_token = $token;
        return $this;
    }

    /**
     * get context for request by file_get_contents
     * @param $method
     * @return resource
     */
    private function _context($method)
    {
        /**
         * Authorization: OAuth <key>
         */
        $opts = array(
            'http'=>array(
                'method'=>$method,
                'header'=>"Authorization: OAuth ".$this->_token."\r\n"
            )
        );

        $context = stream_context_create($opts);
        return $context;
    }

}


Рассмотрим ключевые методы данного класса для запроса методов api в основном я использовал file_get_contents, и так как мне приходилось использовать, его при запросе многих методов я написал метод генерации контекста:

    /**
     * get context for request by file_get_contents
     * @param $method
     * @return resource
     */
    private function _context($method)
    {
        /**
         * Authorization: OAuth <key>
         */
        $opts = array(
            'http'=>array(
                'method'=>$method,
                'header'=>"Authorization: OAuth ".$this->_token."\r\n"
            )
        );

        $context = stream_context_create($opts);
        return $context;
    }

Он тоже довольно прост мы создаём контекст с определённом методом запроса и информации об аутентификации…

Далее нам необходимо «создать файл на yandex.disk» это действие мы производим следующим методом:

  /**
     * set file path on disk
     * @param $filePath
     * @return $this
     */
    public function setFileNameOnDisk($name)
    {
        /**
         * https://cloud-api.yandex.net/v1/disk/resources/upload ?
         * path=<путь, по которому следует загрузить файл>
         */
        $link = 'https://cloud-api.yandex.net/v1/disk/resources/upload?path='.urlencode('/'.trim($name,'/'));
        $response = file_get_contents($link,false,$this->_context('GET'));
        $responseAsArray = json_decode($response,true);
        $this->_href = $responseAsArray['href'];
        $this->_method = $responseAsArray['method'];
        $this->_fileName = $name;
        return $this;
    }

Как я говорил ранее мы запрашиваем api с помощью функции file_get_contents. После того как этот метод отработает, и вся информация будет запрошена, запускаеться метод upload:

    /**
     * upload file to disk
     */
    public function upload()
    {
        $ch = curl_init($this->_href);

        curl_setopt($ch,CURLOPT_HTTPHEADER,
            array(
                'Authorization',
                'OAuth '.$this->_token
            )
        );

        curl_setopt($ch,CURLOPT_INFILE,fopen($this->_filePath,"r"));
        curl_setopt($ch,CURLOPT_INFILESIZE,filesize($this->_filePath));
        curl_setopt($ch,CURLOPT_PUT,true);

        curl_exec($ch);
        curl_close($ch);
        return $this;
    }

В данно случае можно было бы использовать для отправки файла одну из функций `file_get_contents` или `file_put_contents` но это не целесообразно, по причине того что пришлось бы, в контексте данных функций, в ручную имитировать заголовки и другие вытекающие из этого проблемы, так что проще просто использовать для этих целей curl.

И так файл загружен, остаёться только опубликовать его и получить прямую ссылку для просмотра, это выполняет функция publicateFile():

    /**
     * public file and get public url for screenshot
     * @return mixed
     */
    public function publicateFile()
    {
        /**
         * https://cloud-api.yandex.net/v1/disk/resources/publish ?
         * path=<путь к публикуемому ресурсу>
         */
        $link = 'https://cloud-api.yandex.net/v1/disk/resources/publish?path='.urlencode('/'.trim($this->_fileName,'/'));
        $response = file_get_contents($link,false,$this->_context('PUT'));
        $responseAsArray = json_decode($response,true);
        $publicateFile = file_get_contents($responseAsArray['href'],false,$this->_context($responseAsArray['method']));
        $publicateFileAsArray = json_decode($publicateFile,true);
        return $publicateFileAsArray;
    }

В этом методе тоже всё довольно просто, мы запрашиваем публикацию файла методом PUT у api, яндекс возвращает ссылку, на которую мы должны выполнить запрос для подтвержедения публикации, и метод запроса в диску. И в конце концов, после второго запроса мы получаем массив который соддержит ссылку на публичный файл.

Установка скрипта


Чтож мы закончили, теперь нам предстоит придумать а как же этот скрипт будет работать из консоли? как его запустить там в «глобальной области»? ответом на эти вопросы будет phar — архив содержащий файлы php и способный выполняться как отдельное приложение, похож на тот же jar.

phar мы будем собирать с помощью утилиты box.phar для этого мы пишем простой конфигурационный файл box.json:

{
    "files": [
	"classis/autoload.php",
        "classis/Request.php",
	"screen.php"
    ],
    "main": "screen.php",
    "output": "srcphp.phar",
    "stub": true
}

Для сборки запускаем:

$ php box.phar build

И наш проект готов теперь осталось только установить права на исполнение файла, и скопировать в директорию /usr/bin/srcphp:

$ chmod +x srcphp.phar
$ cp srcphp.phar /usr/bin/srcphp

Не забываем о конфигурации файла /home/myname/.config/srcphp/config.php:

<?php 
// copy this file to /home/user/.config/srcphp/config.php
return array(
    'token' => 'your token'
);

в token необходимо вписать полученный oAuth токен от яндекса, при переходе сгенерированной по средством запуска скрипта с ключом --getToken:

$ srcphp --getToken

Выводы


Главная мысль статьи — рассмотреть как создать консольное приложение с помощью php, на примере программы скриншотера, в следующих я буду поднимать тему использования php в различных сферах применения, и следующая статью будет посвящена разработки простого драйвера usb устройства для linux. Спасибо за внимание и доброго дня.

p.s. Исходный код приложения
Поделиться с друзьями
-->

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


  1. PaulZi
    22.09.2016 14:56

    А ещё есть функция gd `imagegrabscreen()`, что позволит избавиться от одного системного вызова.
    Согласен с автором — PHP давно перестал течь, а с 7 версии и производительность подтянули, так что вполне успешно может использоваться вне рамок веба.


    1. Delphinum
      22.09.2016 14:59

      imagegrabscreen только для Windows


      1. lnroma
        22.09.2016 15:02

        Сейчас протестирую на linux, да она не работает в linux. Но он захватывает весь экран я так понимаю. А scrot даёт выбрать область захвата мышкой.


        1. Delphinum
          22.09.2016 15:04

          Так в манах черным по белому:
          Note:

          This function is only available on Windows


  1. Pingvi
    22.09.2016 15:22

    Класс Request, метод getOauthLink – у Вас в коде зашит client_id. Неплохо бы его вынести в конфигурационный файл.


    1. lnroma
      22.09.2016 15:35
      -1

      Из начально было так, но из за соображения того что есть лишний какой то пункт в конфиге, который навряд ли кто то будет заполнять, я решил его просто прописать(хотя да некий хардкод, но он не так принципиален).


      1. Pingvi
        22.09.2016 19:00

        По Вашей логике и токен не нужен в конфигурационном файле. Вообще печально, что Вы так думаете…


        1. lnroma
          22.09.2016 19:09

          Токен как раз в конфигурации находиться, так как он меняется от клиента к клиенту, я опять повторяюсь вынести можно и может даже нужно. Но логика была проста скачав проэкт, пользователь получает токен пишет его в конфиг и копирует конфиг в домашнюю директорию, в другом случае пользователю надо сначала скопировать только потом получить токен, и позже вставить его в конфиг. Что добавляет ещё один не очевидный шаг для инсталяции.


  1. barker
    22.09.2016 16:57
    +1

    Многие люди считают что php подходит только для разработки сайтиков, и никак не может быть использован, в других областях применения языков программирования, для создания программ
    Ну, вообще-то между «подходит» и «может быть использован» огромная пропасть для адекватных людей, стоящих перед выбором инструмента. Любой язык (с несущественными оговорками) можно использовать для решения любой задачи. А вот «подходит» — это отдельный разговор совсем.


  1. KorP
    22.09.2016 17:04

    Вы мне только объясните пожалуйста — а почему был выбран именно php? Тем более что для непосредственного снятия скриншота вообще системная утилита вызывается… или вы либу для работы с апи яндекса только под php нашли?
    Я не против php, у меня у самого проект на нём написан, ибо когда начинал, кроме него вообще ничего не мог и не знал, а сейчас переписывать всё это уже не решусь, по этому мне просто интересны аргументы выбора языка, ибо скрипт из 20 строк такой, можно написать на любом ЯП с помощью гугла за 15 минут…


    1. lnroma
      22.09.2016 17:12

      Можно и какой то подойдёт безусловно лучше чем php. Но в этом то и суть моей статьи что всё таки php можно рассматривать и с точки зрения «системного программирования» (тут я имею ввиду системных скриптов и приложений) а не только как ЯП для создания сайтов.


      1. zzzmmtt
        22.09.2016 17:27
        +1

        Пользовательские скрипты != системное программирование.
        И да, в данном случае это именно пользовательский скрипт.


        1. lnroma
          22.09.2016 17:33

          Да замечание верное на счёт терминологии, постараюсь учесть в будущем… Спасибо.


  1. lexore
    22.09.2016 18:22
    +5

    Мне вспоминается притча Мастер Фу и десять тысяч строк кода.
    На bash это можно сделать вот так:


    AUTH='USER:PASSWORD'
    FILE=$(date +'%Y_%m_%d_%H_%M_%S_screen.png')
    XML='<propertyupdate xmlns="DAV:"><set><prop><public_url xmlns="urn:yandex:disk:meta">true</public_url></prop></set></propertyupdate>'
    
    scrot -s "/tmp/$FILE"
    curl -s --user "$AUTH" -T "/tmp/$FILE" -X PUT "https://webdav.yandex.ru/"
    curl -s --user "$AUTH" -d "$XML" -X PROPPATCH "https://webdav.yandex.ru/$FILE" | grep -Eo 'https://yadi.sk/[^<]+'
    rm -f "/tmp/$FILE"


    1. lnroma
      22.09.2016 18:52

      Да и он даже больше для этого подходит. Но цель статьи показать на живом примере, что php можно так же использовать для пользовательских скриптов.


      1. lexore
        22.09.2016 19:23

        Конечно, можно. Но зачем? Позвольте, пояснить свою мысль.


        Многие люди считают что php подходит только для разработки сайтиков, и никак не может быть использован, в других областях применения языков программирования, для создания программ…

        Я думаю, те самые "многие люди", сами того не желая, ввели вас в заблуждение, говоря так категорично.
        Имхо, на любом языке программирования можно решить любую задачу, которую обычно решают на другом языке. Язык программирования — это просто набор инструкций, команд, которые нужно выполнить. Естественно, составить инструкцию можно на любом языке.
        Просто обычно смотрят на себестоимость решения задачи на том или ином языке. Под себестоимостью я имею в виду удобство, скорость и простоту написания кода, получившийся объем этого кода. Сюда же можно добавить и скорость выполнения написанного кода.
        На php можно написать что угодно, но во что это обойдется…
        Демон на php может упасть или начать есть память — нужно написать кучу оберток и следить за этим (и перезапускать его). Для драйверов код на php слишком медленный и жрущий память. Для системных скриптов — слишком много кода на единицу функциональности.
        В итоге, если использовать язык вне его ниши, можно больше времени тратить на борьбу с языком и изобретение костылесипедов, чем на создание функциональности. Я думаю, именно это и имели в виду те люди, говоря "никак не может быть использован". Это как "нельзя тыкать шпилькой в розетку" — тыкать-то можно, просто результат вам не понравится.


        1. zzzmmtt
          23.09.2016 09:44

          Плюс шелл-скрипты — набор системных команд, как правило. А PHP-скрипт — лишний слой, которому ещё и интерпретатор нужен (память, время на обработку), перед тем, как запустить системные же команды/приложения. Можно использовать php таким образом, но нет смысла. На вопрос «Зачем?» ответ может быть в данном случае только один — «Потому что можем».


        1. onotole5g
          23.09.2016 10:47

          По-моему, для таких системых задач, как создание скриншотов и выгрузки их на Яндекс.Диск — php идеален.


    1. kalobyte
      23.09.2016 02:12

      вот я тоже читал и думал: может быть автор круче меня и у него есть какая-то фишка, которая делает пхп круче для решения этой задачи, нежели баш + курл
      я как бы не программист и стараюсь не вякать в таких темах и послушать умных людей или сам автор может быть умный, ведь все программисты умные и все такое…

      б-же, никак не избавлюсь от этого комплекса неполноценности, но твой пост дает мне надежду на это
      оказывается не все программисты умные и наверное я не хуже крутых программистов даже (только почему я не умею писать код?)

      вобщем если бы мне надо было залить картинку на яндыкс, я бы заюзал баш и курл

      сейчас мне надо брать гугл календарь за неделю и постить записи в пейсбук
      для этого у гугла ессть целая пхп библиотека аж на 25 мегабайт и она даже работает

      для получения списка записей в календаре надо просто сделать гет запрос
      GET https://www.googleapis.com/calendar/v3/calendars/ид-календаря/events?key={YOUR_API_KEY}
      получаем

      200 OK

      — Show headers —
      {
      «kind»: «calendar#events»,
      «etag»: "\«p338ejguvkehcu0g\»",
      «summary»: «kalobyte»,
      «updated»: «2016-09-22T05:50:07.906Z»,
      «timeZone»: «Europe/Berlin»,
      «accessRole»: «owner»,
      «defaultReminders»: [
      {
      «method»: «popup»,
      «minutes»: 30
      }
      ],
      «nextSyncToken»: «CNDpw9-jos8CENDpw9-jos8CGAU=»,
      «items»: [
      {

      «kind»: «calendar#event»,
      «etag»: "\«2946678667918000\»",
      «id»: «6uvg9e46eaaenh7hnctojt2d9o»,
      «status»: «confirmed»,
      «htmlLink»: «https://www.google.com/calendar/event?eid=NnV2ZzllNDZlYWFlbmg3aG5jdG9qdDJkOW8ga2Fsb2J5dGVAaW5ib3gucnU»,
      «created»: «2016-09-08T12:55:33.000Z»,
      «updated»: «2016-09-08T12:55:33.959Z»,
      «summary»: «aaa»,
      «creator»: {
      «email»: "",
      «self»: true
      },
      «organizer»: {
      «email»: "",
      «self»: true
      },
      «start»: {
      «dateTime»: «2016-09-08T09:30:00+02:00»
      },
      «end»: {
      «dateTime»: «2016-09-08T15:00:00+02:00»
      },
      «iCalUID»: «6uvg9e46eaaenh7hnctojt2d9o@google.com»,
      «sequence»: 0,
      «guestsCanInviteOthers»: false,
      «guestsCanSeeOtherGuests»: false,
      «reminders»: {
      «useDefault»: true
      }
      }
      ]
      }

      проще поди было бы вызвать гет запрос обычной пхп функцией, там ведь должно что-то быть
      потом распарсить джейсона и готовы данные
      но в пхп библиотеке надо для этого еще кучу всего написать и почему-то нет автодополнения синтаксиса методов и полей из этой библиотеки в пхп шторме

      на ковыряние и работу я убил 2 недели, потому что мануал библиотеки и примеров не соответствовал апи и вываливалась ошибка 404 и что там не найдено — непонятно и мануал молчит
      чисто интуитивно поменял ид календаря с primary на конкретный ид и все заработало

      теперь мне осталось разобраться с апи пейсбука и может быть там тоже есть библиотека пхп


      1. lnroma
        23.09.2016 11:02

        Ну если это GET запрос можно обойтись одной строчкой на php

        $result = json_decode(file_get_contents('https://www.googleapis.com/calendar/v3/calendars/ид-календаря/events?key={YOUR_API_KEY}'));
        


        Но вот на bash это уже сложнее http://stackoverflow.com/questions/1955505/parsing-json-with-unix-tools.


    1. POPSuL
      23.09.2016 08:15

      Я к середине статьи тоже задумался о том, мол «блин, да это же можно сделать скриптом из 10 строк на баше, причем тут PHP?».


      На PHP, в общем-то, можно и GUI-приложения лепить, но зачем?