Статьи

Сборка хакерских новостей с Lumen

В этом уроке мы собираемся создать читатель для Hacker News. Для этого мы будем использовать Hacker News API и инфраструктуру Lumen .

Окончательный результат выглядит примерно так:

Работающий хакер News Reader

Если вы взволнованы, давайте идти вперед и прыгать прямо в него.

Установка и настройка Lumen

Первое, что вам нужно сделать, это установить Lumen. Вы можете сделать это с помощью следующей команды, где hnreader — это папка, в которую вы хотите установить проект, а --prefer-dist просто ускоряет загрузку необходимых пакетов Composer:

 composer create-project laravel/lumen hnreader --prefer-dist 

Создайте файл .env с содержимым:

 APP_DEBUG=true APP_TITLE=HnReader DB_CONNECTION=mysql DB_HOST=localhost DB_PORT=3306 DB_DATABASE=hnreader DB_USERNAME=homestead DB_PASSWORD=secret 

APP_DEBUG позволяет нам включить отладку в Lumen, чтобы мы могли видеть ошибки в приложении. И DB_* для конфигурации базы данных. Мы будем использовать базу данных MySQL для хранения элементов, которые мы будем получать из Hacker News API. Таким образом, нам не нужно будет делать отдельный HTTP-запрос каждый раз, когда пользователь обращается к приложению. Вероятно, вы просто оставите значения для DB_CONNECTION , DB_HOST , DB_PORT как они есть, если вы используете Homestead Improved . Конечно, нам нужно создать базу данных тоже.

 mysql -u homestead -psecret CREATE DATABASE hnreader; 

bootstrap/app.php файл bootstrap/app.php и раскомментируем следующую строку:

 Dotenv::load(__DIR__.'/../'); 

Эта конкретная строка загружает параметры конфигурации из файла .env созданного ранее.

Также раскомментируйте следующую строку, чтобы вы могли использовать такие фасады, как DB :

 $app->withFacades(); 

База данных

Для этого приложения нам понадобится только одна таблица для хранения элементов, которые мы извлекли из API. Вы можете создать таблицу, создав новую миграцию с помощью следующей команды:

 php artisan make:migration create_items_table 

Это создаст новую миграцию в каталоге database/migrations . Откройте файл и обновите содержимое метода up и down следующим образом:

 public function up() { Schema::create('items', function(Blueprint $table){ $table->integer('id')->primary(); $table->string('title'); $table->text('description'); $table->string('username'); $table->char('item_type', 20); $table->string('url'); $table->integer('time_stamp'); $table->integer('score'); $table->boolean('is_top'); $table->boolean('is_show'); $table->boolean('is_ask'); $table->boolean('is_job'); $table->boolean('is_new'); }); } public function down() { Schema::drop('items'); } 

Метод up создает таблицу элементов . Вот краткое описание каждого из полей:

  • id — уникальный идентификатор элемента, который приходит из API.
  • title — название элемента Это поле, которое мы будем отображать позже на странице новостей.
  • описание — краткое описание товара. Это будет отображаться при наведении на всплывающую подсказку.
  • username — имя пользователя, отправившего элемент в хакерские новости.
  • item_type — тип элемента. Это может быть история или работа .
  • url — URL-адрес, указывающий на полную информацию об элементе. Обычно это веб-сайт добавленного элемента, но он также может быть пустым, и в этом случае полное описание элемента доступно на веб-сайте хакерских новостей.
  • time_stamp — метка времени unix для времени отправки .
  • оценка — текущий рейтинг товара.

Ниже приведены флаги, представляющие, принадлежит ли элемент в топ-сюжеты, показывают HN, спрашивают HN, опубликованы ли вакансии в Hacker News или недавно опубликованы.

  • я остановился
  • is_show
  • is_ask
  • is_job
  • новый

Под методом down мы просто опускаем стол.

 Schema::drop('items'); 

Для запуска миграции используйте следующую команду:

 php artisan migrate 

Это создаст таблицу в базе данных.

Добавление маршрутов

Откройте файл app/routes.php и добавьте следующее:

 $app->get('/{type}', 'HomeController@index'); $app->get('/', 'HomeController@index'); 

Первый маршрут позволяет нам отвечать на запросы для определенного типа предметов. Такие вещи, как работа, новые истории, главные истории и другие. Второй маршрут для домашней страницы.

News Updater

