Symfony ElasticSearch – Front Controller and API documentation

S

Hi, and welcome to the 3d article devoted to the theme:  “How to work with ElasticSearch using Symfony PHP framework”. Previous article (Part 2: Symfony ElasticSearch and docker environment) is located here. Here we will start to investigate symfony skeleton project. But at first it would be great to refresh at mind our architecture scheme from the Part 1: Symfony ElasticSearch

Search microservice architecture
Search microservice architecture

 All starts from the Controller. So let’s find it at project structure. It is located the src/Controller folder. Here you will find a SearchController file with according php class.

Below is code of SearchController class

<?php

namespace App\Controller;

use App\DependencyInjection\HotelSearch\Search;
use App\DependencyInjection\HotelSearchCriteria\HotelSearchCriteriaUrlBuilder;
use App\DependencyInjection\HotelSearchCriteria\HotelSearchCriteriaDirector;
use App\Model\Response\SimpleSearchItemView;
use Exception;
use FOS\RestBundle\Controller\AbstractFOSRestController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Swagger\Annotations as SWG;
use Nelmio\ApiDocBundle\Annotation\Model;
use App\DependencyInjection\Swagger\Annotation\OperationWithParametersModel;
use App\Model\Request\SearchRequestView;
use App\Model\Response\SearchResponseView;

/**
 * @Route("/api/search", options={"expose": true})
 */
class SearchController extends AbstractFOSRestController
{
    /**
     * @Route("/hotels", name="get_hotels_search", methods={"GET"})
     * @OperationWithParametersModel(
     *     summary="Get list of hotels",
     *     tags={"search"},
     *     parametersModel=SearchRequestView::class,
     *     @SWG\Response(
     *        response=200,
     *        description="List of hotels",
     *        @Model(type=SearchResponseView::class)
     *     ),
     *     @SWG\Response(
     *         response="500",
     *         description="Error"
     *     )
     * )
     *
     * @param Search $search
     * @param Request $request
     * @return Response
     * @throws Exception
     */
    public function getHotelsSearchAction(Search $search, Request $request): Response
    {
        $requestQueryData = $request->query->all();
        $builder = new HotelSearchCriteriaUrlBuilder($requestQueryData);

        $director = new HotelSearchCriteriaDirector($builder);
        $director->buildCriteria();

        $results = $search->searchRaw($director->getCriteria());

        $itemsViewsArray = [];
        foreach ($results as $item) {
            $itemsViewsArray[] = SimpleSearchItemView::createFromHotel($item);
        }

        $resultsView = new SearchResponseView();
        $resultsView->setResults($itemsViewsArray);
        $view = $this->view($resultsView);

        return $this->handleView($view);
    }
}

Please, take a look at current controller annotations. That is used by nelmio bundle which is able to read it properly and create ready documentation for our api. And that url api/doc is also created by nelmio bundle. I will try to explain briefly what we have here.

We are saying that our API point is able to process all parameters that are defined by the SearchRequestView model. Let’s go inside and see what we have.

<?php

namespace App\Model\Request;

use Swagger\Annotations as SWG;
use App\Entity\HotelSearchCriteria;

class SearchRequestView
{
    /**
     * Page number
     * @SWG\Parameter(
     *     default=1,
     *     minimum=1
     * )
     * @var int
     */
    protected $page = 1;

    /**
     * Page size
     * @SWG\Parameter(
     *     default=HotelSearchCriteria::DEFAULT_SIZE,
     *     minimum=HotelSearchCriteria::SIZE_MIN,
     *     maximum=HotelSearchCriteria::SIZE_MAX
     * )
     * @var int
     */
    protected $size = 10;
    /**
     * Hotel name
     * @var string|null
     */
    protected $n;

    /**
     * City name at English
     * @var string|null
     */
    protected $c;

    /**
     * Latitude of the city center
     * @var double|null
     */
    protected $lat;

    /**
     * Longitude of the city center
     * @var double|null
     */
    protected $lng;

    /**
     * Hotel stars
     * @var int|null
     */
    protected $stars;

    /**
     * Hotel free places are present
     * @var bool|null
     */
    protected $fpn;

    /**
     * Hotel age
     * @var int|null
     */
    protected $age;

    /**
     * @return int
     */
    public function getPage(): int
    {
        return $this->page;
    }

    /**
     * @param int $page
     */
    public function setPage(int $page): void
    {
        $this->page = $page;
    }

    /**
     * @return int
     */
    public function getSize(): int
    {
        return $this->size;
    }

    /**
     * @param int $size
     */
    public function setSize(int $size): void
    {
        $this->size = $size;
    }

    /**
     * @return string|null
     */
    public function getN(): ?string
    {
        return $this->n;
    }

    /**
     * @param string|null $n
     */
    public function setN(?string $n): void
    {
        $this->n = $n;
    }

