SlideShare a Scribd company logo
France Télévisions
Leveraging API Platform
for our content APIs
2023 SERIES OF EVENT
New York
May 16&17
Australia
October 11&12
Singapore
April 12&13
Helsinki & North
June 5&6
Paris
SEPTEMBER
London
November
15&16
June 28-30
SILICON VALLEY
March 14&15
Dubai & Middle East
February 22&23
France Télévisions :
Leveraging API Platform
for our content APIs
@gknjockbot
01 - whoami
Georges-King
Strasbourg, Grand-Est, France
Software Engineer since 2009
Freelance IT Consultant
@gknjockbot
02 - France Télévisions
News Articles
TV Broadcasts
@gknjockbot
02 - France Télévisions
866 755 articles
29M visitors / month
860 770 articles
157M visitors / month
220 771 articles
11M visitors / month
@gknjockbot
03 - What are we trying to solve ?
Franceinfo needed rebuilding :
@gknjockbot
03 - What are we trying to solve ?
Franceinfo needed rebuilding :
● Front-Back coupling
@gknjockbot
03 - What are we trying to solve ?
Franceinfo needed rebuilding :
● Front-Back coupling
● Competing DB access
@gknjockbot
03 - What are we trying to solve ?
Franceinfo needed rebuilding :
● Front-Back coupling
● Competing DB access
● Obsolete Technology
@gknjockbot
03 - What are we trying to solve ?
Franceinfo needed rebuilding :
● Front-Back coupling
● Competing DB access
● Obsolete Technology
● Monolithic platform
@gknjockbot
04 - The proposed architecture
@gknjockbot
05 - Choosing an implementation
Proof Of Concept
MariaDB MongoDB ElasticSearch
PHP / Symfony Doctrine ORM Doctrine ODM -
Node.js / Koa Sequelize Mongoose Built In
@gknjockbot
05 - Choosing an implementation
Cons Pros
Node / ElasticSearch
Reindexing
Scalability
Performance
Resource consumption
Node / MariaDB
Performance
Sequelize
Simple
Node / MongoDB Reindexing
Performance
Mongoose
Symfony / MongoDB New DB
Unified stacks (Front/Back)
ODM / Framework
Symfony / MariaDB
Well known DB stack internally
Unified stacks (Front/Back)
ORM / Framework
@gknjockbot
05 - Choosing an implementation
@gknjockbot
05 - Choosing an implementation
@gknjockbot
06 - Towards a shared platform
@gknjockbot
06 - Towards a shared platform
@gknjockbot
06.1 - Introducing PIC - Plateforme d’Information Commune
@gknjockbot
06.2 - The challenges for PIC
Additional concerns :
● Must share editorial concepts
● Must share a generic codebase
● Must have dedicated deployments
@gknjockbot
06.2 - PIC platform high level overview
@gknjockbot
07 - The Tech Stack
@gknjockbot
08 - API Front
@gknjockbot
08 - API Front
API Call : GET /contents?taxonomy=les-jeux-olympiques/paris-2024
Endpoint : /contents
Filter : taxonomy=les-jeux-olympiques/paris-2024
Groups : default, content
@gknjockbot
08 - API Front
@gknjockbot
08 - API Front
API Call : GET /contents/<id>
Endpoint : /contents
Filter : N/A
Groups : default, content, media, taxonomy
@gknjockbot
08 - API Front / resource endpoints
class DirectTv extends Content
{
// ...
}
@gknjockbot
08 - API Front / resource endpoints
#[ORMEntity]
#[ORMTable(name: 'direct_tv' )]
class DirectTv extends Content
{
// ...
}
@gknjockbot
08 - API Front / resource endpoints
#[ApiResource (
collectionOperations: [
'list' => [
'method' => 'GET',
'normalization_context' => ['enable_max_depth' => true]
]],
itemOperations: [
'get' => [
'method' => 'GET'
]],
attributes: [
'order' => ['lastPublicationDate' => 'DESC'],
'filters' => ['snapshot.code' , 'media.type' ]
]
)]
#[ApiFilter(ProgramFilter ::class, properties: ['program.channel' ,
'program.type' ])]
#[ApiFilter(BeforeBeginDateFilter ::class)]
#[ApiFilter(OrderBeginDateFilter ::class)]
#[ORMEntity]
#[ORMTable(name: 'direct_tv' )]
class DirectTv extends Content
{
// ...
}
@gknjockbot
08 - API Front / resource endpoints
#[ApiResource (
collectionOperations: [
'list' => [
'method' => 'GET',
'normalization_context' => ['enable_max_depth' => true]
]],
itemOperations: [
'get' => [
'method' => 'GET'
]],
attributes: [
'order' => ['lastPublicationDate' => 'DESC'],
'filters' => ['snapshot.code' , 'media.type' ]
]
)]
#[ApiFilter(ProgramFilter ::class, properties: ['program.channel' ,
'program.type' ])]
#[ApiFilter(BeforeBeginDateFilter ::class)]
#[ApiFilter(OrderBeginDateFilter ::class)]
#[ORMEntity]
#[ORMTable(name: 'direct_tv' )]
class DirectTv extends Content
{
// ...
}
@gknjockbot
08 - API Front / resource payload
class DirectTv extends Content
{
private ?string $presentation = null;
private ?bool $isSubject = false;
public function getPresentation (): ?string
{
return $this->presentation ;
}
public function isIsSubject (): ?bool
{
return $this->isSubject;
}
}
@gknjockbot
08 - API Front / resource payload
class DirectTv extends Content
{
#[ORMColumn(type: 'text', nullable: true)]
private ?string $presentation = null;
#[ORMColumn(type: 'boolean', nullable: true, options: ['default' => 0])]
private ?bool $isSubject = false;
public function getPresentation (): ?string
{
return $this->presentation ;
}
public function isIsSubject (): ?bool
{
return $this->isSubject;
}
}
@gknjockbot
08 - API Front / resource payload
class DirectTv extends Content
{
#[Groups(['content:get:default' ])]
#[ApiProperty (attributes: ['swagger_context' => ['example' => 'JT du mercredi 6 février 2019' ]])]
#[ORMColumn(type: 'text', nullable: true)]
private ?string $presentation = null;
#[Groups(['content:get:default' ])]
#[ApiProperty ()]
#[ORMColumn(type: 'boolean', nullable: true, options: ['default' => 0])]
private ?bool $isSubject = false;
public function getPresentation (): ?string
{
return $this->presentation ;
}
public function isIsSubject (): ?bool
{
return $this->isSubject;
}
}
@gknjockbot
08 - API Front / resource payload
class DirectTv extends Content
{
#[Groups(['content:get:default' ])]
#[ApiProperty (attributes: ['swagger_context' => ['example' => 'JT du mercredi 6 février 2019' ]])]
#[ORMColumn(type: 'text', nullable: true)]
private ?string $presentation = null;
#[Groups(['content:get:default' ])]
#[ApiProperty ()]
#[ORMColumn(type: 'boolean', nullable: true, options: ['default' => 0])]
private ?bool $isSubject = false;
public function getPresentation (): ?string
{
return $this->presentation ;
}
public function isIsSubject (): ?bool
{
return $this->isSubject;
}
}
@gknjockbot
08 - API Front / resource payload
class DirectTv extends Content
{
#[Groups(['content:get:default' ])]
#[ApiProperty (attributes: ['swagger_context' => ['example' => 'JT du mercredi 6 février 2019' ]])]
#[ORMColumn(type: 'text', nullable: true)]
private ?string $presentation = null;
#[Groups(['content:get:default' ])]
#[ApiProperty ()]
#[ORMColumn(type: 'boolean', nullable: true, options: ['default' => 0])]
private ?bool $isSubject = false;
public function getPresentation (): ?string
{
return $this->presentation ;
}
public function isIsSubject (): ?bool
{
return $this->isSubject;
}
}
@gknjockbot
09 - API Back
@gknjockbot
09 - API Back
@gknjockbot
#[ApiResource ( /* ... */ )]
#[ApiFilter (GroupFilter ::class, arguments: [
'parameterName' => 'groups',
'overrideDefaultGroups' => true,
'whitelist' => ['indexation' , 'sdk', 'details']
])]
#[ApiFilter ( /* ... */ )]
abstract class Content implements Lockable, Loggable
{
// ...
}
09 - API Back
@gknjockbot
#[ApiResource ( /* ... */ )]
#[ApiFilter (GroupFilter ::class, arguments: [
'parameterName' => 'groups',
'overrideDefaultGroups' => true,
'whitelist' => ['indexation' , 'sdk', 'details']
])]
#[ApiFilter ( /* ... */ )]
abstract class Content implements Lockable, Loggable
{
// ...
}
09 - API Back
@gknjockbot
09 - API Back
#[ApiResource (
collectionOperations: [
'get',
'post' => ['security' => "is_granted('CREATE_CONTENT_ARTICLE')" ],
],
itemOperations: [
'get',
'put' => ['security' => "is_granted('UPDATE_CONTENT_ARTICLE')" ],
],
attributes: ['order' => ['lastUpdateDate' => 'DESC']],
normalizationContext: ['groups' => ['resource']]
)]
#[ORMEntity]
#[ORMTable(name: 'article')]
class Article extends Content
{
/**
* Texte de l'article.
*/
#[ApiProperty ()]
#[ORMColumn(name: 'art_text', type: 'text', nullable: true)]
#[AssertNotBlank(groups: ['publication' ], message: 'article.text.message' )]
#[Groups(['article:*:default' ])]
private ?string $text = null;
@gknjockbot
09 - API Back
#[ApiResource (
collectionOperations: [
'get',
'post' => ['security' => "is_granted('CREATE_CONTENT_ARTICLE')" ],
],
itemOperations: [
'get',
'put' => ['security' => "is_granted('UPDATE_CONTENT_ARTICLE')" ],
],
attributes: ['order' => ['lastUpdateDate' => 'DESC']],
normalizationContext: ['groups' => ['resource']]
)]
#[ORMEntity]
#[ORMTable(name: 'article')]
class Article extends Content
{
/**
* Texte de l'article.
*/
#[ApiProperty ()]
#[ORMColumn(name: 'art_text', type: 'text', nullable: true)]
#[AssertNotBlank(groups: ['publication' ], message: 'article.text.message' )]
#[Groups(['article:*:default' ])]
private ?string $text = null;
@gknjockbot
09 - API Back
#[ApiResource (
collectionOperations: [
'get',
'post' => ['security' => "is_granted('CREATE_CONTENT_ARTICLE')" ],
],
itemOperations: [
'get',
'put' => ['security' => "is_granted('UPDATE_CONTENT_ARTICLE')" ],
],
attributes: ['order' => ['lastUpdateDate' => 'DESC']],
normalizationContext: ['groups' => ['resource']]
)]
#[ORMEntity]
#[ORMTable(name: 'article')]
class Article extends Content
{
/**
* Texte de l'article.
*/
#[ApiProperty ()]
#[ORMColumn(name: 'art_text', type: 'text', nullable: true)]
#[AssertNotBlank(groups: ['publication' ], message: 'article.text.message' )]
#[Groups(['article:*:default' ])]
private ?string $text = null;
@gknjockbot
09 - API Back
article : post : *
media : get : default
* : * : indexation
resource method group
@gknjockbot
09 - API Back
@gknjockbot
09 - API Back
# config/services.yaml
AppSerializerContextBuilder :
arguments:
$decorated: '@AppSerializerContextBuilder.inner'
decorates: api_platform.serializer.context_builder.filter
@gknjockbot
09 - API Back
final class ContextBuilder implements SerializerContextBuilderInterface
{
public function createFromRequest(Request $request, bool $normalization, array $extractedAttributes = null): array
{
$context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
if (empty($context[AbstractNormalizer::GROUPS])) {
return $context;
}
$context[AbstractNormalizer::GROUPS] = $context[AbstractNormalizer::GROUPS] ?? [];
$groups = $context[AbstractNormalizer::GROUPS];
$groups[] = 'default';
$operation = $this->extractOperation($extractedAttributes);
$context[AbstractNormalizer::GROUPS] = [];
foreach ($this->extractResourceNames($extractedAttributes) as $resourceName) {
foreach ($groups as $group) {
$context[AbstractNormalizer::GROUPS][] = '*'.':'.'*'.':'.$group;
$context[AbstractNormalizer::GROUPS][] = '*'.':'.$operation.':'.$group;
$context[AbstractNormalizer::GROUPS][] = $resourceName.':'.'*'.':'.$group;
$context[AbstractNormalizer::GROUPS][] = $resourceName.':'.$operation.':'.'*';
$context[AbstractNormalizer::GROUPS][] = $resourceName.':'.$operation.':'.$group;
}
}
$context[AbstractNormalizer::GROUPS] = array_merge([], array_unique($context[AbstractNormalizer::GROUPS]));
return $context;
}
@gknjockbot
09 - API Back
final class ContextBuilder implements SerializerContextBuilderInterface
{
public function createFromRequest(Request $request, bool $normalization, array $extractedAttributes = null): array
{
$context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
if (empty($context[AbstractNormalizer::GROUPS])) {
return $context;
}
$context[AbstractNormalizer::GROUPS] = $context[AbstractNormalizer::GROUPS] ?? [];
$groups = $context[AbstractNormalizer::GROUPS];
$groups[] = 'default';
$operation = $this->extractOperation($extractedAttributes);
$context[AbstractNormalizer::GROUPS] = [];
foreach ($this->extractResourceNames($extractedAttributes) as $resourceName) {
foreach ($groups as $group) {
$context[AbstractNormalizer::GROUPS][] = '*'.':'.'*'.':'.$group;
$context[AbstractNormalizer::GROUPS][] = '*'.':'.$operation.':'.$group;
$context[AbstractNormalizer::GROUPS][] = $resourceName.':'.'*'.':'.$group;
$context[AbstractNormalizer::GROUPS][] = $resourceName.':'.$operation.':'.'*';
$context[AbstractNormalizer::GROUPS][] = $resourceName.':'.$operation.':'.$group;
}
}
$context[AbstractNormalizer::GROUPS] = array_merge([], array_unique($context[AbstractNormalizer::GROUPS]));
return $context;
}
@gknjockbot
09 - API Back
final class ContextBuilder implements SerializerContextBuilderInterface
{
public function createFromRequest(Request $request, bool $normalization, array $extractedAttributes = null): array
{
$context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
if (empty($context[AbstractNormalizer::GROUPS])) {
return $context;
}
$context[AbstractNormalizer::GROUPS] = $context[AbstractNormalizer::GROUPS] ?? [];
$groups = $context[AbstractNormalizer::GROUPS];
$groups[] = 'default';
$operation = $this->extractOperation($extractedAttributes);
$context[AbstractNormalizer::GROUPS] = [];
foreach ($this->extractResourceNames($extractedAttributes) as $resourceName) {
foreach ($groups as $group) {
$context[AbstractNormalizer::GROUPS][] = '*'.':'.'*'.':'.$group;
$context[AbstractNormalizer::GROUPS][] = '*'.':'.$operation.':'.$group;
$context[AbstractNormalizer::GROUPS][] = $resourceName.':'.'*'.':'.$group;
$context[AbstractNormalizer::GROUPS][] = $resourceName.':'.$operation.':'.'*';
$context[AbstractNormalizer::GROUPS][] = $resourceName.':'.$operation.':'.$group;
}
}
$context[AbstractNormalizer::GROUPS] = array_merge([], array_unique($context[AbstractNormalizer::GROUPS]));
return $context;
}
@gknjockbot
09 - API Back
final class ContextBuilder implements SerializerContextBuilderInterface
{
public function createFromRequest(Request $request, bool $normalization, array $extractedAttributes = null): array
{
$context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
if (empty($context[AbstractNormalizer::GROUPS])) {
return $context;
}
$context[AbstractNormalizer::GROUPS] = $context[AbstractNormalizer::GROUPS] ?? [];
$groups = $context[AbstractNormalizer::GROUPS];
$groups[] = 'default';
$operation = $this->extractOperation($extractedAttributes);
$context[AbstractNormalizer::GROUPS] = [];
foreach ($this->extractResourceNames($extractedAttributes) as $resourceName) {
foreach ($groups as $group) {
$context[AbstractNormalizer::GROUPS][] = '*'.':'.'*'.':'.$group;
$context[AbstractNormalizer::GROUPS][] = '*'.':'.$operation.':'.$group;
$context[AbstractNormalizer::GROUPS][] = $resourceName.':'.'*'.':'.$group;
$context[AbstractNormalizer::GROUPS][] = $resourceName.':'.$operation.':'.'*';
$context[AbstractNormalizer::GROUPS][] = $resourceName.':'.$operation.':'.$group;
}
}
$context[AbstractNormalizer::GROUPS] = array_merge([], array_unique($context[AbstractNormalizer::GROUPS]));
return $context;
}
@gknjockbot
09 - API Back
@gknjockbot
09 - API Back
@gknjockbot
09 - API Back
# config/services.yaml
AppEventListenerCmsContentEntityListener :
tags:
- { name: 'doctrine.orm.entity_listener' ,
entity: AppEntityCmsContent,
event: postPersist,
entity_manager : edito }
- { name: 'doctrine.orm.entity_listener' ,
entity: AppEntityCmsContent,
event: postPersist,
method: postPersistPublish,
entity_manager : publish }
@gknjockbot
09 - API Back
class ContentEntityListener
{
public function postPersist (Content $content): void
{
$this->logger->logCreateEntity ($content);
$this->index($content, IndexerSubscriberAction ::ACTION_SAVE);
$this->registerForCacheInvalidation ($content, CacheInvalidatorInterface ::CONTEXT_EDITO);
}
public function postPersistPublish (Content $content): void
{
$this->logger->logPublishEntity ($content);
$this->index($content, IndexerSubscriberAction ::ACTION_PUBLISH);
$this->registerForCacheInvalidation ($content, CacheInvalidatorInterface ::CONTEXT_PUBLISH);
}
// ...
}
@gknjockbot
10 - API Mobile
@gknjockbot
10 - API Mobile
@gknjockbot
10 - API Mobile
@gknjockbot
10.1 - API Mobile | DataProvider
final class ContentItemDataProvider implements ItemDataProviderInterface,
SerializerAwareDataProviderInterface, RestrictedDataProviderInterface
{
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return Content::class === $resourceClass;
}
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?object
{
// Retrieve Content with all dependencies all at once: CHT, CHM.
$response = $this->apiFront->callContent($id);
if (Response::HTTP_NOT_FOUND === $response->getStatusCode()) {
throw new NotFoundHttpException(sprintf('Content not found with ID : %s', $id));
}
$apiFrontContent = $this->getSerializer()
->deserialize($response->getContent(), '', JsonEncoder::FORMAT, $context);
$this->setContentsByRelateds($apiFrontContent, $context);
return $apiFrontContent;
}
@gknjockbot
10.1 - API Mobile | DataProvider
final class ContentItemDataProvider implements ItemDataProviderInterface,
SerializerAwareDataProviderInterface, RestrictedDataProviderInterface
{
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return Content::class === $resourceClass;
}
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?object
{
// Retrieve Content with all dependencies all at once: CHT, CHM.
$response = $this->apiFront->callContent($id);
if (Response::HTTP_NOT_FOUND === $response->getStatusCode()) {
throw new NotFoundHttpException(sprintf('Content not found with ID : %s', $id));
}
$apiFrontContent = $this->getSerializer()
->deserialize($response->getContent(), '', JsonEncoder::FORMAT, $context);
$this->setContentsByRelateds($apiFrontContent, $context);
return $apiFrontContent;
}
@gknjockbot
10.1 - API Mobile | DataProvider
final class ContentItemDataProvider implements ItemDataProviderInterface,
SerializerAwareDataProviderInterface, RestrictedDataProviderInterface
{
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return Content::class === $resourceClass;
}
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?object
{
// Retrieve Content with all dependencies all at once: CHT, CHM.
$response = $this->apiFront->callContent($id);
if (Response::HTTP_NOT_FOUND === $response->getStatusCode()) {
throw new NotFoundHttpException(sprintf('Content not found with ID : %s', $id));
}
$apiFrontContent = $this->getSerializer()
->deserialize($response->getContent(), '', JsonEncoder::FORMAT, $context);
$this->setContentsByRelateds($apiFrontContent, $context);
return $apiFrontContent;
}
@gknjockbot
10.1 - API Mobile | DataProvider
final class ContentItemDataProvider implements ItemDataProviderInterface,
SerializerAwareDataProviderInterface, RestrictedDataProviderInterface
{
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return Content::class === $resourceClass;
}
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?object
{
// Retrieve Content with all dependencies all at once: CHT, CHM.
$response = $this->apiFront->callContent($id);
if (Response::HTTP_NOT_FOUND === $response->getStatusCode()) {
throw new NotFoundHttpException(sprintf('Content not found with ID : %s', $id));
}
$apiFrontContent = $this->getSerializer()
->deserialize($response->getContent(), '', JsonEncoder::FORMAT, $context);
$this->setContentsByRelateds($apiFrontContent, $context);
return $apiFrontContent;
}
@gknjockbot
10.1 - API Mobile | DataProvider
final class ContentItemDataProvider implements ItemDataProviderInterface,
SerializerAwareDataProviderInterface, RestrictedDataProviderInterface
{
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return Content::class === $resourceClass;
}
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?object
{
// Retrieve Content with all dependencies all at once: CHT, CHM.
$response = $this->apiFront->callContent($id);
if (Response::HTTP_NOT_FOUND === $response->getStatusCode()) {
throw new NotFoundHttpException(sprintf('Content not found with ID : %s', $id));
}
$apiFrontContent = $this->getSerializer()
->deserialize($response->getContent(), '', JsonEncoder::FORMAT, $context);
$this->setContentsByRelateds($apiFrontContent, $context);
return $apiFrontContent;
}
@gknjockbot
10.2 - API Mobile | DataTransformer
class ArticleDetailDataTransformer implements CmsDataTransformerAwareInterface,
DataTransformerInterface
{
public function supportsTransformation ($data, string $to, array $context = []): bool
{
return $data instanceof ArticleInput && !$this->transformer ->isDigest($context);
}
public function transform($object, string $to, array $context = [])
{
$content = $this->decorated->transform($object, $to, $context);
$this->hydrateDetail ($content, $object, $context);
$richText = $this->parser->parse($object->getText());
$this->mergeRichTextHtml ($richText);
$this->populateRichTextEntity ($richText, $object, [
'operation_type' => 'collection'
]);
$content->setText($richText);
return $content;
}
// ...
}
@gknjockbot
10.2 - API Mobile | DataTransformer
class ArticleDetailDataTransformer implements CmsDataTransformerAwareInterface,
DataTransformerInterface
{
public function supportsTransformation ($data, string $to, array $context = []): bool
{
return $data instanceof ArticleInput && !$this->transformer ->isDigest($context);
}
public function transform($object, string $to, array $context = [])
{
$content = $this->decorated->transform($object, $to, $context);
$this->hydrateDetail ($content, $object, $context);
$richText = $this->parser->parse($object->getText());
$this->mergeRichTextHtml ($richText);
$this->populateRichTextEntity ($richText, $object, [
'operation_type' => 'collection'
]);
$content->setText($richText);
return $content;
}
// ...
}
@gknjockbot
10.2 - API Mobile | DataTransformer
class ArticleDetailDataTransformer implements CmsDataTransformerAwareInterface,
DataTransformerInterface
{
public function supportsTransformation ($data, string $to, array $context = []): bool
{
return $data instanceof ArticleInput && !$this->transformer ->isDigest($context);
}
public function transform($object, string $to, array $context = [])
{
$content = $this->decorated->transform($object, $to, $context);
$this->hydrateDetail ($content, $object, $context);
$richText = $this->parser->parse($object->getText());
$this->mergeRichTextHtml ($richText);
$this->populateRichTextEntity ($richText, $object, [
'operation_type' => 'collection'
]);
$content->setText($richText);
return $content;
}
// ...
}
@gknjockbot
10.2 - API Mobile | DataTransformer
class ArticleDetailDataTransformer implements CmsDataTransformerAwareInterface,
DataTransformerInterface
{
public function supportsTransformation ($data, string $to, array $context = []): bool
{
return $data instanceof ArticleInput && !$this->transformer ->isDigest($context);
}
public function transform($object, string $to, array $context = [])
{
$content = $this->decorated->transform($object, $to, $context);
$this->hydrateDetail ($content, $object, $context);
$richText = $this->parser->parse($object->getText());
$this->mergeRichTextHtml ($richText);
$this->populateRichTextEntity ($richText, $object, [
'operation_type' => 'collection'
]);
$content->setText($richText);
return $content;
}
// ...
}
@gknjockbot
10.2 - API Mobile | DataTransformer
class ArticleDetailDataTransformer implements CmsDataTransformerAwareInterface,
DataTransformerInterface
{
public function supportsTransformation ($data, string $to, array $context = []): bool
{
return $data instanceof ArticleInput && !$this->transformer ->isDigest($context);
}
public function transform($object, string $to, array $context = [])
{
$content = $this->decorated->transform($object, $to, $context);
$this->hydrateDetail ($content, $object, $context);
$richText = $this->parser->parse($object->getText());
$this->mergeRichTextHtml ($richText);
$this->populateRichTextEntity ($richText, $object, [
'operation_type' => 'collection'
]);
$content->setText($richText);
return $content;
}
// ...
}
@gknjockbot
11 - HTTP Cache
@gknjockbot
11 - HTTP Cache
@gknjockbot
11 - HTTP Cache
● Backend : Varnish Cache Plus
● Tag-based : xkeys module
xkey pattern
{context}/{type}/{id}
publish/Cms-Article/1
edito/Cms-MediaImage/1
@gknjockbot
11 - HTTP Cache
@gknjockbot
11 - HTTP Cache
@gknjockbot
11 - HTTP Cache
@gknjockbot
11 - HTTP Cache
@gknjockbot
11 - HTTP Cache
@gknjockbot
11 - HTTP Cache
@gknjockbot
11 - HTTP Cache
@gknjockbot
11 - HTTP Cache
@gknjockbot
12 - What about the future ?
@gknjockbot
12 - What about the future ?
UPGRADE :
● Standards/RFCs : compliance
● Symfony Framework : 5.4 → 6.x → ?
● API Platform : 2.7 → 3.x → ?
ENHANCE :
● Autogenerate API Front from API Back resource metadata
● ElasticSearch as backend for resource collections endpoints
@gknjockbot
Thank you !
@gknjockbot
linkedin.com/in/georgeskingnjockbot

More Related Content

apidays Paris 2022 - France Televisions : How we leverage API Platform for our content APIs, Georges-King Njock-Bôt, Freelance Developer

  • 1. France Télévisions Leveraging API Platform for our content APIs
  • 2. 2023 SERIES OF EVENT New York May 16&17 Australia October 11&12 Singapore April 12&13 Helsinki & North June 5&6 Paris SEPTEMBER London November 15&16 June 28-30 SILICON VALLEY March 14&15 Dubai & Middle East February 22&23
  • 3. France Télévisions : Leveraging API Platform for our content APIs
  • 4. @gknjockbot 01 - whoami Georges-King Strasbourg, Grand-Est, France Software Engineer since 2009 Freelance IT Consultant
  • 5. @gknjockbot 02 - France Télévisions News Articles TV Broadcasts
  • 6. @gknjockbot 02 - France Télévisions 866 755 articles 29M visitors / month 860 770 articles 157M visitors / month 220 771 articles 11M visitors / month
  • 7. @gknjockbot 03 - What are we trying to solve ? Franceinfo needed rebuilding :
  • 8. @gknjockbot 03 - What are we trying to solve ? Franceinfo needed rebuilding : ● Front-Back coupling
  • 9. @gknjockbot 03 - What are we trying to solve ? Franceinfo needed rebuilding : ● Front-Back coupling ● Competing DB access
  • 10. @gknjockbot 03 - What are we trying to solve ? Franceinfo needed rebuilding : ● Front-Back coupling ● Competing DB access ● Obsolete Technology
  • 11. @gknjockbot 03 - What are we trying to solve ? Franceinfo needed rebuilding : ● Front-Back coupling ● Competing DB access ● Obsolete Technology ● Monolithic platform
  • 12. @gknjockbot 04 - The proposed architecture
  • 13. @gknjockbot 05 - Choosing an implementation Proof Of Concept MariaDB MongoDB ElasticSearch PHP / Symfony Doctrine ORM Doctrine ODM - Node.js / Koa Sequelize Mongoose Built In
  • 14. @gknjockbot 05 - Choosing an implementation Cons Pros Node / ElasticSearch Reindexing Scalability Performance Resource consumption Node / MariaDB Performance Sequelize Simple Node / MongoDB Reindexing Performance Mongoose Symfony / MongoDB New DB Unified stacks (Front/Back) ODM / Framework Symfony / MariaDB Well known DB stack internally Unified stacks (Front/Back) ORM / Framework
  • 15. @gknjockbot 05 - Choosing an implementation
  • 16. @gknjockbot 05 - Choosing an implementation
  • 17. @gknjockbot 06 - Towards a shared platform
  • 18. @gknjockbot 06 - Towards a shared platform
  • 19. @gknjockbot 06.1 - Introducing PIC - Plateforme d’Information Commune
  • 20. @gknjockbot 06.2 - The challenges for PIC Additional concerns : ● Must share editorial concepts ● Must share a generic codebase ● Must have dedicated deployments
  • 21. @gknjockbot 06.2 - PIC platform high level overview
  • 22. @gknjockbot 07 - The Tech Stack
  • 24. @gknjockbot 08 - API Front API Call : GET /contents?taxonomy=les-jeux-olympiques/paris-2024 Endpoint : /contents Filter : taxonomy=les-jeux-olympiques/paris-2024 Groups : default, content
  • 26. @gknjockbot 08 - API Front API Call : GET /contents/<id> Endpoint : /contents Filter : N/A Groups : default, content, media, taxonomy
  • 27. @gknjockbot 08 - API Front / resource endpoints class DirectTv extends Content { // ... }
  • 28. @gknjockbot 08 - API Front / resource endpoints #[ORMEntity] #[ORMTable(name: 'direct_tv' )] class DirectTv extends Content { // ... }
  • 29. @gknjockbot 08 - API Front / resource endpoints #[ApiResource ( collectionOperations: [ 'list' => [ 'method' => 'GET', 'normalization_context' => ['enable_max_depth' => true] ]], itemOperations: [ 'get' => [ 'method' => 'GET' ]], attributes: [ 'order' => ['lastPublicationDate' => 'DESC'], 'filters' => ['snapshot.code' , 'media.type' ] ] )] #[ApiFilter(ProgramFilter ::class, properties: ['program.channel' , 'program.type' ])] #[ApiFilter(BeforeBeginDateFilter ::class)] #[ApiFilter(OrderBeginDateFilter ::class)] #[ORMEntity] #[ORMTable(name: 'direct_tv' )] class DirectTv extends Content { // ... }
  • 30. @gknjockbot 08 - API Front / resource endpoints #[ApiResource ( collectionOperations: [ 'list' => [ 'method' => 'GET', 'normalization_context' => ['enable_max_depth' => true] ]], itemOperations: [ 'get' => [ 'method' => 'GET' ]], attributes: [ 'order' => ['lastPublicationDate' => 'DESC'], 'filters' => ['snapshot.code' , 'media.type' ] ] )] #[ApiFilter(ProgramFilter ::class, properties: ['program.channel' , 'program.type' ])] #[ApiFilter(BeforeBeginDateFilter ::class)] #[ApiFilter(OrderBeginDateFilter ::class)] #[ORMEntity] #[ORMTable(name: 'direct_tv' )] class DirectTv extends Content { // ... }
  • 31. @gknjockbot 08 - API Front / resource payload class DirectTv extends Content { private ?string $presentation = null; private ?bool $isSubject = false; public function getPresentation (): ?string { return $this->presentation ; } public function isIsSubject (): ?bool { return $this->isSubject; } }
  • 32. @gknjockbot 08 - API Front / resource payload class DirectTv extends Content { #[ORMColumn(type: 'text', nullable: true)] private ?string $presentation = null; #[ORMColumn(type: 'boolean', nullable: true, options: ['default' => 0])] private ?bool $isSubject = false; public function getPresentation (): ?string { return $this->presentation ; } public function isIsSubject (): ?bool { return $this->isSubject; } }
  • 33. @gknjockbot 08 - API Front / resource payload class DirectTv extends Content { #[Groups(['content:get:default' ])] #[ApiProperty (attributes: ['swagger_context' => ['example' => 'JT du mercredi 6 février 2019' ]])] #[ORMColumn(type: 'text', nullable: true)] private ?string $presentation = null; #[Groups(['content:get:default' ])] #[ApiProperty ()] #[ORMColumn(type: 'boolean', nullable: true, options: ['default' => 0])] private ?bool $isSubject = false; public function getPresentation (): ?string { return $this->presentation ; } public function isIsSubject (): ?bool { return $this->isSubject; } }
  • 34. @gknjockbot 08 - API Front / resource payload class DirectTv extends Content { #[Groups(['content:get:default' ])] #[ApiProperty (attributes: ['swagger_context' => ['example' => 'JT du mercredi 6 février 2019' ]])] #[ORMColumn(type: 'text', nullable: true)] private ?string $presentation = null; #[Groups(['content:get:default' ])] #[ApiProperty ()] #[ORMColumn(type: 'boolean', nullable: true, options: ['default' => 0])] private ?bool $isSubject = false; public function getPresentation (): ?string { return $this->presentation ; } public function isIsSubject (): ?bool { return $this->isSubject; } }
  • 35. @gknjockbot 08 - API Front / resource payload class DirectTv extends Content { #[Groups(['content:get:default' ])] #[ApiProperty (attributes: ['swagger_context' => ['example' => 'JT du mercredi 6 février 2019' ]])] #[ORMColumn(type: 'text', nullable: true)] private ?string $presentation = null; #[Groups(['content:get:default' ])] #[ApiProperty ()] #[ORMColumn(type: 'boolean', nullable: true, options: ['default' => 0])] private ?bool $isSubject = false; public function getPresentation (): ?string { return $this->presentation ; } public function isIsSubject (): ?bool { return $this->isSubject; } }
  • 38. @gknjockbot #[ApiResource ( /* ... */ )] #[ApiFilter (GroupFilter ::class, arguments: [ 'parameterName' => 'groups', 'overrideDefaultGroups' => true, 'whitelist' => ['indexation' , 'sdk', 'details'] ])] #[ApiFilter ( /* ... */ )] abstract class Content implements Lockable, Loggable { // ... } 09 - API Back
  • 39. @gknjockbot #[ApiResource ( /* ... */ )] #[ApiFilter (GroupFilter ::class, arguments: [ 'parameterName' => 'groups', 'overrideDefaultGroups' => true, 'whitelist' => ['indexation' , 'sdk', 'details'] ])] #[ApiFilter ( /* ... */ )] abstract class Content implements Lockable, Loggable { // ... } 09 - API Back
  • 40. @gknjockbot 09 - API Back #[ApiResource ( collectionOperations: [ 'get', 'post' => ['security' => "is_granted('CREATE_CONTENT_ARTICLE')" ], ], itemOperations: [ 'get', 'put' => ['security' => "is_granted('UPDATE_CONTENT_ARTICLE')" ], ], attributes: ['order' => ['lastUpdateDate' => 'DESC']], normalizationContext: ['groups' => ['resource']] )] #[ORMEntity] #[ORMTable(name: 'article')] class Article extends Content { /** * Texte de l'article. */ #[ApiProperty ()] #[ORMColumn(name: 'art_text', type: 'text', nullable: true)] #[AssertNotBlank(groups: ['publication' ], message: 'article.text.message' )] #[Groups(['article:*:default' ])] private ?string $text = null;
  • 41. @gknjockbot 09 - API Back #[ApiResource ( collectionOperations: [ 'get', 'post' => ['security' => "is_granted('CREATE_CONTENT_ARTICLE')" ], ], itemOperations: [ 'get', 'put' => ['security' => "is_granted('UPDATE_CONTENT_ARTICLE')" ], ], attributes: ['order' => ['lastUpdateDate' => 'DESC']], normalizationContext: ['groups' => ['resource']] )] #[ORMEntity] #[ORMTable(name: 'article')] class Article extends Content { /** * Texte de l'article. */ #[ApiProperty ()] #[ORMColumn(name: 'art_text', type: 'text', nullable: true)] #[AssertNotBlank(groups: ['publication' ], message: 'article.text.message' )] #[Groups(['article:*:default' ])] private ?string $text = null;
  • 42. @gknjockbot 09 - API Back #[ApiResource ( collectionOperations: [ 'get', 'post' => ['security' => "is_granted('CREATE_CONTENT_ARTICLE')" ], ], itemOperations: [ 'get', 'put' => ['security' => "is_granted('UPDATE_CONTENT_ARTICLE')" ], ], attributes: ['order' => ['lastUpdateDate' => 'DESC']], normalizationContext: ['groups' => ['resource']] )] #[ORMEntity] #[ORMTable(name: 'article')] class Article extends Content { /** * Texte de l'article. */ #[ApiProperty ()] #[ORMColumn(name: 'art_text', type: 'text', nullable: true)] #[AssertNotBlank(groups: ['publication' ], message: 'article.text.message' )] #[Groups(['article:*:default' ])] private ?string $text = null;
  • 43. @gknjockbot 09 - API Back article : post : * media : get : default * : * : indexation resource method group
  • 45. @gknjockbot 09 - API Back # config/services.yaml AppSerializerContextBuilder : arguments: $decorated: '@AppSerializerContextBuilder.inner' decorates: api_platform.serializer.context_builder.filter
  • 46. @gknjockbot 09 - API Back final class ContextBuilder implements SerializerContextBuilderInterface { public function createFromRequest(Request $request, bool $normalization, array $extractedAttributes = null): array { $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes); if (empty($context[AbstractNormalizer::GROUPS])) { return $context; } $context[AbstractNormalizer::GROUPS] = $context[AbstractNormalizer::GROUPS] ?? []; $groups = $context[AbstractNormalizer::GROUPS]; $groups[] = 'default'; $operation = $this->extractOperation($extractedAttributes); $context[AbstractNormalizer::GROUPS] = []; foreach ($this->extractResourceNames($extractedAttributes) as $resourceName) { foreach ($groups as $group) { $context[AbstractNormalizer::GROUPS][] = '*'.':'.'*'.':'.$group; $context[AbstractNormalizer::GROUPS][] = '*'.':'.$operation.':'.$group; $context[AbstractNormalizer::GROUPS][] = $resourceName.':'.'*'.':'.$group; $context[AbstractNormalizer::GROUPS][] = $resourceName.':'.$operation.':'.'*'; $context[AbstractNormalizer::GROUPS][] = $resourceName.':'.$operation.':'.$group; } } $context[AbstractNormalizer::GROUPS] = array_merge([], array_unique($context[AbstractNormalizer::GROUPS])); return $context; }
  • 47. @gknjockbot 09 - API Back final class ContextBuilder implements SerializerContextBuilderInterface { public function createFromRequest(Request $request, bool $normalization, array $extractedAttributes = null): array { $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes); if (empty($context[AbstractNormalizer::GROUPS])) { return $context; } $context[AbstractNormalizer::GROUPS] = $context[AbstractNormalizer::GROUPS] ?? []; $groups = $context[AbstractNormalizer::GROUPS]; $groups[] = 'default'; $operation = $this->extractOperation($extractedAttributes); $context[AbstractNormalizer::GROUPS] = []; foreach ($this->extractResourceNames($extractedAttributes) as $resourceName) { foreach ($groups as $group) { $context[AbstractNormalizer::GROUPS][] = '*'.':'.'*'.':'.$group; $context[AbstractNormalizer::GROUPS][] = '*'.':'.$operation.':'.$group; $context[AbstractNormalizer::GROUPS][] = $resourceName.':'.'*'.':'.$group; $context[AbstractNormalizer::GROUPS][] = $resourceName.':'.$operation.':'.'*'; $context[AbstractNormalizer::GROUPS][] = $resourceName.':'.$operation.':'.$group; } } $context[AbstractNormalizer::GROUPS] = array_merge([], array_unique($context[AbstractNormalizer::GROUPS])); return $context; }
  • 48. @gknjockbot 09 - API Back final class ContextBuilder implements SerializerContextBuilderInterface { public function createFromRequest(Request $request, bool $normalization, array $extractedAttributes = null): array { $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes); if (empty($context[AbstractNormalizer::GROUPS])) { return $context; } $context[AbstractNormalizer::GROUPS] = $context[AbstractNormalizer::GROUPS] ?? []; $groups = $context[AbstractNormalizer::GROUPS]; $groups[] = 'default'; $operation = $this->extractOperation($extractedAttributes); $context[AbstractNormalizer::GROUPS] = []; foreach ($this->extractResourceNames($extractedAttributes) as $resourceName) { foreach ($groups as $group) { $context[AbstractNormalizer::GROUPS][] = '*'.':'.'*'.':'.$group; $context[AbstractNormalizer::GROUPS][] = '*'.':'.$operation.':'.$group; $context[AbstractNormalizer::GROUPS][] = $resourceName.':'.'*'.':'.$group; $context[AbstractNormalizer::GROUPS][] = $resourceName.':'.$operation.':'.'*'; $context[AbstractNormalizer::GROUPS][] = $resourceName.':'.$operation.':'.$group; } } $context[AbstractNormalizer::GROUPS] = array_merge([], array_unique($context[AbstractNormalizer::GROUPS])); return $context; }
  • 49. @gknjockbot 09 - API Back final class ContextBuilder implements SerializerContextBuilderInterface { public function createFromRequest(Request $request, bool $normalization, array $extractedAttributes = null): array { $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes); if (empty($context[AbstractNormalizer::GROUPS])) { return $context; } $context[AbstractNormalizer::GROUPS] = $context[AbstractNormalizer::GROUPS] ?? []; $groups = $context[AbstractNormalizer::GROUPS]; $groups[] = 'default'; $operation = $this->extractOperation($extractedAttributes); $context[AbstractNormalizer::GROUPS] = []; foreach ($this->extractResourceNames($extractedAttributes) as $resourceName) { foreach ($groups as $group) { $context[AbstractNormalizer::GROUPS][] = '*'.':'.'*'.':'.$group; $context[AbstractNormalizer::GROUPS][] = '*'.':'.$operation.':'.$group; $context[AbstractNormalizer::GROUPS][] = $resourceName.':'.'*'.':'.$group; $context[AbstractNormalizer::GROUPS][] = $resourceName.':'.$operation.':'.'*'; $context[AbstractNormalizer::GROUPS][] = $resourceName.':'.$operation.':'.$group; } } $context[AbstractNormalizer::GROUPS] = array_merge([], array_unique($context[AbstractNormalizer::GROUPS])); return $context; }
  • 52. @gknjockbot 09 - API Back # config/services.yaml AppEventListenerCmsContentEntityListener : tags: - { name: 'doctrine.orm.entity_listener' , entity: AppEntityCmsContent, event: postPersist, entity_manager : edito } - { name: 'doctrine.orm.entity_listener' , entity: AppEntityCmsContent, event: postPersist, method: postPersistPublish, entity_manager : publish }
  • 53. @gknjockbot 09 - API Back class ContentEntityListener { public function postPersist (Content $content): void { $this->logger->logCreateEntity ($content); $this->index($content, IndexerSubscriberAction ::ACTION_SAVE); $this->registerForCacheInvalidation ($content, CacheInvalidatorInterface ::CONTEXT_EDITO); } public function postPersistPublish (Content $content): void { $this->logger->logPublishEntity ($content); $this->index($content, IndexerSubscriberAction ::ACTION_PUBLISH); $this->registerForCacheInvalidation ($content, CacheInvalidatorInterface ::CONTEXT_PUBLISH); } // ... }
  • 57. @gknjockbot 10.1 - API Mobile | DataProvider final class ContentItemDataProvider implements ItemDataProviderInterface, SerializerAwareDataProviderInterface, RestrictedDataProviderInterface { public function supports(string $resourceClass, string $operationName = null, array $context = []): bool { return Content::class === $resourceClass; } public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?object { // Retrieve Content with all dependencies all at once: CHT, CHM. $response = $this->apiFront->callContent($id); if (Response::HTTP_NOT_FOUND === $response->getStatusCode()) { throw new NotFoundHttpException(sprintf('Content not found with ID : %s', $id)); } $apiFrontContent = $this->getSerializer() ->deserialize($response->getContent(), '', JsonEncoder::FORMAT, $context); $this->setContentsByRelateds($apiFrontContent, $context); return $apiFrontContent; }
  • 58. @gknjockbot 10.1 - API Mobile | DataProvider final class ContentItemDataProvider implements ItemDataProviderInterface, SerializerAwareDataProviderInterface, RestrictedDataProviderInterface { public function supports(string $resourceClass, string $operationName = null, array $context = []): bool { return Content::class === $resourceClass; } public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?object { // Retrieve Content with all dependencies all at once: CHT, CHM. $response = $this->apiFront->callContent($id); if (Response::HTTP_NOT_FOUND === $response->getStatusCode()) { throw new NotFoundHttpException(sprintf('Content not found with ID : %s', $id)); } $apiFrontContent = $this->getSerializer() ->deserialize($response->getContent(), '', JsonEncoder::FORMAT, $context); $this->setContentsByRelateds($apiFrontContent, $context); return $apiFrontContent; }
  • 59. @gknjockbot 10.1 - API Mobile | DataProvider final class ContentItemDataProvider implements ItemDataProviderInterface, SerializerAwareDataProviderInterface, RestrictedDataProviderInterface { public function supports(string $resourceClass, string $operationName = null, array $context = []): bool { return Content::class === $resourceClass; } public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?object { // Retrieve Content with all dependencies all at once: CHT, CHM. $response = $this->apiFront->callContent($id); if (Response::HTTP_NOT_FOUND === $response->getStatusCode()) { throw new NotFoundHttpException(sprintf('Content not found with ID : %s', $id)); } $apiFrontContent = $this->getSerializer() ->deserialize($response->getContent(), '', JsonEncoder::FORMAT, $context); $this->setContentsByRelateds($apiFrontContent, $context); return $apiFrontContent; }
  • 60. @gknjockbot 10.1 - API Mobile | DataProvider final class ContentItemDataProvider implements ItemDataProviderInterface, SerializerAwareDataProviderInterface, RestrictedDataProviderInterface { public function supports(string $resourceClass, string $operationName = null, array $context = []): bool { return Content::class === $resourceClass; } public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?object { // Retrieve Content with all dependencies all at once: CHT, CHM. $response = $this->apiFront->callContent($id); if (Response::HTTP_NOT_FOUND === $response->getStatusCode()) { throw new NotFoundHttpException(sprintf('Content not found with ID : %s', $id)); } $apiFrontContent = $this->getSerializer() ->deserialize($response->getContent(), '', JsonEncoder::FORMAT, $context); $this->setContentsByRelateds($apiFrontContent, $context); return $apiFrontContent; }
  • 61. @gknjockbot 10.1 - API Mobile | DataProvider final class ContentItemDataProvider implements ItemDataProviderInterface, SerializerAwareDataProviderInterface, RestrictedDataProviderInterface { public function supports(string $resourceClass, string $operationName = null, array $context = []): bool { return Content::class === $resourceClass; } public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?object { // Retrieve Content with all dependencies all at once: CHT, CHM. $response = $this->apiFront->callContent($id); if (Response::HTTP_NOT_FOUND === $response->getStatusCode()) { throw new NotFoundHttpException(sprintf('Content not found with ID : %s', $id)); } $apiFrontContent = $this->getSerializer() ->deserialize($response->getContent(), '', JsonEncoder::FORMAT, $context); $this->setContentsByRelateds($apiFrontContent, $context); return $apiFrontContent; }
  • 62. @gknjockbot 10.2 - API Mobile | DataTransformer class ArticleDetailDataTransformer implements CmsDataTransformerAwareInterface, DataTransformerInterface { public function supportsTransformation ($data, string $to, array $context = []): bool { return $data instanceof ArticleInput && !$this->transformer ->isDigest($context); } public function transform($object, string $to, array $context = []) { $content = $this->decorated->transform($object, $to, $context); $this->hydrateDetail ($content, $object, $context); $richText = $this->parser->parse($object->getText()); $this->mergeRichTextHtml ($richText); $this->populateRichTextEntity ($richText, $object, [ 'operation_type' => 'collection' ]); $content->setText($richText); return $content; } // ... }
  • 63. @gknjockbot 10.2 - API Mobile | DataTransformer class ArticleDetailDataTransformer implements CmsDataTransformerAwareInterface, DataTransformerInterface { public function supportsTransformation ($data, string $to, array $context = []): bool { return $data instanceof ArticleInput && !$this->transformer ->isDigest($context); } public function transform($object, string $to, array $context = []) { $content = $this->decorated->transform($object, $to, $context); $this->hydrateDetail ($content, $object, $context); $richText = $this->parser->parse($object->getText()); $this->mergeRichTextHtml ($richText); $this->populateRichTextEntity ($richText, $object, [ 'operation_type' => 'collection' ]); $content->setText($richText); return $content; } // ... }
  • 64. @gknjockbot 10.2 - API Mobile | DataTransformer class ArticleDetailDataTransformer implements CmsDataTransformerAwareInterface, DataTransformerInterface { public function supportsTransformation ($data, string $to, array $context = []): bool { return $data instanceof ArticleInput && !$this->transformer ->isDigest($context); } public function transform($object, string $to, array $context = []) { $content = $this->decorated->transform($object, $to, $context); $this->hydrateDetail ($content, $object, $context); $richText = $this->parser->parse($object->getText()); $this->mergeRichTextHtml ($richText); $this->populateRichTextEntity ($richText, $object, [ 'operation_type' => 'collection' ]); $content->setText($richText); return $content; } // ... }
  • 65. @gknjockbot 10.2 - API Mobile | DataTransformer class ArticleDetailDataTransformer implements CmsDataTransformerAwareInterface, DataTransformerInterface { public function supportsTransformation ($data, string $to, array $context = []): bool { return $data instanceof ArticleInput && !$this->transformer ->isDigest($context); } public function transform($object, string $to, array $context = []) { $content = $this->decorated->transform($object, $to, $context); $this->hydrateDetail ($content, $object, $context); $richText = $this->parser->parse($object->getText()); $this->mergeRichTextHtml ($richText); $this->populateRichTextEntity ($richText, $object, [ 'operation_type' => 'collection' ]); $content->setText($richText); return $content; } // ... }
  • 66. @gknjockbot 10.2 - API Mobile | DataTransformer class ArticleDetailDataTransformer implements CmsDataTransformerAwareInterface, DataTransformerInterface { public function supportsTransformation ($data, string $to, array $context = []): bool { return $data instanceof ArticleInput && !$this->transformer ->isDigest($context); } public function transform($object, string $to, array $context = []) { $content = $this->decorated->transform($object, $to, $context); $this->hydrateDetail ($content, $object, $context); $richText = $this->parser->parse($object->getText()); $this->mergeRichTextHtml ($richText); $this->populateRichTextEntity ($richText, $object, [ 'operation_type' => 'collection' ]); $content->setText($richText); return $content; } // ... }
  • 69. @gknjockbot 11 - HTTP Cache ● Backend : Varnish Cache Plus ● Tag-based : xkeys module xkey pattern {context}/{type}/{id} publish/Cms-Article/1 edito/Cms-MediaImage/1
  • 78. @gknjockbot 12 - What about the future ?
  • 79. @gknjockbot 12 - What about the future ? UPGRADE : ● Standards/RFCs : compliance ● Symfony Framework : 5.4 → 6.x → ? ● API Platform : 2.7 → 3.x → ? ENHANCE : ● Autogenerate API Front from API Back resource metadata ● ElasticSearch as backend for resource collections endpoints