Weitere ähnliche Inhalte Ähnlich wie PHPCR e API Platform: cosa significa davvero sviluppare un CMF con Symfony (20) Kürzlich hochgeladen (20) PHPCR e API Platform: cosa significa davvero sviluppare un CMF con Symfony2. 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 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. 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
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
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 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. 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’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. 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
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: ~
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);
}
}
}
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. 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. 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;
}
}
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' ]
...
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
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
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
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
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
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"
}
]
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
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 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
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