
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
------------- ------------------------------------- ---------------------