Web Development: Aplikacja serwerowa do wysyłki powiadomień push z FCM [część 2]

in #polish6 years ago (edited)

email-marketing-2362038_640.png

Czego nauczę się w tym poradniku?

  • Jak stworzyć rejestr dla wysłanych wiadomości push
  • Jak wyświetlić historię wiadomości push wysłanych na dane urządzenie mobilne
  • Jak wysłać wiadomość do kilku urządzeń jednocześnie

Wymagania

  • System z rodziny UNIX/Linux
  • Serwer Apache2 z PHP w wersji 7.1
  • Baza danych MySQL
  • Projekt wykonany w framework'u Symfony
  • Dowolny edytor tekstowy
  • Istniejący projekt w Firebase
  • Narzędzie composer

Poziom trudności

  • Średni

Treść poradnika

Ten poradnik, jest kolejnym z serii Web Development. Poprzednio opisany został proces implementacji aplikacji konsolowej do zarządzania wysyłką powiadomień push do aplikacji mobilnych.

W dzisiejszym artykule, do istniejących już funkcjonalności, zostaną dopisane nowe, rozszerzające możliwości aplikacji o bardziej szczegółową obsługę wiadomości, wyświetlanie historii wysłanych wiadomości i zdolność do wysyłki wiadomości do kilku urządzeń mobilnych jednocześnie.

Informacja: Proces tworzenia modułu PtrioMessageBundle został opisany szczegółowo w poprzednim artykule z serii Web Development. Jeżeli nie posiadasz kodu źródłowego, dowiedz się jak go utworzyć lub pobierz przykładowy projekt z repozytorium git.

Jak utworzyć usługę do zarządzania wiadomościami?

Usługa managera wiadomości pozwoli na:
  • przechowywanie wiadomości w bazie danych
  • wyświetlenie histori wiadomości wysłanych do urządzenia

Informacja: Na początku należy wykonać deklaracje dla obiektów przechowujących dane na temat wiadomości.

Klasa wiadomości

Każda wiadomość posiadać będzie cztery parametry: identyfikator, treść, odbiorcę oraz datę wysyłki wiadomości.

Odbiorcą wiadomości będzie urządzenie mobilne o zdefiniowanej nazwie i tokenie FCM.

Warstwa abstrakcyjna

Zacznijmy od interfejsu. W katalogu src/Ptrio/MessageBundle/Model należy utworzyć plik o nazwie MessageInterface.php.

<?php
// src/Ptrio/MessageBundle/Model/MessageInterface.php
namespace App\Ptrio\MessageBundle\Model;
interface MessageInterface
{
    public function getId(): int;
    public function setBody(string $body);
    public function getBody(): string;
    public function setDevice(DeviceInterface $device);
    public function getDevice(): DeviceInterface;
    public function setSentAt(\DateTime $sentAt);    
    public function getSentAt(): ?\DateTime;
}

W kolejnym kroku, w tym samym katalogu powinna zostać utworzona klasa abstrakcyjna Message (plik Message.php), implementująca interfejs MessageInterface.

<?php
// src/Ptrio/MessageBundle/Model/Message.php
namespace App\Ptrio\MessageBundle\Model;
abstract class Message implements MessageInterface
{
    /**
     * @var int
     */
    protected $id;
    /**
     * @var string
     */
    protected $body;
    /**
     * @var DeviceInterface
     */
    protected $device;
    /**
     * @var \DateTime
     */
    protected $sentAt;
    /**
     * @return int
     */
    public function getId(): int
    {
        return $this->id;
    }
    /**
     * @return string
     */
    public function getBody(): string
    {
        return $this->body;
    }
    /**
     * @param string $body
     */
    public function setBody(string $body)
    {
        $this->body = $body;
    }
    /**
     * @return DeviceInterface
     */
    public function getDevice(): DeviceInterface
    {
        return $this->device;
    }
    /**
     * @param DeviceInterface $device
     */
    public function setDevice(DeviceInterface $device)
    {
        $this->device = $device;
    }
    /**
     * @return \DateTime|null
     */
    public function getSentAt(): ?\DateTime
    {
        return $this->sentAt;
    }
    /**
     * @param \DateTime $sentAt
     */
    public function setSentAt(\DateTime $sentAt)
    {
        $this->sentAt = $sentAt;
    }
}
Klasa właściwa

