Web Development: A Symfony based application capable of sending push messages to mobile devices via FCM

in #utopian-io7 years ago (edited)

server-24915_640.png

Source: pixabay.com. Licensed under CC0 Creative Commons

What will I learn?

  • How to create a HTTP Client to send upstream messages via Firebase Cloud Messaging
  • How to create a device entity mapping
  • How to create a service to manage devices FCM tokens
  • How to create an interface to dispatch push messages

Requirements

  • UNIX/Linux based OS
  • Apache2 server with PHP7 installed
  • MySQL database
  • Existing Symfony based project
  • Text editor of choice
  • Existing Firebase project
  • Composer

Difficulty

  • Intermediate

Tutorial contents

The guidelines which are to be found in this tutorial will allow you to successfully implement functionalities required to communicate with FCM servers, manage a list of devices FCM tokens and dispatch push notifications to those devices directly from a Symfony command.

The logic behind the chosen coding solutions is to make the code clean, portable and easily extendable.

Usage examples are listed at the end of the tutorial.

A sample project can be found on github.com.

Note: Symfony 4 based project was used in this tutorial. Since Symfony 4 projects differ from those written in Symfony 3, the bundle generation process described below will not apply to Symfony versions prior to 4. Information on how to generate a Symfony 3 bundle can be found here.

How to create a HTTP Client to send upstream messages via Firebase Cloud Messaging?

Let’s start with a client. An object that is capable of communicating with FCM servers is needed. It has to be authorized to make requests that will trigger the dispatch procedure.

The client class from the guzzlehttp library will be extended, as it provides all the necessary functionalities to make HTTP requests. It can be installed with Composer.

$ composer require guzzlehttp/guzzle
Creating a bundle

This section explains how to create and enable a Symfony 4 bundle.

Start off by creating a src/Ptrio/MessageBundle directory where Ptrio is an example name that can be replaced with the actual vendor. Next add a file called PtrioMessageBundle.php.

<?php
// src/Ptrio/MessageBundle/PtrioMessageBundle.php
namespace App\Ptrio\MessageBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class PtrioMessageBundle extends Bundle
{
}

The bundle can be enabled by adding the following to the config/bundles.php file.

return [
    // other bundles
    App\Ptrio\MessageBundle\PtrioMessageBundle::class => ['all' => true],
];

Note: The src/Ptrio/MessageBundle directory is the bundle’s root directory.

Creating a client

Create a Client directory within the bundle’s root directory and add an interface file called ClientInterface.php.

<?php
// src/Ptrio/MessageBundle/Client/ClientInterface.php
namespace App\Ptrio\MessageBundle\Client;
interface ClientInterface
{
    public function sendMessage(string $messageBody, string $recipient);
}

Note: A class that implements the ClientInterface will also have to implement the sendMessage(string $messageBody, string $recipient) method.

The concrete class implementing the ClientInterface will be called FirebaseClient.

<?php
namespace App\Ptrio\MessageBundle\Client;
use GuzzleHttp\Client;
class FirebaseClient extends Client implements ClientInterface
{
    public function __construct(string $apiURL, string $serverKey)
    {
        parent::__construct([
           'base_uri' => $apiURL,
            'headers' => [
                'Authorization' => 'key='.$serverKey,
            ],
        ]);
    }
    public function sendMessage(string $messageBody, string $recipient): string
    {
        $response = $this->post('/fcm/send', [
            'json' => [
                'to' => $recipient,
                'notification' => [
                    'body' => $messageBody,
                ],
            ],
        ]);
        return (string) $response->getBody();
    }
}

$apiURL is the address of the FCM’s REST API.

$serverKey is the server key of the mobile application registered with the FCM services.

Creating dependency injection classes

Both parameters will be passed in client’s constructor through dependency injection.

Start by creating a directory called DependencyInjection inside the bundle’s root dir and add a Configuration.php file to it.

<?php
// src/Ptrio/MessageBundle/DependencyInjection/Configuration.php
namespace App\Ptrio\MessageBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('ptrio_message');
        $rootNode
            ->children()
                ->arrayNode('firebase')
                    ->children()
                        ->scalarNode('api_url')->isRequired()->cannotBeEmpty()->end()
                        ->scalarNode('server_key')->isRequired()->cannotBeEmpty()->end()
                    ->end()
                ->end()
            ->end()
        ;
        return $treeBuilder;
    }
}

