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

in polish •  7 years ago 

email-marketing-2362038_640.png

Czego nauczę się w tym poradniku?

  • Jak przechwycić parametry żądania GET podczas interakcji z RESTful 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 rozszerzony został mechanizm głosujący, tak aby umożliwić weryfikację uprawnień do interakcji z obiektem urządzenia na podstawie ról przypisanych do użytkownika wykonującego żądanie.

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 implementacji funkcjonalności Param Fetcher dostarczonej z modułem FOSRestBundle, pozwalającej na przechwytywanie parametrów GET z żądania. Przechwycone parametry mogą zostać wykorzystane np. do stronicowania wyników odnalezionych w bazie danych oraz do zmiany typu ich sortowania.

Przykłady zastosowania funkcjonalności utworzonych w tym poradniku znajdują się na jego końcu.

Jak przechwycić parametry żądania GET podczas interakcji z RESTful API?

Param fetcher będący częścią modułu FOSRestBundle pozwala na przechwytywanie parametrów GET żądania, jednocześnie umożliwiając weryfikację ich zawartości. Przechwytywane parametry oraz odpowiadające im reguły definiowane są za pomocą adnotacji umieszczonych w blokach phpdoc.

W dzisiejszym artykule, param fetcher zostanie wykorzystany do utworzenia funkcjonalności umożliwiającej stronicowanie wyników wiadomości wysłanych na konkretne urządzenie oraz pozwalającej na definiowanie typu sortowania.

Klasa repozytorium wiadomości

W tym etapie zostanie utworzona klasa repozytorium wiadomości, która posiadać będzie metodę odpowiedzialną za odnalezienie wiadomości powiązanych z konkretnym urządzeniem. Dodatkowo, wspomniana metoda pozwoli na ustawienie sortowania dla zwracanych wyników oraz definicję wartości offset oraz limit.

Wartość offset oznacza pozycję, która będzie pierwszym elementem zwracanej tablicy. Z kolei wartość limit pozwala zdefiniować ilość rekordów, licząc od pozycji wskazanej w wartości offset, które mają zostać zawarte w zwracanej tablicy.

Warstwa abstrakcyjna

Nim przejdziemy do definicji klasy właściwej repozytorium wiadomości, wskazane jest utworzenie interfejsu MessageRepositoryInterface, zawierającego deklarację dla metody findMessagesByDevice(DeviceInterface $device, string $sort, int $offset, int $limit) odpowiedzialnej za zwrócenie listy wiadomości powiązanych z konkretnym urządzeniem.

<?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;
}

Jak widać na powyższym przykładzie, metoda findMessagesByDevice(DeviceInterface $device, string $sort, int $offset, int $limit) przyjmować będzie cztery argumenty. Pierwszy to obiekt urządzenia, drugi to typ sortowania, trzeci to przesunięcie, czyli numer wiersza, od którego należy rozpocząć definiowanie wyników. Ostatnim argumentem jest jest limit, czyli maksymalna ilość wyników zwracanych w tablicy.

Klasa właściwa

Klasa właściwa repozytorium wiadomości zawierać będzie logikę określającą w jaki sposób metoda MessageRepository::findMessagesByDevice(DeviceInterface $device, string $sort = 'DESC', int $offset, int $limit) będzie obsługiwana.

<?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();
    }
}

Metoda MessageRepository::findMessagesByDevice(DeviceInterface $device, string $sort = 'DESC', int $offset, int $limit) korzysta z obiektu typu QueryBuilder w celu przygotowania zapytania w DQL, które w pierwszej kolejności zwróci wiadomości wysłane do konkretnego urządzenia, następnie posortuje je rosnąco lub malejąco po identyfikatorze wiadomości. Zwrócona zostanie liczba elementów zdefiniowana w argumencie $limit, począwszy od pozycji wskazanej w argumencie $offset.

Konfiguracja usług

Konieczne jest dodanie definicji dla usługi repozytorium ptrio_message.message_repository do pliku services.yaml, aby odpowiednie zależności zostały wstrzyknięte.

    # 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%'
Aktualizacja klasy managera wiadomości

Na tę chwilę metoda MessageManager::findMessagesByDevice(DeviceInterface $device) umożliwia jedynie odnalezienie wiadomości wysłanych na konkretne urządzenie. Aby funkcjonalność sortowania wyników oraz stronicowania mogła zostać wykorzystana przez usługę managera wiadomości, należy ją zmodyfikować, tak by korzystała z obiektu typu MessageRepository.

Warstwa abstrakcyjna

W pliku interfejsu MessageManagerInterface.php należy zaktualizować deklarację dla metody odpowiedzialnej za wyszukiwanie wiadomości wysłanych na dane urządzenie.

    // src/Ptrio/MessageBundle/Model/MessageManagerInterface.php
    // other method declarations
    public function findMessagesByDevice(DeviceInterface $device, string $sort, int $offset, int $limit): array;
