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

Source: pixabay.com. Licensed under CC0 Creative Commons
What will I learn?
- How to intercept GET parameters while interacting with a RESTful API
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 seventh 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 process of extending a voting mechanism to check permissions based on a role currently assigned to a user making a request was described.
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 implementing the param fetcher functionality bundled with the
FOSRestBundle
, that allows to intercept GET parameters sent with a request by adding annotations, in aphpdoc
block, over a controller method. Custom validation rules can be applied to the intercepted parameters and additionally default values can be set in case no parameters were provided with a request.
An example usage for the created functionalities will be shown at the end of this tutorial.
How to intercept GET parameters while interacting with a RESTful API?
The param fetcher, which comes pre-packaged with the FOSRestBundle
bundle and allows for fetching GET parameters from a request will be used to implement pagination and sorting functionality for a device messages list which will be accessible via RESTful API web services.
A message repository class
At this stage, a message repository class, which contains one method responsible for fetching device messages from a database, will be created. It will allow for setting a sorting type (ascending or descending) and applying an offset
and limit
operators to filter out returned messages.
An offset
value will define the first element which is to be contained within a returned array. On the other hand, a limit
value defines the number of elements, beginning from the position set as offset, which will be returned from a database.
An abstraction layer
Before going to the implementation part, creating a MessageRepositoryInterface
interface, which will hold a findMessagesByDevice(DeviceInterface $device, string $sort, int $offset, int $limit)
metod declaration, is recommended.
The aforementioned method will be responsible for returning a list of messages related to a device.
<?php
// src/Ptrio/MessageBundle/Repository/MessageRepositoryInterface.php
namespace App\Ptrio\MessageBundle\Repository;
use App\Ptrio\MessageBundle\Model\DeviceInterface;
use Doctrine\Common\Persistence\ObjectRepository;
interface MessageRepositoryInterface extends ObjectRepository
{
/**
* @param DeviceInterface $device
* @param string $sort
* @param int $offset
* @param int $limit
* @return array
*/
public function findMessagesByDevice(DeviceInterface $device, string $sort, int $offset, int $limit): array;
}
As shown in the example above, a findMessagesByDevice(DeviceInterface $device, string $sort, int $offset, int $limit)
metod will take up four arguments. The first one is a device instance, the second one will hold a sorting type definition, the third and the fourth ones are consecutively: an offset and a limit values explained above.
A concrete class
A message repository concrete class will contain logic which will define how a
MessageRepository::findMessagesByDevice(DeviceInterface $device, string $sort = 'DESC', int $offset, int $limit)
method should be handled.
<?php
// src/Ptrio/MessageBundle/Repository/MessageRepository.php
namespace App\Ptrio\MessageBundle\Repository;
use App\Ptrio\MessageBundle\Model\DeviceInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
class MessageRepository extends EntityRepository implements MessageRepositoryInterface
{
public function __construct(EntityManagerInterface $em, string $class)
{
parent::__construct($em, $em->getClassMetadata($class));
}
public function findMessagesByDevice(DeviceInterface $device, string $sort = 'DESC', int $offset, int $limit): array
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->select('m')
->from($this->getEntityName(), 'm')
->andWhere($qb->expr()->eq('m.device', $device->getId()))
->orderBy('m.id', $sort)
->setFirstResult($offset)
->setMaxResults($limit)
;
return $qb->getQuery()->getResult();
}
}
A MessageRepository::findMessagesByDevice(DeviceInterface $device, string $sort = 'DESC', int $offset, int $limit)
method uses a QueryBuilder
class instance to prepare a DQL query responsible for fetching messages related to a given device, sorting them according to a given type and finally slicing them with the use of given offset and limit values.
Service configuration
Adding a ptrio_message.message_repository
message repository service definition to a services.yaml
is necessary in order for the dependencies to be injected.
# src/Ptrio/MessageBundle/Resources/config/services.yaml
ptrio_message.message_repository:
class: 'App\Ptrio\MessageBundle\Repository\MessageRepository'
arguments:
- '@doctrine.orm.entity_manager'
- '%ptrio_message.model.message.class%'
Updating a message manager class
At the moment, a MessageManager::findMessagesByDevice(DeviceInterface $device)
method allows for finding messages related to a given device. Before a sorting and a pagination functionalities can be used with our application, a message manager service has to make a use of a MessageRepository
instance.
An abstraction layer
Prior to implementing an updated MessageManager::findMessagesByDevice(DeviceInterface $device)
method, updating an abstraction layer is also necessary.
Begin by replacing a MessageManager::findMessagesByDevice(DeviceInterface $device)
method definition with the one presented below.
// src/Ptrio/MessageBundle/Model/MessageManagerInterface.php
// other method declarations
public function findMessagesByDevice(DeviceInterface $device, string $sort, int $offset, int $limit): array;
A concrete class
Since a MessageManager::$repository
property will hold a message repository instance, updating a phpdoc
block found over the aforementioned property declaration ,is recommended in order for the type-hinting to work in your editor of choice.
// src/Ptrio/MessageBundle/Doctrine/MessageManager.php
/**
* @var MessageRepositoryInterface
*/
private $repository;
In the next step, a MessageManager
class constructor should be updated with an argument holding a MessageRepositoryInterface
type instance.
/**
* MessageManager constructor.
* @param ObjectManager $objectManager
* @param string $class
* @param MessageRepositoryInterface $repository
*/
public function __construct(
ObjectManager $objectManager,
string $class,
MessageRepositoryInterface $repository // add this line
)
{
$this->objectManager = $objectManager;
$this->repository = $repository; // modify this line
$metadata = $objectManager->getClassMetadata($class);
$this->class = $metadata->getName();
}
Next, the existing MessageManager::findMessagesByDevice(DeviceInterface $device)
method definition should be replaced with the one found below.
/**
* {@inheritdoc}
*/
public function findMessagesByDevice(DeviceInterface $device, string $sort, int $offset, int $limit): array
{
return $this->repository->findMessagesByDevice($device, $sort, $offset, $limit);
}
Service configuration
In the last step, a ptrio_message.message_manager
service definition should be amended by adding a ptrio_message.message_repository
message repository service as the third argument.
# src/Ptrio/MessageBundle/Resources/config/services.yaml
ptrio_message.message_manager:
class: 'App\Ptrio\MessageBundle\Doctrine\MessageManager'
arguments:
- '@doctrine.orm.entity_manager'
- '%ptrio_message.model.message.class%'
- '@ptrio_message.message_repository' // add this line
A message controller class
In order for a list of messages, related to a specific device, to be returned as a HTTP response, creating a message controller class is required. It will contain a definition for a MessageController::getMessagesAction(string $deviceName, ParamFetcher $paramFetcher)
method, which will be decorated with appropriate annotations that will allow for GET parameters interception by a ParamFetcherListener
instance. Specifying a sort type along with a requested page number and a results per page value will be possible by adding a ?sort=sort_type&page=page_no&results_per_page=limit
like value to a request URI.
Note: Due to the fact that a message controller class will extend a base
FOSRestController
controller class, creating an abstraction layer is not necessary.
A concrete class
A concrete message controller class will utilise a device manager service to look up a specific device by a given name and a message manager service to fetch messages related to the aforementioned device. Both services will be injected to a MessageController
class as constructor arguments.
<?php
// src/Ptrio/MessageBundle/Controller/MessageController.php
namespace App\Ptrio\MessageBundle\Controller;
use App\Ptrio\MessageBundle\Model\DeviceManagerInterface;
use App\Ptrio\MessageBundle\Model\MessageManagerInterface;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Request\ParamFetcher;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations\QueryParam;
class MessageController extends FOSRestController
{
/**
* @var DeviceManagerInterface
*/
private $deviceManager;
/**
* @var MessageManagerInterface
*/
private $messageManager;
/**
* MessageController constructor.
* @param DeviceManagerInterface $deviceManager
* @param MessageManagerInterface $messageManager
*/
public function __construct(
DeviceManagerInterface $deviceManager,
MessageManagerInterface $messageManager
)
{
$this->deviceManager = $deviceManager;
$this->messageManager = $messageManager;
}
/**
* @param string $deviceName
* @param ParamFetcher $paramFetcher
* @return Response
*
* @QueryParam(name="sort", requirements="(asc|desc)", default="asc")
* @QueryParam(name="page", requirements="\d+", default="0")
* @QueryParam(name="results_per_page", requirements="\d+", default="25")
*/
public function getMessagesAction(string $deviceName, ParamFetcher $paramFetcher): Response
{
if ($device = $this->deviceManager->findDeviceByName($deviceName)) {
$this->denyAccessUnlessGranted(null, $device);
$sort = $paramFetcher->get('sort');
$page = $paramFetcher->get('page');
$resultsPerPage = $paramFetcher->get('results_per_page');
list($offset, $limit) = [($page * $resultsPerPage), $resultsPerPage];
$messages = $this->messageManager->findMessagesByDevice($device, $sort, $offset, $limit);
$view = $this->view($messages, Response::HTTP_OK);
} else {
$view = $this->view(null, Response::HTTP_NOT_FOUND);
}
return $this->handleView($view);
}
}
Let’s go over some of the most important aspects found in an example MessageController
controller class presented above.
The GET parameters that are supposed to be intercepted by a ParamFetcherListener
type object can be defined with a @QueryParam
annotation. As shown above, in this particular case, these parameters are: sort
, page
and results_per_page
.
A sort
parameter will hold either a asc
(stand for ascending) or a desc
(descending) value. A corresponding validation rule will be defined in a requirements
argument. In case there is no sort
value passed, a default one, defined in a default
argument, will be set.
A page
parameter will hold a numeric value that indicates a requested page number with 0
being the default value.
Note: Given the fact that pagination starts from 0, the first page will be marked as 0, the second one as 1 and so on.
A results_per_page
parameter will indicate how many elements should be returned on a given page (25 being the default value).
A MessageController::getMessagesAction(string $deviceName, ParamFetcher $paramFetcher)
method accepts two arguments. The first one in a device object for which the messages are supposed to be listed and the second one in a ParamFetcher
class instance which is capable of fetching values from parameters defined with @QueryParam
annotations. These values can be accessed by calling a ParamFetcher::get($name, $strict = null)
method, where a $name
argument indicates a GET parameter name.
Note: Our application uses class and type reflection for controller methods arguments, so the order in which these arguments are defined is irrelevant.
Additionally, a voting mechanism service is used to verify whether a user has appropriate permissions to view messages sent to a given device.
A value for an $offset
variable is created by multiplying a page number by a results per page value.
Service configuration
In order for the dependencies to be injected to a message controller service, creating it’s definition in a services.yaml
file is necessary.
# src/Ptrio/MessageBundle/Resources/config/services.yaml
ptrio_message.message_controller:
class: 'App\Ptrio\MessageBundle\Controller\MessageController'
arguments:
- '@ptrio_message.device_manager'
- '@ptrio_message.message_manager'
tags:
- { name: controller.service_arguments }
A ptrio_message.message_controller
service has to be tagged as controller.service_arguments
.
Route configuration
Adding a route configuration related to a message controller is required to display a messages list via the HTTP protocol.
# src/Ptrio/MessageBundle/Resources/config/routes.yaml
message:
type: rest
parent: device
resource: 'ptrio_message.message_controller'
A parent
parameter allows for specifying a parent route. In this particular scenario, a device controller related routes serve as parent routes for a message controller related ones.
The existing routes can be displayed by calling a php bin/console debug:router
command.
--------------------- -------- -------- ------ -------------------------------------------------
Name Method Scheme Host Path
--------------------- -------- -------- ------ -------------------------------------------------
get_device GET ANY ANY /api/v1/devices/{deviceName}.{_format}
post_device POST ANY ANY /api/v1/devices.{_format}
delete_device DELETE ANY ANY /api/v1/devices/{deviceName}.{_format}
get_device_messages GET ANY ANY /api/v1/devices/{deviceName}/messages.{_format}
--------------------- -------- -------- ------ -------------------------------------------------
As shown in the example above, a messages list for a given device can be displayed by requesting a /api/v1/devices/{deviceName}/messages
URI.
FOSRestBundle configuration
In the last step, updating the FOSRestBundle
configuration, found in a config/packages/fos_rest.yaml
, is necessary.
# config/packages/fos_rest.yaml
param_fetcher_listener: true
By setting a param_fetcher_listener
parameter value to true
we inform the FOSRestBundle
bundle that the param fetcher listener service, responsible for fetching all GET parameters defined in a @QueryParam
annotation, should be enabled.
Examples
A list of messages sent to a iphone-piotr
device can be displayed by calling a http://localhost/api/v1/devices/iphone-piotr/messages
url.
curl -H 'Accept: application/json' -H 'X-AUTH-TOKEN: MMYKUE63gCyFc9blOwcrdr0EP9ghb7yiWZ2lr4fRauM' http://localhost/api/v1/devices/iphone-piotr/messages
In this case, the default values for a sort
, page
and results_per_page
parameters will be used. Therefore a maximum of twenty five messages will be returned from the first page, sorted in a descending order.
To display five messages from the second page a api/v1/devices/iphone-piotr/messages?page=1&results_per_page=5
API endpoint should be called.
curl -H 'Accept: appllOwcrdr0EP9ghb7yiWZ2lr4fRauM' 'http://localhost/api/v1/devices/iphone-piotr/messages?page=1&results_per_page=5'
An example output is presented below.
[
{
"id": 16,
"body": "Hi, how is it going?",
"device": {
"id": 1,
"name": "iphone-piotr",
"token": "d1KeQHgkIoo:APA91b...",
"user": {
"id": 1,
"username": "piotr42",
"apiKey": "MMYKUE63gCyFc9blOwcrdr0EP9ghb7yiWZ2lr4fRauM",
"roles": [
"ROLE_ADMIN"
],
"password": null,
"salt": null
}
},
"sentAt": "2018-03-14T00:00:00+00:00"
},
{
"id": 18,
"body": "Hi all, team meeting in 15 minutes!",
"device": {
"id": 1,
"name": "iphone-piotr",
"token": "d1KeQHgkIoo:APA91b...",
"user": {
"id": 1,
"username": "piotr42",
"apiKey": "MMYKUE63gCyFc9blOwcrdr0EP9ghb7yiWZ2lr4fRauM",
"roles": [
"ROLE_ADMIN"
],
"password": null,
"salt": null
}
},
"sentAt": "2018-03-14T00:00:00+00:00"
},
{
"id": 20,
"body": "Aren't you forgetting something?",
"device": {
"id": 1,
"name": "iphone-piotr",
"token": "d1KeQHgkIoo:APA91b...",
"user": {
"id": 1,
"username": "piotr42",
"apiKey": "MMYKUE63gCyFc9blOwcrdr0EP9ghb7yiWZ2lr4fRauM",
"roles": [
"ROLE_ADMIN"
],
"password": null,
"salt": null
}
},
"sentAt": "2018-03-15T00:00:00+00:00"
},
{
"id": 21,
"body": "Please check your mail!",
"device": {
"id": 1,
"name": "iphone-piotr",
"token": "d1KeQHgkIoo:APA91b...",
"user": {
"id": 1,
"username": "piotr42",
"apiKey": "MMYKUE63gCyFc9blOwcrdr0EP9ghb7yiWZ2lr4fRauM",
"roles": [
"ROLE_ADMIN"
],
"password": null,
"salt": null
}
},
"sentAt": "2018-03-15T00:00:00+00:00"
},
{
"id": 22,
"body": "test",
"device": {
"id": 1,
"name": "iphone-piotr",
"token": "d1KeQHgkIoo:APA91b...",
"user": {
"id": 1,
"username": "piotr42",
"apiKey": "MMYKUE63gCyFc9blOwcrdr0EP9ghb7yiWZ2lr4fRauM",
"roles": [
"ROLE_ADMIN"
],
"password": null,
"salt": null
}
},
"sentAt": "2018-03-20T00:00:00+00:00"
}
]
To sort messages in an ascending order, a sort
parameter with an asc
value should be added to a request uri.
curl -H 'Accept: appllOwcrdr0EP9ghb7yiWZ2lr4fRauM' 'http://localhost/api/v1/devices/iphone-piotr/messages?page=1&results_per_page=5&sort=asc'
Curriculum
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 7]
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 6]
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 5]
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 4]
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 3]
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 2]
- Web Development with Symfony: An application capable of dispatching push notifications to mobile devices via FCM [part 1]
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