12 KiB
CMS architecture
Introduction
A lot can be achieved in SilverStripe by adding properties and form fields
to your own page types (via [api:SiteTree->getCMSFields()]
), as well as creating
your own data management interfaces through [api:ModelAdmin]
. But sometimes
you'll want to go deeper and tailor the underlying interface to your needs as well.
For example, to build a personalized CMS dashboard, or content "slots" where authors
can drag their content into. At its core, SilverStripe is a web application
built on open standards and common libraries, so lots of the techniques should
feel familiar to you. This is just a quick run down to get you started
with some special conventions.
For a more practical-oriented approach to CMS customizations, refer to the Howto: Extend the CMS Interface which builds
Markup and Style Conventions
While SilverStripe is intended to work with JavaScript only, we're following the principles of "Progressive Enhancement" where feasible, relying on a comparatively light layer of JavaScript to enhance forms and markup generated on the server. This allows seamless customization of aspects like form fields. We're explaining this philosophy in more detail on our blog).
All CSS in the CMS UI is written in the SCSS language extensions
and the Compass framework, which helps
us maintain expressive and concise style declarations. The files are located in sapphire/admin/scss
(and if you have the cms
module installed, in cms/scss
), and are compiled to a css
folder on the
same directory path. Changes to the SCSS files can be automatically converted by installing
the "compass" module for SilverStripe,
although installing the compass framework directly works as well.
Each file describes its purpose at the top of the declarations. Note that you can write
plain CSS without SCSS for your custom CMS interfaces as well, we just mandate SCSS for core usage.
As there's a whole lot of CSS driving the CMS, we have certain best practives around writing it:
- Use the
id
attribute sparingly. Remember that it "closes off" the structure to code reuse, as HTML elements require uniqueid
attributes. Code reuse can happen both in CSS and JavaScript behaviour. - Separate presentation from structure in class names, e.g.
left-menu
is encoding the component position (which might change later on). A more structural name could becms-menu
(orcms-tools-menu
for a more specific version) - Class naming: Use the
cms-
class prefix for major components in the cms interface, and thess-ui-
prefix for extensions to jQuery UI. Don't use theui-
class prefix, its reserved for jQuery UI built-in styles. - Use jQuery UI's built-in styles where possible, e.g.
ui-widget
for a generic container, orui-state-highlight
to highlight a specific component. See the jQuery UI Theming API for a full list.
See our system requirements for a list of supported browsers.
Templates and Controllers
The CMS backend is handled through the [api:LeftAndMain]
controller class,
which contains base functionality like displaying and saving a record.
This is extended through various subclasses, e.g. to add a group hierarchy ([api:SecurityAdmin]
),
a search interface ([api:ModelAdmin]
) or an "Add Page" form ([api:CMSPageAddController]
).
The controller structure is too complex to document here, a good starting point
for following the execution path in code are [api:LeftAndMain->getRecord()]
and [api:LeftAndMain->getEditForm()]
.
If you have the cms
module installed, have a look at [api:CMSMain->getEditForm()]
for a good
example on how to extend the base functionality (e.g. by adding page versioning hints to the form).
CMS templates are inherited based on their controllers, similar to subclasses of
the common Page
object (a new PHP class MyPage
will look for a MyPage.ss
template).
We can use this to create a different base template with LeftAndMain.ss
(which corresponds to the LeftAndMain
PHP controller class).
In case you want to retain the main CMS structure (which is recommended),
just create your own "Content" template (e.g. MyCMSController_Content.ss
),
which is in charge of rendering the main content area apart from the CMS menu.
Depending on the complexity of your layout, you'll also need to overload the
"EditForm" template (e.g. MyCMSController_EditForm.ss
), e.g. to implement
a tabbed form which only scrolls the main tab areas, while keeping the buttons at the bottom of the frame.
This requires manual assignment of the template to your form instance, see [api:CMSMain->getEditForm()]
for details.
Layout and Panels
The CMS markup is structured into "panels", which are the base units containing
interface components (or other panels), as declared by the class cms-panel
. Panels can be made collapsible, and
get the ability to be resized and aligned with a layout manager, in our case jLayout.
This layout manager applies CSS declarations (mostly dimensions and positioning) via JavaScript,
by extracting additional metadata from the markup in the form of HTML5 data attributes.
We're using a "border layout" which separates the panels into five areas: north, south, east, west and center (all of which are optional).
As layouts can be nested, this allows for some powerful combinations. Our
Howto: Extend the CMS Interface has a practical example on
how to add a bottom panel to the CMS UI.
The various panels and UI components within them are not tightly coupled
to the layout engine, so any changes in dimension which impact the overall layout
need to be handled manually. In SilverStripe, we've established a convention for a redraw()
method on each panel and UI component for this purpose, which is usually invoked
through its parent container. Invocation order is crucial here, generally going from
innermost to outermost elements. For example, the tab panels have be applied in the CMS form
before the form itself is layouted with its sibling panels to avoid incorrect dimensions.
Forms
SilverStripe constructs forms and its fields within PHP,
mainly through the [getCMSFields()](api:DataObject->getCMSFields())
method.
This in turn means that the CMS loads these forms as HTML via Ajax calls,
e.g. after saving a record (which requires a form refresh), or switching the section in the CMS>
JavaScript through jQuery.entwine
jQuery.entwine is a thirdparty library
which allows us to attach behaviour to DOM elements in a flexible and structured mannger.
It replaces the behaviour.js
library used in previous versions of the CMS interface.
See Topics: JavaScript for more information on how to use it.
In the CMS interface, all entwine rules should be placed in the "ss" entwine namespace.
If you want to call methods defined within these rules outside of entwine logic,
you have to use this namespace, e.g. $('.cms-menu').entwine('ss').collapse()
.
Note that only functionality that is custom to the CMS application needs to be built in jQuery.entwine, we're trying to reuse library code wherever possible. The most prominent example of this is the usage of jQuery UI for dialogs and buttons.
JavaScript and CSS dependencies via Requirements and Ajax
The JavaScript logic powering the CMS is divided into many files,
which typically are included via the [api:Requirements]
class, by adding
them to [api:LeftAndMain->init()]
and its subclassed methods.
This class also takes care of minification and combination of the files,
which is crucial for the CMS performance (see [api:Requirements::combine_files()]
).
Due to the procedural and selector-driven style of UI programming in jQuery.entwine,
it can be difficult to find the piece of code responsible for a certain behaviour.
Therefore it is important to adhere to file naming conventions.
E.g. a feature only applicable to ModelAdmin
should be placed in
sapphire/admin/javascript/ModelAdmin.js
, while something modifying all forms (including ModelAdmin forms)
would be better suited in sapphire/admin/javascript/LeftAndMain.EditForm.js
.
Selectors used in these files should mirrow the "scope" set by its filename,
so don't place a rule applying to all form buttons inside ModelAdmin.js
.
The CMS relies heavily on Ajax-loading of interfaces, so each interface and the JavaScript driving it have to assume its underlying DOM structure is appended via Ajax callback rather than being available when the browser window first loads. jQuery.entwine is effectively an advanced version of jQuery.live and jQuery.delegate, so takes care of dynamic event binding.
Most interfaces will require their own JavaScript and CSS files, so the Ajax loading has
to ensure they're loaded unless already present. A custom-built library called
jQuery.ondemand
(located in sapphire/thirdparty
) takes care of this transparently -
so as a developer just declare your dependencies through the [api:Requirements]
API.
Ajax Loading and Browser History
SilverStripe uses the HTML5 browser history to modify the URL without a complete window refresh, and load its UI via Ajax by hooking into browser navigation events (through the history.js wrapper library). This technique has an impact on how any Ajax load needs to happen: In order to support browser history (and change the URL state), a CMS developer needs to fire a navigation event rather than invoking the Ajax call directly.
The main point of contact here is $('.cms-container').loadPanel(<url>, <title>, <data>)
in LeftAndMain.js
. The data
object can contain additional state which is required
in case the same navigation event is fired again (e.g. when the user pressed the back button).
Most commonly, the (optional) data.selector
property declares which DOM element to replace
with the newly loaded HTML (it defaults to .cms-content
). This is handy to only replace
e.g. an edit form, but leave the search panel in the same "content area" unchanged.
No callbacks are allowed in this style of Ajax loading, as all state needs
to be "repeatable". Any logic required to be exected after the Ajax call
should be placed in jQuery.entinwe onmatch()
rules which apply to the newly created DOM structures.
See $('.cms-container').handleStateChange()
in LeftAndMain.js
for details.
Alternatively, form-related Ajax calls can be invoked through their own wrappers,
which don't cause history events and hence allow callbacks: $('.cms-content').loadForm()
and $('.cms-content').submitForm()
.
Menu
The navigation menu in the CMS is created through the [api:CMSMenu]
API,
which auto-detects all subclasses of LeftAndMain
. This means that your custom
ModelAdmin
subclasses will already appear in there without any explicit definition.
To modify existing menu entries or create new ones, see [api:CMSMenu::add_menu_item()]
and [api:CMSMenu::remove_menu_item()]
.