Klasa właściwa

W klasie właściwej managera wiadomości należy zaktualizować blok phpdoc dla własności MessageManager::$repository, aby powiązać z nią typ abstrakcyjny MessageRepositoryInterface.

    // src/Ptrio/MessageBundle/Doctrine/MessageManager.php
    /**
     * @var MessageRepositoryInterface
     */
    private $repository;

W kolejnym kroku konieczne jest uzupełnienie konstruktora klasy MessageManager o argument zawierający instancję klasy implementującej interfejs MessageRepositoryInterface.

    /**
     * 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();
    }

Od tej chwili własność MessageManager::$repository przechowywać będzie obiekt repozytorium wiadomości.

Następnie konieczne będzie zastąpienie istniejącej definicji metody MessageManager::findMessagesByDevice(DeviceInterface $device) na tę, która znajduje się poniżej.

    /**
     * {@inheritdoc}
     */
    public function findMessagesByDevice(DeviceInterface $device, string $sort, int $offset, int $limit): array
    {
        return $this->repository->findMessagesByDevice($device, $sort, $offset, $limit);
    }
Konfiguracja usług

W ostatnim kroku należy zaktualizować definicję usługi ptrio_message.message_manager, dodając usługę ptrio_message.message_repository jako trzeci 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'
Klasa kontrolera wiadomości

Aby lista wiadomości dla konkretnego urządzenia mogła zostać zwrócona jako odpowiedź HTTP, konieczne będzie utworzenie klasy kontrolera. Będzie ona zawierać definicję dla jednej metody MessageController::getMessagesAction(string $deviceName, ParamFetcher $paramFetcher). Nad wspomnianą metodą zostaną umieszczone odpowiednie adnotacje, umożliwiające zdefiniowanie parametrów GET przechwytywanych przez obiekt typu ParamFetcherListener. W ten sposób możliwe będzie przekazanie typu sortowania, numeru strony oraz ilości wyników na stronie, poprzez dodanie wartości ?sort=sort_type&page=page_no&results_per_page=limit do ścieżki żądania.

Informacja: Ze względu na fakt, że klasa kontrolera rozszerzać będzie klasę bazową FOSRestController, tworzenie warstwy abstrakcyjnej nie będzie konieczne.

Klasa właściwa

Klasa właściwa kontrolera wiadomości korzystać będzie z usługi managera urządzeń w celu odszukania urządzenia po nazwie oraz z usługi managera wiadomości, aby przygotować listę wiadomości wysłanych na to urządzenie. Obie wspomniane usługi zostaną wstrzyknięte jako argumenty konstruktora klasy MessageController.

<?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);
    }
}

Po krótce omówmy ważniejsze aspekty klasy MessageController znajdującej się powyżej.

Parametry przechwytywane przez obiekt typu ParamFetcherListener definiowane są za pomocą adnotacji @QueryParam. Jak widać w przedstawionym przykładzie, tymi parametrami będą sort, page oraz results_per_page.

Parametr sort będzie mógł przechowywać dwie wartości: asc oraz desc. Adekwatna reguła weryfikacyjna zdefiniowana została w argumencie requirements. W przypadku gdy parametr sort nie został przekazany, zostanie wykorzystana domyślna wartość, przechowywana w argumencie default.

Parametr page przechowywał będzie wartość numeryczną, reprezentującą numer strony listy wiadomości. Domyślną wartością będzie 0.

Informacja: Ze względu na fakt, że numeracja stron zaczyna się od wartości 0, numer pierwszej strony to 0, drugiej to 1, trzeciej 2 i tak dalej.

Z kolei parametr results_per_page pozwalał będzie na przekazanie ilości wyników prezentowanych na danej stronie. W tym przypadku domyślną wartością jest 25.

Metoda MessageControllergetMessagesAction(string $deviceName, ParamFetcher $paramFetcher) przyjmuje dwa argumenty. Pierwszy to nazwa urządzenia, dla którego chcemy wyszukać wiadomości, natomiast drugi to obiekt typu ParamFetcher, który umożliwia m.in. pobranie wartości parametrów zdefiniowanych za pomocą adnotacji @QueryParam.

Informacja: Aplikacja korzysta z prześwietlenia klas i typów, dlatego też kolejność argumentów dla metod kontrolera nie ma znaczenia.

Dodatkowo, usługa mechanizmu głosującego wykorzystywana jest do weryfikacji czy dany użytkownik posiada odpowiednie uprawnienia do wyświetlenia listy wiadomości wysłanych na konkretne urządzenie.

Wartości przechwytywanych parametrów mogą zostać pobrane poprzez odwołanie się do metody ParamFetcher::get($name, $strict = null), gdzie argument $name to nazwa parametru, którego wartość chcemy pobrać.

Wartość zmiennej $offset tworzona jest poprzez pomnożenie numeru żądanej strony przez liczbę wyników na stronie.

