keyboard

Hugo Soltys

Symfony developer

Since 2013

Use API Platform with ElasticSearch instead of Doctrine in your Symfony application

Posted on by Hugo - 1474 views - 1 comment


API Platform is a REST and GraphQL framework designed to build API-driven projects.

It is pretty simply to include in a Symfony application and will be a must have if you want to provide APIs without investing a lot of time in it.

In this article we will learn how to plug API Platform with Elasticsearch for a faster response time.


As written above, API Platform is a powerful tool and can help you in so many ways.

However, it is first and foremost done to work with Doctrine ORM, and this could be a problem if you want to search into a huge database.

Typically if your project requires to search into a table containing millions of entries you will probably want to couple your database with an Elasticsearch to increase the performances.

The API Platform documentation explains it is allowed to use another searching tool than Doctrine but it doesn't show you the way to do so.

I was confronted to this problematic so I decided to write an article explaining how I did it awaiting the official integration of Elasticsearch in API Platform.

 

INSTALLING API PLATFORM

To install API Platform in your Symfony project, just open a terminal and type the following command.

cd /path/to/your/project
composer require api-platform/core

Once done, enable the bundle in your AppKernel.php file.

public function registerBundles()
{
    $bundles = [
        // your other bundles...
        new ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle(),
    ];

    return $bundles;
}

Aaaaaand it's done !

 

CONFIGURING THE BUNDLE

Now you have installed the bundle, add the configuration reference in your config.yml file.

NB : In case of the configuration reference change in the future, here is the documentation page where you can find it.

# app/config/config.yml
api_platform:

    # The title of the API.
    title: ''

    # The description of the API.
    description: ''

    # The version of the API.
    version: '0.0.0'

    # Specify a name converter to use.
    name_converter: ~

    # Specify a path name generator to use.
    path_segment_name_generator: 'api_platform.path_segment_name_generator.underscore'

    eager_loading:
        # To enable or disable eager loading.
        enabled: true

        # Fetch only partial data according to serialization groups.
        # If enabled, Doctrine ORM entities will not work as expected if any of the other fields are used.
        fetch_partial: false

        # Max number of joined relations before EagerLoading throws a RuntimeException.
        max_joins: 30

        # Force join on every relation.
        # If disabled, it will only join relations having the EAGER fetch mode.
        force_eager: true

    # Enable the FOSUserBundle integration.
    enable_fos_user: false

    # Enable the Nelmio Api doc integration.
    enable_nelmio_api_doc: false

    # Enable the Swagger documentation and export.
    enable_swagger: true

    # Enable Swagger ui.
    enable_swagger_ui: true

    oauth:
        # To enable or disable oauth.
        enabled: false

        # The oauth client id.
        clientId: ''

        # The oauth client secret.
        clientSecret: ''

        # The oauth type.
        type: 'oauth2'

        # The oauth flow grant type.
        flow: 'application'

        # The oauth token url.
        tokenUrl: '/oauth/v2/token'

        # The oauth authentication url.
        authorizationUrl: '/oauth/v2/auth'

        # The oauth scopes.
        scopes: []

    swagger:
        # The swagger api keys.
        api_keys: []      

    collection:
        # The default order of results.
        order: 'ASC'

        # The name of the query parameter to order results.
        order_parameter_name: 'order'

        pagination:
            # To enable or disable pagination for all resource collections by default.
            enabled: true

            # To allow the client to enable or disable the pagination.
            client_enabled: false

            # To allow the client to set the number of items per page.
            client_items_per_page: false

            # The default number of items per page.
            items_per_page: 30

            # The maximum number of items per page.
            maximum_items_per_page: ~

            # The default name of the parameter handling the page number.
            page_parameter_name: 'page'

            # The name of the query parameter to enable or disable pagination.
            enabled_parameter_name: 'pagination'

            # The name of the query parameter to set the number of items per page.
            items_per_page_parameter_name: 'itemsPerPage'

    mapping:
        # The list of paths with files or directories where the bundle will look for additional resource files.
        paths: []

    http_cache:
        # Automatically generate etags for API responses.
        etag: true

        # Default value for the response max age.
        max_age: ~

        # Default value for the response shared (proxy) max age.
        shared_max_age: ~

        # Default values of the "Vary" HTTP header.
        vary: ['Accept']

        # To make all responses public by default.
        public: ~

        invalidation:
          # To enable the tags-based cache invalidation system.
          enabled: false

          # URLs of the Varnish servers to purge using cache tags when a resource is updated.
          varnish_urls: []

    # The list of exceptions mapped to their HTTP status code.
    exception_to_status:
        # With a status code.
        Symfony\Component\Serializer\Exception\ExceptionInterface: 400

        # Or with a constant defined in the 'Symfony\Component\HttpFoundation\Response' class.
        ApiPlatform\Core\Exception\InvalidArgumentException: !php/const:Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST

        # ...

    # The list of enabled formats. The first one will be the default.
    formats:
        jsonld:
            mime_types: ['application/ld+json']

        json:
            mime_types: ['application/json']

        html:
            mime_types: ['text/html']

        # ...

    # The list of enabled error formats. The first one will be the default.
    error_formats:
        jsonproblem:
            mime_types: ['application/problem+json']

        jsonld:
            mime_types: ['application/ld+json']

        # ...

Now in your routing.yml file, add the following lines :

api:
    resource: '.'
    type:     'api_platform'
    prefix:   '/api' # Optional

 

USAGE

For the rest of the tutorial, we will assume that you have an Article entity mapped in a working Elasticsearch. If you use the FOSElasticaBundle, your configuration should be similar as the following one.

fos_elastica:
    clients:
        default: { host: %elastic_host%, port: %elastic_port% }
    indexes:
        my_index_name:
            types:
                article:
                    mappings:
                        title: ~
                        content: ~
                        publishedAt: ~
                        author: ~
                    persistence:
                        driver: orm
                        model: AppBundle\Entity\Article
                        provider: ~
                        finder: ~
                        repository: AppBundle\SearchRepository\ArticleRepository

 

Now, we are going to tell to API Platform that we want to our articles availables in an API. To do this, open your Article entity and add the @ApiResource annotation.

<?php

namespace AppBundle\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ApiResource
 * @ORM\Entity
 */
class Article // The class name will be used to name exposed resources
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    public $id;

    // your code here
}

 

From the moment when you add this annotation in any of your entities, it become a fully working API, but all the queries are made with Doctrine.

To use Elasticsearch instead, we will have to override some API Platform default classes called DataProviders. You can find the documentation about DataProviders on the official documentation.

In our example we have an API resource Article mapped on our Elasticsearch. Therefore we are going to create an ArticleCollectionDataProvider for the list API and a ArticleItemDataProvider for the single item API. Those providers will be two Symfony services that you can either declare yourself or let the autowire do the job if you have enabled it.

COLLECTION DATA PROVIDER

Service declaration :

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <services>
        <service id="app.data_provider.article_collection" class="AppBundle\DataProvider\ArticleCollectionDataProvider">
            <argument type="service" id="fos_elastica.repository_manager" />
            <tag name="api_platform.collection_data_provider" />
        </service>
    </services>
</container>

Provider code :

<?php

namespace AppBundle\DataProvider;

use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use AppBundle\SearchRepository\ArticleRepository;
use FOS\ElasticaBundle\Manager\RepositoryManager;

final class CollectionDataProvider implements CollectionDataProviderInterface
{
    private $repositoryManager;

    public function __construct(RepositoryManager $repositoryManager)
    {
        $this->repositoryManager = $repositoryManager;
    }

    public function getCollection(string $resourceClass, string $operationName = null)
    {
        $classAsArray = explode('\\', $resourceClass);
        $className = strtolower(end($classAsArray));

        /** @var SearchRepository $repository */
        $repository = $this->repositoryManager->getRepository('AppBundle:' . $className);

        // your custom elastic query
        $results = $repository->getQueryForMultipleItems();

        return $results;
    }
}

ITEM DATA PROVIDER

Service declaration :

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <services>
        <service id="app.data_provider.article_item" class="AppBundle\DataProvider\ArticleItemDataProvider">
            <argument type="service" id="fos_elastica.repository_manager" />
            <tag name="api_platform.item_data_provider" />
        </service>
    </services>
</container>

Provider code :

<?php

namespace AppBundle\DataProvider;

use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
use AppBundle\SearchRepository\ArticleRepository;
use FOS\ElasticaBundle\Finder\TransformedFinder;
use FOS\ElasticaBundle\Manager\RepositoryManager;

class ItemDataProvider implements ItemDataProviderInterface
{
    private $repositoryManager;

    public function __construct(RepositoryManager $repositoryManager)
    {
        $this->repositoryManager = $repositoryManager;
    }

    public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])
    {
        $classAsArray = explode('\\', $resourceClass);
        $className = strtolower(end($classAsArray));

        /** @var SearchRepository $repository */
        $repository = $this->repositoryManager->getRepository('AppBundle:' . $className);

        // your custom elastic query
        $results = $repository->getQueryForSingleItem($id);

        if (is_array($results) && array_key_exists(0, $results)) {
            return $results[0];
        }

        return null;
    }
}

So those providers will allow you to search for articles with Elasticsearch instead of a classic Doctrine query builder. If you want to filter your API like searching a specific title or something, you can pass the request_stack service to your provider and use the query parameters to make your elastic query more precise.

NB : I know this may be not the best way to make your API Platform working with Elasticsearch but this is a very quick and easy way to do so. In the case of you want to list some resources from a very large database with a good response time this solution will help you.

I hope this tutorial helped you. I made it to answer the lack of help concerning ES with API Platform, but in my opinion I think it would be better to wait a complete official integration of other sources than Doctrine, that should be come in the following monthes.

Hugo. 

 


Hugo Soltys

My name is Hugo, I'm 25 and I'm a Symfony developer since 2013. I love to create websites by myself to learn new technologies or increase my skills.
I love movies, books, music and video games. I also like to drink a few beers with my friends. I'm from Lille (France) and I currently work as Symfony developer at Decathlon since 2016. Before that, I worked as Symfony developer for the IT Room company, in Roubaix, France.