Часть 1

На вашем компьютере потребуется следующее: Xcode, Laravel CLI, SQLite и Cocoapods. Знакомство с Xcode IDE будет полезно.

Вступление

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

В этой статье мы создадим приложение, которое будет отслеживать изменения на рынке криптовалют. Приложение будет ориентировано на BTC и ETH и позволит пользователям приложения устанавливать минимальные и максимальные суммы, когда они хотят получать уведомления о текущей цене монеты. Приложение будет построено с использованием Swift, Laravel, Pusher Channels и Pusher Beams.

Предпосылки

Для выполнения вам потребуются следующие требования:

Что мы будем строить

Мы начнем с создания серверной части приложения с помощью Laravel. Затем мы создадим приложение для iOS с помощью Swift. Если вы хотите протестировать push-уведомления, вам нужно будет запустить приложение на живом устройстве.

Как будет работать клиентское приложение

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

В приложении вы сможете установить минимальное и максимальное изменение цены, когда вы хотите получать оповещения. Например, вы можете настроить приложение для отправки push-уведомления приложению, когда цена одного эфириума (ETH) опускается ниже 500 долларов. Вы также можете настроить приложение для получения уведомления, когда цена биткойнов превышает 5000 долларов.

Как будет работать серверное приложение

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

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

В производственной среде мы установим планировщик как cronjob, но, поскольку мы находимся в разработке, мы вручную запустим команду, чтобы инициировать изменение цен.

Как будет выглядеть приложение

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

Давайте начнем.

Настройка толкающих балок и каналов

Настройка каналов толкателя

Авторизуйтесь в Личном кабинете Pusher. Если у вас нет учетной записи, создайте ее. Ваша панель управления должна выглядеть так:

Создайте новое приложение "Каналы". Вы можете легко сделать это, нажав большую карточку Создать новое приложение каналов в правом нижнем углу. Когда вы создаете новое приложение, вам предоставляются ключи. Держите их в безопасности, так как они вам скоро понадобятся.

Настройка толкающих балок

Затем войдите в новую панель управления Pusher, здесь мы создадим экземпляр Pusher Beams. Вам следует зарегистрироваться, если у вас еще нет учетной записи. Нажмите кнопку Балки на боковой панели, затем нажмите Создать. Появится всплывающее окно с надписью Создать новый экземпляр балки. Назовите его cryptoalat.

Как только вы создадите экземпляр, вам будет представлено краткое руководство. Выберите быстрый запуск IOS и следуйте инструкциям мастера.

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

Настройка серверного приложения

В вашем терминале выполните команду ниже, чтобы создать новый проект Laravel:

$ laravel new cryptoapi

Эта команда создаст новый проект Laravel и установит все необходимые зависимости Laravel.

Затем давайте установим некоторые зависимости проекта. Откройте файл composer.json и в свойстве require добавьте следующие зависимости:

// File: composer.json
    "require": {
        [...]
        "neo/pusher-beams": "^1.0",
        "pusher/pusher-php-server": "~3.0"
    },

Теперь запустите команду ниже, чтобы установить эти зависимости.

$ composer update

Когда установка будет завершена, откройте проект в любом текстовом редакторе. Visual Studio Code довольно приятный.

Настройка нашей библиотеки Pusher Beams

Первое, что мы хотим сделать, это настроить библиотеку Pusher Beams, которую мы только что использовали с помощью composer. Для настройки откройте файл .env и добавьте следующие ключи:

PUSHER_BEAMS_SECRET_KEY="PUSHER_BEAMS_SECRET_KEY"
    PUSHER_BEAMS_INSTANCE_ID="PUSHER_BEAMS_INSTANCE_ID"

Вам следует заменить PUSHER_BEAMS_* заполнители ключами, полученными при настройке приложения Beams.

Затем откройте файл config/broadcasting.php и прокрутите его, пока не увидите клавишу connections. Там у вас будут pusher настройки, добавьте следующее в pusher конфигурацию:

'pusher' => [
        // [...]
        'beams' => [
            'secret_key' => env('PUSHER_BEAMS_SECRET_KEY'),
            'instance_id' => env('PUSHER_BEAMS_INSTANCE_ID'),
        ],
    ],

Настройка нашей библиотеки каналов Pusher

Следующим шагом является настройка каналов толкателя. Laravel имеет встроенную поддержку Pusher Channels, поэтому нам не нужно много делать для его настройки.

Откройте файл .env и обновите следующие ключи:

BROADCAST_DRIVER=pusher
    // [...]
    PUSHER_APP_ID="PUSHER_APP_ID"
    PUSHER_APP_KEY="PUSHER_APP_KEY"
    PUSHER_APP_SECRET="PUSHER_APP_SECRET"
    PUSHER_APP_CLUSTER="PUSHER_APP_CLUSTER"

