www.innoteam.it
PHPCR & API Platform
What it really means to build a CMF
Case Study - CMS
Premessa:
Un nostro cliente prestigioso ci ha chiesto di sviluppare un CMS custom

Requisiti:
• Restful

• Versionamento dei contenuti

• Multilingua

• Multisite
2
• Content Management Framework

• Un framework che offre gli strumenti
per la gestione dei contenuti

• E’ un toolbox per creare CMS custom

• Esempi:

• eZ Publish / eZ Platform

• Symfony CMF
• Content Management System

• E’ un sistema “pronto all’uso” per la
gestione dei contenuti

• Fornisce un interfaccia admin ben
precisa

• Esempi:

• Wordpress

• Craft CMS
3
CMS CMF
Symfony CMF
• E’ un insieme di bundle che possono essere usati per aggiungere
funzionalità CMS ad applicativi Symfony

• content-bundle, routing-bundle, menu-bundle, …

• Nato per applicativi Symfony server side(twig)

• SonataDoctrinePHPCRAdminBundle
4
La nostra proposta
5
6
• Creare un bundle riutilizzabile(vendor) che
sfrutta i bundle necessari di Symfony CMF

• Esporre in maniera RESTFul le operazioni con
API Platform

Back-end
• Applicativo Angular che consuma le API del
CMF per la gestione dei contenuti(CMS)
Back-office
• Applicativo Angular che consuma in GET le API
del CMF per la visualizzazione dei contenuti

Front-end
Symfony CMF e PHPCR
7
Architettura Symfony CMF
8
Content Repository | Apache Jackrabbit
PHPCR API | Jackalope
Doctrine PHPCR-ODM
DoctrinePHPCRBundle
Symfony CMF Bundles
Content Repository
• E’ uno storage engine che permette di accedere e manipolare contenuti anche di natura
eterogenea (e.g. pagine, video, immagini, recensioni, ecc..) in maniera uniforme.
• Esempio: Apache Jackrabbit
9
Albero dei Contenuti
1)Nodo
• rappresenta un contenuto

• raggiungibile da un path come in un
filesystem

2)Proprietà di un nodo
• contiene l’informazione

• semplice(stringa, bool, int)

• binaria(binary stream)
/
a b c
d e
path: /a/d

p1: true
path: /a/e

p1: “Titolo Pagina”

p2:
path: /a path: /b

p1: 25
path: /c

p1: 3.5
Workspace
• Un content repository è formato da n workspace.

• Ogni workspace ha il suo albero di contenuti.

• Sessione: E’ una connessione autenticata ad un singolo workspace
10
Content Repository
Workspace a Workspace b Workspace c
/
a b c
/ /
PHPCR(PHP Content Repository) API
• E’ una specifica di API Standard per interfacciarsi con qualsiasi
Content Repository in una maniera uniforme.

• E’ un porting di JCR(Java Content Repository) API
11
Jackalope
• E’ un’implementazione open-source di PHPCR API

• Supporta diversi driver backend (transport)
12
Jackalope Jackrabbit Jackalope DBAL
Content Repository
(Apache Jackrabbit)
RDBMS
(MySQL, SQLite,
Postgres)
Doctrine PHPCR-ODM
• E’ un ODM (Object Document Mapper)

• Utilizza il “Data Mapper” pattern per mappare Nodi PHPCR ad oggetti
PHP (Document)

• Supporta concetti PHPCR come children, references, versioning
13
DoctrinePHPCRBundle
• Interagisce con PHPCR API & Doctrine PHPCR-ODM per fornire il
Document Manager come servizio Symfony
14
Il nostro setup
15
Workspace default & live
16
Content Repository
Workspace Default Workspace Live
/
a
/
persist /a

v1 - DRAFT
Workspace default & live
17
Content Repository
Workspace Default Workspace Live
/
a
/
persist /a

