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

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 create a form for an entity
  • How to add validation to a form
  • How to create a controller using a REST/SOAP API design principle

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 third 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 creating an entity repository and extending a manager service were briefly described. In addition, a way of defining service container parameters to ease application maintenance was shown.

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 tutorials. 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 creating a form for an entity, which allows to add new entities through a HTTP POST request.
  • The list of activities required to add validation rules to an entity, which will prevent from persisting incorrect values to the database.
  • The process of creating a controller capable of processing REST/SOAP API requests in JSON and XML formats.

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

How to create a form for an entity?

The Form Component provided by the creators of the Symfony framework is designed to make a life of a web developer much easier. When dealing with HTTP forms, with a bit of help from the Form Component, form objects can be separated out to standalone Type classes which can be used throughout the whole application.

The Form Component can be installed with Composer.

$ composer require form
A form class

A device form will be built in a standalone class called DeviceType and then used in a controller which will be created later on.

Note: By separating a logic responsible for a form building process you are actually following the separation of concerns design principle.

Since a AbstractType base class comes already bundled with the Form Component, an abstraction layer for a form object definition is not necessary.

A concrete class

Begin by creating a directory called src/Ptrio/MessageBundle/Form/Type and add a file named DeviceType.php inside.

<?php
// src/Ptrio/MessageBundle/Form/Type/DeviceType.php
namespace App\Ptrio\MessageBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class DeviceType extends AbstractType
{
    /**
     * @var string
     */
    private $class;
    /**
     * DeviceType constructor.
     * @param string $class
     */
    public function __construct(string $class)
    {
        $this->class = $class;
    }
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name')
            ->add('token')
        ;
    }
    /**
     * @param OptionsResolver $resolver
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'base_class' => $this->class,
            'csrf_protection' => false,
        ]);
    }
}

Let’s go over some of the most important aspects found in an example DeviceType class presented above.

A fully-qualified entity class name is passed as a constructor argument to avoid value hardcoding.

Note: In the previous tutorial a device entity class name was separated out to a service container parameter. That is great news, because it can now be used for dependency injection.

A AbstractType::buildForm(FormBuilderInterface $builder, array $options) method contains the actual logic used to build a form with a FormBuilder object. A device form will contain two properties: name and token. A type for each form property (or field) will be determined automatically.

Note: Field names has to correspond with the properties defined in a device entity class.

A AbstractType::configureOptions(OptionsResolver $resolver) is where the defaults for a form object are set.

The OptionsResolver component is array_replace on steroids. It allows you to create an options system with required options, defaults, validation (type, value), normalization and more.

Source: https://symfony.com.

In our example a base_class option is set to a value held in DeviceType::$class. A form object uses this value to determine the appropriate data mapper.

A csrf_protection option determines if a protection against CSRF attacks should be enabled for a form. This option should be set to false, since a form object will not be rendered (no client interface yet), and thence a csrf_token will not be generated.

Service configuration

A definition service has to be created in a src/Ptrio/MessageBundle/Resources/config/services.yaml configuration file so dependency injection can be performed on a FormType object and an entity class name can be passed as a constructor argument.

    # src/Ptrio/MessageBundle/Resources/config/services.yaml
    ptrio_message.device_type:
        class: 'App\Ptrio\MessageBundle\Form\Type\DeviceType'
        arguments:
            - '%ptrio_message.model.device.class%'
        tags:
            - { name: form.type, alias: 'ptrio_message_device_type' }

A FormType service must be tagged as a form.type for the above configuration to work properly.

Note: A ptrio_message.model.device.class service container parameter which contains a fully-qualified Device entity class name was used as a constructor argument.

How to add validation to a form?

There are two properties, except Device::$id, which are required for every device entity object: Device::$name and Device::$token. Validation rules should be applied to a device entity class to ensure that empty values will not be assigned to the aforementioned properties while persisting an entity to a database. This can be achieved with the Validator Component.

The Validator Component can be installed with Composer.

$ composer require validator

Validation rules are applied to an entity properties by adding appropriate annotations.

Start by adding an import statement for a validator class with a Assert alias.

use Symfony\Component\Validator\Constraints as Assert;

Next, add a @Assert\NotBlank() annotation over Device::$name and Device::$token properties. A rule applied by this annotation, as the name implies, is responsible for preventing a blank value to be passed.

    // src/Ptrio/MessageBundle/Entity/Device.php
    /**
     * @ORM\Column(type="string")
     * @Assert\NotBlank() 
     */
    protected $name;
    /**
     * @ORM\Column(type="string")
     * @Assert\NotBlank()
     */
    protected $token;

How to create a controller using a REST/SOAP API design principle?

At this stage, the process of creating a controller service that follows a REST/SOAP API design principle will be described. By taking this approach, any client capable of communicating via HTTP protocol will be able to interact with our server application.

REST/SOAP principle is a set of rules, which define how the operations should be performed on a API (Application Programming Interface). A REST web service request/response is in JSON, while a corresponding one in SOAP is in XML format.

A component called FOSRestBundle is provided by the community to kickstart a REST/SOAP application development process.

Note: FOSRestBundle needs a serializer service to work properly. The Serializer Component can be installed with Composer by requiring a package called serializer.

Let’s install the FOSRestBundle component with Composer.

$ composer require friendsofsymfony/rest-bundle

Before jumping over to the next step, a configuration for FOSRestBundle has to be created to handle JSON and XML formats for requests/responses made behind a specified API endpoint.

Replace the default configuration in config/packages/fos_rest.yaml with the configuration presented below.

# config/packages/fos_rest.yaml
fos_rest:
    format_listener:
        rules:
            - { path: ^/api/v1, prefer_extension: true, fallback_format: json, priorities: [ json, xml ] }

In the example above, a rule tells the format_listener that all requests/responses made behind a /api/v1 API endpoint should be in JSON and XML formats (JSON being the default one).

A controller class

A controller class will hold definitions responsible for creating a new device entity object, removing it and also displaying details for a device found by a given name.

Note: FOSRestBundle component comes with a base controller class, so an abstraction layer will not be necessary for a controller class definition.

A concrete class

A controller concrete class DeviceController will extend a FOSRestController base class which comes with a bunch of methods to help processing requests and returning responses without being dependent on a specific format. This way a format agnostic controller is created.

<?php
// src/Ptrio/MessageBundle/Controller/DeviceController.php
namespace App\Ptrio\MessageBundle\Controller;
use App\Ptrio\MessageBundle\Model\DeviceInterface;
use App\Ptrio\MessageBundle\Model\DeviceManagerInterface;
use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class DeviceController extends FOSRestController
{
    /**
     * @var DeviceManagerInterface
     */
    private $deviceManager;
    /**
     * @var FormFactoryInterface
     */
    private $formFactory;
    /**
     * DeviceController constructor.
     * @param DeviceManagerInterface $deviceManager
     * @param FormFactoryInterface $formFactory
     */
    public function __construct(
        DeviceManagerInterface $deviceManager,
        FormFactoryInterface $formFactory
    )
    {
        $this->deviceManager = $deviceManager;
        $this->formFactory = $formFactory;
    }
    /**
     * @param string $deviceName
     * @return Response
     */
    public function getDeviceAction(string $deviceName): Response
    {
        if ($device = $this->deviceManager->findDeviceByName($deviceName)) {
            $view = $this->view($device, 200);
        } else {
            $view = $this->view(null, 404);
        }
        return $this->handleView($view);
    }
    /**
     * @param Request $request
     * @return Response
     */
    public function postDeviceAction(Request $request): Response
    {
        /** @var DeviceInterface $device */
        $device = $this->deviceManager->createDevice();
        $form = $this->formFactory->create(DeviceType::class, $device);
        $form->submit($request->request->all());
        if ($form->isSubmitted() && $form->isValid()) {
            $device = $form->getData();
            $this->deviceManager->updateDevice($device);
            $view = $this->view(null, 204);
        } else {
            $view = $this->view($form->getErrors(), 422);
        }
        return $this->handleView($view);
    }
    /**
     * @param string $deviceName
     * @return Response
     */
    public function deleteDeviceAction(string $deviceName): Response
    {
        if ($device = $this->deviceManager->findDeviceByName($deviceName)) {
            $this->deviceManager->removeDevice($device);
            $view = $this->view(null, 204);
        } else {
            $view = $this->view(null, 404);
        }
        return $this->handleView($view);
    }
}

Since there is a lot going on in our controller class, let’s go over the most important aspects found in the code above.

A controller class controller accepts two arguments: DeviceManagerInterface and FormFactoryInterface objects. The first one is a device manager object, which definition was created earlier in this Web Development series. The later one is a FormFactory instance which allows for creating a form object.

A form object is created with a FormFactory::create($type = 'Symfony\Component\Form\Extension\Core\Type\FormType', $data = null, array $options = array()) method. In the example above, two arguments are passed: a fully-qualified class name for a FormType object and an entity object created with a device manager service.

A form is submitted with a Form::submit($submittedData, $clearMissing = true) method. In our example, an array returned by a $request->request->all() method, containing form data, is passed as a $submittedData argument.

A FOSRestController::view($data = null, $statusCode = null, array $headers = []) method is responsible for creating a View object that can be assigned some data, a HTTP response code and additionally headers.

A FOSRestController::handleView(View $view) method takes care of transforming a View object to a Response object which then can be returned to a client.

A list of HTTP Codes returned by our controller

CodeMeaning
204Indicates that a request was processed successfully but no content was returned.
404A server was not able to find a given resource.
422A server was not able to process a request.
Service configuration

A DeviceController will be defined as a service since it is dependent on other services defined in a container.

Add a ptrio_message.device_controller service definition to a services.yaml file.

    # src/Ptrio/MessageBundle/Resources/config/services.yaml
    ptrio_message.device_controller:
        class: 'App\Ptrio\MessageBundle\Controller\DeviceController'
        arguments:
            - '@ptrio_message.device_manager'
            - '@form.factory'
        tags:
            - { name: controller.service_arguments }

A ptrio_message.device_controller service has to be tagged as controller.service_arguments.

Route configuration

For a controller methods to be accessible via HTTP protocol, corresponding routes have to be defined.

Create a file called routes.yaml in a src/Ptrio/MessageBundle/Resources/config directory.

# src/Ptrio/MessageBundle/Resources/config/routes.yaml
device:
    type: rest
    prefix: /
    resource: 'ptrio_message.device_controller'

In order for the route configuration to be loaded, it has to be referenced in a config/routes.yaml file.

ptrio_message:
    type: rest
    prefix: /api/v1
    resource: '@PtrioMessageBundle/Resources/config/routes.yaml'

An API endpoint for a MessageBundle related routes will be available at http://yourdomain.com/api/v1.

Note: Setting a type to rest is necessary for the routes are to be loaded by a correct RouteLoader object.

Routes defined by an application can be listed with 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}  
 --------------- -------- -------- ------ ---------------------------------------- 

Examples

The functionalities created in this tutorial allow for interacting with a web server application through a large variety of client interfaces. In this particular case, tests will be performed with Curl.

There is one more thing to do before we can jump into examples part.

Getter methods Device::getName() and Device::getToken() defined in a Device entity class are supossed to only return string values. However, during the process of adding a new device entity object, a FormFactory object calls the getters while they are not yet assigned any values, and thence null values are returned. In this situation an error will be raised and code execution interrupted since the definition and return types are different from each other. To fix the problem, Device::$name and Device::$token properties should have empty string values assigned as defaults.

    // other class declarations
    protected $name = ''; // assigned an empty value
    // other class declarations
    protected $token = ''; // assigned an empty value
Adding a new device
JSON
$ curl -H 'Content-Type: application/json' -X POST -d '{"name":"test-device","token":"example-token"}' -w 'Response code: %{http_code}\n'  http://localhost/api/v1/devices`
XML
$ curl -H 'Content-Type: application/xml' -X POST -d '<xml><name>test-device</name><token>example-token</token></xml>' -w 'Response code: %{http_code}\n'  http://localhost/api/v1/devices

A HTTP response code should be returned after executing the commands presented above.

Note: Replace test-device, example-token and http://localhost with adequate values.

Removing a device
$ curl -w 'Response code: %{http_code}\n' -X DELETE http://localhost/api/v1/devices/test-device

A HTTP response code should be returned after executing the command presented above.

Note: Replace test-device with a device name that you want to remove.

Displaying device details

JSON
curl -H 'Accept: application/json' http://localhost/api/v1/devices/iphone-piotr

If a device was found, a JSON serialized device object will be returned as a response.

{"id":1,"name":"iphone-piotr","token":"d1KeQHgkIoo:APA91bGBG7vg..."}
XML
curl -H 'Accept: application/xml' http://localhost/api/v1/devices/iphone-piotr

If a device was found, a XML serialized device object will be returned as a response.

<?xml version="1.0"?>
<response><id>1</id><name>iphone-piotr</name><token>d1KeQHgkIoo:APA91bGBG7vg...</token></response>

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]

Nice post. I’m grateful for having you as a friend @piotr42 ♩ •♬

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

Congratulations @piotr42! You have completed some achievement on Steemit and have been rewarded with new badge(s) :

Award for the number of upvotes received

Click on any badge to view your own Board of Honor on SteemitBoard.
For more information about SteemitBoard, click here

If you no longer want to receive notifications, reply to this comment with the word STOP

Upvote this notification to help all Steemit users. Learn why here!

Coin Marketplace

STEEM 0.16
TRX 0.25
JST 0.034
BTC 95780.50
ETH 2701.26
SBD 0.68