Czego nauczę się w tym poradniku?
- Jak utworzyć formularz dla encji
- Jak dodać walidację do formularza encji
- Jak utworzyć kontroler we wzorcu REST/SOAP API
Wymagania
- System z rodziny UNIX/Linux
- Serwer Apache2 z PHP w wersji 7.1
- Baza danych MySQL
- Projekt Symfony pobrany stąd
- Dowolny edytor tekstowy
- Istniejący projekt w Firebase
- Narzędzie composer
Poziom trudności
- Średni
Treść poradnika
Poradnik jest kolejnym z serii Web Development, gdzie opisywane są procesy budowy aplikacji serwerowej w Symfony, zdolnej do wysyłki powiadomień push na urządzenia mobilne. W poprzednim artykule przedstawione zostały m.in. zagadnienia związane z tworzeniem encji, usługi do zarządzania encją oraz komendy konsolowej. Dodatkowo przedstawiony został koncept kontenera dla usług i możliwości jakie oferuje.
Przykładowy projekt wykorzystywany na potrzeby tego poradnika znajduje się tutaj. Jest to w zasadzie kod, który otrzymamy po ukończeniu poprzedniego kompendium. Wskazanym jest pobranie wspomnianego projektu z repozytorium github.
Jakie aspekty Web Development z Symfony zostaną przedstawione w tym poradniku?
- Proces tworzenia klasy formularza dla encji, który pozwoli m.in. na dodawanie nowych obiektów encji za pomocą metody POST w protokole HTTP.
- Czynności potrzebne do implementacji mechanizmu walidacji dla encji, pozwalającą na weryfikację danych wprowadzonych do pól obiektu encji.
- Proces tworzenia kontrolera z zastosowaniem wzorca REST API, zdolnego do przetwarzania danych w formatach JSON i XML.
Przykłady zastosowania funkcjonalności utworzonych w tym poradniku znajdują się na jego końcu.
Jak utworzyć formularz dla encji?
Komponent formularza ułatwi pracę z formularzami każdemu deweloperowi zajmującemu się aplikacjami webowymi. Wykorzystuje on wzorzec programowania Builder do utworzenia reprezentacji formularza dla konkretnej klasy encji. Ponadto stosowanie oddzielnych klas dla formularzy umożliwia tworzenie kodu, który może zostać użyty wielokrotnie bez konieczności jego powtarzania.
Komponent odpowiedzialny za obsługę formularzy może zostać zainstalowany za pomocą narzędzia Composer.
$ composer require form
Klasa formularza
Na tym etapie zostania utworzona klasa formularza DeviceType
, która pozwoli na dodawanie nowych obiektów encji urządzenia za pomocą metody POST będącej częścią protokołu HTTP. Ze względu na fakt, że klasa abstrakcyjna formularza AbstractType
, zawierająca podstawowe funkcjonalności potrzebne do utworzenia nowego obiektu formularza, jest domyślnie dostarczona z komponentem formularza, nie jest konieczne tworzenie warstwy abstrakcyjnej.
Klasa właściwa
W klasie właściwej DeviceType
znajdować się będą deklaracje dla obiektu formularza urządzenia.
Należy utworzyć katalog src/Ptrio/MessageBundle/Form/Type
i dodać do niego plik DeviceType.php
.
<?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,
]);
}
}
Omówmy teraz pokrótce poszczególne elementy klasy formularza DeviceType
.
Pełna nazwa klasy encji urządzenia (wraz z przestrzenią nazw) przekazywana jest jako argument konstruktora klasy formularza. Takie rozwiązanie pozwala uniknąć umieszczania sztywnych wartości w kodzie, co z kolei pozwala na łatwą zmianę klasy encji na inną w przyszłości.
Metoda AbstractType::buildForm(FormBuilderInterface $builder, array $options)
umożliwia budowę formularza za pomocą obiektu typu FormBuilderInterface
. Jak widać na powyższym przykładzie, formularz urządzenia posiadał będzie dwa pola: name
oraz token
. Typy dla wymienionych pól zostaną automatycznie zdefiniowane na podstawie informacji znajdujących się w klasie encji Device
.
Informacja: Nazwy pól muszą pokrywać się z tymi, które zadeklarowane są w klasie encji.
Metoda AbstractType::configureOptions(OptionsResolver $resolver)
pozwala na przygotowanie domyślnej konfiguracji dla obiektu formularza. Obiekt typu OptionsResolver
to zdaniem twórców frameworka Symfony funkcja array_replace
na sterydach. Pozwala on m.in. na utworzenie systemu opcji formularza, definiowanie wymaganych pól, tworzenie konfiguracji i walidacji oraz na normalizację danych. W powyższym przykładzie zdefiniowana została klasa encji powiązana z formularzem 'base_class' => $this->class
. Ze względu na fakt, że obiekt urządzenia będzie mógł zostać dodany w konwencji REST API bez renderowania formularza, wyłączona została ochrona przed atakami CSRF, odpowiedzialna za dodawanie pola _token
do pozostałych pól formularza.
Konfiguracja usług
Konieczne jest przygotowanie definicji dla usługi formularza ptrio_message.device_type
, aby pełna nazwa klasy encji mogła zostać przekazana do obiektu formularza.
Do pliku src/Ptrio/MessageBundle/Resources/config/services.yaml
należy poniższą dodać definicję.
# 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' }
Parametr kontenera ptrio_message.model.device.class
zawierający pełną nazwę klasy encji Device
został przekazany jako jeden z argumentów usługi. Dodatkowo usługa formularza została oznaczona jako form.type
.
Jak dodać walidację do formularza encji?
Parametry name
i token
encji urządzenia Device
są polami wymaganymi dla każdego obiektu urządzenia. Aby uniemożliwić przesyłanie pustych wartości dla wspomnianych pól należy dodać algorytm walidacji, który zweryfikuje czy wprowadzone wartości odpowiadają zdefiniowanym wymaganiom.
Przed dodaniem walidacji, należy zainstalować odpowiedni komponent za pomocą narzędzia Composer.
$ composer require validator
Walidację dodać można poprzez umieszczenie odpowiednich annotacji nad poszczególnymi własnościami klasy encji Device
.
Na początku należy umieścić import przestrzeni nazw walidatora i nadać mu odpowiedni alias. Wspomniany proces odbywa się przy zastosowaniu operatora use
. Poniższy kod należy umieścić pomiędzy operatorami namespace
i class
.
use Symfony\Component\Validator\Constraints as Assert;
Następnie należy umieścić annotację @Assert\NotBlank()
nad deklaracjami pól Device::$name
oraz Device::$token
. Efekt końcowy widoczny jest poniżej.
// src/Ptrio/MessageBundle/Entity/Device.php
/**
* @ORM\Column(type="string")
* @Assert\NotBlank()
*/
protected $name;
/**
* @ORM\Column(type="string")
* @Assert\NotBlank()
*/
protected $token;
Jak utworzyć kontroler we wzorcu REST/SOAP API?
Na tym etapie można rozpocząć proces tworzenia kontrolera DeviceController
we wzorcu REST/SOAP API. Przyjęta konwencja umożliwi łatwiejszą obsługę żądań i odpowiedzi bez konieczności tworzenia interfejsu graficznego dla aplikacji serwerowej.
REST/SOAP jest zbiorem reguł, które definiują sposób operacji na zapytaniach do API, czyli interfejsu programistycznego aplikacji. Żądania i odpowiedzi we wzorcu REST korzystają z formatu JSON. W przypadku SOAP, zastosowany format to XML.
Do obsługi żądań i odpowiedzi we wzorcu REST/SOAP zostanie wykorzystany komponent FOSRestBundle
, który znacznie przyspiesza prace związane z tworzeniem aplikacji REST/SOAP.
Ze względu na fakt, że dane przetwarzane będą w formatach JSON lub XML konieczna jest również instalacja komponentu odpowiadającego za ich serializację.
$ composer require serializer
Następnie, komponent FOSRestBundle
można zainstalować za pomocą narzędzia Composer.
$ composer require friendsofsymfony/rest-bundle
Przed przejściem do następnego kroku należy skonfigurować komponent, zastępując kod fos_rest: ~
znajdujący się w pliku config/packages/fos_rest.yaml
tym, który znajduje się poniżej.
# config/packages/fos_rest.yaml
fos_rest:
format_listener:
rules:
- { path: ^/api/v1, prefer_extension: true, fallback_format: json, priorities: [ json, xml ] }
Powyższa konfiguracja umożliwi komponentowi nasłuchiwanie zapytań do podanego endpointu API. W zależności od nagłówków przesłanych przez klienta, dane w odpowiednim formacie zostaną zwrócone przez kontroler.
Klasa kontrolera
Klasa kontrolera zawierać będzie definicje dla metod umożliwiających dodanie nowego urządzenia, usunięcie go oraz wyświetlenie szczegółów dotyczących konkretnego urządzenia. Urządzenie zostanie odnalezione po nazwie przekazanej w ścieżce.
Informacja: Komponent
FOSRestBundle
domyślnie zawiera bazową klasę dla kontrolera, dlatego też tworzenie warstwy abstrakcyjnej nie będzie konieczne w niniejszym przypadku.
Klasa właściwa
Klasa właściwa kontrolera DeviceController
rozszerzać będzie klasę bazową FOSRestController
, która m.in. pozwala na tworzenie kontrolerów z automatyczną serializacją danych do formatu JSON lub XML w zależności od nagłówków dodanych do żądania (tzw. format agnostic controller). Dane formularza przesyłane metodą POST mogą również być zdefiniowane w jednym ze wspomnianych formatów.
Należy utworzyć katalog src/Ptrio/MessageBundle/Controller
i dodać do niego plik DeviceController.php
.
<?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);
}
}
Pokrótce omówmy kluczowe elementy zdefiniowane w powyższym przykładzie.
Klasa kontrolera przyjmuje w konstruktorze dwa argumenty: obiekt typu DeviceManagerInterface
oraz obiekt typu FormFactoryInterface
. Ten pierwszy to usługa managera urządzeń utworzona wcześniej. Drugi obiekt to fabryka formularzy, umożliwiająca utworzenie konkretnego obiektu formularza. Nowy obiekt formularza może zostać utworzony za pomocą metody FormFactory::create($type = 'Symfony\Component\Form\Extension\Core\Type\FormType', $data = null, array $options = array())
, w której jako argumenty należy przekazać nazwę klasy formularza DeviceType::class
, obiekt encji oraz ewentualne opcje związane z konfiguracją formularza.
Formularz może zostać przedłożony za pomocą metody Form::submit($submittedData, $clearMissing = true)
, gdzie przedłożonymi danymi jest tablica zwracana przez metodę $request->request->all()
.
Metoda FOSRestController::view($data = null, $statusCode = null, array $headers = [])
jest odpowiedzialna za przygotowanie obiektu widoku zwracanego przez kontroler. Argumenty, które przyjmuje to dane zwracane przez kontroler, numer kodu HTTP oraz nagłówki.
Metoda FOSRestController::handleView(View $view)
jest odpowiedzialna za przetworzenie obiektu widoku na obiekt typu Response
.
Kody HTTP zwracane przez kontroler to:
204
- informujący, że zapytanie zostało pomyślnie przetworzone przez serwer i nie zwracane są żadne dane;404
- serwer nie był w stanie odnaleźć szukanej pozycji422
- serwer nie był w stanie przetworzyć zapytania
Konfiguracja usług
Kontroler DeviceController
zdefiniowany będzie jako usługa ze względu na fakt, że jego klasa przyjmuje argumenty w konstruktorze. W związku z tym definicję usługi ptrio_message.device_controller
należy dodać do pliku services.yaml
.
# 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 }
Usługa ptrio_message.device_controller
została oznaczona jako kontroler za pomocą tagu controller.service_arguments
.
Konfiguracja ścieżek
Aby metody kontrolera dostępne były przez protokół HTTP należy skonfigurować ścieżki routes.
W katalogu src/Ptrio/MessageBundle/Resources/config
koniecznym jest utworzenie pliku routes.yaml
.
# src/Ptrio/MessageBundle/Resources/config/routes.yaml
device:
type: rest
prefix: /
resource: 'ptrio_message.device_controller'
Aby konfiguracja ścieżek została zaimplementowana w projekcie, należy do pliku config/routes.yaml
dodać poniższą definicję.
ptrio_message:
type: rest
prefix: /api/v1
resource: '@PtrioMessageBundle/Resources/config/routes.yaml'
Endpoint API dostępny będzie pod adresem http://yourdomain.com/api/v1
.
Informacja: Należy pamiętać o umieszczenie typu
rest
w obu konfiguracjach ścieżek (dla komponentu i dla projektu) aby zostały one poprawnie wczytane.
Dostępne ścieżki można wyświetlić za pomocą polecenia php bin/console debug:router
.
--------------- -------- -------- ------ ----------------------------------------
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}
--------------- -------- -------- ------ ----------------------------------------
Przykłady
Utworzone funkcjonalności pozwolą na zarządzanie urządzeniami za pomocą protokołu HTTP. Polecenia będą testowane za pomocą narzędzia Curl.
Nim jednak wspomniane funkcjonalności zostaną przetestowane, konieczna jest jeszcze drobna modyfikacja klasy encji Device
.
Metody getters Device::getName()
oraz Device::getToken()
zdefiniowane są tak, aby zwracać tylko dane typu string. Podczas tworzenia nowej encji urządzenia w kontrolerze, obiekt fabryki formularzy FormFactory
przetwarzając ją odwołuje się do getterów, które się w niej znajdują. W momencie gdy opisywany proces ma miejsce, własności Device::$name
i Device::$token
nie posiadają jeszcze żadnej wartości, a więc są typu null
. Z tego powodu zostanie podniesiony wyjątek informujący o tym, że zwracana wartość nie jest zgodna z deklaracją i skrypt zostanie przerwany. Rozwiązaniem wspomnianego problemu jest ustawienie domyślnych pustych wartości dla własności Device::$name
i Device::$token
.
// other class declarations
protected $name = ''; // set a default value here
// other class declarations
protected $token = ''; // set a default value here
Dodawanie nowych urządzeń
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
Odpowiedź z aplikacji serwerowej zawierać będzie kod HTTP informujący o tym, czy zapytanie powiodło się.
Informacja: Wartości
test-device
,example-token
orazhttp://localhost
należy zastąpić swoimi własnymi.
Usuwanie nowych urządzeń
$ curl -w 'Response code: %{http_code}\n' -X DELETE http://localhost/api/v1/devices/test-device
Odpowiedź z aplikacji serwerowej zawierać będzie kod HTTP informujący o tym, czy zapytanie powiodło się.
Informacja: Wartość
test-device
należy zastąpić nazwą urządzenie, które chcemy usunąć.
Wyświetlanie szczegółów na temat urządzenia
JSON
curl -H 'Accept: application/json' http://localhost/api/v1/devices/iphone-piotr
Odpowiedź z serwera:
{"id":1,"name":"iphone-piotr","token":"d1KeQHgkIoo:APA91bGBG7vg..."}
XML
curl -H 'Accept: application/xml' http://localhost/api/v1/devices/iphone-piotr
Odpowiedź z serwera:
<?xml version="1.0"?>
<response><id>1</id><name>iphone-piotr</name><token>d1KeQHgkIoo:APA91bGBG7vg...</token></response>
Congratulations! This post has been upvoted from the communal account, @minnowsupport, by piotr42 from the Minnow Support Project. It's a witness project run by aggroed, ausbitbank, teamsteem, theprophet0, someguy123, neoxian, followbtcnews, and netuoso. The goal is to help Steemit grow by supporting Minnows. Please find us at the Peace, Abundance, and Liberty Network (PALnet) Discord Channel. It's a completely public and open space to all members of the Steemit community who voluntarily choose to be there.
If you would like to delegate to the Minnow Support Project you can do so by clicking on the following links: 50SP, 100SP, 250SP, 500SP, 1000SP, 5000SP.
Be sure to leave at least 50SP undelegated on your account.
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit