Slide 1

Slide 1 text

www.innoteam.it PHPCR & API Platform What it really means to build a CMF

Slide 2

Slide 2 text

Case Study - CMS Premessa: Un nostro cliente prestigioso ci ha chiesto di sviluppare un CMS custom Requisiti: • Restful • Versionamento dei contenuti • Multilingua • Multisite 2

Slide 3

Slide 3 text

• 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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

La nostra proposta 5

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Symfony CMF e PHPCR 7

Slide 8

Slide 8 text

Architettura Symfony CMF 8 Content Repository | Apache Jackrabbit PHPCR API | Jackalope Doctrine PHPCR-ODM DoctrinePHPCRBundle Symfony CMF Bundles

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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 / /

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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)

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

DoctrinePHPCRBundle • Interagisce con PHPCR API & Doctrine PHPCR-ODM per fornire il Document Manager come servizio Symfony 14

Slide 15

Slide 15 text

Il nostro setup 15

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Workspace default & live 17 Content Repository Workspace Default Workspace Live / a / persist /a
 v1 - DRAFT v2 - DRAFT v3 - DRAFT v4 - DRAFT v5 - DRAFT

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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: ~

Slide 21

Slide 21 text

Config CMFBundle 21 app/config/config.yml innoteam_cmf: domains: site1: host: http://www.site1.com default_locale: it locales: it: [en] en: [it]

Slide 22

Slide 22 text

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); } } }

Slide 23

Slide 23 text

Organizzazione Albero Contenuti 23

Slide 24

Slide 24 text

24 namespace Innoteam\Bundle\CMFBundle\Document; use Symfony\Cmf\Bundle\ContentBundle\Doctrine\Phpcr\StaticContent; use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCR; /** * @PHPCR\Document( * translator="attribute", * versionable="full", * referenceable=true, * repositoryClass=“Innoteam\Bundle\CMFBundle\Repository\Document\PageRepository" * ) */ class Page extends StaticContent implements WritableDocumentInterface { /** @PHPCR\Field(type="string", nullable=false) */ protected $name; /** @PHPCR\Field(type="string", nullable=false, translated=true) */ protected $nameTranslated; /** @PHPCR\Field(type="string", nullable=false) */ protected $type; /** @PHPCR\Field(type="string", translated=true) */ protected $blocks; /** @PHPCR\Field(type="string", translated=true) */ protected $status; /** @PHPCR\Referrers(referringDocument="AutoRoute", referencedBy="content") */ protected $routes;

Slide 25

Slide 25 text

DocumentWriter 25 namespace Innoteam\Bundle\CMFBundle\DocumentWriter; 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 {} }

Slide 26

Slide 26 text

Persist DocumentWriter 26 namespace Innoteam\Bundle\CMFBundle\DocumentWriter\DocumentPersistWriter; 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; } }

Slide 27

Slide 27 text

API Platform 27

Slide 28

Slide 28 text

Item & Collection Operations 28 /Resources/config/api_resources/resources.yml Item Operation • operazione associata ad un singolo item • getPage (GET /site1/it/pages/) • editPage (PUT /site1/it/pages/) 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: Innoteam\Bundle\CMFBundle\Document\Page: 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' ] ...

Slide 29

Slide 29 text

Operazioni READ 29

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Item Operation Read - getPage 31

Slide 32

Slide 32 text

32 GET /site1/it/pages/ Richiesta GET Data Provider Normalization namespace Innoteam\Bundle\CMFBundle\Controller; 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; } } Innoteam\Bundle\CMFBundle\Document\Page: shortname: 'Page' itemOperations: getPage: route_name: 'api_cms_get_page' normalization_context: groups: [ 'page-details' ]

Slide 33

Slide 33 text

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); }

Slide 34

Slide 34 text

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;

Slide 35

Slide 35 text

35 Richiesta GET Data Provider Normalization namespace Innoteam\Bundle\CMFBundle\Controller; 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; }

Slide 36

Slide 36 text

36 Richiesta GET Data Provider Normalization Innoteam\Bundle\CMFBundle\Document\Page: shortname: 'Page' itemOperations: getPage: route_name: 'api_cms_get_page' normalization_context: groups: [ 'page-details' ] Innoteam\Bundle\CMFBundle\Document\Page: 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" } ]

Slide 37

Slide 37 text

OPERAZIONI WRITE 37

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Collection Operation Write - createPage 39

Slide 40

Slide 40 text

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; } Innoteam\Bundle\CMFBundle\Document\Page: shortname: 'Page' collectionOperations: createPage: route_name: 'api_cms_create_page' normalization_context: groups: [ 'page-details' ] denormalization_context: groups: [ 'page-create' ] 01 02 03 04

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

42 Innoteam\Bundle\CMFBundle\Document\Page: shortname: 'Page' collectionOperations: createPage: route_name: 'api_cms_create_page' normalization_context: groups: [ 'page-details' ] denormalization_context: groups: [ 'page-create' ] Innoteam\Bundle\CMFBundle\Document\Page: 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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

44 Innoteam\Bundle\CMFBundle\Document\Page: shortname: 'Page' collectionOperations: createPage: route_name: 'api_cms_create_page' normalization_context: groups: [ 'page-details' ] denormalization_context: groups: [ 'page-create' ] Innoteam\Bundle\CMFBundle\Document\Page: 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

Slide 45

Slide 45 text

Grazie! 45