
Let's continue to look at the
symfony CMF , which implements the concept of a platform for building a CMS from loosely coupled components. In the
first part of the article, we examined in detail the storage and access to data, in the second part everything else is waiting for us.
The continuation of the article comes out with a significant delay due to my laziness, health problems and the Internet. For these couple of months, the system has grown to version 1.0.0, and all subsequent edits in the master branch for some reason break the system, without being documented. In case anyone wants to install the system with his hands, remember - rely on stable versions marked with tags.
The most impatient can squander down, download a virtual machine with an installed system (VirtualBox is required) and touch everything myself, but for the sake of completeness I would recommend reading the article first.
')
So. What we have on schedule after data storage?
Screenshot of the main page of the demo project
Template engine
Here, everything is familiar to many -
Twig is used. Flexible, powerful, very fast and concise. It supports blocking, inheritance, and compilation of templates into PHP code. The master page template looks like this:
{% extends "SandboxMainBundle::skeleton.html.twig" %} {% block content %} <p><em>We are on the homepage which uses a special template</em></p> {% createphp cmfMainContent as="rdf" %} {{ rdf|raw }} {% endcreatephp %} <hr/> {{ sonata_block_render({ 'name': 'additionalInfoBlock' }, { 'divisible_by': 3, 'divisible_class': 'row', 'child_class': 'span3' }) }} <div class="row"> <div class="span3"> <h2>Some additional links:</h2> <ul> {% for child in cmf_children(cmf_find('/cms/simple')) %} <li> <a href="{{ path(child) }}">{{ child.title|striptags }}</a> </li> {% endfor %} </ul> </div> <div class="span3"> {{ sonata_block_render({ 'name': 'rssBlock' }) }} </div> </div> {% endblock %}
CoreBundle includes a bundle of extensions for Twig, which simplify working with CMF and traversing a PHPCR tree, for example, functions such as
cmf_prev
,
cmf_next
,
cmf_children
and
others .
There is nothing more to look at here anymore, Twig - he is in Africa Twig.
A couple of words about admin
Admin home page
The famous admin generator SonataAdminBundle performs exactly the same function in Symfony CMF, but through a special layer in the form of
SonataDoctrinePhpcrAdminBundle . This was done so that the original bandl could abstract from the data storage.
TreeBrowserBundle, working on
jsTree , is intended for working with tree structures.
The components that have the admin part, described below, will definitely connect their panels here. Therefore, I donât see any reason to dwell on this, detailed screenshots will be further.
Static content
Static content in the CMS - the foundation of everything. In Symfony CMF, ContentBundle is responsible for static content, which provides a basic implementation of static document classes, including multilingualism and route communication.
The basis of the bundle is the class
StaticContent
, whose composition will be familiar to many - self-speaking fields such as
title
,
body
, link to the parent document, and so on. In addition, it implements two interfaces:
RouteReferrersInterface
, provides a bundle with routesPublishWorkflowInterface
, helps to show or hide content using specified publication dates
For multilanguage documents,
MultilangStaticContent
is provided - everything is the same, but translation of fields and a locale declaration are added. How is the translation - we have already seen in the first part of the article.
Bundle relies controller.
ContentController
consists of a single
indexAction
, which accepts the desired document as input and renders it in the desired language if everything is in order with the publication parameters. Optionally, you can set a template with which the page will be displayed. If you do not specify, the default one will be taken.
Routing
In modern large sites, the amount of material can be easily measured in thousands. Multiply by the number of transfers. Add the need for constant revisions of materials and URLs to them in the name of search engine optimization.
At the same time, notice that such things are usually handled by the site administrator (webmaster, content manager, SEO), and not the developer.
What are the requirements for routing in this case?
- URL is set by user
- multisite support
- multilingual support
- tree structure
- content, menus and routes should be separated
If we recall the standard Symfony 2 router, it becomes clear that such flexibility cannot be achieved there. Routes are explicitly spelled out in the config for each controller, and the user is simply not allowed to change them. The maximum that you can count on is any
/page/{slug}
, which can be edited from the admin panel.
Let's take a look at how a functioning SF2 circuit looked like:
If you do not go into the details of the options for configuring the parameters, everything is pretty primitive. A request comes in, the router decides what to call the controller, the controller pulls the necessary data and renders the view, then issues the coveted Response.
This is a fairly familiar scheme.
Why is this option not good for CMS?
Imagine that we have a
PageController
, which takes as an argument a URL-alias of the page, compares it with what is stored in the database and returns a page, or 404.
In my practice, there were cases when among static content there were different forms that would have looked more harmoniously as part of the site section, rather than as a separate component. For example, on one bank site, a credit
/calculator
added to the URL of the text section
/credits/cash
, so that people, after reading the necessary information about loans, could find the necessary numbers on the spot.
Suppose the
PageController
first part of the URL, what to do with the calculator, which obviously will act as a separate controller? Add
pattern: /credits/cash/calculator
in the config and specify a separate controller / action? Somehow ugly. Even if you prioritize the rest of the routes, it is clear that there is no smell of flexibility here - if the alias in the database changes, you will have to edit the config with your hands.
Need something else.
We summarize routing in SF2:
- determine which controller serves the request
- parse URL parameters
- if nothing happens, the default behavior is based on a pre-configured set of routes.
- either from the application configuration
- either from bundles
- users cannot edit routes
- does not scale to a very large number of routes
- in the CMS, the user himself wants to decide what address should be.
The concept of routing in symfony CMF
From the beautiful and powerful, but inconvenient in the case of CMS Symfony 2 router had to be abandoned in favor of the new concept:
- need to separate the content tree from the navigation tree
- The navigation tree consists of links to the elements of the content tree. Due to this, they are easily implemented:
- multi-site (desktop, tablet, mobile versions)
- multilanguage
- rebooting the navigation requires cloning the navigation tree
- on readiness, the result flows back
Immediately a solution comes to mind: we create a default route (
/{url}
with the obligatory parameter
url: .*
), One controller for all requests, and depending on the contents we redirect the request to other controllers. But at the same time, no one cancels conflicts with other routes.
navigation: pattern: "/{url}" defaults: { _controller: service.controller:indexAction } requirements: url: .*
It still does not sound very much.
A better solution was to provide (as long as it was not marked as outdated since the days of Symfony 2.1)
DoctrineRouter
. It is already much more flexible, because it looked for routes by the URL in the database, while the implementation was ready for documents through PHPCR-ODM, and you can attach any of your own. The route was explicitly specified by the controller, otherwise the
ControllerResolver
used, which itself tried to decide which controller would process the request. There were also built-in resolvers:
- binding of nodes of a certain type to the controller
- binding nodes of a certain type to a pattern and using a standard (generic) controller
Up to the heap - route redirection (to other routes or absolute URLs).
At the moment, to solve all the problems with routing in Symfony CMF, two components are used -
ChainRouter
and
DynamicRouter
. The first replaces the standard SF2 router and, despite the name, the operation of the router (the definition of the controller for processing the request) does not actually perform. Instead, it allows you to add your routers to the chain list. In the chain, all configured routers will try to process the request in turn, in order of priority. Router services are searched by tags.
cmf_routing: chain: routers_by_id: # DynamicRouter # # cmf_routing.dynamic_router: 20 # acme_core.my_router: 50 # router.default: 100 services: acme_core.my_router: class: %my_namespace.my_router_class% tags: - { name: cmf_routing.router, priority: 300 }
Well, we have an infinite number of routers available for use.
Now we recall the search for routes in the database and
DynamicRouter
. Its task is to load routes from the
provider , the provider can be (and usually is) a database. In the standard package there are implementations of providers for Doctrine PHPCR-ODM, Doctrine ORM and of course, you can add to the list of providers by implementing
RouteProviderInterface .
What do providers do?
DynamicRouter
request, providers issue an ordered subset of candidate routes that may suit the incoming request, and
DynamicRouter
makes the final decision and matches the request with a specific
Route
object.
The route determines which controller will handle a particular request.
DynamicRouter
uses several methods in descending order of priority:
- explicitly:
Route
document itself explicitly declares the final controller if it returns from the getDefault('_controller')
call. - by alias: route returns the value of
getDefault('type')
, which is mapped to the configuration from config.yml
- by class: The
Route
document must implement the RouteObjectInterface
and return an object for getContent()
. The return type of the class is again mapped to the config. - default: the default controller will be used, if any is configured
Similarly (explicitly or by class), the route can also specify the pattern with which the page should be rendered.
Optionally, using the above-mentioned
RouteObjectInterface
you can teach the route to return an instance of the model associated with it.
Redirects are also supported. In general, there is the
RedirectRouteInterface
interface, but for PHPCR-ODM, the implementation is ready as a
RedirectRoute
document. It can redirect to an absolute URI and to a named route generated by any router in the chain.
Another important feature that might be interesting for those who did not work with Symfony is the two-wayness of the router. In addition to recognizing routes based on specified parameters, these routes can also be generated by passing parameters as arguments. Unlike the standard SF2 router, as a parameter for the
path()
function, you can pass not only the route name specified in the config, but also the implementation
RouteObjectInterface
,
RouteReferrersInterface
(that is, route object), or a link to an object in the repository using its content_id:
<a href="{{ path(myRoute) }}">Read on</a> <a href="{{ path('/cms/routes') }}">Home</a> <a href="{{ path(myContent) }}">Read on</a> <a href="{{ path(null, {'content_id': '/cms/content/my-content'}) }}"> Read on </a>
If several routes are suitable for the same material, the locale of which matches the query locale will be considered preferable.
Finally, back to what was written a little earlier, the separation of the navigation tree and the content tree. Take a look at the scheme, branched navigation according to certain rules transfers control to the necessary controllers and only after that it requests data:
In the admin for the routes, you can set the format (so that the URL ends in
.html
, for example) and the final slash.
On top of that, the experimental
RoutingAutoBundle proposes to generate routes for the content based on the rules prepared in advance. By generating auto routes, flexibility is achieved: for individual routes, it is easy to translate pseudonyms, generate a site map and change the class of documents to which the route can refer. But in most cases for simple CMS this bundle may not be necessary.
On this with flexible routing finish.
Menu
No CMS can do without a menu system. Although the menu structure usually follows the content structure, it may need its own logic, not defined by the content or existing in several contexts with different options:
Symfony CMF includes MenuBundle, a tool that allows you to define your own menus. It extends the well-known
KnpMenuBundle , complementing it with hierarchical and multilingual elements and tools to write them to the selected repository.
When displaying the menu, MenuBundle relies on default for KnpMenuBundle renderers and helpers.
It is recommended to read
full documentation , but in general in the simplest case, the output looks like this:
{{ knp_menu_render('simple') }}
The menu name passed to the
MenuProviderInterface
will in turn be passed to the
MenuProviderInterface
implementation, which will decide which menu to show.
At the heart of the bundle is
PhpcrMenuProvider
, the implementation of
MenuProviderInterface
, which is responsible for dynamically loading the menu from the PHPCR storage. By default, the provider service is configured by the
menu_basepath
parameter, which specifies where to look for the menu in the PHPCR tree. When rendering a menu, the
name
parameter is passed, which must be a direct descendant of the specified base path. This allows
PhpcrMenuProvider
to work with several menu hierarchies using a single storage mechanism. Recalling the above usage example, the
simple
menu should be located at
/cms/menu/simple
, if the following is specified in the configuration:
cmf_menu: menu_basepath: /cms/menu
Two types of nodes are
MenuNode
:
MenuNode
and
MultilangMenuNode
.
MenuNode
contains information about a specific menu item:
label
,
uri
, a list of
children
, a link to the route, the associated Content element, plus an attributes
attributes
list, which allows you to customize the menu output.
The
MultilangMenuNode
class extends
MenuNode
to support multilingualism: a
locale
field has been added to define the translation to which the item and
label
belong to the
uri
, marked as
translated=true
. These are the only fields that differ between translations.
For integration with the admin panel provides panels and services for SonataDoctrinePhpcrAdminBundle. Panels are available immediately, but to use them, you must explicitly add to the dashboards.
A bundle is configured
as usual , but all parameters are optional.
The link between routing, menu and content is demonstrated here:
Blocks
Provided and bandle to work with blocks. Blocks can implement some kind of logic or simply return static content that can be invoked anywhere in the template. BlockBundle is based on the
SonataBlockBundle and, where
appropriate , replaces the components of the parent bundle with its PHPCR compatible ones.
Typical blocks from the main page
Inside the bundle are a few typical blocks:
StringBlock
is a block with a single body
field, which simply renders a string in a template, without even surrounding it with any tags.SimpleBlock
- add title
to body
ContainerBlock
- renders a specified list of blocks (including other block-containers)ReferenceBlock
- can only refer to another block. When a call is triggered, it is as if the block referred to is called.ActionBlock
- ActionBlock
result of executing a certain action from the controller, you can send the desired request parametersRssBlock
- Shows an RSS feed with the specified template.ImagineBlock
- used by LiipImagineBundle to display images directly from PHPCRSlideshowBlock
is a special kind of container block that allows you to wrap any blocks in markup so that you can organize a slideshow. It is noteworthy that the JS library for this you need to choose yourself, in the bundle it is not.
You can create
your own blocks .
The blocking
caching mechanism works on top of the
SonataCacheBundle , although BlockBundle lacks adapters for MongoDB, Memcached and APC - you have to be content with Varnish or SSI.
Blocks are
sonata_block_render()
function
sonata_block_render()
, but unlike the original bundle, the block name is passed to PHPCR as arguments.
Frontend / Inline Editing
Editing on-the-fly implemented using several components.
The first is
RDFa markup. This is a way to describe metadata in HTML in the style of microformats, but with the help of attributes.
<div id="myarticle" typeof="http://rdfs.org/sioc/ns#Post" about="http://example.net/blog/news_item"> <h1 property="dcterms:title">News item title</h1> <div property="sioc:content">News item contents</div> </div>
After that, the code above ceases to be a âstupidâ set of DOM elements, because information from attributes can be conveniently extracted into JS code and associated with models and collections of Backbone.js using
VIE.js - this is the second component.
The third in the chain is
create.js , which saves us from having to invent an editing interface.
Workflow create.js
create.js runs on top of VIE.js on jQuery widgets. What can he do?
- Modify the contents of RDF-tagged elements using editors - Aloha, Hallo, Redactor, ckEditor
- using localStorage, provide support for saving-restoring edits before they go to CMS
- manage notifications appearing during editing
- organize your toolbars with the right tools
- call custom workflow functions such as "delete", "unpublish"
All content is edited in place, while at the expense of RDFa it is not necessary to generate tons of auxiliary HTML markup, as some CMS do.
ckEditor in the service of good
Well, it closes the
CreatePHP list, a library that links the create.js calls and the backend itself. It is responsible for mapping the properties of the model in PHP to HTML attributes and rendering the entity. The most attentive ones have already seen that there is a Twig extension for CreatePHP and its challenge is in the first listing of this article: we pass the model and specify the output format. Beauty.
The last two components are combined for convenience in CreateBundle.
Mediabundle
One of the bundles of the most minimalist implementation is a bundle for working with media objects. They can be documents, binaries, MP3s, videos, and even what your heart desires. The current version supports uploading pictures and downloading files, and writing everything else.
SonataMediaBundle can help, especially since there is integration.
Bundle provides:
- basic documents for simple models;
- basic
FormType
for simple models; - controller for uploading and downloading files;
- helper, giving an abstraction from uploading to the server;
- controller to display the picture.
As well as helpers and adapters for integration:
- media browsers (elFinder, ckFinder, MceFileManager, etc.);
- image manipulation libraries (Imagine, LiipImagineBundle).
There is a whole scattering of interfaces for creating your own media classes:
MediaInterface
: base class;MetadataInterface
: definition of metadata;FileInterface
: defined as a file;ImageInterface
: defined as a picture;FileSystemInterface
: the file is stored in the file system, as the media object preserves the path to it;BinaryInterface
: mainly used when the file is saved inside the media object;DirectoryInterface
: defined as a directory;HierarchyInterface
: Media objects stored directory path to the media: /path/to/file/filename.ext
.
An interesting approach to file paths. In bundle terminology, the path to the media object is understood, for example, /path/to/my/media.jpg
and the differences between the paths in Windows and * nix-systems are leveled. In PHPCR, this path can be used as an identifier. Several useful methods are available:
getPath
gets the path to the object stored in PHPCR, ORM or another Doctrine storage;getUrlSafePath
transforms the path for safe use into a URL;mapPathToId
transforms the path into an identifier to search the Doctrine storage;mapUrlSafePathToId
transforms the URL back into an id.
Self-supporting features are available in the Twig extension:
<a href="{{ cmf_media_download_url(file) }}" title="Download">Download</a> <img src="{{ cmf_media_display_url(image) }}" alt="" />
You can attach a picture to the document through the provided Type Type:
use Symfony\Component\Form\FormBuilderInterface; protected function configureFormFields(FormBuilderInterface $formBuilder) { $formBuilder ->add('image', 'cmf_media_image', array('required' => false)) ; }
Implemented adapters media browser elFinder , libraries Gaufrette , giving abstraction layer above the file system and LiipImagine , which simplifies the manipulation of the images.
As I said before, the implementation of the bundle is minimalist. Suffice it to say that the pictures are not loaded in a crowd, and in order to physically delete the file attached to the document, you must also delete the document itself. Um
Perspectives
It is planned (and sometimes even ready to some extent) integration with modules:
- SymfonyCmfSearchBundle (full search, extends LiipSearchBundle)
- SymfonyCmfSimpleCms (the simplest CMS that comes with CMF)
- LuneticsLocaleBundle (automatic locale detection)
- other bundles from Sonata
And of course, the development of new features and the elimination of current shortcomings.
Is everything so good?
Iâm already here for a lot of kilobytes of text Iâve been crucified, as everything in Symfony CMF is wonderful, so itâs logical to ask where is the criticism.
Lacks lack.
Symfony CMF is updated infrequently - on the githaba it is indicated that the process of releasing new versions is similar to the release scheme of SF2, that is, every six months (we write new features for four months, fix bugs for two months and prepare a release). Of course, there will be minor fixes aimed at eliminating vulnerabilities, but in general, if you want a new one, you will have to wait considerably. At the same time, this is the stage of development, when no one promises to maintain backward compatibility between releases at any cost. This means that what worked in 1.0, in 1.1, can easily break.
The documentation suffers. In the wiki project is a mess, many articles should already be deleted, outdated code is written somewhere, and in general, the Symfony CMF Book is not as friendly and simple as a similar compilation for SF2.
CMF has a very high threshold of entry. To install a test system, itâs not enough to âunpack everything into webroot and run install.phpâ - you need to understand the connection between the components and be able to handle each of them. Any revision or implementation of your code will require a thoughtful study of the entrails. Although, probably, developers using SF2 will not be scared. And for users, there is no documentation at all ...
The conclusion that suggests itself only on the screenshots is that the system is damp and far from the presentation. Usability of admin panel is in question - it seems like a neat Bootstrap, and it seems that the designerâs hand didnât touch anything here. Despite the cool front-end editor, for body
-elements in the admin panel there is only a pitiful textarea two lines high. For typical operations, one has to perform too many gestures due to ill-conceived navigation.
The documentation often contains promises to make X or Y later. If you want someone to advertise the project - to convince the expediency of use will work with difficulty, I think. There will be no eye-candy-sheets fashionable nowadays that promise how easy and fun your life will be after installing Symfony CMF. In general, there are no âboxesâ from which you can get an attractive working system. And probably not
Separately, I note that the examples of industrial use Symfony CMF until no. It is not known how the system behaves under load and what to do if you suddenly need scaling (including the backend) - these issues are not disclosed in the documentation except for the Cache bundle and the APC installation.
Get down to business
You can immediately download the virtual machine image I prepared for VirtualBox, where everything is installed and configured, including various backends. For convenience, you can register to your hosts file ip_ cmf-sandbox
and go there through the browser, but in general you can go and simply by the IP address, which she will try to prompt immediately after the login (default login and password :) symfony
.
Mirrors (.ova-file, â 1Gb):
If for some reason you donât want to bother with it, I give a link to the online sandbox , but you donât get there to dig deeper inside, although itâs good to look at the quick fix too.
Manually installing a little bit of a chore, so I will not dwell on each step described in the instructions . Just briefly go through the main points.
Requirements for the machine to run CMF are a bit non-standard, although no crime.
First, you need to satisfy the standard requirements for symfony 2 (itâs very likely that this is all right):
- install PHP 5.3.3+
- enable json support
- enable ctype support
- to
php.ini
correctly installdate.timezone
- install doctrine pdo drivers
Everything else (APC and so on) is optional.
Next come the symfony CMF requirements. SQLite is used by default for data storage, so make sure that the extension is installed pdo_sqlite
.
To use other backends, install:
- Apache Jackrabbit and, accordingly, Java (for each distribution kit, its own installation method). At the root of the CMF installed, there must be a script
jack
that downloads and helps launch Jackrabbit without unnecessary gestures. You can use it, but Java is still set separately. - Midgard2 PHPCR PHP. , : , ( sid Debian), , , . , , RPM, deb-. , â Midgard2 CMF .
Also in the folder with a sandbox lies a script written by me switch_backends.py
, which itself will replace the config with the desired one (the original files are corrected in app/config/phpcr/
) and will clean up the production cache so that it all takes off. For obvious reasons, I commented out the midgard-options so far - they still do not work.
I want to warn against the temptation to type in the console git pull
or composer update
- as I said at the beginning of the article, the edits in the master branch violate the performance of the system, wait for the next stable release.
Summary
So, despite the numerous âbutâ, the project looks interesting. Surprisingly, some fundamental content management problems (for example, multilingual and routing / menu) have been successfully resolved. Development is slowly being conducted by an extremely limited circle of people who already have a job, so now the best help is a fork on a githaba and a useful pull-request, whether it is documentation editing or error correction. CMF popularization is only ahead (try searching the web for materials, there are practically none), all hope is only in the open source and the community.
Using CMF now in production can be risky, but itâs still worth being aware of. Who knows, maybe from those millions of euros something will fall here and in a couple of years we will see a wonderful product?
That's all.
A small list of useful links: