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

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-qualifiedDevice
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 calledserializer
.
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
Code | Meaning |
---|---|
204 | Indicates that a request was processed successfully but no content was returned. |
404 | A server was not able to find a given resource. |
422 | A 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 correctRouteLoader
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
andhttp://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
- 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]
Nice post. I’m grateful for having you as a friend @piotr42 ♩ •♬
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
Congratulations @piotr42! You have completed some achievement on Steemit and have been rewarded with new badge(s) :
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