Konfiguracja usług

Aby zależności dla kontrolera wiadomości zostały wstrzyknięte, konieczne będzie dodanie definicji usługi ptrio_message.message_controller oraz oznaczenie jej jako controller.service_arguments.

    # 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 }
Konfiguracja ścieżek

W następnym kroku wymagane jest dodanie konfiguracji dla ścieżek związanych z kontrolerem wiadomości.

# src/Ptrio/MessageBundle/Resources/config/routes.yaml
message:
    type: rest
    parent: device
    resource: 'ptrio_message.message_controller'

Parametr parent pozwala na zdefiniowanie ścieżek nadrzędnych. W tym przypadku, dla ścieżek związanych z kontrolerem wiadomości, nadrzędnymi ścieżkami będą te, powiązane z kontrolerem urządzeń.

Istniejące ś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}           
  get_device_messages   GET      ANY      ANY    /api/v1/devices/{deviceName}/messages.{_format}  
 --------------------- -------- -------- ------ -------------------------------------------------

Jak widać na powyższym przykładzie, listę wiadomości dla konkretnego urządzenia, można wyświetlić odwołując się do ścieżki /api/v1/devices/{deviceName}/messages.

Konfiguracja FOSRestBundle

W ostatnim kroku, należy zmodyfikować konfigurację modułu FOSRestBundle znajdującą się w pliku config/packages/fos_rest.yaml.

    # config/packages/fos_rest.yaml
    param_fetcher_listener: true

Poprzez ustawienie wartości true dla parametru param_fetcher_listener informujemy moduł FOSRestBundle, że parametry GET zdefiniowane w adnotacjach @QueryParam powinny być przechwytywane.

Przykłady

Listę wiadomości dla urządzenia iphone-piotr można wyświetlić odwołując się do adresu http://localhost/api/v1/devices/iphone-piotr/messages.

curl -H 'Accept: application/json' -H 'X-AUTH-TOKEN: MMYKUE63gCyFc9blOwcrdr0EP9ghb7yiWZ2lr4fRauM' http://localhost/api/v1/devices/iphone-piotr/messages

W powyższym przypadku, zostaną wykorzystane domyślne wartości dla parametrów sort, page oraz results_per_page, zostanie więc wyświetlone maksymalnie pierwsze dwadzieścia pięć wiadomości, posortowane od najnowszej do najstarszej.

W celu wyświetlenia wiadomości z drugiej strony, przy założeniu, że pięć to lista wyników na stronę, należy przesłać żądanie do ścieżki api/v1/devices/iphone-piotr/messages?page=2&results_per_page=5.

curl -H 'Accept: appllOwcrdr0EP9ghb7yiWZ2lr4fRauM' 'http://localhost/api/v1/devices/iphone-piotr/messages?page=1&results_per_page=5'

Przykładowa lista zwróconych wiadomości znajduje się poniżej.

[
  {
    "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:APA91bGBG7vg9rfX0PFbtnrY5yX68x63qGrYm7tWjNbPStOnbK8FbE14DfmWPgzd9a_ucgQRDJqGYFJ-0xG8Cg8KG-ZGyOxpzzP9KavOYq7Kpof7beNHE0vfIuFTI-P3OJfshR3s7O-k",
      "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:APA91bGBG7vg9rfX0PFbtnrY5yX68x63qGrYm7tWjNbPStOnbK8FbE14DfmWPgzd9a_ucgQRDJqGYFJ-0xG8Cg8KG-ZGyOxpzzP9KavOYq7Kpof7beNHE0vfIuFTI-P3OJfshR3s7O-k",
      "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:APA91bGBG7vg9rfX0PFbtnrY5yX68x63qGrYm7tWjNbPStOnbK8FbE14DfmWPgzd9a_ucgQRDJqGYFJ-0xG8Cg8KG-ZGyOxpzzP9KavOYq7Kpof7beNHE0vfIuFTI-P3OJfshR3s7O-k",
      "user": {
        "id": 1,
        "username": "piotr42",
        "apiKey": "MMYKUE63gCyFc9blOwcrdr0EP9ghb7yiWZ2lr4fRauM",
        "roles": [
          "ROLE_ADMIN"
        ],
        "password": null,
        "salt": null
      }
    },
    "sentAt": "2018-03-20T00:00:00+00:00"
  }
]

By posortować wiadomości od najstarszej do najnowszej, do żądania należy dodać parametr sort, o wartości asc.

curl -H 'Accept: appllOwcrdr0EP9ghb7yiWZ2lr4fRauM' 'http://localhost/api/v1/devices/iphone-piotr/messages?page=1&results_per_page=5&sort=asc'

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.

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

Award for the number of posts published
Award for the total payout received

Click on any badge to view your own Board of Honor on SteemitBoard.

To support your work, I also upvoted your post!
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!

Do not miss the last announcement from @steemitboard!