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

in #utopian-io7 years ago (edited)

email-marketing-2362038_640.png

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

Sort:  

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

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

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

Coin Marketplace

STEEM 0.16
TRX 0.25
JST 0.034
BTC 94343.80
ETH 2658.47
USDT 1.00
SBD 0.67