Klasa encji wiadomości Message znajdować się będzie w katalogu src/Ptrio/MessageBundle/Entity, a w niej instrukcje potrzebne do utworzenia reprezentacji wiadomości w bazie danych.

<?php
// src/Ptrio/MessageBundle/Entity/Message.php
namespace App\Ptrio\MessageBundle\Entity;
use App\Ptrio\MessageBundle\Model\Message as BaseMessage;
use Doctrine\ORM\Mapping as ORM;
/**
 * @ORM\Entity
 */
class Message extends BaseMessage
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;
    /**
     * @ORM\Column(type="string")
     */
    protected $body;
    /**
     * @ORM\ManyToOne(targetEntity="Device")
     * @ORM\JoinColumn(name="device_id", referencedColumnName="id")
     */
    protected $device;
    /**
     * @ORM\Column(type="date")
     */
    protected $sentAt;
}

Następnie należy wykonać migracje bazy danych, aby zaktualizować strukturę tabel.

Klasa managera wiadomości

Potrzebna jest usługa odpowiedzialna za zarządzanie wiadomościami. Obiekt managera wiadomości będzie mógł utworzyć nową wiadomość i zapisać ją w bazie danych oraz odszukać wiadomości wysłane na konkretne urządzenie mobilne.

Warstwa abstrakcyjna

Plik interfejsu MessageManagerInterface.php należy uzupełnić kodem, który zlokalizowany jest poniżej.

<?php
// src/Ptrio/MessageBundle/Model/MessageManagerInterface.php
namespace App\Ptrio\MessageBundle\Model;
interface MessageManagerInterface
{
    public function createMessage(): MessageInterface;  
    public function updateMessage(MessageInterface $message);    
    public function findMessagesByDevice(DeviceInterface $device): array;
    public function getClass();
}

Następnie należy utworzyć abstrakcyjną klasę MessageManager implementującą interfejs MessageManagerInterface.

<?php
// src/Ptrio/MessageBundle/Model/MessageManager.php
namespace App\Ptrio\MessageBundle\Model;
abstract class MessageManager implements MessageManagerInterface
{
    public function createMessage(): MessageInterface
    {
        $class = $this->getClass();
        return new $class;
    }
}
Klasa właściwa

W katalogu src/Ptrio/MessageBundle/Doctrine należy utworzyć plik o nazwie MessageManager.php.

<?php
// src/Ptrio/MessageBundle/Doctrine/MessageManager.php
namespace App\Ptrio\MessageBundle\Doctrine;
use App\Ptrio\MessageBundle\Model\MessageInterface;
use App\Ptrio\MessageBundle\Model\MessageManager as BaseMessageManager;
use Doctrine\Common\Persistence\ObjectManager;
class MessageManager extends BaseMessageManager
{
    /**
     * @var ObjectManager
     */
    private $objectManager;
    /**
     * @var \Doctrine\Common\Persistence\ObjectRepository
     */
    private $repository;
    /**
     * @var string
     */
    private $class;
    /**
     * MessageManager constructor.
     * @param ObjectManager $objectManager
     * @param string $class
     */
    public function __construct(
        ObjectManager $objectManager,
        string $class
    )
    {
        $this->objectManager = $objectManager;
        $this->repository = $objectManager->getRepository($class);
        $metadata = $objectManager->getClassMetadata($class);
        $this->class = $metadata->getName();
    }
    /**
     * @param MessageInterface $message
     */
    public function updateMessage(MessageInterface $message)
    {
        $this->objectManager->persist($message);
        $this->objectManager->flush();
    }
    /**
     * @return string
     */
    public function getClass()
    {
        return $this->class;
    }
    /**
     * @param DeviceInterface $device
     * @return array
     */
    public function findMessagesByDevice(DeviceInterface $device): array
    {
        return $this->repository->findBy(['device' => $device]);
    }
}
Konfiguracja usługi managera

