Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 2]

Source: pixabay.com. Licensed under CC0 Creative Commons
What will I learn?
- How to make custom queries with an entity repository
- How to define container parameters in a bundle’s service configuration file
- How to pass an array of arguments to a command
Requirements
- UNIX/Linux based OS
- Apache2 server with PHP7 installed
- MySQL database
- Text editor of choice
- Existing Firebase project
- Composer
- Base project found here
Difficulty
- Intermediate
Tutorial contents
This tutorial is a second one in the Web Development series where we jump into details on how to develop applications with Symfony using a sample project capable of sending push notifications to mobile devices. In the previous article the processes of creating an entity with a corresponding entity manager and a command service were briefly described. Also the concept of dependency injection with a service container was laid down.
A sample project used with this tutorial can be found here. It is basically the code you will end up with after completing the previous tutorial. It is recommended to clone the aforementioned project from the given github repository.
What aspects of Symfony web development will be covered in this tutorial?
- The process of extending an entity manager’s capabilities with a custom query service called an entity repository. Doctrine Query Builder will be used to construct a DQL (Doctrine Query Language) query which will allow for fetching results from the database.
- The guidelines to define a container parameters within a bundle’s service configuration file to avoid value hard-coding.
- The process of extending a command service to make it capable of accepting an array of arguments as a command input.
An example usage for the created functionalities will be shown at the end of this tutorial.
How to make custom queries with an entity repository?
A EntityRepository
class object available under ObjectManager::getEntityRepository()
is only capable of performing simple queries such as finding an entity (or multiple entities) by a given property or fetching all entities from the storage.
In this section we are going to extend the EntityRepository
class with a method that will be capable of querying multiple device entities by their names. As mentioned before, it will be possible thanks to Doctrine Query Builder.
A
QueryBuilder
provides an API that is designed for conditionally constructing a Doctrine Query Language query in several steps.
Source: https://doctrine-project.org
What is Doctrine Query Language?
In essence, DQL provides powerful querying capabilities over your object model. Imagine all your objects lying around in some storage (like an object database). When writing DQL queries, think about querying that storage to pick a certain subset of your objects.
Source: https://doctrine-project.org
In other words, DQL allows you to construct complex queries to fetch objects from a storage without the need of writing a SQL code.
If you worked with Java before, chances are you might have heard of the Hibernate Query Language or Java Persistent Query Language which are Java’s equivalent to the Doctrine Query Language.
How to create an entity repository?
An entity repository class is where the DQL queries reside. It has to extend a EntityRepository
base class, since it contains some important declarations which are required to properly setup a repository.
A repository class
A repository concrete class will be placed in a src/Ptrio/MessageBundle/Repository
directory and then it will be used to create a repository service.
An abstraction layer
Since we are provided with a base EntityRepository
class it is only necessary to create an interface as a mean of abstraction.
Start by creating a file called DeviceRepositoryInterface.php
in a src/Ptrio/MessageBundle/Repository
directory.
<?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;
}
As shown above, a repository class will have to implement findDevicesByNames(array $deviceNames)
method which will accept an array of device names and return an array of device objects in exchange. It is also worth noticing that a DeviceRepositoryInterface
extends a ObjectRepository
interface.
A concrete class
Add a repository class file called DeviceRepository.php
to a src/Ptrio/MessageBundle/Repository
directory.
<?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();
}
}
The constructor method is overridden in the example above, since we want to pass a fully qualified class name string instead of a Mapping\ClassMetadata
instance.
Note: A
Mapping\ClassMetadata
object holds all the object-relational mapping of an entity along with it’s all associations.
A $this->getEntityManager()->createQueryBuilder()
method is used to create a new query builder instance.
Let’s investigate the very beginning of the query:
$qb
->select('d')
->from($this->getEntityName(), 'd')
;
A d
value is an alias that refers to a App\Ptrio\MessageBundle\Entity\Device
entity class. This is how we tell Doctrine that all queried device objects should be returned as the query result.
A $this->getEntityName()
method resolves to a fully qualified class name and it is used to avoid hard-coding. The method is followed by an alias for the aforementioned class name.
Let’s examine the query further:
for ($i = 0; $i < count($deviceNames); $i++) {
$qb
->orWhere($qb->expr()->eq('d.name', ':param_'.$i))
->setParameter(':param_'.$i, $deviceNames[$i])
;
}
As mentioned before, the goal is to query all devices with the given names. It can be achieved by adding a where condition clauses.
For each given device name, a where condition is applied to the query with the help of QueryBuilder::orWhere()
method. Since using string based queries should be avoided when possible, a $qb->expr()->eq()
method is used to generate a condition body. It will resolve to ’d.name = :param_’.$i
. Also because we are using parameter binding, a particular device name string has to be bound to a corresponding parameter.
A $qb->getQuery()->getResult()
method is used to get the query result.
Extending a device manager class
To make a use of a repository created in the previous step a refactoring of a device manager service is necessary. A repository service will be used by a device manager to perform custom queries.
In the first step, a DeviceManagerInterface
interface should be extended with a findDevicesByNames(array $deviceNames)
method declaration.
public function findDevicesByNames(array $deviceNames): array;
A DeviceRepositoryInterface $repository
argument should be added to a DeviceManager
class constructor.
public function __construct(
// other arguments
DeviceRepositoryInterface $repository
)
Replace $objectManager->getRepository($class);
with $this->repository = $repository;
inside a constructor method.
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();
}
Implement a findDevicesByNames(array $deviceNames)
method.
/**
* @param array $deviceNames
* @return array
*/
public function findDevicesByNames(array $deviceNames)
{
return $this->repository->findDevicesByNames($deviceNames);
}
Service configuration
In the last step, a repository should be defined as a service. This will allow to inject it to a device manager service.
Let’s add a configuration for a repository service.
# src/Ptrio/MessageBundle/Resources/config/services.yaml
ptrio_message.device_repository:
class: 'App\Ptrio\MessageBundle\Repository\DeviceRepository'
arguments:
- '@doctrine.orm.entity_manager'
- 'App\Ptrio\MessageBundle\Entity\Device'
Now, a ptrio_message.device_manager
service definition can be updated.
# src/Ptrio/MessageBundle/Resources/config/services.yaml
ptrio_message.device_manager:
class: 'App\Ptrio\MessageBundle\Doctrine\DeviceManager'
arguments:
- '@doctrine.orm.entity_manager'
- 'App\Ptrio\MessageBundle\Entity\Device'
- '@ptrio_message.device_repository' # add this line
How to define container parameters in a bundle’s service configuration file?
A container parameter is a string based value that can be used for a service definition. By defining container parameters, values that might change in the future can be separated out to ease the configuration process.
As of now, a App\Ptrio\MessageBundle\Entity\Device
class name is being used twice inside a bundle’s service configuration file. If you would like to change the entity class to a new value, you would have to update ptrio_message.device_repository
and ptrio_message.device_manager
services definitions. This might be even more problematic if you reference the value more often.
Note: When referencing container parameters, surround them with a percentage sign
%
.
To define a container parameter, add the following code to a src/Ptrio/MessageBundle/Resources/config/services.yaml
file.
# src/Ptrio/MessageBundle/Resources/config/services.yaml
parameters:
ptrio_message.model.device.class: 'App\Ptrio\MessageBundle\Entity\Device'
Now, a App\Ptrio\MessageBundle\Entity\Device
string can be referenced by calling a ptrio_message.model.device.class
container parameter.
Let’s update a ptrio_message.device_manager
service definition so it makes a use of the previously defined parameter.
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'
The same thing should be done for a ptrio_message.device_repository
service.
# 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%' # replace this line
How to pass an array of arguments to a command?
At the moment, a SendMessageCommand
command is capable of sending a push notification to a single mobile device. It accepts only one argument called recipient
which is used to query a device details from the database.
Let’s adjust a SendMessageCommand
class so it allows for submitting multiple device names as command-line arguments and then uses them to query the corresponding devices with the use of a DeviceManager::findDevicesByNames($deviceNames)
method which was implemented earlier.
Begin by changing a recipient
argument name to device-names
and switch its type to InputArgument::IS_ARRAY
.
protected function configure()
{
$this
->setDefinition([
// other arguments
new InputArgument('device-names', InputArgument::IS_ARRAY), // change here
]);
}
A InputArgument::IS_ARRAY
type informs a command service that a passed argument will be an array.
The next required step is a SendMessageCommand::execute(InputInterface $input, OutputInterface $output)
method adjustments.
The method has to reference a device-names
array argument. To achieve that, $recipient = $input->getArgument('recipient’);
should be replaced with $deviceNames = $input->getArgument('device-names’);
.
The whole if ($device = $this->deviceManager->findDeviceByName($recipient))
condition has to be replaced with the following code.
$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);
}
As shown in the example above, devices are first queried from the database. Later, an algorithm iterates over an array of found devices, persists a message object and sends a push message to each device. Also, a corresponding console output is returned to a user.
Examples
Sending a message to multiple devices
A message can be send to a predefined list of devices with a piotr:message:send-message 'My Message text' iphone-piotr redmi-ewa
command, where the first argument is an example message text and the remaining two are the target device names.
$ php bin/console piotr:message:send-message 'Hi all, team meeting in 15 minutes!' iphone-piotr redmi-ewa
For every request made to FCM servers, a response is returned to a client. Results are returned as a console output.
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"}]}
A message history can be displayed to review dispatched messages.
php bin/console piotr:message:list-device-messages iphone-piotr
An example list of messages dispatched to a device called 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
-------------- ------------------------------------- ---------------------
An example list of messages dispatched to a device called redmi-ewa
.
------------- ------------------------------------- ---------------------
Device Name Message Body Sent At
------------- ------------------------------------- ---------------------
xiaomi-ewa Hi all, team meeting in 15 minutes! 2018-03-14 13:33
------------- ------------------------------------- ---------------------
Curriculum
Posted on Utopian.io - Rewarding Open Source Contributors
Thank you for the contribution. It has been approved.
You can contact us on Discord.
[utopian-moderator]
Hey @piotr42 I am @utopian-io. I have just upvoted you!
Achievements
Community-Driven Witness!
I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!
Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x