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

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



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