Do pliku src/Ptrio/MessageBundle/Resources/config/services.yaml należy dodać informacje na temat konfiguracji managera wiadomości.

    # src/Ptrio/MessageBundle/Resources/config/services.yaml
    ptrio_message.message_manager:
        class: 'App\Ptrio\MessageBundle\Doctrine\MessageManager'
        arguments:
            - '@doctrine.orm.entity_manager'
            - 'App\Ptrio\MessageBundle\Entity\Message'

Implementacja usługi do wyświetlania historii wiadomości

Aktualizacja klasy do wysyłki wiadomości

Przed rozpoczęciem implementacji usługi należy zaktualizować klasę SendMessageCommand. Klasa musi zostać dostosowana tak, aby podczas wysyłki wiadomości były one też rejestrowane w bazie danych. W tym celu zostanie wykorzystana usługa managera wiadomości.

Wewnątrz klasy należy dodać deklarację dla zmiennej dla obiektu typu MessageManagerInterface.

    // other class scope variables
    private $messageManager;

Do argumentów konstruktora należy dodać MessageManagerInterface $messageManager, a następnie upewnić się, że obiekt managera wiadomości zapisywany jest do zmiennej $this->messageManager.

    public function __construct(
        ClientInterface $client,
        DeviceManagerInterface $deviceManager,
        MessageManagerInterface $messageManager // add this argument
    )
    {
        $this->client = $client;
        $this->deviceManager = $deviceManager;
        $this->messageManager = $messageManager; // add this line
        parent::__construct();
    }

W metodzie execute(InputInterface $input, OutputInterface $output), wewnątrz warunku if ($device = $this->deviceManager->findDeviceByName($recipient)) należy dodać polecenia odpowiedzialne za umieszczenie wpisu o wiadomości w bazie danych.

            $message = $this->messageManager->createMessage();
            $message->setBody($messageBody);
            $message->setDevice($device);
            $message->setSentAt(new \DateTime('now'));
            $this->messageManager->updateMessage($message);
Aktualizacja pliku services.yaml

Należy zaktualizować konfigurację dla usługi komendy ptrio_message.send_message_command, dodając usługę @ptrio_message.message_manager jako argument.

    ptrio_message.send_message_command:
        class: 'App\Ptrio\MessageBundle\Command\SendMessageCommand'
        arguments:
            - '@ptrio_message.firebase_client'
            - '@ptrio_message.device_manager'
            - '@ptrio_message.message_manager' # add this argument
        tags:
            - { name: 'console.command' }
Utworzenie klasy komendy do wyświetlania historii wiadomości

Obiekt klasy ListDeviceMessagesCommand będzie odpowiedzialny za odnalezienie wpisów dla danego urządzenia mobilnego i wyświetlenie wyniku w postaci tabeli.