Для добавления или обновления новостей в базе данных мы используем планировщик задач Laravel . Lumen — это в основном облегченная версия Laravel, поэтому планировщик задач доступен и в Lumen. Это позволяет нам обновлять базу данных в определенное время. Например, в 8:00 вечера каждый день.

Для работы с планировщиком задач сначала нужно создать задачу. Вы можете сделать это, создав новый файл в каталоге app/Console/Commands . Назовите файл UpdateNewsItems.php :

 <?php namespace App\Console\Commands; use Illuminate\Console\Command; use DB; use GuzzleHttp\Client; class UpdateNewsItems extends Command { protected $name = 'update:news_items'; public function fire() { $client = new Client(array( 'base_uri' => 'https://hacker-news.firebaseio.com' )); $endpoints = array( 'top' => '/v0/topstories.json', 'ask' => '/v0/askstories.json', 'job' => '/v0/jobstories.json', 'show' => '/v0/showstories.json', 'new' => '/v0/newstories.json' ); foreach($endpoints as $type => $endpoint){ $response = $client->get($endpoint); $result = $response->getBody(); $items = json_decode($result, true); foreach($items as $id){ $item_res = $client->get("/v0/item/" . $id . ".json"); $item_data = json_decode($item_res->getBody(), true); if(!empty($item_data)){ $item = array( 'id' => $id, 'title' => $item_data['title'], 'item_type' => $item_data['type'], 'username' => $item_data['by'], 'score' => $item_data['score'], 'time_stamp' => $item_data['time'], ); $item['is_' . $type] = true; if(!empty($item_data['text'])){ $item['description'] = strip_tags($item_data['text']); } if(!empty($item_data['url'])){ $item['url'] = $item_data['url']; } $db_item = DB::table('items') ->where('id', '=', $id) ->first(); if(empty($db_item)){ DB::table('items')->insert($item); }else{ DB::table('items')->where('id', $id) ->update($item); } } } } return 'ok'; } } 

Этот класс наследуется от класса Command Illuminate, поэтому нам нужно его импортировать.

 use Illuminate\Console\Command; 

Импортируйте класс базы данных и Guzzle, чтобы мы могли работать с базой данных и делать HTTP-запросы с помощью HTTP-клиента Guzzle.

 use DB; use GuzzleHttp\Client; 

Укажите название команды:

 protected $name = 'update:news_items'; 

Это позволяет нам использовать artisan для выполнения этой команды из терминала следующим образом:

 php artisan update:news_items 

Под методом fire создайте новый экземпляр клиента Guzzle и объявите массив, содержащий различные конечные точки в Hacker News API. В случае этого приложения мы будем использовать только конечные точки для главных новостей, спрашивать HN, вакансии, показывать HN и новые истории.

 $client = new Client(array( 'base_uri' => 'https://hacker-news.firebaseio.com' )); $endpoints = array( 'top' => '/v0/topstories.json', 'ask' => '/v0/askstories.json', 'job' => '/v0/jobstories.json', 'show' => '/v0/showstories.json', 'new' => '/v0/newstories.json' ); 

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

 foreach($endpoints as $type => $endpoint){ ... } 

Внутри цикла мы делаем запрос к API Hacker News и преобразуем содержимое тела ответа в массив. Этот массив содержит идентификаторы новостей, которые были возвращены для текущей конечной точки.

 $response = $client->get($endpoint); $result = $response->getBody(); $items = json_decode($result, true); 