The Configuration.php file is responsible for defining the configuration structure for the bundle. This structure then is used when merging the values from the bundle’s configuration file.

A file called PtrioMessageExtension.php will handle the configuration file and load services and parameters from the container.

<?php
// src/Ptrio/MessageBundle/DependencyInjection/PtrioMessageExtension.php
namespace App\Ptrio\MessageBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class PtrioMessageExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new YamlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config'));
        $loader->load('services.yaml');
        $configuration = $this->getConfiguration($configs, $container);
        $config = $this->processConfiguration($configuration, $configs);
        $container->setParameter('ptrio_message.api_url', $config['firebase']['api_url']);
        $container->setParameter('ptrio_message.server_key', $config['firebase']['server_key']);
    }
}
Creating a service configuration

Start the process by creating a directory called src/Ptrio/MessageBundle/Resources/config and add a file named services.yaml inside it.

# src/Ptrio/MessageBundle/Resources/config/services.yaml
services:
    ptrio_message.firebase_client:
        class: 'App\Ptrio\MessageBundle\Client\FirebaseClient'
        arguments:
            - '%ptrio_message.api_url%'
            - '%ptrio_message.server_key%'

Note: The bundle’s services are not autowired, which means the autowire value in the config/services.yaml file has to be set to false for the above service configuration to work.

Adding bundle configuration file

The configuration file called ptrio_message.yaml should be added to the config/packages directory.

# config/packages/ptrio_message.yaml
ptrio_message:
    firebase:
        api_url: "https://fcm.googleapis.com"
        server_key: "AAAARwFTzY4:APA91b…”

How to create a device entity?

A MySQL database will be used to store devices data.

The device class will be mapped to a database table through Doctrine ORM, which will allow for slick object management.

Creating an abstraction layer

Begin by adding a directory called src\Ptrio\MessageBundle\Model and create the device interface file named DeviceInterface.php.

<?php
// src/Ptrio/MessageBundle/Model/DeviceInterface.php
namespace App\Ptrio\MessageBundle\Model;
interface DeviceInterface
{
    public function getId(): int;
    public function setName(string $name);   
    public function getName(): string;   
    public function setToken(string $fcmToken);  
    public function getToken(): string;
}

Next create an abstract class in a file called Device.php in the previously created directory.

<?php
// src/Ptrio/MessageBundle/Model/Device.php
namespace App\Ptrio\MessageBundle\Model;
abstract class Device implements DeviceInterface 
{
    protected $id;
    protected $name;    
    protected $token;
    public function getId(): int
    {
        return $this->id;
    }    
    public function getName(): string
    {
        return $this->name;
    }    
    public function setName(string $name)
    {
        $this->name = $name;
    }    
    public function getToken(): string
    {
        return $this->token;
    }    
    public function setToken(string $token)
    {
        $this->token = $token;
    }
}

A Device object will have two parameters: it’s name and FCM token.

Configuring database connection

Start by installing Doctrine library with Composer.

$ composer require doctrine

Provide the database name, host and login credentials such as username and password by adjusting the .env file.

DATABASE_URL=mysql://db_user:[email protected]:3306/db_name
Creating a device entity

A directory called src/Ptrio/MessageBundle/Entity should be created to begin with. Then the entity file named Device.php can be added.

<?php
// src/Ptrio/MessageBundle/Entity/Device.php
namespace App\Ptrio\MessageBundle\Entity;
use App\Ptrio\MessageBundle\Model\Device as BaseDevice;
use Doctrine\ORM\Mapping as ORM;
/**
 * @ORM\Entity
 */
class Device extends BaseDevice
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;
    /**
     * @ORM\Column(type="string")
     */
    protected $name;
    /**
     * @ORM\Column(type="string")
     */
    protected $token;
}
Executing database migrations

To update MySQL database schema, Doctrine migrations script has to be executed.

$ php bin/console doctrine:migration:diff # generates mysql queries
$ php bin/console doctrine:migration:migrate # executes queries

How to create a service to manage devices FCM tokens?

At this stage, a service to manage device entities will be created.

Creating an abstraction layer for the service

Start off by adding an interface file called DeviceManagerInterface.php.