Выше вы установили BROADCAST_DRIVER на pusher, а затем для других PUSHER_APP_* ключей замените заполнители ключами, полученными с панели инструментов Pusher. Это все, что нам нужно сделать, чтобы настроить каналы Pusher для этого приложения.

Создание серверного приложения

Теперь, когда мы настроили все зависимости, мы можем приступить к созданию приложения. Начнем с создания маршрутов. Однако вместо того, чтобы создавать контроллеры для подключения к маршрутам, мы будем добавлять логику непосредственно в маршруты.

Настройка базы данных, миграции и модели

Поскольку мы будем работать с базой данных, нам нужно настроить базу данных, с которой мы собираемся работать. Чтобы упростить задачу, мы будем использовать SQLite. Создайте пустой файл database.sqlite в каталоге database.

Откройте файл .env и замените:

DB_CONNECTION=mysql
    DB_HOST=127.0.0.1
    DB_PORT=3306
    DB_DATABASE=homestead
    DB_USERNAME=homestead
    DB_PASSWORD=secret

С участием

DB_CONNECTION=sqlite
    DB_DATABASE=/full/path/to/your/database.sqlite

Затем давайте создадим миграцию для devices таблицы. Мы будем использовать эту таблицу для хранения устройств и их настроек уведомлений. Это поможет нам узнать, на какие устройства отправлять push-уведомления.

Выполните команду ниже, чтобы создать миграцию и модель:

$ php artisan make:model Device -m

Флаг -m укажет мастеру создать миграцию вместе с моделью.

Эта команда сгенерирует два файла: файл миграции в database/migrations и модель в каталоге app. Давайте сначала отредактируем файл миграции.

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

<?php
    use Illuminate\Support\Facades\Schema;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Database\Migrations\Migration;
    class CreateDevicesTable extends Migration
    {
        /**
         * Run the migrations.
         *
         * @return void
         */
        public function up()
        {
            Schema::create('devices', function (Blueprint $table) {
                $table->increments('id');
                $table->string('uuid')->unique();
                $table->float('btc_min_notify')->default(0);
                $table->float('btc_max_notify')->default(0);
                $table->float('eth_min_notify')->default(0);
                $table->float('eth_max_notify')->default(0);
            });
        }
        /**
         * Reverse the migrations.
         *
         * @return void
         */
        public function down()
        {
            Schema::dropIfExists('devices');
        }
    }

В методе up мы определили структуру таблицы devices. У нас есть поле uuid, которое будет уникальной строкой для каждого зарегистрированного устройства. У нас есть два btc_notify поля, в которых можно сохранить минимальную и максимальную цену BTC, после чего устройство должно быть уведомлено. То же самое относится к полям * eth_*_notify.

Чтобы запустить миграцию, выполните следующую команду:

$ php artisan migrate

Откройте модель app/Device.php и замените содержимое приведенным ниже кодом:

<?php
    namespace App;
    use Illuminate\Database\Eloquent\Model;
    use Illuminate\Notifications\Notifiable;
    class Device extends Model
    {
        use Notifiable;
        public $timestamps = false;
        protected $fillable = [
            'uuid', 
            'btc_min_notify', 
            'btc_max_notify', 
            'eth_min_notify', 
            'eth_max_notify',
        ];
        protected $cast = [
            'btc_min_notify' => 'float',
            'btc_max_notify' => 'float',
            'eth_min_notify' => 'float',
            'eth_max_notify' => 'float'
        ];
        public function scopeAffected($query, string $currency, $currentPrice)
        {
            return $query->where(function ($q) use ($currency, $currentPrice) {
                $q->where("${currency}_min_notify", '>', 0)
                  ->where("${currency}_min_notify", '>', $currentPrice);
            })->orWhere(function ($q) use ($currency, $currentPrice) {
                $q->where("${currency}_max_notify", '>', 0)
                  ->where("${currency}_max_notify", '<', $currentPrice);
            });
        }
    }

В модели выше мы установили для свойства $timestamps значение false, чтобы убедиться, что Eloquent не пытается обновить поля created_at и updated_at, что является нормальным поведением.

У нас также есть метод scopeAffected, который является примером красноречивой области видимости. Мы используем это, чтобы получить затронутые устройства после изменения цены валюты. Поэтому, если, например, цена BTC упадет, этот метод проверит устройства и настройки, чтобы увидеть устройства, которые должны быть уведомлены об этом изменении.

Локальные области видимости позволяют вам определять общие наборы ограничений, которые вы можете легко повторно использовать в своем приложении. Например, вам может потребоваться часто получать всех пользователей, которые считаются популярными. Чтобы определить область действия, добавьте к методу модели Eloquent префикс scope. - Документация Laravel.