Просмотрите все эти идентификаторы и сделайте отдельный запрос к API, чтобы получить больше информации о каждом элементе. Здесь мы используем конечную точку элемента ( /v0/item/{ITEM_ID}.json ). Как только мы получим ответ, мы создадим данные, которые мы будем сохранять в базе данных, на основе данных, которые были возвращены API. Обратите внимание, что мы проверяем, содержит ли ответ какое-либо содержимое. Это связано с тем, что иногда идентификаторы, возвращаемые из определенной конечной точки (например, главных новостей), на самом деле не указывают на фактический элемент.

 foreach($items as $id){ $item_res = $client->get("/v0/item/" . $id . ".json"); $item_data = json_decode($item_res->getBody(), true); if(!empty($item_data)){ $item = array( 'id' => $id, 'title' => $item_data['title'], 'item_type' => $item_data['type'], 'username' => $item_data['by'], 'score' => $item_data['score'], 'time_stamp' => $item_data['time'], ); } ... 

Чтобы иметь возможность легко извлекать элементы на основе маршрута, к которому пользователь обращается позже, мы устанавливаем значение определенного типа в true .

 $item['is_' . $type] = true; 

Установите описание и URL, если они присутствуют в элементе.

 if(!empty($item_data['text'])){ $item['description'] = strip_tags($item_data['text']); } if(!empty($item_data['url'])){ $item['url'] = $item_data['url']; } 

Создайте новую строку для элемента в базе данных, если он еще не существует, и обновите его, если он уже существует.

 $db_item = DB::table('items') ->where('id', '=', $id) ->first(); if(empty($db_item)){ DB::table('items')->insert($item); }else{ DB::table('items')->where('id', $id) ->update($item); } 

Возвратите что-нибудь в конце функции, чтобы сигнализировать, что это где функция заканчивается.

 return 'ok'; 

Теперь, когда мы закончили создание новой задачи, пришло время добавить ее в ядро ​​консоли. Откройте файл app/Console/Kernel.php чтобы сделать это. Под массивом команд добавьте путь к задаче, которую мы только что создали.

 protected $commands = [ 'App\Console\Commands\UpdateNewsItems', ]; 

Под функцией schedule добавьте команду и укажите время, когда она будет выполняться. 19:57 здесь означает, что команда update:news_items должна запускаться каждый день в 19:57 .

 protected function schedule(Schedule $schedule) { $schedule->command('update:news_items')->dailyAt('19:57'); } 

Далее необходимо убедиться, что приложение использует тот же часовой пояс, что и на сервере. Откройте файл .env и добавьте конфигурацию часового пояса:

 APP_TIMEZONE=Asia/Manila 

Просто измените Asia/Manila на действительный часовой пояс PHP, который применяется к вашему серверу Вы можете получить список допустимых часовых поясов на странице Список поддерживаемых часовых поясов .

Наконец, добавьте новый элемент в cron, выполнив следующую команду:

 sudo crontab -e 

И в последней строке добавьте следующее:

 * * * * * php /path/to/hn-reader/artisan schedule:run >> /dev/null 2>&1 

Обязательно измените /path/to/hn-reader/ на фактический путь приложения в вашей файловой системе. Вы можете проверить это, выполнив следующую команду, находясь в корневом каталоге приложения.

 php artisan schedule:run 

Конечно, это не будет иметь эффекта, если время, указанное в планировщике, не совпадает со временем, когда вы запускаете команду, поэтому настройте его для реального теста.

Страница новостей

Для страницы новостей создайте app/Http/controllers/HomeController.php :

 <?php namespace App\Http\Controllers; use Laravel\Lumen\Routing\Controller as BaseController; use DB; class HomeController extends BaseController { private $types; public function __construct(){ $this->types = array( 'top', 'ask', 'job', 'new', 'show' ); } public function index($type = 'top'){ $items = DB::table('items') ->where('is_' . $type, true) ->get(); $page_data = array( 'title' => $type, 'types' => $this->types, 'items' => $items ); return view('home', $page_data); } } 

Внутри класса мы объявляем приватную переменную с именем $types . Здесь мы храним типы элементов, которые можно просмотреть на странице новостей. Обратите внимание, что это те же ключи, которые мы использовали ранее в массиве $endpoints в задаче обновления новостей.

В функции index мы принимаем тип в качестве аргумента и устанавливаем его по умолчанию как top . Таким образом, домашняя страница показывает главные новости по умолчанию.

Затем мы выбираем элементы в зависимости от type который был установлен в true . Для каждого элемента может быть включено более одного типа. Например, публикация задания также может быть одной из главных статей, поэтому она имеет значение true для обоих is_job и is_top в таблице, что означает, что некоторые элементы повторяются на разных страницах.

После того, как мы получили элементы, мы можем теперь добавить их в массив, который будет предоставлен в качестве данных для страницы новостей. Здесь мы также передаем различные типы, чтобы пользователь мог выбрать, какие элементы HN они хотят просмотреть. Название страницы установлено на текущий тип.

 $page_data = array( 'title' => $type, 'types' => $this->types, 'items' => $items ); return view('home', $page_data); 

Представление для отображения новостей ( resources/views/home.blade.php ) содержит следующее:

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{{ env('APP_TITLE') }}</title> <link rel="stylesheet" href="{{ url('assets/css/hint.min.css') }}"> <link rel="stylesheet" href="{{ url('assets/css/style.css') }}"> </head> <body> <div id="sidebar"> <h3>{{ env('APP_TITLE') }}</h3> <ul id="types"> @foreach($types as $type) <li> <a href="/{{ $type }}">{{ ucwords($type) }}</a> </li> @endforeach </ul> </div> <div id="items-container"> <h1>{{ $title }}</h1> <ul id="items"> @foreach($items as $item) <li class="item"> <span class="item-score">{{ $item->score }}</span> <a href="{{ URLHelper::getUrl($item->id, $item->url) }}"> <span class="item-title hint--bottom" data-hint="{{ str_limit(strip_tags($item->description), 160) }}">{{ $item->title }}</span> <span class="item-info">posted {{ \Carbon\Carbon::createFromTimestamp($item->time_stamp)->diffForHumans() }} by {{ $item->username }}</span> </a> </li> @endforeach </ul> </div> </body> </html> 

Мы загружаем APP_TITLE из файла .env ранее, вызывая функцию env .

Затем мы генерируем URL для hint.css и основной таблицы стилей с помощью помощника url . После этого мы перебираем все типы и соответственно форматируем неупорядоченный список.

Следующая часть показывает текущий просматриваемый тип и проходит по всем элементам, которые были извлечены из базы данных. Здесь мы используем специальный вспомогательный класс URLHelper для возврата правильного URL, который ссылается на фактический элемент. Это необходимо, потому что некоторые элементы на самом деле не имеют веб-сайта, поэтому URL будет хакерской новостной страницей, назначенной этому конкретному элементу. Это верно для всех предметов Ask HN . Мы рассмотрим код для этого помощника в ближайшее время. А пока, просто помните, что мы передаем идентификатор и URL-адрес функции getURL этого класса.

За время публикации элемента мы конвертируем метку времени Unix в удобное для человека время, например 4 seconds ago . Это делается с помощью Carbon .

Для описания элемента мы используем hint.css для его отображения. В промежуток, который содержит заголовок элемента, мы добавили класс hint--bottom для управления положением всплывающей подсказки, в то время как data-hint содержит текст описания, который ограничен 160 символами с помощью вспомогательной функции str_limit .

В URLHelper ( app/Helpers/URLHelper.php ) getURL проверяет, является ли URL пустым. Если это не так, то он возвращает URL. В противном случае он возвращает URL HN, указывающий на страницу, назначенную элементу.

 <?php class URLHelper { public static function getURL ( $id , $url = '' ) { if ( ! empty ( $url ) ) { return $url ; } return "https://news.ycombinator.com/item?id={$id}" ; } } 

Прежде чем мы сможем использовать этот помощник, нам нужно сделать еще одну модификацию в composer.json . Под объектом autoload найдите classmap . Это массив, содержащий пути к каталогам, файлы которых загружаются автоматически. Поскольку мы сохранили URLHelper в URLHelper app/Helpers , мы добавляем app/Helpers в массив classmap .

 "autoload": { "psr-4": { "App\\": "app/" }, "classmap": [ "database/", "app/Helpers" ] }, 

Наконец, мы добавляем таблицу стилей ( public/assets/css/style.css ).

 body { font-family: Helvetica Neue, Helvetica, Arial, sans-serif; padding: 0; margin: 0; } h1 { padding-left: 40px; } #sidebar { width: 20%; float: left; background-color: #EBEBEB; position: fixed; height: 100%; } #items-container { width: 80%; float: left; position: relative; margin-left: 20%; background-color: #F7F7F7; } ul li { list-style: none; } #sidebar h3 { border-bottom: 3px solid; padding: 0; padding-left: 30px; } #types li { padding: 10px 30px; } ul#types { padding: 0; } #types li a { text-decoration: none; color: #575757; } #items { padding: 0 20px; } #items li a { text-decoration: none; color: #3A3A3A; display: inline-block; } #items li { padding: 20px; } #items li:hover { background-color: #DFDFDF; } .item-score { font-weight: bold; display: inline-block; width: 50px; border-radius: 50%; background-color: #ccc; height: 50px; text-align: center; line-height: 50px; margin-right: 10px; } .item-info { display: inline-block; width: 100%; font-size: 15px; color: #8A8A8A; margin-top: 5px; } 

Вывод

Это оно! Из этого руководства вы узнали, как работать с Hacker News API для создания программы чтения новостей. В этом руководстве мы использовали только часть API, поэтому обязательно ознакомьтесь с документацией, если хотите использовать больше его функций. Весь исходный код, используемый в этом руководстве, доступен в репозитории Github . Вопросов? Комментарии? Предложения? Оставь их ниже!