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

in #utopian-io7 years ago (edited)

email-marketing-2362038_640.png

What will I learn?

  • How to create an entity
  • How to create a service to manage an entity
  • How to create a command
  • How to do dependency injection with a container

Requirements

  • UIX/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 intended for users that are familiar with PHP OOP concepts and want to learn the basics behind Symfony based web development.

Symfony is a set of PHP Components, a Web Application framework, a Philosophy, and a Community — all working together in harmony.

Source: https://symfony.com/what-is-symfony.

Symfony is consisted of decoupled components that can be reused in a large variety of applications without the need of reinventing the wheel all over again. This way, the development process can be speeded up dramatically. Symfony components are used for example in Magento (an e-commerce platform), Laravel (PHP framework), phpBB (a forum application) and in many, many more web applications.

A sample project used with this tutorial can be found here. It already provides some basic functionalities, that we can work upon with to grasp more knowledge about Symfony based web development. 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?

  • We will work with Doctrine ORM library, which is an object relational mapper. It will allow for mapping a PHP class (called an entity) to a table in the database. This way writing SQL code will not be necessary as all corresponding tables and columns will be created automatically out of the box.
  • The process of creating a dedicated entity manager will be presented, which will be capable of creating an entity, persisting it to the database, along with an ability to query stored records and return them to an array.
  • The process of creating a Symfony command will be described to give some insights on how to create a command-line interface to perform code execution.
  • The concept behind a service container delivered with Symfony will be laid down to allow for a slick service creation process using dependency injection.

An example usage for the created functionalities will be shown at the end of this tutorial.

How to create an entity?

An entity is a term to describe a PHP object that is supposed to be saved in the database. Each entity has it’s own unique identity which is achieved by assigning a unique identifier to it.

Since we are in the world of Object-Oriented Programming, starting off with an abstraction layer for a entity class is highly recommended. This approach should be in general applied to each concrete class that is created, since it allows for class extending in a clean manner, removes coupling between classes (references are made to an abstract types instead) and separates code into smaller, readable code blocks - remember, programming is for humans, not for machines.

A message entity

An example message entity will be created at this stage that will represent a single, unique record in the database. It will contain four parameters: an identifier, a message text, a recipient object reference and a date when the message was dispatched.

An abstraction layer

Start by creating an interface called MessageInterface that will contain all the methods that a class has to implement. This way the future references to a message entity will be made by referencing an interface, instead of a concrete class. It will come in handy if for example you need to change the concrete class to a different one. Another advantage of using interfaces is the ability to specify methods without the need of defining how these methods are handled.

Start by adding a file called MessageInterface.php inside a src/Ptrio/MessageBundle/Model directory.

<?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;
}

Prior to PHP 7 defining a type returned by a method was not possible. Types were assigned automatically by a PHP interpreter. PHP 7 allows to explicitly set a type on a method or on an argument, which makes the code more understandable to humans. By assigning static types we can also avoid having to work with a wrong kind of value, since an error will be raised at the code execution.

Next, create an abstract class Message. Abstract classes play an important role in OOP. These are base classes that provide basic (or complex) functionalities which can be then inherited by other classes, without the need for writing a repetitive code.

Note: Abstract classes cannot be instantiated. They serve as template classes for classes extending them.

The properties declared in the class scope will be defined as protected. This means that they will not be accessible from outside the class by direct reference. They can be exposed by creating so called getters and setters. Defining a property as protected allows a child or a parent class to access it directly. This is also the only difference between a private and a protected property.

Note: You might notice that there is no getId() method defined in the interface. The reason behind this is the fact, that an id for an entity will not be assigned manually. A AUTO INCREMENT value will be assigned as id instead.

To continue, create a file called Message.php in the same directory.

<?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;
    }
}

There is one thing that is worth noticing in the above class. A Message::$device parameter has a DeviceInterface abstract type assigned to it. This means that a message entity object will contain a reference to an existing device entity object. Establishing a relation between objects allows for a fine-grained object management.

A concrete class

Now it is the time to create an entity class. Since all the required methods were already implemented in the base class, the only thing left is the ORM mapping. By applying the mapping we will tell Doctrine which class should be mapped to a database and what type columns a database table will hold.

Mapping can be done by annotating properties (or methods) with phpdoc blocks or directly with yaml, xml or PHP code itself.

Note: In this tutorial Doctrine annotation mapping will be performed.

An entity Message class should placed in a directory called src/Ptrio/MessageBundle/Entity. It will hold the instructions necessary for the ORM mapping process.

<?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;
}

A @ORM\Entity annotation is responsible for notifying Doctrine that a class is an entity.

A @ORM\Column annotation allows for defining a column data type.

The Message::$id parameter is an entity unique identifier. A @ORM\Id annotation indicates that a Message::$id parameter is a unique identifier for an entity. By adding @ORM\GeneratedValue(strategy="AUTO”) annotation, we tell Doctrine that this parameter will be generated automatically by applying a AUTO INCREMENT constraint. A default starting value for AUTO INCREMENT will be 1, and it will be incremented by 1 for each record inserted to a database.

As you can see in the above example, a Message::$device parameters contains a bit more sophisticated mapping. Since a relation between a message and a device objects has to be established, Doctrine has to also know how to handle it. This happens by defining a type of a relation (@ORM\ManyToOne) along with the information about a column which holds an identifier of the related device entity object (@ORM\JoinColumn).

The Doctrine migrations script has to be executed to update the database schema.

$ php bin/console doctrine:migration:diff
$ php bin/console doctrine:migration:migrate

How to create a service to manage an entity?

A message manager object will capable of creating new message instances and saving them in the database. It will also contain definitions required to fetch message records from the database.

A message manager class

Before creating a concrete class to host a message manager object declarations, let’s add an abstraction layer.

An abstraction layer

Create an interface file named MessageManagerInterface.php inside a src/Ptrio/MessageBundle/Model directory.

<?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();
}