<?php
// src/Ptrio/MessageBundle/Command/ListDeviceMessagesCommand.php
namespace App\Ptrio\MessageBundle\Command;
use App\Ptrio\MessageBundle\Model\DeviceManagerInterface;
use App\Ptrio\MessageBundle\Model\MessageInterface;
use App\Ptrio\MessageBundle\Model\MessageManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class ListDeviceMessagesCommand extends Command
{
    private $messageManager;
    private $deviceManager;
    public static $defaultName = 'ptrio:message:list-device-messages';
    public function __construct(
        MessageManagerInterface $messageManager,
        DeviceManagerInterface $deviceManager
    )
    {
        $this->messageManager = $messageManager;
        $this->deviceManager = $deviceManager;
        parent::__construct();
    }
    protected function configure()
    {
        $this->setDefinition([
            new InputArgument('device-name', InputArgument::REQUIRED),
        ]);
    }
    public function execute(InputInterface $input, OutputInterface $output)
    {
        $deviceName = $input->getArgument('device-name');
        if ($device = $this->deviceManager->findDeviceByName($deviceName)) {
            $deviceMessages = $this->messageManager->findMessagesByDevice($device);
            $io = new SymfonyStyle($input, $output);
            $tableHeader = ['Device Name', 'Message Body', 'Sent At'];
            $tableBody = [];
            foreach ($deviceMessages as $message) {
                /** @var MessageInterface $message */
                $tableBody[] = [$message->getDevice()->getName(), $message->getBody(), $message->getSentAt()->format('Y-m-d H:i')];
            }
            $io->table($tableHeader, $tableBody);
        }
    }
}
Aktualizacja pliku konfiguracji usług

Dodatkowo do pliku services.yaml należy dodać konfigurację dla usługi ListDeviceMessagesCommand.

    # src/Ptrio/MessageBundle/Resources/config/services.yaml
    ptrio_message.list_device_messages_command:
        class: 'App\Ptrio\MessageBundle\Command\ListDeviceMessagesCommand'
        arguments:
            - '@ptrio_message.message_manager'
            - '@ptrio_message.device_manager'
        tags:
            - { name: 'console.command' }

Jak wyświetlić historię wiadomości push wysłanych na dane urządzenie mobilne?

Wiadomości można wyświetlić za pomocą polecenia ptrio:message:list-device-messages iphone-piotr, gdzie przekazywany argument to nazwa urządzenia mobilnego.

$ php bin/console ptrio:message:list-device-messages iphone-piotr

Przykładowa lista wiadomości jest widoczna poniżej.

 -------------- ----------------------------------- --------------------- 
  Device Name    Message Body                        Sent At              
 -------------- ----------------------------------- --------------------- 
  iphone-piotr   Remember, the meeting is at 10am!   2018-03-13 23:59  
  iphone-piotr   Open your mail!                     2018-03-13 00:09  
  iphone-piotr   Are you not forgetting something?   2018-03-13 00:17 
 -------------- ----------------------------------- ---------------------

Informacja: Należy wysłać kilka testowych wiadomości za pomocą polecenia ptrio:message:send-message aby wpisy pojawiły się w rejestrze.

Jak wysłać wiadomość do kilku urządzeń jednocześnie?

Aby dodać funkcjonalność wysyłki wiadomości push do kilku urządzeń jednocześnie, koniecznym jest rozszerzenie usługi managera urządzeń o możliwość wyszukiwania wielu urządzeń w bazie. Następnie zostanie zaktualizowana klasa SendMessageCommand aby przyjmować tablicę elementów jako nazwy urządzeń.

Rozbudowa usługi managera urządzeń

Należy rozpocząć od utworzenia katalogu src/Ptrio/MessageBundle/Repository.

Utworzenie repozytorium

Plik interfejsu DeviceRepositoryInterface.php powinien zostać dodany do poprzednio utworzonego katalogu.

<?php
// src/Ptrio/MessageBundle/Repository/DeviceRepositoryInterface.php
namespace App\Ptrio\MessageBundle\Repository;
use Doctrine\Common\Persistence\ObjectRepository;
interface DeviceRepositoryInterface extends ObjectRepository
{
    public function findDevicesByNames(array $deviceNames): array;
}

Następny krok to utworzenie pliku klasy repozytorium DeviceRepository.php.

