Web Development z Symfony: Aplikacja serwerowa do wysyłki powiadomień push z FCM [część 3]

in polish •  7 years ago  (edited)

email-marketing-2362038_640.png

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 pozycji
  • 422 - 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 oraz http://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>

Curriculum

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

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.