Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHPCR & API Platform: What it really means to build a CMF

PHPCR & API Platform: What it really means to build a CMF

Scrivere un CMF da zero: uno degli strumenti che il mondo PHP/Symfony mette a disposizione per la gestione dei contenuti è PHPCR. In questo talk vi spiegherò come abbiamo utilizzato Doctrine PHPCR ODM e API Platform per poter costruire un CMF ed esporre le sue funzionalità in maniera REST. Inoltre vi mostrerò gli imprevisti che ci ha lasciato e come li abbiamo superati.

Salvatore Pollaci

October 19, 2018
Tweet

More Decks by Salvatore Pollaci

Other Decks in Programming

Transcript

  1. Case Study - CMS Premessa: Un nostro cliente prestigioso ci

    ha chiesto di sviluppare un CMS custom Requisiti: • Restful • Versionamento dei contenuti • Multilingua • Multisite 2
  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
  3. 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
  4. 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
  5. Architettura Symfony CMF 8 Content Repository | Apache Jackrabbit PHPCR

    API | Jackalope Doctrine PHPCR-ODM DoctrinePHPCRBundle Symfony CMF Bundles
  6. 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
  7. 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 / /
  8. 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
  9. 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)
  10. 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
  11. DoctrinePHPCRBundle • Interagisce con PHPCR API & Doctrine PHPCR-ODM per

    fornire il Document Manager come servizio Symfony 14
  12. Workspace default & live 17 Content Repository Workspace Default Workspace

    Live / a / persist /a
 v1 - DRAFT v2 - DRAFT v3 - DRAFT v4 - DRAFT v5 - DRAFT
  13. 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
  14. 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
  15. 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: ~
  16. 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); } } }
  17. 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;
  18. 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 {} }
  19. 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; } }
  20. 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: 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' ] ...
  21. 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
  22. 32 GET /site1/it/pages/<uuid> 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' ]
  23. 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); }
  24. 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;
  25. 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; }
  26. 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" } ]
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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