<?php
// src/Ptrio/MessageBundle/Repository/DeviceRepository.php
namespace App\Ptrio\MessageBundle\Repository;
use Doctrine\ORM\{
    EntityManagerInterface, EntityRepository
};
class DeviceRepository extends
    EntityRepository implements
    DeviceRepositoryInterface
{
    public function __construct(EntityManagerInterface $em, string $class)
    {
        parent::__construct($em, $em->getClassMetadata($class));
    }
    public function findDevicesByNames(array $deviceNames): array
    {
        $qb = $this->getEntityManager()->createQueryBuilder();
        $qb
            ->select('d')
            ->from($this->getEntityName(), 'd')
        ;
        for ($i = 0; $i < count($deviceNames); $i++) {
            $qb
                ->orWhere($qb->expr()->eq('d.name', ':param_'.$i))
                ->setParameter(':param_'.$i, $deviceNames[$i])
            ;
        }
        return $qb->getQuery()->getResult();
    }
}

Do pliku src/Ptrio/MessageBundle/Resources/config/services.yaml należy dodać parameter ptrio_message.model.device.class.

# src/Ptrio/MessageBundle/Resources/config/services.yaml
parameters:
    ptrio_message.model.device.class: 'App\Ptrio\MessageBundle\Entity\Device'

W kolejnym kroku wymagane jest dodanie deklaracji dla usługi managera urządzeń.

    # src/Ptrio/MessageBundle/Resources/config/services.yaml
    ptrio_message.device_repository:
        class: 'App\Ptrio\MessageBundle\Repository\DeviceRepository'
        arguments:
            - '@doctrine.orm.entity_manager'
            - '%ptrio_message.model.device.class%'

Następnie wartość App\Ptrio\MessageBundle\Entity\Device w argumentach usługi ptrio_message.device_manager powinna zostać zaktualizowana na wartość odpowiadającą wcześniej utworzonemu parametrowi ptrio_message.model.device.class.

    ptrio_message.device_manager:
        class: 'App\Ptrio\MessageBundle\Doctrine\DeviceManager'
        arguments:
            - '@doctrine.orm.entity_manager'
            - '%ptrio_message.model.device.class%' # replace this line
            - '@ptrio_message.device_repository'
Aktualizacja usługi managera urządzeń

Do interfejsu DeviceManagerInterface powinna zostać deklaracja metody findDevicesByNames(array $deviceNames).

    public function findDevicesByNames(array $deviceNames);

Do konstruktora klasy DeviceManager należy dodać argument DeviceRepositoryInterface $repository.

    public function __construct(
        // other arguments
        DeviceRepositoryInterface $repository
    )

Wewnątrz metody konstruktora konieczna jest zamiana pozycji $objectManager->getRepository($class); na $this->repository = $repository;

    public function __construct(
        ObjectManager $objectManager,
        string $class,
        DeviceRepositoryInterface $repository
    )
    {
        $this->objectManager = $objectManager;
        $this->repository = $repository; // this line has changed
        $metadata = $objectManager->getClassMetadata($class);
        $this->class = $metadata->getName();
    }

Następnie do klasy należy dodać metodę findDevicesByNames(array $deviceNames).

    /**
     * @param array $deviceNames
     * @return array
     */
    public function findDevicesByNames(array $deviceNames)
    {
        return $this->repository->findDevicesByNames($deviceNames);
    }

Na koniec, wymagana jest aktualizacja pliku src/Ptrio/MessageBundle/Resources/config/services.yaml. Należy dodać @ptrio_message.device_repository do listy argumentów usługi ptrio_message.device_manager.

     # src/Ptrio/MessageBundle/Resources/config/services.yaml
    ptrio_message.device_manager:
        class: 'App\Ptrio\MessageBundle\Doctrine\DeviceManager'
        arguments:
            - '@doctrine.orm.entity_manager'
            - '%ptrio_message.model.device.class%'
            - '@ptrio_message.device_repository' # add this line
Aktualizacja klasy SendMessageCommand

Argument recipient należy zamienić na device-names. Dodatkowo konieczna jest zmiana typu argumentu na InputArgument::IS_ARRAY.

    protected function configure()
    {
        $this
            ->setDefinition([
            // other arguments
            new InputArgument('device-names', InputArgument::IS_ARRAY), // replaced `recipient` with `device-names`
        ]);
    }