Мы будем использовать эту область позже в нашем приложении, когда нам нужно знать, на какие устройства отправлять push-уведомления.

Создание маршрутов

Откройте файл routes/api.php и замените его содержимое следующим кодом:

// File: routes/api.php
    <?php
    use App\Device;
    use Illuminate\Http\Request;

Затем давайте добавим первый маршрут. Добавьте приведенный ниже код в файл маршрутов:

// File: routes/api.php
    Route::get('/settings', function (Request $request) {
        return Device::whereUuid($request->query('u'))->firstOrFail()['settings'];
    });

В приведенном выше маршруте мы возвращаем настройки для устройства, указанные в параметре запроса u. Это означает, что если зарегистрированное устройство попадает в конечную точку /settings и передает UUID устройства через параметр u, будут возвращены настройки для этого устройства.

Затем в том же файле маршрутов вставьте следующее внизу файла:

Route::post('/settings', function (Request $request) {
        $settings = $request->validate([
            'btc_min_notify' => 'int|min:0',
            'btc_max_notify' => 'int|min:0',
            'eth_min_notify' => 'int|min:0',
            'eth_max_notify' => 'int|min:0',
        ]);
        $settings = array_filter($settings, function ($value) { return $value > 0; });
        $device = Device::firstOrNew(['uuid' => $request->query('u')]);
        $device->fill($settings);
        $saved = $device->save();
        return response()->json([
            'status' => $saved ? 'success' : 'failure'
        ], $saved ? 200 : 400);
    });

Выше мы определили маршрут для маршрута POST /settings. Этот маршрут сохраняет настройки в базе данных. Он создаст новую запись, если параметр еще не существует, или обновит существующий, если он существует.

Это все, что касается маршрутов.

Создание заданий, событий и уведомителей

Затем нам нужно создать задание Laravel, которое будет запускаться через определенные промежутки времени, чтобы проверять, изменилась ли цена валюты.

Выполните команду ниже, чтобы создать новое задание Laravel:

$ php artisan make:job CheckPrices

Это создаст новый класс CheckPrices в каталоге app. Откройте этот класс и замените его содержимое следующим:

<?php
    namespace App\Jobs;
    use App\Device;
    use Illuminate\Bus\Queueable;
    use Illuminate\Queue\SerializesModels;
    use Illuminate\Queue\InteractsWithQueue;
    use Illuminate\Contracts\Queue\ShouldQueue;
    use Illuminate\Foundation\Bus\Dispatchable;
    use App\Events\CurrencyUpdated;
    use App\Notifications\CoinPriceChanged;
    class CheckPrices implements ShouldQueue
    {
        use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
        protected $supportedCurrencies = ['ETH', 'BTC'];
        /**
         * Execute the job.
         *
         * @return void
         */
        public function handle()
        {
            $payload = $this->getPricesForSupportedCurrencies();
            if (!empty($payload)) {
                $this->triggerPusherUpdate($payload);
                $this->triggerPossiblePushNotification($payload);
            }
        }
        private function triggerPusherUpdate($payload)
        {
            event(new CurrencyUpdated($payload));
        }
        private function triggerPossiblePushNotification($payload)
        {
            foreach ($this->supportedCurrencies as $currency) {
                $currentPrice = $payload[$currency]['current'];
                $currency = strtolower($currency);
                foreach (Device::affected($currency, $currentPrice)->get() as $device) {
                    $device->notify(new CoinPriceChanged($currency, $device, $payload));
                }
            }
        }
        public function getPricesForSupportedCurrencies(): array
        {
            $payload = [];
            foreach ($this->supportedCurrencies as $currency) {
                if (config('app.debug') === true) {
                    $response = [
                        $currency => [
                            'USD' => (float) rand(100, 15000)
                        ]
                    ];
                } else {
                    $url = "https://min-api.cryptocompare.com/data/pricehistorical?fsym={$currency}&tsyms=USD&ts={$timestamp}";
                    $response = json_decode(file_get_contents($url), true);
                }
                if (json_last_error() === JSON_ERROR_NONE) {
                    $currentPrice = $response[$currency]['USD'];
                    $previousPrice = cache()->get("PRICE_${currency}", false);
                    if ($previousPrice == false or $previousPrice !== $currentPrice) {
                        $payload[$currency] = [
                            'current' => $currentPrice,
                            'previous' => $previousPrice,
                        ];
                    }
                    cache()->put("PRICE_${currency}", $currentPrice, (24 * 60 * 60));
                }
            }
            return $payload;
        }
    }

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

