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

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 anid
for an entity will not be assigned manually. AAUTO 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
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
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