Zawartość metody execute(InputInterface $input, OutputInterface $output) należy zaktualizować. Ze względu na fakt, że komenda ptrio:message:send-message przyjmuje teraz listę urządzeń w argumencie device-names, do obsłużenia polecenia wysyłki zostanie wykorzystana metoda managera urządzeń $this->deviceManager->findDevicesByNames(array $deviceNames).

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $messageBody = $input->getArgument('body');
        $deviceNames = $input->getArgument('device-names');
        $devices = $this->deviceManager->findDevicesByNames($deviceNames);
        foreach ($devices as $device) {
            /** @var DeviceInterface $device */
            $message = $this->messageManager->createMessage();
            $message->setBody($messageBody);
            $message->setDevice($device);
            $message->setSentAt(new \DateTime('now'));
            $this->messageManager->updateMessage($message);
            $response = $this->client->sendMessage($messageBody, $device->getToken());
            $output->writeln('Message successfully sent do device `'.$device->getName().'`.');
            $output->writeln('Response: '.$response);
        }
    }
Wysyłka wiadomości do kilku urządzeń jednocześnie

Wiadomość można wysłać do wybranych urządzeń za pomocą polecenia piotr:message:send-message 'My Message text' iphone-piotr redmi-ewa, gdzie pierwszy argument to treść wiadomości, a pozostałe dwa to nazwy urządzeń w bazie danych.

$ php bin/console piotr:message:send-message 'Hi all, team meeting in 15 minutes!' iphone-piotr redmi-ewa

Dla każdego zapytania do FCM zwracana jest odpowiedź z serwera. Rezultat i dodatkowe informacje są wyświetlane w narzędziu terminal.

Message successfully sent do device `iphone-piotr`.
Response: {"multicast_id":7721001967451123181,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"0:1521029321425249%7167cd087167cd08"}]}
Message successfully sent do device `redmi-ewa`.
Response: {"multicast_id":5554868321735047816,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"0:1521029322087330%7167cd087167cd08"}]}

Historię wiadomości można przejrzeć ponownie, aby zobaczyć dodane wpisy.

php bin/console piotr:message:list-device-messages iphone-piotr

Historia wiadomości wysłanych na urządzenie iphone-piotr.

 -------------- ------------------------------------- --------------------- 
  Device Name    Message Body                          Sent At              
 -------------- ------------------------------------- --------------------- 
  iphone-piotr   Remember, the meeting is at 10am!   2018-03-13 23:59  
  iphone-piotr   Open your mail!                     2018-03-13 00:09  
  iphone-piotr   Are you not forgetting something?   2018-03-13 00:17 
  iphone-piotr   Hi all, team meeting in 15 minutes! 2018-03-14 13:33 
 -------------- ------------------------------------- --------------------- 

Historia wiadomości wysłanych na urządzenie redmi-ewa.

 ------------- ------------------------------------- --------------------- 
  Device Name   Message Body                          Sent At              
 ------------- ------------------------------------- --------------------- 
  xiaomi-ewa    Hi all, team meeting in 15 minutes!   2018-03-14 13:33
 ------------- ------------------------------------- --------------------- 

Curriculum

Sort:  

Congratulations! This post has been upvoted from the communal account, @minnowsupport, by piotr42 from the Minnow Support Project. It's a witness project run by aggroed, ausbitbank, teamsteem, theprophet0, someguy123, neoxian, followbtcnews, and netuoso. The goal is to help Steemit grow by supporting Minnows. Please find us at the Peace, Abundance, and Liberty Network (PALnet) Discord Channel. It's a completely public and open space to all members of the Steemit community who voluntarily choose to be there.

If you would like to delegate to the Minnow Support Project you can do so by clicking on the following links: 50SP, 100SP, 250SP, 500SP, 1000SP, 5000SP.
Be sure to leave at least 50SP undelegated on your account.

Coin Marketplace

STEEM 0.29
TRX 0.12
JST 0.032
BTC 63510.21
ETH 3068.27
USDT 1.00
SBD 3.81