Review and clean up of extending section

This commit is contained in:
Will Rossiter 2014-10-22 23:47:56 +13:00 committed by Cam Findlay
parent ce02d01274
commit 19259e0497
13 changed files with 970 additions and 1050 deletions

View File

@ -1,78 +1,89 @@
title: Rich-Text Editing (WYSIWYG)
# Rich-Text Editing (WYSIWYG)
## Introduction
Editing and formatting content is the bread and butter of every content management system, which is why SilverStripe
has a tight integration with our preferred editor library, [TinyMCE](http://tinymce.com).
Editing and formatting content is the bread and butter of every content management system,
which is why SilverStripe has a tight integration with our preferred editor library, [TinyMCE](http://tinymce.com).
On top of the base functionality, we use our own insertion dialogs to ensure
you can effectively select and upload files. In addition to the markup managed by TinyMCE,
we use [shortcodes](/reference/shortcodes) to store information about inserted
images or media elements.
## Usage
On top of the base functionality, we use our own insertion dialogs to ensure you can effectively select and upload
files. In addition to the markup managed by TinyMCE, we use [shortcodes](../../extending/shortcodes) to store
information about inserted images or media elements.
The framework comes with a `[api:HTMLEditorField]` form field class which encapsulates most of the required
functionality. It is usually added through the `[api:DataObject->getCMSFields()]` method:
**mysite/code/MyObject.php**
:::php
<?php
class MyObject extends DataObject {
private static $db = array('Content' => 'HTMLText');
private static $db = array(
'Content' => 'HTMLText'
);
public function getCMSFields() {
return new FieldList(new HTMLEditorField('Content'));
return new FieldList(
new HTMLEditorField('Content')
);
}
}
## Configuration
To keep the JavaScript editor configuration manageable and extensible,
we've wrapped it in a PHP class called `[api:HtmlEditorConfig]`.
The class comes with its own defaults, which are extended through [configuration files](/topics/configuration)
To keep the JavaScript editor configuration manageable and extensible, we've wrapped it in a PHP class called
`[api:HtmlEditorConfig]`. The class comes with its own defaults, which are extended through the [Configuration API](../../configuration)
in the framework (and the `cms` module in case you've got that installed).
There can be multiple configs, which should always be created / accessed using `[api:HtmlEditorConfig::get]`.
You can then set the currently active config using `set_active()`.
There can be multiple configs, which should always be created / accessed using `[api:HtmlEditorConfig::get]`. You can
then set the currently active config using `set_active()`.
<div class="info" markdown="1">
By default, a config named 'cms' is used in any field created throughout the CMS interface.
<div class="notice" markdown='1'>
Caveat: currently the order in which the `_config.php` files are executed depends on the module directory
names. Execution order is alphabetical, so if you set a TinyMCE option in the `aardvark/_config.php`, this
will be overriden in `framework/admin/_config.php` and your modification will disappear.
This is a general problem with `_config.php` files - it may be fixed in the future by making it possible to
configure the TinyMCE with the new [configuration system](../topics/configuration).
</div>
### Adding and removing capabilities
<div class="notice" markdown='1'>
Currently the order in which the `_config.php` files are executed depends on the module directory names. Execution
order is alphabetical, so if you set a TinyMCE option in the `aardvark/_config.php`, this will be overridden in
`framework/admin/_config.php` and your modification will disappear.
</div>
## Adding and removing capabilities
In its simplest form, the configuration of the editor includes adding and removing buttons and plugins.
You can add plugins to the editor using the Framework's `[api:HtmlEditorConfig::enablePlugins]` method. This will
transparently generate the relevant underlying TinyMCE code.
**mysite/_config.php**
:::php
// File: mysite/_config.php
HtmlEditorConfig::get('cms')->enablePlugins('media');
Note: this utilises the TinyMCE's `PluginManager::load` function under the hood (check the
[TinyMCE documentation on plugin
loading](http://www.tinymce.com/wiki.php/API3:method.tinymce.AddOnManager.load) for details).
<div class="notice" markdown="1">
This utilities the TinyMCE's `PluginManager::load` function under the hood (check the
[TinyMCE documentation on plugin loading](http://www.tinymce.com/wiki.php/API3:method.tinymce.AddOnManager.load) for
details).
</div>
Plugins and advanced themes can provide additional buttons that can be added (or removed) through the
configuration. Here is an example of adding a `ssmacron` button after the `charmap` button:
**mysite/_config.php**
:::php
// File: mysite/_config.php
HtmlEditorConfig::get('cms')->insertButtonsAfter('charmap', 'ssmacron');
Buttons can also be removed:
**mysite/_config.php**
:::php
// File: mysite/_config.php
HtmlEditorConfig::get('cms')->removeButtons('tablecontrols', 'blockquote', 'hr');
Note: internally `[api:HtmlEditorConfig]` uses the TinyMCE's `theme_advanced_buttons` option to configure these. See
the [TinyMCE documentation of this option](http://www.tinymce.com/wiki.php/Configuration:theme_advanced_buttons_1_n)
<div class="notice" markdown="1">
Internally `[api:HtmlEditorConfig]` uses the TinyMCE's `theme_advanced_buttons` option to configure these. See the
[TinyMCE documentation of this option](http://www.tinymce.com/wiki.php/Configuration:theme_advanced_buttons_1_n)
for more details.
</div>
### Setting options
@ -83,6 +94,7 @@ One example of the usage of this capability is to redefine the TinyMCE's [whitel
tags](http://www.tinymce.com/wiki.php/Configuration:extended_valid_elements) - the tags that will not be stripped
from the HTML source by the editor.
**mysite/_config.php**
:::php
// Add start and type attributes for <ol>, add <object> and <embed> with all attributes.
HtmlEditorConfig::get('cms')->setOption(
@ -97,17 +109,19 @@ from the HTML source by the editor.
'ol[start|type]'
);
Note: the default setting for the CMS's `extended_valid_elements` we are overriding here can be found in
<div class="notice" markdown="1">
The default setting for the CMS's `extended_valid_elements` we are overriding here can be found in
`framework/admin/_config.php`.
</div>
### Writing custom plugins
## Writing custom plugins
It is also possible to add custom buttons to TinyMCE. A simple example of this is SilverStripe's `ssmacron`
plugin. The source can be found in the Framework's `thirdparty/tinymce_ssmacron` directory.
It is also possible to add custom buttons to TinyMCE. A simple example of this is SilverStripe's `ssmacron` plugin. The
source can be found in the Framework's `thirdparty/tinymce_ssmacron` directory.
Here is how we can create a project-specific plugin. Create a `mysite/javascript/myplugin` directory,
add the plugin button icon - here `myplugin.png` - and the source code - here `editor_plugin.js`. Here is a very
simple example of a plugin that adds a button to the editor:
Here is how we can create a project-specific plugin. Create a `mysite/javascript/myplugin` directory, add the plugin
button icon - here `myplugin.png` - and the source code - here `editor_plugin.js`. Here is a very simple example of a
plugin that adds a button to the editor:
:::js
(function() {
@ -140,8 +154,9 @@ simple example of a plugin that adds a button to the editor:
tinymce.PluginManager.add('myplugin', tinymce.plugins.myplugin);
})();
You can then enable this plugin through the `[api:HtmlEditorConfig::enablePlugins]`:
You can then enable this plugin through the [api:HtmlEditorConfig::enablePlugins]:
**mysite/_config.php**
:::php
HtmlEditorConfig::get('cms')->enablePlugins(array('myplugin' => '../../../mysite/javascript/myplugin/editor_plugin.js'));
@ -150,50 +165,49 @@ documentation, or browse through plugins that come with the Framework at `thirdp
## Image and Media Insertion
The `[api:HtmlEditorField]` API also handles inserting images and media
files into the managed HTML content. It can be used both for referencing
files on the webserver filesystem (through the `[api:File]` and `[api:Image]` APIs),
as well as hotlinking files from the web.
The `[api:HtmlEditorField]` API also handles inserting images and media files into the managed HTML content. It can be
used both for referencing files on the webserver filesystem (through the `[api:File]` and `[api:Image]` APIs), as well
as hotlinking files from the web.
We use [shortcodes](/reference/shortcodes) to store information about inserted images or media elements.
The `[api:ShortcodeParser]` API post-processes the HTML content on rendering,
and replaces the shortcodes accordingly. It also takes care of care of placing the
shortcode replacements relative to its surrounding markup (e.g. left/right alignment).
We use [shortcodes](../../configuration/shortcodes) to store information about inserted images or media elements. The
[api:ShortcodeParser] API post-processes the HTML content on rendering, and replaces the shortcodes accordingly. It also
takes care of care of placing the shortcode replacements relative to its surrounding markup (e.g. left/right alignment).
## oEmbed: Embedding media through external services
The ["oEmbed" standard](http://www.oembed.com/) is implemented by many media services
around the web, allowing easy representation of files just by referencing a website URL.
For example, a content author can insert a playable youtube video just by knowing
its URL, as opposed to dealing with manual HTML code.
The ["oEmbed" standard](http://www.oembed.com/) is implemented by many media services around the web, allowing easy
representation of files just by referencing a website URL. For example, a content author can insert a playable youtube
video just by knowing its URL, as opposed to dealing with manual HTML code.
oEmbed powers the "Insert from web" feature available through `[api:HtmlEditorField]`.
Internally, it makes HTTP queries to a list of external services
if it finds a matching URL. These services are described in the `Oembed.providers` configuration.
Since these requests are performed on page rendering, they typically have a long cache time (multiple days). To refresh
a cache, append `?flush=1` to a URL.
oEmbed powers the "Insert from web" feature available through `[api:HtmlEditorField]`. Internally, it makes HTTP
queries to a list of external services if it finds a matching URL. These services are described in the
`Oembed.providers` configuration. Since these requests are performed on page rendering, they typically have a long
cache time (multiple days).
<div class="info" markdown="1">
To refresh a oEmbed cache, append `?flush=1` to a URL.
</div>
To disable oEmbed usage, set the `Oembed.enabled` configuration property to "false".
### Doctypes
Since TinyMCE generates markup, it needs to know which doctype your documents
will be rendered in. You can set this through the [element_format](http://www.tinymce.com/wiki.php/Configuration:element_format) configuration variable. It defaults to the stricter 'xhtml'
setting, for example rendering self closing tags like `<br/>` instead of `<br>`.
Since TinyMCE generates markup, it needs to know which doctype your documents will be rendered in. You can set this
through the [element_format](http://www.tinymce.com/wiki.php/Configuration:element_format) configuration variable. It
defaults to the stricter 'xhtml' setting, for example rendering self closing tags like `<br/>` instead of `<br>`.
In case you want to adhere to HTML4 instead, use the following configuration:
:::php
HtmlEditorConfig::get('cms')->setOption('element_format', 'html');
By default, TinyMCE and SilverStripe will generate valid HTML5 markup,
but it will strip out HTML5 tags like `<article>` or `<figure>`.
If you plan to use those, add them to the [valid_elements](http://www.tinymce.com/wiki.php/Configuration:valid_elements)
configuration setting.
By default, TinyMCE and SilverStripe will generate valid HTML5 markup, but it will strip out HTML5 tags like
`<article>` or `<figure>`. If you plan to use those, add them to the
[valid_elements](http://www.tinymce.com/wiki.php/Configuration:valid_elements) configuration setting.
Also, the `[api:SS_HTMLValue]` API underpinning the HTML processing parses the markup
into a temporary object tree which can be traversed and modified before saving.
The built-in parser only supports HTML4 and XHTML syntax. In order to successfully
process HTML5 tags, please use the
Also, the `[api:SS_HTMLValue]` API underpinning the HTML processing parses the markup into a temporary object tree
which can be traversed and modified before saving. The built-in parser only supports HTML4 and XHTML syntax. In order
to successfully process HTML5 tags, please use the
['silverstripe/html5' module](https://github.com/silverstripe/silverstripe-html5).
## Recipes
@ -299,7 +313,3 @@ Now change the default spellchecker in `framework/thirdparty/tinymce-spellchecke
:::php
// ...
$config['general.engine'] = 'PSpell';
## Related
* [Howto: Extend the CMS Interface](../howto/extend-cms-interface)

View File

@ -1,292 +0,0 @@
# DataExtension
## Introduction
Extensions allow for adding additional functionality to a `[api:DataObject]` or
modifying existing functionality without the hassle of creating a subclass.
## Usage
Extensions are defined as subclasses of either `[api:DataExtension]` for
extending a `[api:DataObject]` subclass or the `[api:Extension]` class for non
DataObject subclasses (such as Controllers)
:::php
<?php
// mysite/code/MyMemberExtension.php
class MyMemberExtension extends DataExtension {
}
This defines your own extension where we can add our own functions, database
fields or other properties. After this class has been created, it
does not yet apply it to your object. Next you need to tell SilverStripe what
class you want to extend.
### Adding a extension to a built-in class
For example, you may might want to add a `MyMemberExtension` class to the
`[api:Member]` object to provide a custom method.
In order to active this extension, you need to add the following to your
[config.yml](/topics/configuration).
:::yml
Member:
extensions:
- MyMemberExtension
Alternatively, you can add extensions through PHP code (in your `_config.php`
file).
:::php
Member::add_extension('MyMemberExtension');
### Extending code to allow for extensions
If you're providing a module or working on code that may need to be extended by
other code, it can provide a *hook* which allows an Extension to modify the
results. This is done through the `[api:Object->extend()]` method.
:::php
public function myFunc() {
$foo = // ..
$this->extend('alterFoo', $foo);
return $foo;
}
In this example, the myFunc() method adds a hook to allow `DataExtension`
subclasses added to the instance to define an `alterFoo($foo)` method to modify
the result of the method.
The `$foo` parameter is passed by reference, as it is an object.
### Accessing the original Object from an Extension
In your extension class you can refer to the source object through the `owner`
property on the class.
:::php
<?php
class MyMemberExtension extends DataExtension {
public function alterFoo($foo) {
// outputs the original class
var_dump($this->owner);
}
}
### Checking to see if an Object has an Extension
To see what extensions are currently enabled on an object, you can use
`[api:Object->getExtensionInstances()]` and `[api:Object->hasExtension($extension)]`.
## Implementation
### Adding extra database fields
Extra database fields can be added with a extension in the same manner as if
they were placed on the `DataObject` class they're applied to. These will be
added to the table of the base object - the extension will actually edit the
$db, $has_one, etc static variables on load.
The function should return a map where the keys are the names of the static
variables to update:
:::php
<?php
class MyMemberExtension extends DataExtension {
private static $db = array(
'Position' => 'Varchar',
);
private static $has_one = array(
'Image' => 'Image',
);
}
### Modifying CMS Fields
The member class demonstrates an extension that allows you to update the default
CMS fields for an object in an extension:
:::php
<?php
class MyMemberExtension extends DataExtension {
private static $db = array(
'Position' => 'Varchar',
);
private static $has_one = array(
'Image' => 'Image',
);
public function updateCMSFields(FieldList $fields) {
$fields->push(new TextField('Position'));
$fields->push(new UploadField('Image', 'Profile Image'));
}
}
### Adding/modifying fields prior to extensions
User code can intervene in the process of extending cms fields by using
`beforeUpdateCMSFields` in its implementation of `getCMSFields`. This can be
useful in cases where user code will add fields to a dataobject that should be
present in the `$fields` parameter when passed to `updateCMSFields` in
extensions.
This method is preferred to disabling, enabling, and calling cms field
extensions manually.
:::php
function getCMSFields() {
$this->beforeUpdateCMSFields(function($fields) {
// Include field which must be present when updateCMSFields is called on extensions
$fields->addFieldToTab("Root.Main", new TextField('Detail', 'Details', null, 255));
});
$fields = parent::getCMSFields();
// ... additional fields here
return $fields;
}
### Object extension injection points
`Object` now has two additional methods, `beforeExtending` and `afterExtending`,
each of which takes a method name and a callback to be executed immediately
before and after `Object::extend()` is called on extensions.
This is useful in many cases where working with modules such as `Translatable`
which operate on `DataObject` fields that must exist in the `FieldList` at the
time that `$this->extend('UpdateCMSFields')` is called.
<div class="notice" markdown='1'>
Please note that each callback is only ever called once, and then cleared, so
multiple extensions to the same function require that a callback is registered
each time, if necessary.
</div>
Example: A class that wants to control default values during object
initialization. The code needs to assign a value if not specified in
`self::$defaults`, but before extensions have been called:
:::php
function __construct() {
$self = $this;
$this->beforeExtending('populateDefaults', function() use ($self) {
if(empty($self->MyField)) {
$self->MyField = 'Value we want as a default if not specified in $defaults, but set before extensions';
}
});
parent::__construct();
}
### Custom database generation
Some extensions are designed to transparently add more sophisticated
data-collection capabilities to your `DataObject`. For example, `[api:Versioned]`
adds version tracking and staging to any `DataObject` that it is applied to.
To do this, define an **augmentDatabase()** method on your extension. This will
be called when the database is rebuilt.
* You can query `$this->owner` for information about the data object, such as
the fields it has
* You can use **DB::requireTable($tableName, $fieldList, $indexList)** to set
up your new tables. This function takes care of creating, modifying, or leaving
tables as required, based on your desired schema.
### Custom write queries
If you have customised the generated database, then you probably want to change
the way that writes happen. This isused by `[api:Versioned]` to get an entry
written in ClassName_versions whenever an insert/update happens.
To do this, define the **augmentWrite(&$manipulation)** method. This method is
passed a manipulation array representing the write about to happen, and is able
to amend this as desired, since it is passed by reference.
### Custom relation queries
The other queries that you will want to customise are the selection queries,
called by get & get_one. For example, the Versioned object has code to redirect
every request to ClassName_live, if you are browsing the live site.
To do this, define the **augmentSQL(SQLSelect $query)** method. Again, the $query object is passed by reference and can
be modified as needed by your method. Instead of a manipulation array, we have a `[api:SQLSelect]` object.
### Additional methods
The other thing you may want to do with a extension is provide a method that can
be called on the `[api:DataObject]` that is being extended. For instance, you
may add a publish() method to every `[api:DataObject]` that is extended with
`[api:Versioned]`.
This is as simple as defining a method called publish() on your extension. Bear
in mind, however, that instead of $this, you should be referring to
`$this->owner`.
* $this = The `[api:DataExtension]` object.
* $this->owner = The related `[api:DataObject]` object.
If you want to add your own internal properties, you can add this to the
`[api:DataExtension]`, and these will be referred to as `$this->propertyName`.
Every `[api:DataObject]` has an associated `[api:DataExtension]` instance for
each class that it is extended by.
:::php
<?php
class Customer extends DataObject {
private static $has_one = array(
'Account' => 'Account'
);
private static $extensions = array(
'CustomerWorkflow'
);
}
class Account extends DataObject {
private static $db = array(
'IsMarkedForDeletion'=>'Boolean'
);
private static $has_many = array(
'Customers' => 'Customer'
);
}
class CustomerWorkflow extends DataExtension {
public function IsMarkedForDeletion() {
return (bool) $this->owner->Account()->IsMarkedForDeletion;
}
}
## API Documentation
* `[api:Extension]`
* `[api:DataExtension]`
## See Also
* [Injector](injector/)
* `[api:Object::useCustomClass]`

View File

@ -0,0 +1,107 @@
title: Modules
summary: Extend core functionality with modules.
# Modules
SilverStripe is designed to be a modular application system - even the CMS is simply a module that plugs into the core
framework.
A module is a collection of classes, templates, and other resources that is loaded into a top-level directory such as
the `framework`, `cms` or `mysite` folders. The only thing that identifies a folder as a SilverStripe module is the
existence of a `_config` directory or `_config.php` at the top level of the directory.
mysite/
|
+-- _config/
+-- code/
+-- ..
|
my_custom_module/
|
+-- _config/
+-- ...
SilverStripe will automatically include any PHP classes and templates from within your module when you next flush your
cache.
<div class="info" markdown="1">
In a default SilverStripe installation, even resources in `framework` and `mysite` are treated in exactly the same as
every other module. Order of priority is usually alphabetical unless stated.
</div>
Creating a module is a good way to re-use abstract code and templates across multiple projects. SilverStripe already
has certain modules included, for example the `cms` module and core functionality such as commenting and spam protection
are also abstracted into modules allowing developers the freedom to choose what they want.
## Finding Modules
* [Official module list on silverstripe.org](http://addons.silverstripe.org/)
* [Packagist.org "silverstripe" tag](https://packagist.org/search/?tags=silverstripe)
* [Github.com "silverstripe" search](https://github.com/search?q=silverstripe&ref=commandbar)
## Installation
Modules should exist in the root folder of your SilverStripe installation.
<div class="info" markdown="1">
The root directory is the one containing the *framework* and *mysite* subdirectories. If your site is installed under
`/Users/sam.minnee/Sites/website/` your modules will go in the `/Users/sam.minnee/Sites/website/` directory.
</div>
<div class="notice" markdown="1">
After you add or remove modules make sure you rebuild the database by going to http://yoursite.com/dev/build?flush=1
</div>
### From Composer
Our preferred way to manage module dependencies is through the [Composer](http://getcomposer.org) package manager. It
enables you to install modules from specific versions, checking for compatibilities between modules and even allowing
to track development branches of them. To install modules using this method, you will first need to setup SilverStripe
with [Composer](../../getting_started/composer).
Each module has a unique identifier, consisting of a vendor prefix and name. For example, the "blog" module has the
identifier `silverstripe/blog` as it is published by *silverstripe*. To install, use the following command executed in
the root folder:
:::bash
composer require "silverstripe/blog" "*@stable"
This will fetch the latest compatible stable version of the module. To install a specific version of the module give the
tag name.
:::bash
composer require "silverstripe/blog" "1.1.0"
<div class="info" markdown="1">
To lock down to a specific version, branch or commit, read up on
[Composer "lock" files](http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file).
</div>
## From an Archive Download
<div class="alert" markdown="1">
Some modules might not work at all with this approach since they rely on the
Composer [autoloader](http://getcomposer.org/doc/01-basic-usage.md#autoloading), additional modules or post-install
hooks, so we recommend using Composer.
</div>
Alternatively, you can download the archive file from the [modules page](http://www.silverstripe.org/modules) and
extract it to the root folder mentioned above.
<div class="notice" markdown="1">
The main folder extracted from the archive might contain the version number or additional "container" folders above the
actual module codebase. You need to make sure the folder name is the correct name of the module (e.g. "blog/" rather
than "silverstripe-blog/"). This folder should contain a `_config/` directory. While the module might register and
operate in other structures, paths to static files such as CSS or JavaScript won't work.
</div>
## Publishing your own SilverStripe module
See the [How to Publish a SilverStripe Module](how_tos/publish_a_module) for details on how to publish your SilverStripe
modules with the community
## Related
* [How to Publish a SilverStripe Module](how_tos/publish_a_module)

View File

@ -0,0 +1,283 @@
title: Extensions
summary: Extensions and DataExtensions let you modify and augment objects transparently.
# Extensions and DataExtensions
An [api:Extension] allows for adding additional functionality to a [api:Object] or modifying existing functionality
without the hassle of creating a subclass. Developers can add Extensions to any [api:Object] subclass within core, modules
or even their own code to make it more reusable.
Extensions are defined as subclasses of either [api:DataExtension] for extending a [api:DataObject] subclass or
the [api:Extension] class for non DataObject subclasses (such as [api:Controllers])
**mysite/code/extensions/MyMemberExtension.php**
:::php
<?php
class MyMemberExtension extends DataExtension {
private static $db = array(
'DateOfBirth' => 'SS_Datetime'
);
public function SayHi() {
// $this->owner refers to the original instance. In this case a `Member`.
return "Hi ". $this->owner->Name;
}
}
<div class="info" markdown="1">
Convention is for extension class names to end in `Extension`. This isn't a requirement but makes it clearer
</div>
After this class has been created, it does not yet apply it to any object. We need to tell SilverStripe what classes
we want to add the `MyMemberExtension` too. To activate this extension, add the following via the [Configuration API](../configuration).
**mysite/_config/app.yml**
:::yml
Member:
extensions:
- MyMemberExtension
Alternatively, we can add extensions through PHP code (in the `_config.php` file).
:::php
Member::add_extension('MyMemberExtension');
This class now defines a `MyMemberExtension` that applies to all `Member` instances on the website. It will have
transformed the original `Member` class in two ways:
* Added a new [api:SS_Datetime] for the users date of birth, and;
* Added a `SayHi` method to output `Hi <User>`
From within the extension we can add more functions, database fields, relations or other properties and have them added
to the underlying `DataObject` just as if they were added to the original `Member` class but without the need to edit
that file directly.
### Adding Database Fields
Extra database fields can be added with a extension in the same manner as if they were placed on the `DataObject` class
they're applied to. These will be added to the table of the base object - the extension will actually edit the $db,
$has_one etc.
**mysite/code/extensions/MyMemberExtension.php**
:::php
<?php
class MyMemberExtension extends DataExtension {
private static $db = array(
'Position' => 'Varchar',
);
private static $has_one = array(
'Image' => 'Image',
);
public function SayHi() {
// $this->owner refers to the original instance. In this case a `Member`.
return "Hi ". $this->owner->Name;
}
}
**mysite/templates/Page.ss**
:::ss
$CurrentMember.Position
$CurrentMember.Image
## Adding Methods
Methods that have a unique name will be called as part of the `__call` method on [api:Object]. In the previous example
we added a `SayHi` method which is unique to our extension.
**mysite/templates/Page.ss**
:::ss
<p>$CurrentMember.SayHi</p>
// "Hi Sam"
**mysite/code/Page.php**
:::php
$member = Member::currentUser();
echo $member->SayHi;
// "Hi Sam"
## Modifying Existing Methods
If the `Extension` needs to modify an existing method it's a little tricker. It requires that the method you want to
customize has provided an *Extension Hook* in the place where you want to modify the data. An *Extension Hook* is done
through the `[api:Object->extend]` method.
**framework/security/Member.php**
:::php
public function getValidator() {
// ..
$this->extend('updateValidator', $validator);
// ..
}
Extension Hooks can be located anywhere in the method and provide a point for any `Extension` instances to modify the
variables at that given point. In this case, the core function `getValidator` on the `Member` class provides an
`updateValidator` hook for developers to modify the core method. The `MyMemberExtension` would modify the core member's
validator by defining the `updateValidator` method.
**mysite/code/extensions/MyMemberExtension.php**
:::php
<?php
class MyMemberExtension extends DataExtension {
// ..
public function updateValidator($validator) {
// we want to make date of birth required for each member
$validator->addRequiredField('DateOfBirth');
}
}
<div class="info" markdown="1">
The `$validator` parameter is passed by reference, as it is an object.
</div>
Another common example of when you will want to modify a method is to update the default CMS fields for an object in an
extension. The `CMS` provides a `updateCMSFields` Extension Hook to tie into.
:::php
<?php
class MyMemberExtension extends DataExtension {
private static $db = array(
'Position' => 'Varchar',
);
private static $has_one = array(
'Image' => 'Image',
);
public function updateCMSFields(FieldList $fields) {
$fields->push(new TextField('Position'));
$fields->push(new UploadField('Image', 'Profile Image'));
}
}
<div class="notice" markdown="1">
If you're providing a module or working on code that may need to be extended by other code, it should provide a *hook*
which allows an Extension to modify the results.
</div>
:::php
public function Foo() {
$foo = // ..
$this->extend('updateFoo', $foo);
return $foo;
}
The convention for extension hooks is to provide an `update{$Function}` hook at the end before you return the result. If
you need to provide extension hooks at the beginning of the method use `before{..}`.
## Owner
In your [api:Extension] class you can only refer to the source object through the `owner` property on the class as
`$this` will refer to your `Extension` instance.
:::php
<?php
class MyMemberExtension extends DataExtension {
public function updateFoo($foo) {
// outputs the original class
var_dump($this->owner);
}
}
## Checking to see if an Object has an Extension
To see what extensions are currently enabled on an object, use [api:Object->getExtensionInstances] and
[api:Object->hasExtension]
:::php
$member = Member::currentUser();
print_r($member->getExtensionInstances());
if($member->hasExtension('MyCustomMemberExtension')) {
// ..
}
## Object extension injection points
`Object` has two additional methods, `beforeExtending` and `afterExtending`, each of which takes a method name and a
callback to be executed immediately before and after `Object::extend()` is called on extensions.
This is useful in many cases where working with modules such as `Translatable` which operate on `DataObject` fields
that must exist in the `FieldList` at the time that `$this->extend('UpdateCMSFields')` is called.
<div class="notice" markdown='1'>
Please note that each callback is only ever called once, and then cleared, so multiple extensions to the same function
require that a callback is registered each time, if necessary.
</div>
Example: A class that wants to control default values during object initialization. The code needs to assign a value
if not specified in `self::$defaults`, but before extensions have been called:
:::php
function __construct() {
$self = $this;
$this->beforeExtending('populateDefaults', function() use ($self) {
if(empty($self->MyField)) {
$self->MyField = 'Value we want as a default if not specified in $defaults, but set before extensions';
}
});
parent::__construct();
}
Example 2: User code can intervene in the process of extending cms fields.
<div class="notice" markdown="1">
This method is preferred to disabling, enabling, and calling field extensions manually.
</div>
:::php
public function getCMSFields() {
$this->beforeUpdateCMSFields(function($fields) {
// Include field which must be present when updateCMSFields is called on extensions
$fields->addFieldToTab("Root.Main", new TextField('Detail', 'Details', null, 255));
});
$fields = parent::getCMSFields();
// ... additional fields here
return $fields;
}
## Related Documentaion
* [Injector](injector/)
* [api:Object::useCustomClass]
## API Documentation
* [api:Extension]
* [api:DataExtension]

View File

@ -1,118 +0,0 @@
# Modules
SilverStripe is designed to be a modular application system - even the CMS is simply a module that plugs into it.
A module is, quite simply, a collection of classes, templates, and other resources that is loaded into a top-level
directory. In a default SilverStripe download, even resources in 'framework' and 'mysite' are treated in exactly the
same as every other module.
SilverStripe's `[api:ManifestBuilder]` will find any class, css or template files anywhere under the site's main
directory. The `_config.php` file in the module directory as well as the [_config/*.yml files](/topics/configuration)
can be used to define director rules, add
extensions, etc. So, by unpacking a module into site's main directory and viewing the site with
?flush=1 on the end of the URL, all the module's new behaviour will be incorporated to your site:
* You can create subclasses of base classes such as SiteTree to extend behaviour.
* You can use Object::useCustomClass() to replace a built in class with a class of your own.
* You can use [an extension](api:DataExtension) to extend or alter the behaviour of a built-in class without replacing
it.
* You can provide additional director rules to define your own controller for particular URLs.
For more information on creating modules, see [module-development](/topics/module-development).
## Types of Modules
Because of the broad definition of modules, they can be created for a number of purposes:
* **Applications:** A module can define a standalone application that may work out of the box, or may get customisation
from your mysite folder. "cms" is an example of this.
* **CMS Add-ons:** A module can define an extension to the CMS, usually by defining special page types with their own
templates and behaviour. "blog", "ecommerce", "forum", and "gallery" are examples of this.
* **Widgets:** Small pieces of functionality such as showing the latest Comments or Flickr Photos. Since SilverStripe 3.0, they have been moved into a standalone module at [github.com/silverstripe/silverstripe-widgets](https://github.com/silverstripe/silverstripe-widgets).
* **Developer Tools:** A module can provide a number of classes or resource files that do nothing by themselves, but
instead make it easier for developers to build other applications.
## Finding Modules
* [Official module list on silverstripe.org](http://addons.silverstripe.org/)
* [Packagist.org "silverstripe" tag](https://packagist.org/search/?tags=silverstripe)
* [Github.com "silverstripe" search](https://github.com/search?q=silverstripe&ref=commandbar)
## Installation
Modules should exist in the root folder of your SilverStripe installation
(the directory containing the *framework* and *cms* subdirectories).
The following article explains the generic installation of a module. Individual modules have their own requirements such
as creating folders or configuring API keys. For information about installing or configuring a specific module see the
modules *README* file. Modules should adhere to the [directory-structure](/topics/directory-structure)
guidelines.
### From a Composer Package
Our preferred way to manage module dependencies is through the [Composer][http://getcomposer.org]
package manager. It enables you to install modules from specific versions, checking for
compatibilities between modules and even allowing to track development branches of them.
After [installing Composer](/installation/composer) itself,
you can run a simple command to install a module.
Each module has a unique identifier, consisting of a vendor prefix and name.
For example, the popular "blog" module has the identifier `silverstripe/blog`,
and would be installed with the following command executed in the root folder:
composer require silverstripe/blog:*@stable
This will fetch the latest compatible stable version. Every time you run
`composer update` afterwards, Composer will check for a new stable version.
To lock down to a specific version, branch or commit, read up on
[Composer "lock" files](http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file).
You can also add modules by editing the "require" section of the `composer.json` file.
To find modules and their identifiers, search for them on [packagist.org](http://packagist.org).
<div class="notice" markdown="1">
Older releases (<3.0.3, <2.4.9) don't come with a `composer.json` file in your root folder,
which is required for its operation. In this case, we recommend upgrading to a newer release.
</div>
### From an Archive Download
Alternatively, you can download the archive file from the
[modules page](http://www.silverstripe.org/modules)
and extract it to the root folder mentioned above.
Github also provides archive downloads which are generated automatically for every tag/version.
<div class="notice" markdown="1">
The main folder extracted from the archive
might contain the version number or additional "container" folders above the actual module
codebase. You need to make sure the folder name is the correct name of the module
(e.g. "blog/" rather than "silverstripe-blog/"). This folder should contain a `_config/` directory.
While the module might register and operate in other structures,
paths to static files such as CSS or JavaScript won't work.
</div>
<div class="warning" markdown="1">
Some modules might not work at all with this approach since they rely on the
Composer [autoloader](http://getcomposer.org/doc/01-basic-usage.md#autoloading)
or post-install hooks, so we recommend using Composer.
</div>
### Git Submodules and Subversion Externals
Git and Subversion provide their own facilities for managing dependent repositories.
This is essentially a variation of the "Archive Download" approach,
and comes with the same caveats.
## Configuration as a module marker
Configuration files also have a secondary sub-role. Modules are identified by the `[api:ManifestBuilder]` by the
presence of a `_config/` directory (or a `_config.php` file) as a top level item in the module directory.
Although your module may choose not to set any configuration, it must still have a _config directory to be recognised
as a module by the `[api:ManifestBuilder]`, which is required for features such as autoloading of classes and template
detection to work.
## Related
* [Modules Development](/topics/module-development)

View File

@ -1,203 +0,0 @@
# Module Development
## Introduction
Creating a module is a good way to re-use abstract code and templates across
multiple projects. SilverStripe already has certain modules included, for
example "framework" and "cms". These two modules are the core functionality and
templates for any initial installation.
If you want to add generic functionality that isn't specific to your
project, like a forum, an ecommerce package or a blog you can do it like this:
1. Create another directory at the root level (same level as "framework"
and "cms"). This will contain all your module files.
2. The module directory must contain a `_config` sub-directory, or a
`_config.php` file to be recognised.
3. Inside your module directory, follow our
[directory structure guidelines](/topics/directory-structure#module_structure)
Once this is done, SilverStripe will automatically include any PHP classes and
templates from within your module.
## Tips
Try to keep your module as generic as possible - for example if you're making a
forum module, your members section shouldn't contain fields like 'Games You
Play' or 'Your LiveJournal Name' - if people want to add these fields they can
sub-class your class, or extend the fields on to it.
If you're using [api:Requirements] to include generic support files for your project
like CSS or Javascript, and want to override these files to be more specific in
your project, the following code is an example of how to do so using the init()
function on your module controller classes:
:::php
class Forum_Controller extends Page_Controller {
public function init() {
if(Director::fileExists(project() . "/css/forum.css")) {
Requirements::css(project() . "/css/forum.css");
} else {
Requirements::css("forum/css/forum.css");
}
parent::init();
}
}
This will use `<projectname>/css/forum.css` if it exists, otherwise it falls
back to using `forum/css/forum.css`.
## Conventions
### Configuration
SilverStripe has a comprehensive [Configuration](/topics/configuration) system
built on YAML which allows developers to set configuration values in core
classes.
If your module allows developers to customize specific values (for example API
key values) use the existing configuration system for your data.
:::php
// use this in your module code
$varible = Config::inst()->get('ModuleName', 'SomeValue');
Then developers can set that value in their own configuration file. As a module
author, you can set the default configuration values.
// yourmodule/_config/module.yml
---
Name: modulename
---
ModuleName:
SomeValue: 10
But by using the Config system, developers can alter the value for their
application without editing your code.
// mysite/_config/module_customizations.yml
---
Name: modulecustomizations
After: "#modulename"
---
ModuleName:
SomeValue: 10
If you want to make the configuration value user editable in the backend CMS,
provide an extension to [SiteConfig](/reference/siteconfig).
## Publication
If you wish to submit your module to our public directory, you take
responsibility for a certain level of code quality, adherence to conventions,
writing documentation, and releasing updates. See
[contributing](/misc/contributing). All modules should be published
on [addons.silverstripe.org](http://addons.silverstripe.org) to make them
discoverable by others.
### Composer and Packagist
SilverStripe uses [Composer](/installation/composer/) to manage module releases
and dependencies between modules. If you plan on releasing your module to the
public, ensure that you provide a `composer.json` file in the root of your
module containing the meta-data about your module.
For more information about what your `composer.json` file should include,
consult the [Composer Documentation](http://getcomposer.org/doc/01-basic-usage.md).
A basic usage of a module for 3.1 that requires the CMS would look similar to
this:
{
"name": "your-vendor-name/module-name",
"description": "One-liner describing your module",
"type": "silverstripe-module",
"homepage": "http://github.com/your-vendor-name/module-name",
"keywords": ["silverstripe", "some-tag", "some-other-tag"],
"license": "BSD-3-Clause",
"authors": [
{"name": "Your Name","email": "your@email.com"}
],
"support": {
"issues": "http://github.com/your-vendor-name/module-name/issues"
},
"require": {
"silverstripe/cms": "~3.1",
"silverstripe/framework": "~3.1"
},
"extra": {
"installer-name": "module-name",
"screenshots": [
"relative/path/screenshot1.png",
"http://myhost.com/screenshot2.png"
]
}
}
Once your module is released, submit it to [Packagist](https://packagist.org/)
to have the module accessible to developers. It'll automatically get picked
up by [addons.silverstripe.org](http://addons.silverstripe.org/).
### Versioning
Over time you may have to release new versions of your module to continue to
work with newer versions of SilverStripe. By using Composer, this is made easy
for developers by allowing them to specify what version they want to use. Each
version of your module should be a separate branch in your version control and
each branch should have a `composer.json` file explicitly defining what versions
of SilverStripe you support.
Say you have a module which supports SilverStripe 3.0.
A new release of this module takes advantage of new features
in SilverStripe 3.1. In this case, you would create a new branch
for the 3.0 compatible codebase of your module.
This allows you to continue fixing bugs on this older release branch.
As a convention, the `master` or `trunk` branch of your
module should always work with the `master` branch of SilverStripe.
Other branches should be created on your module as needed if they're
required to support specific SilverStripe releases.
You can have an overlap in supported versions,
e.g two branches in your module both support SilverStripe 3.1.
In this case, you should explain the differences in your `README.md` file.
Here's some common values for your `require` section
(see [getcomposer.org](http://getcomposer.org/doc/01-basic-usage.md#package-versions) for details):
* `3.0.*`: Version `3.0`, including `3.0.1`, `3.0.2` etc, excluding `3.1`
* `~3.0`: Version `3.0` or higher, including `3.0.1` and `3.1` etc, excluding `4.0`
* `~3.0,<3.2`: Version `3.0` or higher, up until `3.2`, which is excluded
* `~3.0,>3.0.4`: Version `3.0` or higher, starting with `3.0.4`
## Reference
### How To:
* [How to customize the CMS Menu](/howto/customize-cms-menu)
* [How to extend the CMS interface](/howto/extend-cms-interface)
### Reference:
Provide custom functionality for the developer via:
* [DataExtension](/reference/dataextension)
* [SiteConfig](/reference/siteconfig)
* [Page types](/topics/page-types)
Follow SilverStripe best practice:
* [Partial Caching](/reference/partial-caching)
* [Injector](/reference/injector)
## Useful Links
* [Introduction to Composer](http://getcomposer.org/doc/00-intro.md)
* [Modules](modules)
* [Directory structure guidelines](/topics/directory-structure#module_structure)
* [Debugging methods](/topics/debugging)
* [URL Variable Tools](/reference/urlvariabletools) - Lists a number of page options, rendering tools or special URL variables that you can use to debug your SilverStripe applications

View File

@ -1,63 +1,67 @@
# Shortcodes: Flexible Content Embedding
title: Shortcodes
summary: Flexible content embedding
## Overview
# Shortcodes
The `[api:ShortcodeParser]` API is simple parser that allows you to map specifically
formatted content to a callback to transform them into something else.
You might know this concept from forum software which don't allow you to insert
The [api:ShortcodeParser] API is simple parser that allows you to map specifically formatted content to a callback to
transform them into something else. You might know this concept from forum software which don't allow you to insert
direct HTML, instead resorting to a custom syntax.
In the CMS, authors often want to insert content elements which go beyond
standard formatting, at an arbitrary position in their WYSIWYG editor.
Shortcodes are a semi-technical solution for this. A good example would
be embedding a 3D file viewer or a Google Map at a certain location.
In the CMS, authors often want to insert content elements which go beyond standard formatting, at an arbitrary position
in their WYSIWYG editor. Shortcodes are a semi-technical solution for this. A good example would be embedding a 3D file
viewer or a Google Map at a certain location.
:::php
$text = "<h1>My Map</h1>[map]"
// Will output
// <h1>My Map</h1><iframe ..></iframe>
Here's some syntax variations:
:::php
[my_shortcode]
#
[my_shortcode /]
#
[my_shortcode,myparameter="value"]
#
[my_shortcode,myparameter="value"]Enclosed Content[/my_shortcode]
## Usage
Shortcodes are automatically parsed on any database field which is declared as [api:HTMLValue] or [api:HTMLText],
when rendered into a template. This means you can use shortcodes on common fields like `SiteTree.Content`, and any
other `[api:DataObject::$db]` definitions of these types.
In its most basic form, you can invoke the `[api:ShortcodeParser]` directly:
Other fields can be manually parsed with shortcodes through the `parse` method.
:::php
ShortcodeParser::get_active()->parse($myvalue);
In addition, shortcodes are automatically parsed on any database field which is declared
as `[api:HTMLValue]` or `[api:HTMLText]`, when rendered into a template.
This means you can use shortcodes on common fields like `SiteTree.Content`,
and any other `[api:DataObject::$db]` definitions of these types.
In order to allow shortcodes in your own template placeholders,
ensure they're casted correctly:
:::php
class MyObject extends DataObject {
private static $db = array('Content' => 'HTMLText');
private static $casting = array('ContentHighlighted' => 'HTMLText');
public function ContentHighlighted($term) {
return str_replace($term, "<em>$term</em>", $this->Content);
}
}
There is currently no way to allow shortcodes directly in template markup
(as opposed to return values of template placeholders).
$text = "My awesome [my_shortcode] is here.";
ShortcodeParser::get_active()->parse($text);
## Defining Custom Shortcodes
All you need to do to define a shortcode is to register a callback with the parser that will be called whenever a
shortcode is encountered. This callback will return a string to replace the shortcode with.
If the shortcode is used for template placeholders of type `HTMLText` or `HTMLVarchar`,
the returned value should be valid HTML
To register a shortcode you call:
First we need to define a callback for the shortcode.
ShortcodeParser::get('default')->register('my_shortcode', <callback>);
These parameters are passed to the callback:
**mysite/code/Page.php**
:::php
<?php
class Page extends SiteTree {
private static $casting = array(
'MyShortCodeMethod' => 'HTMLText'
);
public function MyShortCodeMethod($arguments, $content = null, $parser = null, $tagName) {
return str_replace($content, "<em>$content</em>", $this->Content);
}
}
These parameters are passed to the `MyShortCodeMethod` callback:
- Any parameters attached to the shortcode as an associative array (keys are lower-case).
- Any content enclosed within the shortcode (if it is an enclosing shortcode). Note that any content within this
@ -68,37 +72,15 @@ These parameters are passed to the callback:
is inside an attribute, the `element` key contains a reference to the parent `DOMElement`, and the `node`
key the attribute's `DOMNode`.
## Example: Google Maps Iframe by Address
To demonstrate how easy it is to build custom shortcodes, we'll build one to display
a Google Map based on a provided address. Format:
To register a shortcode you call the following.
[googlemap,width=500,height=300]97-99 Courtenay Place, Wellington, New Zealand[/googlemap]
So we've got the address as "content" of our new `googlemap` shortcode tags,
plus some `width` and `height` arguments. We'll add defaults to those in our shortcode parser so they're optional.
**mysite/_config.php**
:::php
ShortcodeParser::get('default')->register('googlemap', function($arguments, $address, $parser, $shortcode) {
$iframeUrl = sprintf(
'http://maps.google.com/maps?q=%s&amp;hnear=%s&amp;ie=UTF8&hq=&amp;t=m&amp;z=14&amp;output=embed',
urlencode($address),
urlencode($address)
);
$width = (isset($arguments['width']) && $arguments['width']) ? $arguments['width'] : 400;
$height = (isset($arguments['height']) && $arguments['height']) ? $arguments['height'] : 300;
return sprintf(
'<iframe width="%d" height="%d" src="%s" frameborder="0" scrolling="no" marginheight="0" marginwidth="0"></iframe>',
$width,
$height,
$iframeUrl
);
});
// ShortcodeParser::get('default')->register($shortcode, $callback);
The hard bits are taken care of (parsing out the shortcodes), everything we need to do is a bit of string replacement.
CMS users still need to remember the specific syntax, but these shortcodes can form the basis
for more advanced editing interfaces (with visual placeholders). See the built-in `embed` shortcode as an example
for coupling shortcodes with a form to create and edit placeholders.
ShortcodeParser::get('default')->register('my_shortcode', array('Page', 'MyShortCodeMethod'));
## Built-in Shortcodes
@ -107,34 +89,32 @@ SilverStripe comes with several shortcode parsers already.
### Links
Internal page links keep references to their database IDs rather than
the URL, in order to make these links resilient against moving the target page to a different
location in the page tree. This is done through the `[sitetree_link]` shortcode, which
takes an `id` parameter. Example: `<a href="[sitetree_link,id=99]">`
Internal page links keep references to their database IDs rather than the URL, in order to make these links resilient
against moving the target page to a different location in the page tree. This is done through the `[sitetree_link]`
shortcode, which takes an `id` parameter.
:::php
<a href="[sitetree_link,id=99]">
Links to internal `File` database records work exactly the same, but with the `[file_link]` shortcode.
:::php
<a href="[file_link,id=99]">
### Media (Photo, Video and Rich Content)
Many media formats can be embedded into websites through the `<object>`
tag, but some require plugins like Flash or special markup and attributes.
OEmbed is a standard to discover these formats based on a simple URL,
for example a Youtube link pasted into the "Insert Media" form of the CMS.
Many media formats can be embedded into websites through the `<object>` tag, but some require plugins like Flash or
special markup and attributes. OEmbed is a standard to discover these formats based on a simple URL, for example a
Youtube link pasted into the "Insert Media" form of the CMS.
Since TinyMCE can't represent all these varations, we're showing a placeholder
instead, and storing the URL with a custom `[embed]` shortcode.
Since TinyMCE can't represent all these variations, we're showing a placeholder instead, and storing the URL with a
custom `[embed]` shortcode.
Example: `.[embed width=480 height=270 class=left thumbnail=http://i1.ytimg.com/vi/lmWeD-vZAMY/hqdefault.jpg?r=8767]http://www.youtube.com/watch?v=lmWeD-vZAMY[/embed]`
## Syntax
[embed width=480 height=270 class=left thumbnail=http://i1.ytimg.com/vi/lmWeD-vZAMY/hqdefault.jpg?r=8767]
http://www.youtube.com/watch?v=lmWeD-vZAMY
[/embed]
* Unclosed - `[shortcode]`
* Explicitly closed - `[shortcode/]`
* With parameters, mixed quoting - `[shortcode parameter=value parameter2='value2' parameter3="value3"]`
* Old style parameter separation - `[shortcode,parameter=value,parameter2='value2',parameter3="value3"]`
* With contained content & closing tag - `[shortcode]Enclosed Content[/shortcode]`
* Escaped (will output `[just] [text]` in response) - `[[just] [[text]]`
### Attribute and element scope
@ -152,19 +132,18 @@ change the name of a tag. These usages are forbidden:
<a [titleattribute]>link</a>
You may need to escape text inside attributes `>` becomes `&gt;`,
You can include HTML tags inside a shortcode tag, but you need to be careful of nesting to ensure you don't
break the output
You may need to escape text inside attributes `>` becomes `&gt;`, You can include HTML tags inside a shortcode tag, but
you need to be careful of nesting to ensure you don't break the output.
Good:
:::ss
<!-- Good -->
<div>
[shortcode]
<p>Caption</p>
[/shortcode]
</div>
Bad:
<!-- Bad: -->
<div>
[shortcode]
@ -175,12 +154,12 @@ Bad:
### Location
Element scoped shortcodes have a special ability to move the location they are inserted at to comply with
HTML lexical rules. Take for example this basic paragraph tag:
Element scoped shortcodes have a special ability to move the location they are inserted at to comply with HTML lexical
rules. Take for example this basic paragraph tag:
<p><a href="#">Head [figure,src="assets/a.jpg",caption="caption"] Tail</a></p>
When converted naively would become
When converted naively would become:
<p><a href="#">Head <figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure> Tail</a></p>
@ -198,17 +177,17 @@ When the location attribute is "leftAlone" or "center" then the DOM is split aro
### Parameter values
Here is a summary of the callback parameter values based on some example shortcodes.
:::php
public function MyCustomShortCode($arguments, $content = null, $parser = null, $tagName) {
// ..
}
Short
[my_shortcodes]
$attributes => array()
$enclosedContent => null
$parser => ShortcodeParser instance
$tagName => 'my_shortcode'
Short with attributes
[my_shortcode]
$attributes => array();
$content => null;
$parser => ShortcodeParser instance,
$tagName => 'myshortcode')
[my_shortcode,attribute="foo",other="bar"]
@ -217,8 +196,6 @@ Short with attributes
$parser => ShortcodeParser instance
$tagName => 'my_shortcode'
Long with attributes
[my_shortcode,attribute="foo"]content[/my_shortcode]
$attributes => array('attribute' => 'foo')
@ -237,6 +214,11 @@ example the below code will not work as expected:
The parser will raise an error if it can not find a matching opening tag for any particular closing tag
## Related
## Related Documentation
* [Wordpress implementation](http://codex.wordpress.org/Shortcode_API)
* [Wordpress Implementation](http://codex.wordpress.org/Shortcode_API)
* [How to Create a Google Maps Shortcode](how_tos/create_a_google_maps_shortcode)
## API Documentation
* [api:ShortcodeParser]

View File

@ -1,14 +1,13 @@
title: Injector
summary: Introduction to using Dependency Injection within SilverStripe.
# Injector
## Introduction
The [api:Injector] class is the central manager of inter-class dependencies in SilverStripe. It offers developers the
ability to declare the dependencies a class type has, or to change the nature of the dependencies defined by other
developers.
The `[api:Injector]` class is the central manager of inter-class dependencies
in the SilverStripe Framework. In its simplest form it can be considered as
a replacement for Object::create and singleton() calls, but also offers
developers the ability to declare the dependencies a class type has, or
to change the nature of the dependencies defined by other developers.
Some of the goals of dependency injection are
Some of the goals of dependency injection are:
* Simplified instantiation of objects
* Providing a uniform way of declaring and managing inter-object dependencies
@ -17,86 +16,111 @@ Some of the goals of dependency injection are
* Improve testability of code
* Promoting abstraction of logic
A key concept of the injector is whether the object should be managed as
* A pseudo-singleton, in that only one item will be created for a particular
identifier (but the same class could be used for multiple identifiers)
* A prototype, where the same configuration is used, but a new object is
created each time
* unmanaged, in which case a new object is created and injected, but no
information about its state is managed.
These concepts will be discussed further below
## Some simple examples
The following sums up the simplest usage of the injector
Assuming no other configuration is specified
The following sums up the simplest usage of the `Injector` it creates a new object of type `ClassName` through `create`
:::php
$object = Injector::inst()->create('ClassName');
$object = Injector::inst()->create('MyClassName');
Creates a new object of type ClassName
The benefit of constructing objects through this syntax is `ClassName` can be swapped out using the
[Configuration API](../configuration) by developers.
**mysite/_config/app.yml**
:::yml
Injector:
MyClassName:
class: MyBetterClassName
Repeated calls to `create()` create a new class each time.
:::php
$object = Injector::inst()->create('ClassName');
$object2 = Injector::inst()->create('ClassName');
$object !== $object2;
$object = Injector::inst()->create('MyClassName');
$object2 = Injector::inst()->create('MyClassName');
Repeated calls to create() create a new class each time. To create a singleton
object instead, use **get()**
echo $object !== $object2;
// returns true;
## Singleton Pattern
The `Injector` API can be used for the singleton pattern through `get()`. Subsequent calls to `get` return the same
object instance as the first call.
:::php
// sets up ClassName as a singleton
$object = Injector::inst()->get('ClassName');
$object2 = Injector::inst()->get('ClassName');
$object === $object2;
// sets up MyClassName as a singleton
$object = Injector::inst()->get('MyClassName');
$object2 = Injector::inst()->get('MyClassName');
The subsequent call returns the SAME object as the first call.
echo ($object === $object2);
// returns true;
## Dependencies
The `Injector` API can be used to define the types of `$dependancies` that an object requires.
:::php
<?php
class MyController extends Controller {
// both of these properties will be automatically
// set by the injector on object creation
public $permissions;
public $textProperty;
// we declare the types for each of the properties on the object. Anything we pass in via the Injector API must
// match these data types.
static $dependencies = array(
'textProperty' => 'a string value',
'permissions' => '%$PermissionService',
);
}
When creating a new instance of `MyController` the dependencies on that class will be met.
:::php
$object = Injector::inst()->get('MyController');
// results in
$object->permissions instanceof PermissionService;
$object->textProperty == 'a string value';
echo ($object->permissions instanceof PermissionService);
// returns true;
In this case, on creation of the MyController object, the injector will
automatically instantiate the PermissionService object and set it as
the **permissions** property.
echo (is_string($object->textProperty));
// returns true;
## Configuring objects managed by the dependency injector
The [Configuration YAML](../configuration) does the hard work of configuring those `$dependancies` for us.
The above declarative style of dependency management would cover a large
portion of usecases, but more complex dependency structures can be defined
via configuration files.
**mysite/_config/app.yml**
:::yml
Injector:
PermissionService:
class: MyCustomPermissionService
MyController
properties:
textProperty: 'My Text Value'
Configuration can be specified for two areas of dependency management
Now the dependencies will be replaced with our configuration.
* Defining dependency overrides for individual classes
* Injector managed 'services'
:::php
$object = Injector::inst()->get('MyController');
echo ($object->permissions instanceof MyCustomPermissionService);
// returns true;
### Factories
echo ($object->textProperty == 'My Text Value');
// returns true;
## Factories
Some services require non-trivial construction which means they must be created by a factory class. To do this, create
a factory class which implements the `[api:SilverStripe\Framework\Injector\Factory]` interface. You can then specify
a factory class which implements the [api:SilverStripe\Framework\Injector\Factory] interface. You can then specify
the `factory` key in the service definition, and the factory service will be used.
An example using the `MyFactory` service to create instances of the `MyService` service is shown below:
**mysite/_config/app.yml**
:::yml
Injector:
MyService:
@ -104,8 +128,13 @@ An example using the `MyFactory` service to create instances of the `MyService`
MyFactory:
class: MyFactoryImplementation
**mysite/code/MyFactoryImplementation.php**
:::php
<?php
class MyFactoryImplementation implements SilverStripe\Framework\Injector\Factory {
public function create($service, array $params = array()) {
return new MyServiceImplementation();
}
@ -114,35 +143,33 @@ An example using the `MyFactory` service to create instances of the `MyService`
// Will use MyFactoryImplementation::create() to create the service instance.
$instance = Injector::inst()->get('MyService');
### Dependency overrides
## Dependency overrides
To override the **static $dependency;** declaration for a class, you could
define the following configuration file (module/_config/MyController.yml)
To override the `$dependency` declaration for a class, define the following configuration file.
**mysite/_config/app.yml**
name: MyController
---
MyController:
dependencies:
textProperty: a string value
permissions: %$PermissionService
At runtime, the **dependencies** configuration would be read and used in
place of that declared on the object.
## Managed objects
### Managed objects
Simple dependencies can be specified by the `$dependencies`, but more complex configurations are possible by specifying
constructor arguments, or by specifying more complex properties such as lists.
Simple dependencies can be specified by the **dependencies**, but more complex
configurations are possible by specifying constructor arguments, or by
specifying more complex properties such as lists.
These more complex configurations are defined in 'Injector' configuration
blocks and are read by the injector at runtime
These more complex configurations are defined in `Injector` configuration blocks and are read by the `Injector` at
runtime.
Assuming a class structure such as
:::php
<?php
class RestrictivePermissionService {
private $database;
public function setDatabase($d) {
$this->database = $d;
}
@ -158,50 +185,45 @@ Assuming a class structure such as
}
}
and the following configuration
And the following configuration..
:::yml
name: MyController
---
MyController:
dependencies:
permissions: %$PermissionService
Injector:
PermissionService:
class: RestrictivePermissionService
properties:
database: %$MySQLDatabase
MySQLDatabase
constructor:
0: 'dbusername'
1: 'dbpassword'
permissions: %$PermissionService
Injector:
PermissionService:
class: RestrictivePermissionService
properties:
database: %$MySQLDatabase
MySQLDatabase
constructor:
0: 'dbusername'
1: 'dbpassword'
calling
Calling..
:::php
// sets up ClassName as a singleton
$controller = Injector::inst()->get('MyController');
would
Would setup the following
* Create an object of type MyController
* Create an object of type `MyController`
* Look through the **dependencies** and call get('PermissionService')
* Load the configuration for PermissionService, and create an object of
type RestrictivePermissionService
* Look at the properties to be injected and look for the config for
MySQLDatabase
* Create a MySQLDatabase class, passing dbusername and dbpassword as the
parameters to the constructor
* Load the configuration for PermissionService, and create an object of type `RestrictivePermissionService`
* Look at the properties to be injected and look for the config for `MySQLDatabase`
* Create a MySQLDatabase class, passing dbusername and dbpassword as the parameters to the constructor.
### Testing with Injector in a sandbox environment
In situations where injector states must be temporarily overridden, it is possible
to create nested Injector instances which may be later discarded, reverting the
application to the original state.
## Testing with Injector
This is useful when writing test cases, as certain services may be necessary to
override for a single method call.
In situations where injector states must be temporarily overridden, it is possible to create nested Injector instances
which may be later discarded, reverting the application to the original state. This is done through `nest` and `unnest`.
For instance, a temporary service can be registered and unregistered as below:
This is useful when writing test cases, as certain services may be necessary to override for a single method call.
:::php
// Setup default service
@ -209,24 +231,16 @@ For instance, a temporary service can be registered and unregistered as below:
// Test substitute service temporarily
Injector::nest();
Injector::inst()->registerService(new TestingService(), 'ServiceName');
$service = Injector::inst()->get('ServiceName');
// ... do something with $service
// revert changes
Injector::unnest();
// ... future requests for 'ServiceName' will return the LiveService instance
## API Documentation
### What are Services?
Without diving too deep down the rabbit hole, the term 'Service' is commonly
used to describe a piece of code that acts as an interface between the
controller layer and model layer of an MVC architecture. Rather than having
a controller action directly operate on data objects, a service layer provides
that logic abstraction, stopping controllers from implementing business logic,
and keeping that logic packaged in a way that is easily reused from other
classes.
By default, objects are managed like a singleton, in that there is only one
object instance used for a named service, and all references to that service
are returned the same object.
* [api:Injector]
* [api:Factory]

View File

@ -3,187 +3,195 @@ summary: Introduction to using aspect-oriented programming with SilverStripe.
# Aspects
## Introduction
Aspect oriented programming is the idea that some logic abstractions can be applied across various type hierarchies
"after the fact", altering the behavior of the system without altering the code structures that are already in place.
Aspect oriented programming is the idea that some logic abstractions can be applied across various type hierarchies "after the fact", altering the behaviour of the system without altering the code structures that are already in place.
> In computing, aspect-oriented programming (AOP) is a programming paradigm which isolates secondary or supporting
> functions from the main program's business logic. It aims to increase modularity by allowing the separation of
> cross-cutting concerns, forming a basis for aspect-oriented software development.
> In computing, aspect-oriented programming (AOP) is a programming paradigm
> which isolates secondary or supporting functions from the main program's
> business logic. It aims to increase modularity by allowing the separation of
> cross-cutting concerns, forming a basis for aspect-oriented software
> development.
<div class="notice" markdown="1">
[Wikipedia](http://en.wikipedia.org/wiki/Aspect-oriented_programming) provides a much more in-depth explanation.
</div>
[The wiki article](http://en.wikipedia.org/wiki/Aspect-oriented_programming)
provides a much more in-depth explanation!
In the context of the SilverStripe [Dependency Injector](injector), Aspects are achieved thanks to PHP's `__call` magic
method combined with the `Proxy` Design Pattern.
Assume an existing service declaration exists called `MyService`. An `AopProxyService` class instance is created, and
the existing `MyService` object is bound in as a member variable of the `AopProxyService` class.
Objects are added to the `AopProxyService` instance's "beforeCall" and "afterCall" lists; each of these implements
either the beforeCall or afterCall method.
When client code declares a `$dependency` on MyService, it is actually passed in the `AopProxyService` instance.
Client code calls a method `MyMethod` that it knows exists on `MyService` - this doesn't exist on `AopProxyService`, so
__call is triggered.
All classes bound to the `beforeCall` list are executed; if any explicitly returns 'false', `myMethod` is not executed.
Otherwise, `myMethod` is executed.
All classes bound to the `afterCall` list are executed.
To provide some context, imagine a situation where we want to direct all `write` queries made in the system to a
specific database server, whereas all read queries can be handled by slave servers.
A simplified implementation might look like the following.
<div class="notice" markdown="1">
This doesn't cover all cases used by SilverStripe so is not a complete solution, more just a guide to how it would be
used.
</div>
**mysite/code/MySQLWriteDbAspect.php**
:::php
<?php
class MySQLWriteDbAspect implements BeforeCallAspect {
/**
* @var MySQLDatabase
*/
public $writeDb;
public $writeQueries = array(
'insert','update','delete','replace'
);
In the context of this dependency injector, AOP is achieved thanks to PHP's
__call magic method combined with the Proxy design pattern.
public function beforeCall($proxied, $method, $args, &$alternateReturn) {
if (isset($args[0])) {
$sql = $args[0];
$code = isset($args[1]) ? $args[1] : E_USER_ERROR;
## In practice
* Assume an existing service declaration exists called MyService
* An AopProxyService class instance is created, and the existing MyService object is bound in as a member variable of the AopProxyService class
* Objects are added to the AopProxyService instance's "beforeCall" and "afterCall" lists; each of these implements either the beforeCall or afterCall method
* When client code declares a dependency on MyService, it is actually passed in the AopProxyService instance
* Client code calls a method `myMethod` that it knows exists on MyService - this doesn't exist on AopProxyService, so __call is triggered.
* All classes bound to the beforeCall list are executed; if any explicitly returns 'false', `myMethod` is not executed.
* Otherwise, myMethod is executed
* All classes bound to the afterCall list are executed
## A worked example
To provide some context, imagine a situation where we want to direct all 'write' queries made in the system to a specific
database server, whereas all read queries can be handled by slave servers. A simplified implementation might look
like the following - note that this doesn't cover all cases used by SilverStripe so is not a complete solution, more
just a guide to how it would be used.
```
<?php
/**
* Redirects write queries to a specific database configuration
*
* @author <marcus@silverstripe.com.au>
* @license BSD License http://www.silverstripe.org/bsd-license
*/
class MySQLWriteDbAspect implements BeforeCallAspect {
/**
*
* @var MySQLDatabase
*/
public $writeDb;
public $writeQueries = array('insert','update','delete','replace');
public function beforeCall($proxied, $method, $args, &$alternateReturn) {
if (isset($args[0])) {
$sql = $args[0];
$code = isset($args[1]) ? $args[1] : E_USER_ERROR;
if (in_array(strtolower(substr($sql,0,strpos($sql,' '))), $this->writeQueries)) {
$alternateReturn = $this->writeDb->query($sql, $code);
return false;
if (in_array(strtolower(substr($sql,0,strpos($sql,' '))), $this->writeQueries)) {
$alternateReturn = $this->writeDb->query($sql, $code);
return false;
}
}
}
}
}
```
To actually make use of this class, a few different objects need to be configured. First up, define the `writeDb`
object that's made use of above
object that's made use of above.
```
WriteMySQLDatabase:
class: MySQLDatabase
constructor:
- type: MySQLDatabase
server: write.hostname.db
username: user
password: pass
database: write_database
```
**mysite/_config/app.yml**
This means that whenever something asks the injector for the `WriteMySQLDatabase` object, it'll receive an object of
type `MySQLDatabase`, configured to point at the 'write' database
:::yml
WriteMySQLDatabase:
class: MySQLDatabase
constructor:
- type: MySQLDatabase
server: write.hostname.db
username: user
password: pass
database: write_database
Next, this should be bound into an instance of the aspect class
This means that whenever something asks the [api:Injector] for the `WriteMySQLDatabase` object, it'll receive an object
of type `MySQLDatabase`, configured to point at the 'write_database'.
```
MySQLWriteDbAspect:
properties:
writeDb: %$WriteMySQLDatabase
```
Next, this should be bound into an instance of the `Aspect` class
**mysite/_config/app.yml**
:::yml
MySQLWriteDbAspect:
properties:
writeDb: %$WriteMySQLDatabase
Next, we need to define the database connection that will be used for all non-write queries
```
ReadMySQLDatabase:
class: MySQLDatabase
constructor:
- type: MySQLDatabase
server: slavecluster.hostname.db
username: user
password: pass
database: read_database
```
**mysite/_config/app.yml**
:::yml
ReadMySQLDatabase:
class: MySQLDatabase
constructor:
- type: MySQLDatabase
server: slavecluster.hostname.db
username: user
password: pass
database: read_database
The final piece that ties everything together is the AopProxyService instance that will be used as the replacement
object when the framework creates the database connection in DB.php
The final piece that ties everything together is the [api:AopProxyService] instance that will be used as the replacement
object when the framework creates the database connection.
```
MySQLDatabase:
class: AopProxyService
properties:
proxied: %$ReadMySQLDatabase
beforeCall:
query:
- %$MySQLWriteDbAspect
```
**mysite/_config/app.yml**
:::yml
MySQLDatabase:
class: AopProxyService
properties:
proxied: %$ReadMySQLDatabase
beforeCall:
query:
- %$MySQLWriteDbAspect
The two important parts here are in the `properties` declared for the object
- **proxied** : This is the 'read' database connectino that all queries should be initially directed through
- **beforeCall** : A hash of method\_name => array containing objects that are to be evaluated _before_ a call to the defined method\_name
The two important parts here are in the `properties` declared for the object.
- **proxied** : This is the 'read' database connection that all queries should be initially directed through.
- **beforeCall** : A hash of method\_name => array containing objects that are to be evaluated _before_ a call to the
defined method\_name
Overall configuration for this would look as follows
```
Injector:
ReadMySQLDatabase:
class: MySQLDatabase
constructor:
- type: MySQLDatabase
server: slavecluster.hostname.db
username: user
password: pass
database: read_database
MySQLWriteDbAspect:
properties:
writeDb: %$WriteMySQLDatabase
WriteMySQLDatabase:
class: MySQLDatabase
constructor:
- type: MySQLDatabase
server: write.hostname.db
username: user
password: pass
database: write_database
MySQLDatabase:
class: AopProxyService
properties:
proxied: %$ReadMySQLDatabase
beforeCall:
query:
- %$MySQLWriteDbAspect
```
**mysite/_config/app.yml**
:::yml
Injector:
ReadMySQLDatabase:
class: MySQLDatabase
constructor:
- type: MySQLDatabase
server: slavecluster.hostname.db
username: user
password: pass
database: read_database
MySQLWriteDbAspect:
properties:
writeDb: %$WriteMySQLDatabase
WriteMySQLDatabase:
class: MySQLDatabase
constructor:
- type: MySQLDatabase
server: write.hostname.db
username: user
password: pass
database: write_database
MySQLDatabase:
class: AopProxyService
properties:
proxied: %$ReadMySQLDatabase
beforeCall:
query:
- %$MySQLWriteDbAspect
## Changing what a method returns
One major feature of an aspect is the ability to modify what is returned from the client's call to the proxied method.
As seen in the above example, the `beforeCall` method modifies the byref `&$alternateReturn` variable, and returns
`false` after doing so.
```
One major feature of an `Aspect` is the ability to modify what is returned from the client's call to the proxied method.
As seen in the above example, the `beforeCall` method modifies the `&$alternateReturn` variable, and returns `false`
after doing so.
:::php
$alternateReturn = $this->writeDb->query($sql, $code);
return false;
```
By returning false from the `beforeCall()` method, the wrapping proxy class will _not_ call any additional `beforeCall`
handlers defined for the called method. Assigning the $alternateReturn variable also indicates to return that value
By returning `false` from the `beforeCall()` method, the wrapping proxy class will_not_ call any additional `beforeCall`
handlers defined for the called method. Assigning the `$alternateReturn` variable also indicates to return that value
to the caller of the method.
Similarly the `afterCall()` aspect can be used to manipulate the value to be returned to the calling code. All the
`afterCall()` method needs to do is return a non-null value, and that value will be returned to the original calling
code instead of the actual return value of the called method.
## API Documentation
* [api:AopProxyService]
* [api:BeforeCallAspect]
* [api:AfterCallAspect]
* [api:Injector]

View File

@ -0,0 +1,10 @@
title: Custom Templates
summary: Override templates from core and modules in your application
# Custom Templates
See [Template Inheritance](../templates).
## Form Templates
See [Form Templates](../forms/form_templates).

View File

@ -0,0 +1,77 @@
title: How to Publish a SilverStripe module
# How to Publish a SilverStripe module.
If you wish to submit your module to our public directory, you take responsibility for a certain level of code quality,
adherence to conventions, writing documentation, and releasing updates.
SilverStripe uses [Composer](../../getting_started/composer/) to manage module releases and dependencies between
modules. If you plan on releasing your module to the public, ensure that you provide a `composer.json` file in the root
of your module containing the meta-data about your module.
For more information about what your `composer.json` file should include, consult the
[Composer Documentation](http://getcomposer.org/doc/01-basic-usage.md).
A basic usage of a module for 3.1 that requires the CMS would look similar to
this:
**mycustommodule/composer.json**
:::js
{
"name": "your-vendor-name/module-name",
"description": "One-liner describing your module",
"type": "silverstripe-module",
"homepage": "http://github.com/your-vendor-name/module-name",
"keywords": ["silverstripe", "some-tag", "some-other-tag"],
"license": "BSD-3-Clause",
"authors": [
{"name": "Your Name","email": "your@email.com"}
],
"support": {
"issues": "http://github.com/your-vendor-name/module-name/issues"
},
"require": {
"silverstripe/cms": "~3.1",
"silverstripe/framework": "~3.1"
},
"extra": {
"installer-name": "module-name",
"screenshots": [
"relative/path/screenshot1.png",
"http://myhost.com/screenshot2.png"
]
}
}
Once your module is published online with a service like Github.com or Bitbucket.com, submit the repository to
[Packagist](https://packagist.org/) to have the module accessible to developers. It'll automatically get picked
up by [addons.silverstripe.org](http://addons.silverstripe.org/) website.
## Releasing versions
Over time you may have to release new versions of your module to continue to work with newer versions of SilverStripe.
By using Composer, this is made easy for developers by allowing them to specify what version they want to use. Each
version of your module should be a separate branch in your version control and each branch should have a `composer.json`
file explicitly defining what versions of SilverStripe you support.
Say you have a module which supports SilverStripe 3.0. A new release of this module takes advantage of new features
in SilverStripe 3.1. In this case, you would create a new branch for the 3.0 compatible code base of your module. This
allows you to continue fixing bugs on this older release branch.
<div class="info" markdown="1">
As a convention, the `master` branch of your module should always work with the `master` branch of SilverStripe.
</div>
Other branches should be created on your module as needed if they're required to support specific SilverStripe releases.
You can have an overlap in supported versions, e.g two branches in your module both support SilverStripe 3.1. In this
case, you should explain the differences in your `README.md` file.
Here's some common values for your `require` section
(see [getcomposer.org](http://getcomposer.org/doc/01-basic-usage.md#package-versions) for details):
* `3.0.*`: Version `3.0`, including `3.0.1`, `3.0.2` etc, excluding `3.1`
* `~3.0`: Version `3.0` or higher, including `3.0.1` and `3.1` etc, excluding `4.0`
* `~3.0,<3.2`: Version `3.0` or higher, up until `3.2`, which is excluded
* `~3.0,>3.0.4`: Version `3.0` or higher, starting with `3.0.4`

View File

@ -0,0 +1,33 @@
title: How to Create a Google Maps Shortcode
# How to Create a Google Maps Shortcode
To demonstrate how easy it is to build custom shortcodes, we'll build one to display a Google Map based on a provided
address. We want our CMS authors to be able to embed the map using the following code:
:::php
[googlemap,width=500,height=300]97-99 Courtenay Place, Wellington, New Zealand[/googlemap]
So we've got the address as "content" of our new `googlemap` shortcode tags, plus some `width` and `height` arguments.
We'll add defaults to those in our shortcode parser so they're optional.
**mysite/_config.php**
:::php
ShortcodeParser::get('default')->register('googlemap', function($arguments, $address, $parser, $shortcode) {
$iframeUrl = sprintf(
'http://maps.google.com/maps?q=%s&amp;hnear=%s&amp;ie=UTF8&hq=&amp;t=m&amp;z=14&amp;output=embed',
urlencode($address),
urlencode($address)
);
$width = (isset($arguments['width']) && $arguments['width']) ? $arguments['width'] : 400;
$height = (isset($arguments['height']) && $arguments['height']) ? $arguments['height'] : 300;
return sprintf(
'<iframe width="%d" height="%d" src="%s" frameborder="0" scrolling="no" marginheight="0" marginwidth="0"></iframe>',
$width,
$height,
$iframeUrl
);
});

View File

@ -1,8 +1,17 @@
title: Extending SilverStripe
summary: Understand the ways to modify the built-in functionality through Extensions, Subclassing and Dependency Injection.
introduction: SilverStripe is easily extensible to meet custom application requirements. This guide covers the wide range of API's to modify built-in functionality and make your own code easily extensible.
[CHILDREN]
No two applications are ever going to be the same and SilverStripe is built with this in mind. The core framework
includes common functionality and default behaviors easily complemented with add-ons such as modules, widgets and
themes.
## How-to
SilverStripe includes a myriad of extension API's such as *Extension Hooks* and support for programming patterns
such Dependency Injection. Allowing developers to tailor the framework to their needs without modifying the core
framework.
[CHILDREN Folder=How_To]
[CHILDREN Exclude="How_Tos"]
## How to's
[CHILDREN Folder="How_Tos"]