    /**
     * @return string|null
     */
    public function getC(): ?string
    {
        return $this->c;
    }

    /**
     * @param string|null $c
     */
    public function setC(?string $c): void
    {
        $this->c = $c;
    }

    /**
     * @return float|null
     */
    public function getLat(): ?float
    {
        return $this->lat;
    }

    /**
     * @param float|null $lat
     */
    public function setLat(?float $lat): void
    {
        $this->lat = $lat;
    }

    /**
     * @return float|null
     */
    public function getLng(): ?float
    {
        return $this->lng;
    }

    /**
     * @param float|null $lng
     */
    public function setLng(?float $lng): void
    {
        $this->lng = $lng;
    }

    /**
     * @return int|null
     */
    public function getStars(): ?int
    {
        return $this->stars;
    }

    /**
     * @param int|null $stars
     */
    public function setStars(?int $stars): void
    {
        $this->stars = $stars;
    }

    /**
     * @return bool|null
     */
    public function getFpn(): ?bool
    {
        return $this->fpn;
    }

    /**
     * @param bool|null $fpn
     */
    public function setFpn(?bool $fpn): void
    {
        $this->fpn = $fpn;
    }

    /**
     * @return int|null
     */
    public function getAge(): ?int
    {
        return $this->age;
    }

    /**
     * @param int|null $age
     */
    public function setAge(?int $age): void
    {
        $this->age = $age;
    }
}

That is all familiar properties we already discussed at initial article “ElasticSearch how to build search system”. I will repeat here our main task scheme. (p.s if it is the first article which you start reading from, maybe you would like to read the previous articles in ascending order – in that case you may choose symfony tag at menu – that will display articles related to current theme sorted by date)

ElasticSearch search
ElasticSearch search

Another model is used for response. We are saying that our response would be represented as a list of SimpleSearchItemView items

<?php

namespace App\Model\Response;

class SearchResponseView
{
    /**
     * List of the searching results (auctions).
     * @var SimpleSearchItemView[]
     */
    private $results;

    /**
     * @return SimpleSearchItemView[]
     */
    public function getResults(): array
    {
        return $this->results;
    }

    /**
     * @param SimpleSearchItemView[] $results
     */
    public function setResults(array $results): void
    {
        $this->results = $results;
    }
}

Let’s have a look at SimpleSearchItemView class inside

<?php

namespace App\Model\Response;

class SimpleSearchItemView
{
    /**
     * Hotel name
     * @var string|null
     */
    protected $name;

    /**
     * Hotel stars
     * @var int|null
     */
    protected $stars;

    /**
     * @return string|null
     */
    public function getName(): ?string
    {
        return $this->name;
    }

    /**
     * @param string|null $name
     */
    public function setName(?string $name): void
    {
        $this->name = $name;
    }

    /**
     * @return int|null
     */
    public function getStars(): ?int
    {
        return $this->stars;
    }

    /**
     * @param int|null $stars
     */
    public function setStars(?int $stars): void
    {
        $this->stars = $stars;
    }

    public static function createFromHotel(array $document)
    {
        $view = new static;
        $hotel = $document['_source']['hotel'];

        $view->name = $hotel['name'];
        $view->stars = $hotel['stars'];

        return $view;
    }
}

In human words all that means next – after taking all parameters that defined at SearchRequestView, API point returns the list of hotels, where every hotel represented by name and star.

There is one more interesting thing at annotations. It is OperationWithParametersModel annotation, which is created by me. There is a little problem with using models at GET requests. It is possible to use models for describing allowed input parameters while using POST method, but not with GET, hard to say why. To preserve REST API consistency I prefer to have GET but still use the model. To resolve the problem I created custom annotation that extends Swagger operation and class reflection to create parameters from model. You may check how it looks by sighing to my course at udemy (at the end of the article you will find link and coupon) .

So, what is for all those annotations? Lets open our api doc page.

As you see our GET API point was automatically recognized by the nelmio bundle, moreover if we will expand it – you will see all parameters that can be processed by our API point.

We also  can see how the response looks and even how our models look inside.

Wow, cool isn’t it?  We have got clear documentation, now any front end developer is able to make integration with our microservice as he knows exactly what parameters we expect and what the final response would be. That is how a good API should look like. So, great, we already have a controller with a well documented API. At the next lecture (Part 4: Symfony elasticsearch – builder pattern and DTO search criteria object) we will speak about the builder pattern and DTO search criteria object. If you would like to pass all material more fast, then I propose you to view my on-line course at udemy where you will also find full project skeleton. Below is the link to the course. As the reader of that blog you are also getting possibility to use coupon for the best possible low price. Otherwise, please wait at next articles. Thank you for you attention.


architecture AWS cluster cyber-security devops devops-basics docker elasticsearch flask geo high availability java machine learning opensearch php programming languages python recommendation systems search systems spring boot symfony