v1 - DRAFT
v2 - DRAFT
v3 - DRAFT
v4 - DRAFT
v5 - DRAFT
Workspace default & live
18
Content Repository
Workspace Default Workspace Live
/
a
/
a
publish /a v5
v1 - DRAFT
v2 - DRAFT
v3 - DRAFT
v4 - DRAFT
v5 - PUBLISHED
v1 - PUBLISHED
CMFBundle
19
Content Repository | Apache Jackrabbit
PHPCR API | Jackalope
Doctrine PHPCR-ODM
DoctrinePHPCRBundle
Symfony CMF Bundles
API Platform
"require": {
"api-platform/core": "2.0.*",
"symfony-cmf/core-bundle": "2.0.*",
"symfony-cmf/menu-bundle": "2.1.*",
"symfony-cmf/routing-bundle": "2.0.*",
"symfony-cmf/content-bundle": "2.0.*",
"symfony-cmf/routing-auto-bundle": "2.0.*",
"symfony-cmf/routing-auto": "2.0.*",
"doctrine/phpcr-bundle": "1.3.*",
"doctrine/phpcr-odm": "1.4.*",
"jackalope/jackalope-jackrabbit": "1.3.*",
"phpcr/phpcr-shell": "^1.0"
},
composer.json
Config Dipendenze CMFBundle
20
/Resources/config/bundles.yml
# Config DoctrinePHPCRBundle Sessions
doctrine_phpcr:
session:
default_session: default
sessions:
default:
backend:
type: jackrabbit
connection: php_cr
url: "%jackrabbit_url%"
workspace: default
username: "%phpcr_user%"
password: "%phpcr_pass%"
live:
backend:
type: jackrabbit
connection: php_cr
url: "%jackrabbit_url%"
workspace: live
username: "%phpcr_user%"
password: "%phpcr_pass%"
# Config DoctrinePHPCRBundle Locales & DMs
doctrine_phpcr:
odm:
# locales:
# en: [it]
# it: [en]
# default_locale: it
locale_fallback: hardcoded
document_managers:
default:
session: default
mappings:
InnoteamCMFBundle: ~
live:
session: live
mappings:
InnoteamCMFBundle: ~
Config CMFBundle
21
app/config/config.yml
innoteam_cmf:
domains:
site1:
host: http://www.site1.com
default_locale: it
locales:
it: [en]
en: [it]
Iniettare Config
22
/DependencyInjection/InnoteamCMFExtension.php
class InnoteamCMFExtension extends Extension implements PrependExtensionInterface
{
public function prepend(ContainerBuilder $container)
{
$config = $this->processConfiguration(new Configuration(), $container->getExtensionConfig($this->getAlias()));
$extConfigs = Yaml::parse(file_get_contents(__DIR__ . '/../Resources/config/bundles.yml'));
foreach ($extConfigs as $key => $extConfig) {
switch ($key) {
case 'cmf_core':
$extConfig['multilang']['locales'] = array_keys($config['locales']);
break;
case 'doctrine_phpcr':
$extConfig['odm']['locales'] = $config['locales'];
$extConfig['odm']['default_locale'] = $config['default_locale'];
break;
}
$container->prependExtensionConfig($key, $extConfig);
}
}
}
Organizzazione Albero Contenuti
23
24
namespace InnoteamBundleCMFBundleDocument;
use SymfonyCmfBundleContentBundleDoctrinePhpcrStaticContent;
use DoctrineODMPHPCRMappingAnnotations as PHPCR;
/**
* @PHPCRDocument(
* translator="attribute",
* versionable="full",
* referenceable=true,
* repositoryClass=“InnoteamBundleCMFBundleRepositoryDocumentPageRepository"
* )
*/
class Page extends StaticContent implements WritableDocumentInterface
{
/** @PHPCRField(type="string", nullable=false) */
protected $name;
/** @PHPCRField(type="string", nullable=false, translated=true) */
protected $nameTranslated;
/** @PHPCRField(type="string", nullable=false) */
protected $type;
/** @PHPCRField(type="string", translated=true) */
protected $blocks;
/** @PHPCRField(type="string", translated=true) */
protected $status;
/** @PHPCRReferrers(referringDocument="AutoRoute", referencedBy="content") */
protected $routes;
DocumentWriter
25
namespace InnoteamBundleCMFBundleDocumentWriter;
class ChainDocumentWriter implements DocumentWriterInterface
{
/** @var DocumentWriterInterface[] */
protected $documentWriters;
public function __construct(array $documentWriters)
{
$this->documentWriters = $documentWriters;
}
public function publishDocument(WritableDocumentInterface $document, string $domainId, string $locale) : WritableDocumentInterface
{
foreach ($this->documentWriters as $documentWriter) {
try {
return $documentWriter->publishDocument($document, $domainId, $locale);
} catch (DocumentPublishingNotSupportedException $e) {
continue;
}
}
throw new DocumentPublishingNotSupportedException(sprintf(
"No Document Publisher found which supports publishing Document with id '%s' and class '%s'",
$document->getId(),
get_class($document)
));
}
public function persistDocument(WritableDocumentInterface $document, string $domainId, string $locale): WritableDocumentInterface {}
public function deleteDocument(WritableDocumentInterface $document, string $domainId, string $locale) {}
public function hideDocument(WritableDocumentInterface $document, string $domainId, string $locale): WritableDocumentInterface {}
public function unhideDocument(WritableDocumentInterface $document, string $domainId, string $locale): WritableDocumentInterface {}
}
Persist DocumentWriter
26
namespace InnoteamBundleCMFBundleDocumentWriterDocumentPersistWriter;
class PageDocumentPersistWriter extends BaseDocumentActionWriter implements DocumentPersistWriterInterface
{
public function persistDocument(
WritableDocumentInterface $document,
string $domainId,
string $locale
) : WritableDocumentInterface
{
$document->setStatus(StatusType::DRAFT);
$this->defaultManager->persist($document);
$this->defaultManager->bindTranslation($document, $locale);
$this->defaultManager->flush();
$metadata = $this->defaultManager->getClassMetadata(get_class($document));
if (false !== $metadata->versionable)
$this->defaultManager->checkpoint($document);
return $document;
}
}
API Platform
27
Item & Collection Operations
28
/Resources/config/api_resources/resources.yml
Item Operation
• operazione associata ad un singolo item

• getPage (GET /site1/it/pages/<uuid>)

• editPage (PUT /site1/it/pages/<uuid>)

Collection Operation
• Operazione GET che ritorna un listato di item

• es: GET /site1/pages-no-locale

• Operazione di creazione di un item:

• es: POST /site1/it/pages
resources:
InnoteamBundleCMFBundleDocumentPage:
shortname: 'Page'
itemOperations:
getPage:
route_name: 'api_cms_get_page'
normalization_context:
groups: [ 'page-details' ]
...
collectionOperations:
createPage:
route_name: 'api_cms_create_page'
normalization_context:
groups: [ 'page-details' ]
denormalization_context:
groups: [ 'page-create' ]
...
Operazioni READ
29
30
Richiesta GET Data Provider Normalization
Richiesta GET Page Data Provider Normalization
• Applicativo Client
richiede una pagina
• Ottiene l’oggetto
Document dal Content
Repository

• Serializza l’oggetto
Document in JSON

Workflow Operazioni READ
Item Operation Read - getPage
31
32
GET /site1/it/pages/<uuid>
Richiesta GET Data Provider Normalization
namespace InnoteamBundleCMFBundleController;
class PageController extends Controller
{
/**
* @Route(
* path="{domain}/{locale}/pages/{id}",
* methods={"GET"},
* requirements={"id"=".+", "domain"="w+", "locale"="^[a-z]{2}$"},
* name="api_cms_get_page",
* defaults={
* "_api_resource_class"=Page::class,
* "_api_item_operation_name"="getPage",
* "_api_item_operation_field"="id",
* "_api_respond"=true
* }
* )
*
* @param Page $data
* @return Page
*/
public function detailAction(Page $data)
{
return $data;
}
}
InnoteamBundleCMFBundleDocumentPage:
shortname: 'Page'
itemOperations:
getPage:
route_name: 'api_cms_get_page'
normalization_context:
groups: [ 'page-details' ]
33
ReadListener.php
Richiesta GET Data Provider Normalization
/**
* Calls the data provider and sets the data attribute.
*
* @param GetResponseEvent $event
* @throws NotFoundHttpException
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
try {
$attributes = RequestAttributesExtractor::extractAttributes($request);
} catch (RuntimeException $e) {
return;
}
if (isset($attributes['collection_operation_name'])) {
$data = $this->getCollectionData($request, $attributes);
} else {
$data = $this->getItemData($request, $attributes);
}
$request->attributes->set('data', $data);
}
34
Richiesta GET Data Provider Normalization
PageDocumentItemDataProvider.php
protected function doGetItem(
string $resourceClass,
string $id,
string $path,
string $operationName = null,
array $context = []
)
{
try {
/* @var Page $page */
$page = $this->manager->findTranslation(Page::class, $path, $this->locale, false);
} catch (MissingTranslationException $e) {
throw new NotFoundHttpException(sprintf(
"Page Document with path '%s' and locale '%s' not found", $path, $this->locale
));
}
return $page;
35
Richiesta GET Data Provider Normalization
namespace InnoteamBundleCMFBundleController;
class PageController extends Controller
{
/**
* @Route(
* path="{domain}/{locale}/pages/{id}",
* methods={"GET"},
* requirements={"id"=".+", "domain"="w+", "locale"="^[a-z]{2}$"},
* name="api_cms_get_page",
* defaults={
* "_api_resource_class"=Page::class,
* "_api_item_operation_name"="getPage",
* "_api_item_operation_field"="id",
* "_api_respond"=true
* }
* )
*
* @param Page $data
* @return Page
*/
public function detailAction(Page $data)
{
return $data;
}
36
Richiesta GET Data Provider Normalization
InnoteamBundleCMFBundleDocumentPage:
shortname: 'Page'
itemOperations:
getPage:
route_name: 'api_cms_get_page'
normalization_context:
groups: [ 'page-details' ]
InnoteamBundleCMFBundleDocumentPage:
attributes:
name:
groups: ['page-details', ...]
nameTranslated:
groups: ['page-details', ...]
type:
groups: ['page-details', ...]
blocks:
groups: ['page-details', ...]
{
"name": "my-article",
"nameTranslated": "mio-articolo",
"type": "generic-page",
"blocks": [
{
"type": "pb-block-title",
"attributes": {
"title": "Il mio primo articolo"
},
"enabled": true,
"name": "Title"
},
{
"type": "pb-block-intro",
"attributes": {
"title": "Lorem ipsum dolor sit amet",
"subtitle": "Sed ut perspiciatis unde omnis",
},
"enabled": true,
"name": "Introduction"
}
]
OPERAZIONI WRITE
37
38
01
S T E P
02
S T E P
03
S T E P
04
S T E P
Workflow Operazioni WRITE
1)Richiesta POST/PUT/DELETE
• Applicativo Client effettua
un‘operazione WRITE su un
Document/API Resource
2)Denormalization
• API Platform deserializza JSON in
oggetto Document/API Resource
3)WriteListener & Document Writer
• WriteListener mappa la richiesta al
metodo del Document Writer
4)Normalization
• Serializza l’oggetto Document in
JSON
Collection Operation Write -
createPage
39
40
POST /site1/it/pages
{“name”: “my-article”, “nameTranslated”: “mio-articolo”, …}
/**
* @Route(
* path="{domain}/{locale}/pages",
* methods={"POST"},
* requirements={"domain"="w+", "locale"="^[a-z]{2}$"},
* name="api_cms_create_page",
* defaults={
* "_api_resource_class"=Page::class,
* "_api_collection_operation_name"="createPage",
* "_api_respond"=true
* }
* )
*
* @Security("is_granted('ROLE_CMS_USER')")
*
* @param $data
* @return mixed
*/
public function createAction($data)
{
return $data;
}
InnoteamBundleCMFBundleDocumentPage:
shortname: 'Page'
collectionOperations:
createPage:
route_name: 'api_cms_create_page'
normalization_context:
groups: [ 'page-details' ]
denormalization_context:
groups: [ 'page-create' ]
01 02 03 04
41
/vendor/api-platform/core/src/EventListener/DeserializeListener.php
/**
* Deserializes the data sent in the requested format.
*
* @param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if ($request->isMethodSafe(false) || $request->isMethod(Request::METHOD_DELETE)) {
return;
}
...
$request->attributes->set(
'data',
$this->serializer->deserialize(
$request->getContent(), $attributes['resource_class'], $format, $context
)
);
}
01 02 03 04
42
InnoteamBundleCMFBundleDocumentPage:
shortname: 'Page'
collectionOperations:
createPage:
route_name: 'api_cms_create_page'
normalization_context:
groups: [ 'page-details' ]
denormalization_context:
groups: [ 'page-create' ]
InnoteamBundleCMFBundleDocumentPage:
attributes:
name:
groups: ['page-create', ...]
nameTranslated:
groups: ['page-create', ...]
type:
groups: ['page-create', ...]
blocks:
groups: [‘page-create', ...]
/**
* @Route(
* path="{domain}/{locale}/pages",
* methods={"POST"},
* requirements={"domain"="w+", "locale"="^[a-z]{2}$"},
* name="api_cms_create_page",
* defaults={
* "_api_resource_class"=Page::class,
* "_api_collection_operation_name"="createPage",
* "_api_respond"=true
* }
* )
*
* @Security("is_granted('ROLE_CMS_USER')")
*
* @param $data
* @return mixed
*/
public function createAction($data)
{
return $data;
}
01 02 03 04
43
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$this->request = $event->getRequest();
$reqMethod = $this->request->getMethod();
$resourceClass = $this->request->attributes->get('_api_resource_class');
$opName = $this->getOperationName();
$document = $event->getControllerResult();
if (Request::METHOD_POST === $reqMethod &&
$opName === DocumentOperationMapper::getCreateOperationName($resourceClass)
) {
$event->setControllerResult(
$this->documentWriter->persistDocument($document, $this->domain, $this->locale)
);
}
}
/Bridge/Doctrine/PHPCR/EventListener/WriteListener.php
01 02 03 04
44
InnoteamBundleCMFBundleDocumentPage:
shortname: 'Page'
collectionOperations:
createPage:
route_name: 'api_cms_create_page'
normalization_context:
groups: [ 'page-details' ]
denormalization_context:
groups: [ 'page-create' ]
InnoteamBundleCMFBundleDocumentPage:
attributes:
name:
groups: ['page-details', ...]
nameTranslated:
groups: ['page-details', ...]
type:
groups: ['page-details', ...]
blocks:
groups: ['page-details', ...]
{
"name": "my-article",
"nameTranslated": "mio-articolo",
"type": "generic-page",
"blocks": [
{
"type": "pb-block-title",
"attributes": {
"title": "Il mio primo articolo"
},
"enabled": true,
"name": "Title"
},
{
"type": "pb-block-intro",
"attributes": {
"title": "Lorem ipsum dolor sit amet",
"subtitle": "Sed ut perspiciatis unde omnis",
},
"enabled": true,
"name": "Introduction"
}
]
01 02 03 04
Grazie!
45

PHPCR e API Platform: cosa significa davvero sviluppare un CMF con Symfony

  • 1.
    www.innoteam.it PHPCR & APIPlatform What it really means to build a CMF
  • 2.
    Case Study -CMS Premessa: Un nostro cliente prestigioso ci ha chiesto di sviluppare un CMS custom Requisiti: • Restful • Versionamento dei contenuti • Multilingua • Multisite 2
  • 3.
    • Content ManagementFramework • Un framework che offre gli strumenti per la gestione dei contenuti • E’ un toolbox per creare CMS custom • Esempi: • eZ Publish / eZ Platform • Symfony CMF • Content Management System • E’ un sistema “pronto all’uso” per la gestione dei contenuti • Fornisce un interfaccia admin ben precisa • Esempi: • Wordpress • Craft CMS 3 CMS CMF
  • 4.
    Symfony CMF • E’un insieme di bundle che possono essere usati per aggiungere funzionalità CMS ad applicativi Symfony • content-bundle, routing-bundle, menu-bundle, … • Nato per applicativi Symfony server side(twig) • SonataDoctrinePHPCRAdminBundle 4
  • 5.
  • 6.
    6 • Creare unbundle riutilizzabile(vendor) che sfrutta i bundle necessari di Symfony CMF • Esporre in maniera RESTFul le operazioni con API Platform Back-end • Applicativo Angular che consuma le API del CMF per la gestione dei contenuti(CMS) Back-office • Applicativo Angular che consuma in GET le API del CMF per la visualizzazione dei contenuti Front-end
  • 7.
  • 8.
    Architettura Symfony CMF 8 ContentRepository | Apache Jackrabbit PHPCR API | Jackalope Doctrine PHPCR-ODM DoctrinePHPCRBundle Symfony CMF Bundles
  • 9.
    Content Repository • E’uno storage engine che permette di accedere e manipolare contenuti anche di natura eterogenea (e.g. pagine, video, immagini, recensioni, ecc..) in maniera uniforme. • Esempio: Apache Jackrabbit 9 Albero dei Contenuti 1)Nodo • rappresenta un contenuto • raggiungibile da un path come in un filesystem 2)Proprietà di un nodo • contiene l’informazione • semplice(stringa, bool, int) • binaria(binary stream) / a b c d e path: /a/d p1: true path: /a/e p1: “Titolo Pagina” p2: path: /a path: /b p1: 25 path: /c p1: 3.5
  • 10.
    Workspace • Un contentrepository è formato da n workspace. • Ogni workspace ha il suo albero di contenuti. • Sessione: E’ una connessione autenticata ad un singolo workspace 10 Content Repository Workspace a Workspace b Workspace c / a b c / /
  • 11.
    PHPCR(PHP Content Repository)API • E’ una specifica di API Standard per interfacciarsi con qualsiasi Content Repository in una maniera uniforme. • E’ un porting di JCR(Java Content Repository) API 11
  • 12.
    Jackalope • E’ un’implementazioneopen-source di PHPCR API • Supporta diversi driver backend (transport) 12 Jackalope Jackrabbit Jackalope DBAL Content Repository (Apache Jackrabbit) RDBMS (MySQL, SQLite, Postgres)
  • 13.
    Doctrine PHPCR-ODM • E’un ODM (Object Document Mapper) • Utilizza il “Data Mapper” pattern per mappare Nodi PHPCR ad oggetti PHP (Document) • Supporta concetti PHPCR come children, references, versioning 13
  • 14.
    DoctrinePHPCRBundle • Interagisce conPHPCR API & Doctrine PHPCR-ODM per fornire il Document Manager come servizio Symfony 14
  • 15.
  • 16.
    Workspace default &live 16 Content Repository Workspace Default Workspace Live / a / persist /a
 v1 - DRAFT
  • 17.
    Workspace default &live 17 Content Repository Workspace Default Workspace Live / a / persist /a
 v1 - DRAFT v2 - DRAFT v3 - DRAFT v4 - DRAFT v5 - DRAFT
  • 18.
    Workspace default &live 18 Content Repository Workspace Default Workspace Live / a / a publish /a v5 v1 - DRAFT v2 - DRAFT v3 - DRAFT v4 - DRAFT v5 - PUBLISHED v1 - PUBLISHED
  • 19.
    CMFBundle 19 Content Repository |Apache Jackrabbit PHPCR API | Jackalope Doctrine PHPCR-ODM DoctrinePHPCRBundle Symfony CMF Bundles API Platform "require": { "api-platform/core": "2.0.*", "symfony-cmf/core-bundle": "2.0.*", "symfony-cmf/menu-bundle": "2.1.*", "symfony-cmf/routing-bundle": "2.0.*", "symfony-cmf/content-bundle": "2.0.*", "symfony-cmf/routing-auto-bundle": "2.0.*", "symfony-cmf/routing-auto": "2.0.*", "doctrine/phpcr-bundle": "1.3.*", "doctrine/phpcr-odm": "1.4.*", "jackalope/jackalope-jackrabbit": "1.3.*", "phpcr/phpcr-shell": "^1.0" }, composer.json
  • 20.
    Config Dipendenze CMFBundle 20 /Resources/config/bundles.yml #Config DoctrinePHPCRBundle Sessions doctrine_phpcr: session: default_session: default sessions: default: backend: type: jackrabbit connection: php_cr url: "%jackrabbit_url%" workspace: default username: "%phpcr_user%" password: "%phpcr_pass%" live: backend: type: jackrabbit connection: php_cr url: "%jackrabbit_url%" workspace: live username: "%phpcr_user%" password: "%phpcr_pass%" # Config DoctrinePHPCRBundle Locales & DMs doctrine_phpcr: odm: # locales: # en: [it] # it: [en] # default_locale: it locale_fallback: hardcoded document_managers: default: session: default mappings: InnoteamCMFBundle: ~ live: session: live mappings: InnoteamCMFBundle: ~
  • 21.
  • 22.
    Iniettare Config 22 /DependencyInjection/InnoteamCMFExtension.php class InnoteamCMFExtensionextends Extension implements PrependExtensionInterface { public function prepend(ContainerBuilder $container) { $config = $this->processConfiguration(new Configuration(), $container->getExtensionConfig($this->getAlias())); $extConfigs = Yaml::parse(file_get_contents(__DIR__ . '/../Resources/config/bundles.yml')); foreach ($extConfigs as $key => $extConfig) { switch ($key) { case 'cmf_core': $extConfig['multilang']['locales'] = array_keys($config['locales']); break; case 'doctrine_phpcr': $extConfig['odm']['locales'] = $config['locales']; $extConfig['odm']['default_locale'] = $config['default_locale']; break; } $container->prependExtensionConfig($key, $extConfig); } } }
  • 23.
  • 24.
    24 namespace InnoteamBundleCMFBundleDocument; use SymfonyCmfBundleContentBundleDoctrinePhpcrStaticContent; useDoctrineODMPHPCRMappingAnnotations as PHPCR; /** * @PHPCRDocument( * translator="attribute", * versionable="full", * referenceable=true, * repositoryClass=“InnoteamBundleCMFBundleRepositoryDocumentPageRepository" * ) */ class Page extends StaticContent implements WritableDocumentInterface { /** @PHPCRField(type="string", nullable=false) */ protected $name; /** @PHPCRField(type="string", nullable=false, translated=true) */ protected $nameTranslated; /** @PHPCRField(type="string", nullable=false) */ protected $type; /** @PHPCRField(type="string", translated=true) */ protected $blocks; /** @PHPCRField(type="string", translated=true) */ protected $status; /** @PHPCRReferrers(referringDocument="AutoRoute", referencedBy="content") */ protected $routes;
  • 25.
    DocumentWriter 25 namespace InnoteamBundleCMFBundleDocumentWriter; class ChainDocumentWriterimplements DocumentWriterInterface { /** @var DocumentWriterInterface[] */ protected $documentWriters; public function __construct(array $documentWriters) { $this->documentWriters = $documentWriters; } public function publishDocument(WritableDocumentInterface $document, string $domainId, string $locale) : WritableDocumentInterface { foreach ($this->documentWriters as $documentWriter) { try { return $documentWriter->publishDocument($document, $domainId, $locale); } catch (DocumentPublishingNotSupportedException $e) { continue; } } throw new DocumentPublishingNotSupportedException(sprintf( "No Document Publisher found which supports publishing Document with id '%s' and class '%s'", $document->getId(), get_class($document) )); } public function persistDocument(WritableDocumentInterface $document, string $domainId, string $locale): WritableDocumentInterface {} public function deleteDocument(WritableDocumentInterface $document, string $domainId, string $locale) {} public function hideDocument(WritableDocumentInterface $document, string $domainId, string $locale): WritableDocumentInterface {} public function unhideDocument(WritableDocumentInterface $document, string $domainId, string $locale): WritableDocumentInterface {} }
  • 26.
    Persist DocumentWriter 26 namespace InnoteamBundleCMFBundleDocumentWriterDocumentPersistWriter; classPageDocumentPersistWriter extends BaseDocumentActionWriter implements DocumentPersistWriterInterface { public function persistDocument( WritableDocumentInterface $document, string $domainId, string $locale ) : WritableDocumentInterface { $document->setStatus(StatusType::DRAFT); $this->defaultManager->persist($document); $this->defaultManager->bindTranslation($document, $locale); $this->defaultManager->flush(); $metadata = $this->defaultManager->getClassMetadata(get_class($document)); if (false !== $metadata->versionable) $this->defaultManager->checkpoint($document); return $document; } }
  • 27.
  • 28.
    Item & CollectionOperations 28 /Resources/config/api_resources/resources.yml Item Operation • operazione associata ad un singolo item • getPage (GET /site1/it/pages/<uuid>) • editPage (PUT /site1/it/pages/<uuid>) Collection Operation • Operazione GET che ritorna un listato di item • es: GET /site1/pages-no-locale • Operazione di creazione di un item: • es: POST /site1/it/pages resources: InnoteamBundleCMFBundleDocumentPage: shortname: 'Page' itemOperations: getPage: route_name: 'api_cms_get_page' normalization_context: groups: [ 'page-details' ] ... collectionOperations: createPage: route_name: 'api_cms_create_page' normalization_context: groups: [ 'page-details' ] denormalization_context: groups: [ 'page-create' ] ...
  • 29.
  • 30.
    30 Richiesta GET DataProvider Normalization Richiesta GET Page Data Provider Normalization • Applicativo Client richiede una pagina • Ottiene l’oggetto Document dal Content Repository • Serializza l’oggetto Document in JSON Workflow Operazioni READ
  • 31.
    Item Operation Read- getPage 31
  • 32.
    32 GET /site1/it/pages/<uuid> Richiesta GETData Provider Normalization namespace InnoteamBundleCMFBundleController; class PageController extends Controller { /** * @Route( * path="{domain}/{locale}/pages/{id}", * methods={"GET"}, * requirements={"id"=".+", "domain"="w+", "locale"="^[a-z]{2}$"}, * name="api_cms_get_page", * defaults={ * "_api_resource_class"=Page::class, * "_api_item_operation_name"="getPage", * "_api_item_operation_field"="id", * "_api_respond"=true * } * ) * * @param Page $data * @return Page */ public function detailAction(Page $data) { return $data; } } InnoteamBundleCMFBundleDocumentPage: shortname: 'Page' itemOperations: getPage: route_name: 'api_cms_get_page' normalization_context: groups: [ 'page-details' ]
  • 33.
    33 ReadListener.php Richiesta GET DataProvider Normalization /** * Calls the data provider and sets the data attribute. * * @param GetResponseEvent $event * @throws NotFoundHttpException */ public function onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); try { $attributes = RequestAttributesExtractor::extractAttributes($request); } catch (RuntimeException $e) { return; } if (isset($attributes['collection_operation_name'])) { $data = $this->getCollectionData($request, $attributes); } else { $data = $this->getItemData($request, $attributes); } $request->attributes->set('data', $data); }
  • 34.
    34 Richiesta GET DataProvider Normalization PageDocumentItemDataProvider.php protected function doGetItem( string $resourceClass, string $id, string $path, string $operationName = null, array $context = [] ) { try { /* @var Page $page */ $page = $this->manager->findTranslation(Page::class, $path, $this->locale, false); } catch (MissingTranslationException $e) { throw new NotFoundHttpException(sprintf( "Page Document with path '%s' and locale '%s' not found", $path, $this->locale )); } return $page;
  • 35.
    35 Richiesta GET DataProvider Normalization namespace InnoteamBundleCMFBundleController; class PageController extends Controller { /** * @Route( * path="{domain}/{locale}/pages/{id}", * methods={"GET"}, * requirements={"id"=".+", "domain"="w+", "locale"="^[a-z]{2}$"}, * name="api_cms_get_page", * defaults={ * "_api_resource_class"=Page::class, * "_api_item_operation_name"="getPage", * "_api_item_operation_field"="id", * "_api_respond"=true * } * ) * * @param Page $data * @return Page */ public function detailAction(Page $data) { return $data; }
  • 36.
    36 Richiesta GET DataProvider Normalization InnoteamBundleCMFBundleDocumentPage: shortname: 'Page' itemOperations: getPage: route_name: 'api_cms_get_page' normalization_context: groups: [ 'page-details' ] InnoteamBundleCMFBundleDocumentPage: attributes: name: groups: ['page-details', ...] nameTranslated: groups: ['page-details', ...] type: groups: ['page-details', ...] blocks: groups: ['page-details', ...] { "name": "my-article", "nameTranslated": "mio-articolo", "type": "generic-page", "blocks": [ { "type": "pb-block-title", "attributes": { "title": "Il mio primo articolo" }, "enabled": true, "name": "Title" }, { "type": "pb-block-intro", "attributes": { "title": "Lorem ipsum dolor sit amet", "subtitle": "Sed ut perspiciatis unde omnis", }, "enabled": true, "name": "Introduction" } ]
  • 37.
  • 38.
    38 01 S T EP 02 S T E P 03 S T E P 04 S T E P Workflow Operazioni WRITE 1)Richiesta POST/PUT/DELETE • Applicativo Client effettua un‘operazione WRITE su un Document/API Resource 2)Denormalization • API Platform deserializza JSON in oggetto Document/API Resource 3)WriteListener & Document Writer • WriteListener mappa la richiesta al metodo del Document Writer 4)Normalization • Serializza l’oggetto Document in JSON
  • 39.
  • 40.
    40 POST /site1/it/pages {“name”: “my-article”,“nameTranslated”: “mio-articolo”, …} /** * @Route( * path="{domain}/{locale}/pages", * methods={"POST"}, * requirements={"domain"="w+", "locale"="^[a-z]{2}$"}, * name="api_cms_create_page", * defaults={ * "_api_resource_class"=Page::class, * "_api_collection_operation_name"="createPage", * "_api_respond"=true * } * ) * * @Security("is_granted('ROLE_CMS_USER')") * * @param $data * @return mixed */ public function createAction($data) { return $data; } InnoteamBundleCMFBundleDocumentPage: shortname: 'Page' collectionOperations: createPage: route_name: 'api_cms_create_page' normalization_context: groups: [ 'page-details' ] denormalization_context: groups: [ 'page-create' ] 01 02 03 04
  • 41.
    41 /vendor/api-platform/core/src/EventListener/DeserializeListener.php /** * Deserializes thedata sent in the requested format. * * @param GetResponseEvent $event */ public function onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); if ($request->isMethodSafe(false) || $request->isMethod(Request::METHOD_DELETE)) { return; } ... $request->attributes->set( 'data', $this->serializer->deserialize( $request->getContent(), $attributes['resource_class'], $format, $context ) ); } 01 02 03 04
  • 42.
    42 InnoteamBundleCMFBundleDocumentPage: shortname: 'Page' collectionOperations: createPage: route_name: 'api_cms_create_page' normalization_context: groups:[ 'page-details' ] denormalization_context: groups: [ 'page-create' ] InnoteamBundleCMFBundleDocumentPage: attributes: name: groups: ['page-create', ...] nameTranslated: groups: ['page-create', ...] type: groups: ['page-create', ...] blocks: groups: [‘page-create', ...] /** * @Route( * path="{domain}/{locale}/pages", * methods={"POST"}, * requirements={"domain"="w+", "locale"="^[a-z]{2}$"}, * name="api_cms_create_page", * defaults={ * "_api_resource_class"=Page::class, * "_api_collection_operation_name"="createPage", * "_api_respond"=true * } * ) * * @Security("is_granted('ROLE_CMS_USER')") * * @param $data * @return mixed */ public function createAction($data) { return $data; } 01 02 03 04
  • 43.
    43 public function onKernelView(GetResponseForControllerResultEvent$event) { $this->request = $event->getRequest(); $reqMethod = $this->request->getMethod(); $resourceClass = $this->request->attributes->get('_api_resource_class'); $opName = $this->getOperationName(); $document = $event->getControllerResult(); if (Request::METHOD_POST === $reqMethod && $opName === DocumentOperationMapper::getCreateOperationName($resourceClass) ) { $event->setControllerResult( $this->documentWriter->persistDocument($document, $this->domain, $this->locale) ); } } /Bridge/Doctrine/PHPCR/EventListener/WriteListener.php 01 02 03 04
  • 44.
    44 InnoteamBundleCMFBundleDocumentPage: shortname: 'Page' collectionOperations: createPage: route_name: 'api_cms_create_page' normalization_context: groups:[ 'page-details' ] denormalization_context: groups: [ 'page-create' ] InnoteamBundleCMFBundleDocumentPage: attributes: name: groups: ['page-details', ...] nameTranslated: groups: ['page-details', ...] type: groups: ['page-details', ...] blocks: groups: ['page-details', ...] { "name": "my-article", "nameTranslated": "mio-articolo", "type": "generic-page", "blocks": [ { "type": "pb-block-title", "attributes": { "title": "Il mio primo articolo" }, "enabled": true, "name": "Title" }, { "type": "pb-block-intro", "attributes": { "title": "Lorem ipsum dolor sit amet", "subtitle": "Sed ut perspiciatis unde omnis", }, "enabled": true, "name": "Introduction" } ] 01 02 03 04
  • 45.