In the next step, create a base MessageManager class for a message manger object, that implements 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;
    }
}

As you can see in the example above, a MessageManager::createMessage() factory method will be capable of creating an instance of a Message class. A MessageManager::getClass() method will be responsible for returning a fully qualified class name which will then be used to instantiate an object. For now it is not yet defined how a MessageManager::getClass() method will be handled, but since we are using abstraction, we can leave that definition to a MessageManager concrete class.

A concrete class

A concrete class is where the MessageManager::getClass(), MessageManager::updateMessage() and MessageManager::findMessagesByDevice() methods will be defined. A Doctrine ObjectManager object will be injected through the class constructor along with a fully qualified class string. A ObjectManager is capable of persisting an entity object to the database. A class string on the other hand will be used to create a new entity instance. Also, a repository object is used to fetch message objects from the database, and since we are using Doctrine ORM, all fetched records will be hydrated as (replaced with) message objects.

Note: Because a class string is injected as the constructor argument, we can avoid coupling the factory method to a specific class. This will come in handy if we replace an entity class with a new definition.

In a src/Ptrio/MessageBundle/Doctrine directory, create a file called 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]);
    }
}

How to create a command?

The console component shipped with Symfony framework allows you to create command-line commands which can be used to perform any type of recurring tasks. In this section, a process of creating a command which will be capable of fetching messages from the database (by a device name) and returning them to a simple table, will be presented. The result will then be displayed as a command output.

A command class

A command class should extend a Symfony\Component\Console\Command\Command base class which provides some required functionalities. A class that will be created in this example, will be dependent on two services: a message manager created earlier and device manager which has been already declared in the code found here.

Note: Since the Symfony framework provides base command classes out of the box, an abstraction layer is not necessary.

Start by adding a file called ListDeviceMessagesCommand.php to a src/Ptrio/MessageBundle/Command directory.

<?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);
        }
    }
}

By declaring a public static property ListDeviceMessagesCommand::$defaultName we are assigning a name to the command. The command is executed by calling it's name from a command-line interface.

A ListDeviceMessagesCommand::configure() method takes care of the configuration process. It is called during an object construction. Parameters like command description or input arguments shall be defined within a ListDeviceMessagesCommand::configure() method body. As shown in the above example, a required device-name argument is defined there.

A ListDeviceMessagesCommand::execute(InputInterface $input, OutputInterface $output) method, as the name implies, is called during a command execution. This is where all the tasks performed by a command should be placed.

A SymfonyStyle class object is used to format queried records and return them in a user-friendly table.

How to do dependency injection with a container?

There is just one more thing that has to be done in order to make the code executable and that is service configuration. Before we jump any further, let’s explain what a dependency injection container is and what are it’s benefits.

What is a dependency injection container?

DI container or service container is an object where application services reside. Think of it as a service that knows how create a certain object and configure it, so whenever a specified service is needed, it is constructed and returned by a container object.

The benefits of using a service container
  • Object can be constructed in a centralised way - service configuration can be done from one place. This makes maintaining the code much easier.
  • Application performance can be enhanced, since the services are lazy-loaded on demand. A container object will only create a new service instance when it is requested.

Once we know what a service container is, we can use it to configure the message manager service which was created earlier.

Start by opening a file called src/Ptrio/MessageBundle/Resources/config/services.yaml.

Add a ptrio_message.message_manager service declaration.

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

A class property tells the container which class should be used to construct a service object.

A arguments property is where the class constructor arguments are passed. A @doctrine.orm.entity_manager object is a Doctrine ObjectManager instance and the second argument is a fully qualified class name.

Note: The arguments must correspond with those declared within a MessageManager class constructor.

Since a ListDeviceMessagesCommand command class also needs two arguments injected through the constructor, let’s provide a ptrio_message.list_device_messages_command service definition.

    # 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' }

A tags property allows to tag a service as a Symfony command.

Examples

There is actually just one more thing that needs to be done before we can see our code in action. The base project used in this tutorial indeed has a service capable of managing message entities by a SendMessageCommand command responsible for sending push messages to mobile devices, but it does not yet make any use of a message manager service. Let’s change that now.

Start by adding $messageManager variable to the class scope. It will hold a message manager instance of abstract type MessageManagerInterface.

    // other class scope variables
    private $messageManager;

Add a MessageManagerInterface $messageManager instance to constructor arguments and assign it to a $messageManager variable.

    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();
    }

Next, locate the execute(InputInterface $input, OutputInterface $output) method and add the code responsible for saving a message in the database in between the if ($device = $this->deviceManager->findDeviceByName($recipient)) condition brackets.

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

The code above will create a new message entity instance, set body, device and sentAt properties and then will persist the object to the database.

The last step is adding a @ptrio_message.message_manager service to the list of arguments of a ptrio_message.send_message_command service.

    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' }
Sending push messages

A push message can be sent to a device with a 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"}]}
Viewing message history

A ptrio:message:list-device-messages command can be used to display a message history for a given mobile device.

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

The command will return a list of messages sent to iphone-piotr device.

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



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]

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

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!
  • This is your first accepted contribution here in Utopian. Welcome!

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 94126.54
ETH 2654.67
USDT 1.00
SBD 0.69