У нас есть четыре метода в этом классе. Первый - это метод handle. Он вызывается автоматически при выполнении задания. В этом методе мы получаем цены для доступных валют, а затем проверяем, изменилась ли цена. Если это так, мы публикуем событие Pusher Channel, а затем проверяем, есть ли какие-либо устройства, о которых нужно уведомлять, в зависимости от настроек пользователя. Если они есть, мы отправляем push-уведомление на это устройство.

У нас есть метод triggerPusherUpdate, который запускает событие CurrencyUpdated. Мы создадим это событие в следующем разделе. У нас также есть метод triggerPossiblePushNotification, который получает список устройств, которые должны быть уведомлены об изменении валюты, а затем уведомляет пользователя с помощью класса CoinPriceChanged, который мы создадим в следующем разделе.

Наконец, у нас есть метод getPricesForSupportedCurrencies, который просто извлекает текущую цену валюты. В этом методе у нас есть режим отладки, который имитирует текущую цену валюты.

Чтобы убедиться, что этот класс, который мы только что создали, правильно спланирован, откройте файл app/Console/Kernel.php и в методе schedule добавьте следующий код в метод schedule:

$schedule->job(new \App\Jobs\CheckPrices)->everyMinute();

Теперь каждый раз, когда мы запускаем команду php artisan schedule:run, будут выполняться все задания этого schedule метода. Обычно в производственной среде нам нужно добавить команду schedule как задание cron, однако мы будем запускать эту команду вручную.

Следующее, что нужно сделать, - это создать уведомители и события. В вашем терминале выполните следующие команды:

$ php artisan make:event CurrencyUpdated
    $ php artisan make:notification CoinPriceChanged

Это создаст класс в каталогах Events и Notifications.

В классе event CurrencyUpdated вставьте следующий код:

<?php
    namespace App\Events;
    use Illuminate\Broadcasting\Channel;
    use Illuminate\Queue\SerializesModels;
    use Illuminate\Foundation\Events\Dispatchable;
    use Illuminate\Broadcasting\InteractsWithSockets;
    use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
    class CurrencyUpdated implements ShouldBroadcast
    {
        use Dispatchable, InteractsWithSockets, SerializesModels;
        public $payload;
        public function __construct($payload)
        {
            $this->payload = $payload;
        }
        public function broadcastOn()
        {
            return new Channel('currency-update');
        }
        public function broadcastAs()
        {
            return 'currency.updated';
        }
    }

В приведенном выше классе событий у нас есть метод broadcastOn, который указывает канал Pusher, по которому мы хотим транслировать событие. У нас также есть метод broadcastAs, который определяет имя события, которое мы хотим транслировать на канал.

В классе CoinPriceChanged уведомление замените содержимое следующим кодом:

<?php
    namespace App\Notifications;
    use App\Device;
    use Illuminate\Bus\Queueable;
    use Neo\PusherBeams\PusherBeams;
    use Neo\PusherBeams\PusherMessage;
    use Illuminate\Notifications\Notification;
    class CoinPriceChanged extends Notification
    {
        use Queueable;
        private $currency;
        private $device;
        private $payload;
        public function __construct(string $currency, Device $device, array $payload)
        {
            $this->currency = $currency;
            $this->device = $device;
            $this->payload = $payload;
        }
        public function via($notifiable)
        {
            return [PusherBeams::class];
        }
        public function toPushNotification($notifiable)
        {
            $currentPrice = $this->payload[strtoupper($this->currency)]['current'];
            $previousPrice = $this->payload[strtoupper($this->currency)]['current'];
            $direction = $currentPrice > $previousPrice ? 'climbed' : 'dropped';
            $currentPriceFormatted = number_format($currentPrice);
            return PusherMessage::create()
                    ->iOS()
                    ->sound('success')
                    ->title("Price of {$this->currency} has {$direction}")
                    ->body("The price of {$this->currency} has {$direction} and is now \${$currentPriceFormatted}");
        }
        public function pushNotificationInterest()
        {
            $uuid = strtolower(str_replace('-', '_', $this->device->uuid));
            return "{$uuid}_{$this->currency}_changed";
        }
    }

В приведенном выше классе у нас есть класс toPushNotification, который подготавливает push-уведомление с помощью библиотеки Pusher Beams. У нас также есть метод pushNotificationInterest, который устанавливает имя для интереса push-уведомления в зависимости от валюты и идентификатора устройства.

Это все для бэкэнда, теперь просто запустите команду ниже, чтобы запустить сервер:

$ php artisan serve

Это запустит сервер PHP с запущенным нашим приложением. Также, если вам нужно вручную инициировать изменение валюты, выполните следующую команду:

$ php artisan schedule:run

Теперь, когда мы закончили с бэкэндом, мы можем создать приложение, используя Swift и Xcode.

Заключение

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

Исходный код этого приложения доступен на GitHub.

Эта статья впервые была опубликована для pusher.