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.