Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

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

116 views

Published on

Scrivere un CMF da zero: uno degli strumenti che il mondo PHP/Symfony mette a disposizione per la gestione dei contenuti è PHPCR. Nelle slide di questo talk presentato al SymfonyDay 2018 spieghiamo come abbiamo utilizzato Doctrine PHPCR ODM e API Platform per poter costruire un CMF ed esporre le sue funzionalità in maniera REST. Inoltre mostriamo gli imprevisti che ci ha lasciato e come li abbiamo superati.

Published in: Technology
  • Be the first to comment

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

  1. 1. www.innoteam.it PHPCR & API Platform What it really means to build a CMF
  2. 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. 3. • 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
  4. 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. 5. La nostra proposta 5
  6. 6. 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
  7. 7. Symfony CMF e PHPCR 7
  8. 8. Architettura Symfony CMF 8 Content Repository | Apache Jackrabbit PHPCR API | Jackalope Doctrine PHPCR-ODM DoctrinePHPCRBundle Symfony CMF Bundles
  9. 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. 10. 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 / /
  11. 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. 12. 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)
  13. 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. 14. DoctrinePHPCRBundle • Interagisce con PHPCR API & Doctrine PHPCR-ODM per fornire il Document Manager come servizio Symfony 14
  15. 15. Il nostro setup 15
  16. 16. Workspace default & live 16 Content Repository Workspace Default Workspace Live / a / persist /a
 v1 - DRAFT
  17. 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. 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. 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. 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. 21. Config CMFBundle 21 app/config/config.yml innoteam_cmf: domains: site1: host: http://www.site1.com default_locale: it locales: it: [en] en: [it]
  22. 22. 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); } } }
  23. 23. Organizzazione Albero Contenuti 23
  24. 24. 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;
  25. 25. 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 {} }
  26. 26. 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; } }
  27. 27. API Platform 27
  28. 28. 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' ] ...
  29. 29. Operazioni READ 29
  30. 30. 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
  31. 31. Item Operation Read - getPage 31
  32. 32. 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. 33. 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. 34. 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. 35. 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. 36. 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" } ]
  37. 37. OPERAZIONI WRITE 37
  38. 38. 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
  39. 39. Collection Operation Write - createPage 39
  40. 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. 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. 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. 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. 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. 45. Grazie! 45

×