mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Review and clean up of extending section
This commit is contained in:
parent
ce02d01274
commit
19259e0497
@ -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)
|
@ -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]`
|
107
docs/en/02_Developer_Guides/05_Extending/00_Modules.md
Normal file
107
docs/en/02_Developer_Guides/05_Extending/00_Modules.md
Normal 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)
|
283
docs/en/02_Developer_Guides/05_Extending/01_Extensions.md
Normal file
283
docs/en/02_Developer_Guides/05_Extending/01_Extensions.md
Normal 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]
|
@ -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)
|
@ -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
|
@ -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&hnear=%s&ie=UTF8&hq=&t=m&z=14&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 `>`,
|
||||
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 `>`, 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]
|
||||
|
@ -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]
|
@ -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]
|
||||
|
10
docs/en/02_Developer_Guides/05_Extending/07_Templates.md
Normal file
10
docs/en/02_Developer_Guides/05_Extending/07_Templates.md
Normal 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).
|
@ -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`
|
@ -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&hnear=%s&ie=UTF8&hq=&t=m&z=14&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
|
||||
);
|
||||
});
|
@ -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"]
|
Loading…
Reference in New Issue
Block a user