<?php
// src/Ptrio/MessageBundle/Model/DeviceManagerInterface.php
namespace App\Ptrio\MessageBundle\Model;
interface DeviceManagerInterface
{
    public function createDevice(): DeviceInterface;
    public function updateDevice(DeviceInterface $device);    
    public function removeDevice(DeviceInterface $device);    
    public function getClass(): string;
    public function findDeviceByName(string $name);    
    public function findDeviceBy(array $criteria);
}

Create an abstract class within a file named DeviceManager.php.

<?php
// src/Ptrio/MessageBundle/Model/DeviceManager.php
namespace App\Ptrio\MessageBundle\Model;
abstract class DeviceManager implements DeviceManagerInterface 
{
    public function createDevice(): DeviceInterface
    {
        $class = $this->getClass();
        return new $class;
    }
    public function findDeviceByName(string $name)
    {
        return $this->findDeviceBy(['name' => $name]);
    }
}

Creating a device manager concrete class

The concrete class of the device manager will be placed in a src/Ptrio/MessageBundle/Doctrine/ directory in a file called ‘DeviceManager.php.

<?php
// src/Ptrio/MessageBundle/Doctrine/DeviceManager.php
namespace App\Ptrio\MessageBundle\Doctrine;
use App\Ptrio\MessageBundle\Model\DeviceInterface;
use App\Ptrio\MessageBundle\Model\DeviceManager as BaseDeviceManager;
use Doctrine\Common\Persistence\ObjectManager;
class DeviceManager extends BaseDeviceManager
{
    private $objectManager;
    private $repository;
    private $class;
    public function __construct(ObjectManager $objectManager, string $class)
    {
        $this->objectManager = $objectManager;
        $this->repository = $objectManager->getRepository($class);
        $metadata = $objectManager->getClassMetadata($class);
        $this->class = $metadata->getName();
    }
    public function getClass(): string
    {
        return $this->class;
    }    
    public function findDeviceBy(array $criteria)
    {
        return $this->repository->findOneBy($criteria);
    }  
    public function updateDevice(DeviceInterface $device)
    {
        $this->objectManager->persist($device);
        $this->objectManager->flush();
    }    
    public function removeDevice(DeviceInterface $device)
    {
        $this->objectManager->remove($device);
        $this->objectManager->flush();
    }
}

Now is the time to let the container know which dependencies should be injected to create a device manager instance. This can be achieved by adding the following code to the src/Ptrio/MessageBundle/Resources/config/services.yaml file.

    # 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'
Creating commands to manage devices

Command-line interface implementation will be described in this section. It will allow for adding and removing devices from the database.

AddDeviceCommand

The code, responsible for adding devices to database, should reside in the src/Ptrio/MessageBundle/Command/AddDeviceCommand.php file.

<?php
// src/Ptrio/MessageBundle/Command/AddDeviceCommand.php
namespace App\Ptrio\MessageBundle\Command;
use App\Ptrio\MessageBundle\Model\DeviceManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class AddDeviceCommand extends Command 
{
    public static $defaultName = 'ptrio:message:add-device';
    private $deviceManager;    
    public function __construct(DeviceManagerInterface $deviceManager)
    {
        $this->deviceManager = $deviceManager;        
        parent::__construct();
    }    
    protected function configure()
    {
        $this->setDefinition([
            new InputArgument('name', InputArgument::REQUIRED),
            new InputArgument('token', InputArgument::REQUIRED),
        ]);
    }   
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $name = $input->getArgument('name');
        $token = $input->getArgument('token');     
        if (null === $this->deviceManager->findDeviceByName($name)) {
            $device = $this->deviceManager->createDevice();
            $device->setName($name);
            $device->setToken($token);
            $this->deviceManager->updateDevice($device);            
            $output->writeln('Device `'.$name.'` created!');
        } else {
            $output->writeln('Device with this name already exists!');
        }
    }
}

Note: The command will need two arguments to run: a device name and a related FCM token.

RemoveDeviceCommand

Create a file called src/Ptrio/MessageBundle/Command/RemoveDeviceCommand.php with the following contents:

<?php
// src/Ptrio/MessageBundle/Command/RemoveDeviceCommand.php
namespace App\Ptrio\MessageBundle\Command;
use App\Ptrio\MessageBundle\Model\DeviceManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class RemoveDeviceCommand extends Command
{
    public static $defaultName = 'ptrio:message:remove-device';
    private $deviceManager;
    public function __construct(DeviceManagerInterface $deviceManager)
    {
        $this->deviceManager = $deviceManager;
        parent::__construct();
    }
    protected function configure()
    {
        $this->setDefinition([
            new InputArgument('name', InputArgument::REQUIRED),
        ]);
    }
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $name = $input->getArgument('name');
        if ($device = $this->deviceManager->findDeviceByName($name)) {
            $this->deviceManager->removeDevice($device);
            $output->writeln('Device `'.$name.'` removed!');
        } else {
            $output->writeln('Device with this name does not exist!');
        }
    }
}

Update thesrc/Ptrio/MessageBundle/Resources/config/services.yml service configuration file.

    # src/Ptrio/MessageBundle/Resources/config/services.yml
    ptrio_message.add_message_command:
        class: 'App\Ptrio\MessageBundle\Command\AddDeviceCommand'
        arguments:
            - '@ptrio_message.device_manager'
        tags:
            - { name: 'console.command' } 
    ptrio_message.remove_message_command:
        class: 'App\Ptrio\MessageBundle\Command\RemoveDeviceCommand'
        arguments:
            - '@ptrio_message.device_manager'
        tags:
            - { name: 'console.command' }

How to create an interface to dispatch push messages?

The SendMessageCommand class will be responsible for sending push notifications to devices in the existing database.

<?php
// src/Ptrio/MessageBundle/Command/SendMessageCommand.php
namespace App\Ptrio\MessageBundle\Command;
use App\Ptrio\MessageBundle\Client\ClientInterface;
use App\Ptrio\MessageBundle\Model\DeviceManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class SendMessageCommand extends Command
{
    private $client;
    private $deviceManager;
    protected static $defaultName = 'ptrio:message:send-message';
    public function __construct(ClientInterface $client, DeviceManagerInterface $deviceManager)
    {
        $this->client = $client;
        $this->deviceManager = $deviceManager;
        parent::__construct();
    }
    protected function configure()
    {
        $this
            ->setDefinition([
            new InputArgument('body', InputArgument::REQUIRED),
            new InputArgument('recipient', InputArgument::REQUIRED),
        ]);
    }
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $messageBody = $input->getArgument('body');
        $recipient = $input->getArgument('recipient');
        if ($device = $this->deviceManager->findDeviceByName($recipient)) {
            $response = $this->client->sendMessage($messageBody, $device->getToken());
            $output->writeln('Response: '.$response);
        } else {
            $output->writeln('No device found!');
        }
    }
}

Note: The command will need two arguments to run: message body and device name.

In the next step, the src/Ptrio/MessageBundle/Resources/config/services.yaml file should be updated.

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

Examples

Adding a device to database

A new device can be added with the ptrio:message:add-device command.

$ php bin/console ptrio:message:add-device iphone-piotr dN0NEC9aN_w:APA91b…
Removing a device from database

The ptrio:message:remove-device command is used to remove a device.

Note: A device is looked up by name.

$ php bin/console ptrio:message:remove-device iphone-piotr
Sending a push notification to a device

The messages with be dispatched with the ptrio:message:send-message command.

$ php bin/console ptrio:message:send-message 'Hello world!' iphone-piotr

Example response:

Response: {"multicast_id":5653186898440485701,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"0:1520875479975905%7167cd087167cd08"}]}

Once the message is delivered, it should be displayed on a target device.

IMG_3350 copy.png



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

As a follower of @followforupvotes this post has been randomly selected and upvoted! Enjoy your upvote and have a great day!

Your contribution cannot be approved because it does not follow the Utopian Rules.

Violated Rules:

  • Design or video editing related tutorials, gameplay, simple on-screen instructions, ubiquitous functions (Save, Open, Print, etc.) or basic programming concepts (variables, operators, loops, etc.) will not be accepted.

My Opinion:

  • A tutorial must be informative and explanatory, but also "tutor". This tutorial lacks "tutor"ing, and it is nothing more than "Do whatever I do." kind of tutorial.

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.16
TRX 0.25
JST 0.034
BTC 95670.34
ETH 2679.38
SBD 0.69