Difference between revisions of "Extension:ArticleProperties"

From Organic Design
Jump to: navigation, search
(Overriding default MediaWiki actions: strict standards in php 5.4)
(Change source-code blocks to standard format)
 
Line 24: Line 24:
 
== Simple example ==
 
== Simple example ==
 
Here's a basic example ''ArticleProperties'' sub-class definition.
 
Here's a basic example ''ArticleProperties'' sub-class definition.
{{code|<php>
+
<source lang="php">
 
class LocationExample extends ArticleProperties {
 
class LocationExample extends ArticleProperties {
  
Line 43: Line 43:
 
parent::__construct( $title );
 
parent::__construct( $title );
 
}
 
}
}</php>}}
+
}
 +
</source>
  
  
 
A column called "page" is automatically added which will contain the page ID of the MediaWiki article that the properties apply to. There is only a single row for any given page. Here's the "locations" table created by the ''ArticleProperties'' class for the sub-class shown above.
 
A column called "page" is automatically added which will contain the page ID of the MediaWiki article that the properties apply to. There is only a single row for any given page. Here's the "locations" table created by the ''ArticleProperties'' class for the sub-class shown above.
{{code|<pre>
+
<source>
 
+------------------+-------------+------+-----+---------+-------+
 
+------------------+-------------+------+-----+---------+-------+
 
| Field            | Type        | Null | Key | Default | Extra |
 
| Field            | Type        | Null | Key | Default | Extra |
Line 61: Line 62:
 
| example_notes    | text        | YES  |    | NULL    |      |
 
| example_notes    | text        | YES  |    | NULL    |      |
 
+------------------+-------------+------+-----+---------+-------+
 
+------------------+-------------+------+-----+---------+-------+
</pre>}}
+
</source>
  
  
 
== Determining which class a page should instantiate as ==
 
== Determining which class a page should instantiate as ==
 
The extension provides a hook called "ArticlePropertiesClassFromTitle" which allows its sub-classes to easily define rules by which they should become the class used for a MediaWiki page, for example a specified namespace could use a specific ''ArticleProperties'' sub-class instead of the standard Article. In this example, the ''LocationExample'' class defined above will be used if the article is in the ''NS_FOO'' namespace:
 
The extension provides a hook called "ArticlePropertiesClassFromTitle" which allows its sub-classes to easily define rules by which they should become the class used for a MediaWiki page, for example a specified namespace could use a specific ''ArticleProperties'' sub-class instead of the standard Article. In this example, the ''LocationExample'' class defined above will be used if the article is in the ''NS_FOO'' namespace:
{{code|<php>
+
<source lang="php">
 
$wgHooks['ArticlePropertiesClassFromTitle'][] = 'wfUseLocationExampleClass';
 
$wgHooks['ArticlePropertiesClassFromTitle'][] = 'wfUseLocationExampleClass';
 
function wfUseLocationExampleClass( $title, &$class ) {
 
function wfUseLocationExampleClass( $title, &$class ) {
Line 72: Line 73:
 
return true;
 
return true;
 
}
 
}
</php>}}
+
</source>
  
 
== Overriding default MediaWiki actions ==
 
== Overriding default MediaWiki actions ==
Line 82: Line 83:
  
 
To continue the example above, here's an ''edit'' and ''save'' method added to integrate the page with one of the persistent properties (the ''Notes'' property).
 
To continue the example above, here's an ''edit'' and ''save'' method added to integrate the page with one of the persistent properties (the ''Notes'' property).
{{code|<php>
+
<source lang="php">
 
class LocationExample extends ArticleProperties {
 
class LocationExample extends ArticleProperties {
  
Line 111: Line 112:
  
 
}
 
}
</php>}}
+
</source>
 
The '''input''' and '''label''' methods are supplied by the ''ArticleProperties'' class (along with a number of other useful methods for constructing forms and dealing with page properties). It creates an input named '''wpNotes''' and a label for it using an ''i18n'' message. The '''save''' method then writes this value into the page properties. Any existing value will automatically be shown in the input field.
 
The '''input''' and '''label''' methods are supplied by the ''ArticleProperties'' class (along with a number of other useful methods for constructing forms and dealing with page properties). It creates an input named '''wpNotes''' and a label for it using an ''i18n'' message. The '''save''' method then writes this value into the page properties. Any existing value will automatically be shown in the input field.
  
Line 122: Line 123:
  
 
When the ''ArticleProperties'' class creates one of the sub-classes via the [[MW:Manual:Hooks/ArticleFromTitle|ArticleFromTitle hook]] (which is only ever called when a full page render is occurring) it calls the constructor along with the extra '''$passive''' parameter set to ''false''. The state of ''passive'' will also be stored in the instance property called '''$mPassive''' so that methods other than the constructor also have access to it. The following example demonstrates how a sub-class defines its constructor to make adjustments to the skin in the ''non-passive'' case.
 
When the ''ArticleProperties'' class creates one of the sub-classes via the [[MW:Manual:Hooks/ArticleFromTitle|ArticleFromTitle hook]] (which is only ever called when a full page render is occurring) it calls the constructor along with the extra '''$passive''' parameter set to ''false''. The state of ''passive'' will also be stored in the instance property called '''$mPassive''' so that methods other than the constructor also have access to it. The following example demonstrates how a sub-class defines its constructor to make adjustments to the skin in the ''non-passive'' case.
{{code|<php>
+
<source lang="php">
 
function __construct( $title, $passive = true ) {
 
function __construct( $title, $passive = true ) {
 
if( !$passive ) {
 
if( !$passive ) {
Line 131: Line 132:
 
return $this;
 
return $this;
 
}
 
}
</php>}}
+
</source>
  
 
== JavaScript integration ==
 
== JavaScript integration ==
Line 139: Line 140:
  
 
Here's an example continuing with the "ExampleLocation" class defined above.
 
Here's an example continuing with the "ExampleLocation" class defined above.
{{code|<php>
+
<source lang="php">
 
class LocationExample extends ArticleProperties {
 
class LocationExample extends ArticleProperties {
  
Line 145: Line 146:
 
var $jsProp = array( 'Lat', 'Lon' );
 
var $jsProp = array( 'Lat', 'Lon' );
 
var $jsI18n = array( 'welcomecreation', 'userlogin', 'logouttext' );
 
var $jsI18n = array( 'welcomecreation', 'userlogin', 'logouttext' );
</php>}}
+
</source>
 
This makes two of the persistent properties available to the JavaScript as '''window.locexample.Lat'' and ''window.locxample.Lon''', and also supplies three of the MediaWiki system messages which can be accessed by '''mw.config.get('userlogin')''' for example.
 
This makes two of the persistent properties available to the JavaScript as '''window.locexample.Lat'' and ''window.locxample.Lon''', and also supplies three of the MediaWiki system messages which can be accessed by '''mw.config.get('userlogin')''' for example.
  
Line 154: Line 155:
 
=== getClass ===
 
=== getClass ===
 
This returns (and if necessary declares) an ArticleProperties sub-class associated with the passed title. It determines the class name by calling the ''ArticlePropertiesClassFromTitle'' hook. The returned class name could then be used to create an instance as in the following example.
 
This returns (and if necessary declares) an ArticleProperties sub-class associated with the passed title. It determines the class name by calling the ''ArticlePropertiesClassFromTitle'' hook. The returned class name could then be used to create an instance as in the following example.
{{code|<php>$class = ArticleProperties::getClass( $title );
+
<source lang="php">
$obj = new $class( $title );</php>}}
+
$class = ArticleProperties::getClass( $title );
 +
$obj = new $class( $title );
 +
</source>
 
Note that the ''Article::newFromTitle'' method should not be used because this will be regarded as a ''non-passive'' instantiation and may have global side-effects such as skin changes etc depending on the nature of the sub-class being instantiated.
 
Note that the ''Article::newFromTitle'' method should not be used because this will be regarded as a ''non-passive'' instantiation and may have global side-effects such as skin changes etc depending on the nature of the sub-class being instantiated.
  
Line 182: Line 185:
  
 
Here's an example of performing a query on the above example class, it prints the latitude and longitude properties for each item in the city "new York":
 
Here's an example of performing a query on the above example class, it prints the latitude and longitude properties for each item in the city "new York":
{{code|<php>foreach( ArticleProperties::query( 'LocationExample', array( "example_city = 'New York'" ) ) as $title ) {
+
<source lang="php">
 +
foreach( ArticleProperties::query( 'LocationExample', array( "example_city = 'New York'" ) ) as $title ) {
 
$article = new LocationExample( $title );
 
$article = new LocationExample( $title );
 
$lat = $article->getValue( 'Lat' );
 
$lat = $article->getValue( 'Lat' );
 
$lon = $article->getValue( 'Lon' );
 
$lon = $article->getValue( 'Lon' );
 
$wgOut->addHTML( "Location: $lat, $lon" );
 
$wgOut->addHTML( "Location: $lat, $lon" );
}</php>}}
+
}
 +
</source>
  
 
This example shows a new ''ArticleProperties'' sub-class (of the class ''LocationExample'') called ''$article'' being created for each of the resulting ''Title'' objects. The called to the ''query'' method defines the class of article that the resulting ''Title'' objects should refer to, and sends an array of SQL conditions that the articles properties must match, in this case just the single condition that the ''City'' property should be "New York".
 
This example shows a new ''ArticleProperties'' sub-class (of the class ''LocationExample'') called ''$article'' being created for each of the resulting ''Title'' objects. The called to the ''query'' method defines the class of article that the resulting ''Title'' objects should refer to, and sends an array of SQL conditions that the articles properties must match, in this case just the single condition that the ''City'' property should be "New York".
Line 215: Line 220:
 
=== ArticlePropertiesChanged ===
 
=== ArticlePropertiesChanged ===
 
The extension executes this hook whenever the properties of an article are updated. This allows events to be tied in with specific properties, or the property values to be reflected in the article content in some way. One simple use of the hook is to maintain the current property values in the wikitext in standard template syntax so they can be rendered in-wiki and their changes included in the revision history for auditing purposes.
 
The extension executes this hook whenever the properties of an article are updated. This allows events to be tied in with specific properties, or the property values to be reflected in the article content in some way. One simple use of the hook is to maintain the current property values in the wikitext in standard template syntax so they can be rendered in-wiki and their changes included in the revision history for auditing purposes.
{{code|<php>public static function onArticlePropertiesChanged( $article, &$change ) {
+
<source lang="php">
 +
public static function onArticlePropertiesChanged( $article, &$change ) {
 
$summary = 'Article properties updated: ' . join( ', ', array_keys( $change ) );
 
$summary = 'Article properties updated: ' . join( ', ', array_keys( $change ) );
 
$content = '{{' . get_class( $article ) . "\n";
 
$content = '{{' . get_class( $article ) . "\n";
Line 222: Line 228:
 
$article->doEdit( $content, $summary, EDIT_UPDATE );
 
$article->doEdit( $content, $summary, EDIT_UPDATE );
 
return true;
 
return true;
}</php>}}
+
}
 +
</source>
  
 
=== ArticlePropertiesClassFromTitle ===
 
=== ArticlePropertiesClassFromTitle ===
Line 248: Line 255:
  
 
This first method is called whenever some part of the project needs to display a message for the user. The first parameter is the message content, and the second is an optional message type which can be used by the rendering code to include appropriate CSS classes to the message container. The method appends the type and message data to any current messages in the user's queue.
 
This first method is called whenever some part of the project needs to display a message for the user. The first parameter is the message content, and the second is an optional message type which can be used by the rendering code to include appropriate CSS classes to the message container. The method appends the type and message data to any current messages in the user's queue.
{{code|<php>static function message( $msg, $type = 'success' ) {
+
<source lang="php">
 +
static function message( $msg, $type = 'success' ) {
 
global $wgUser;
 
global $wgUser;
 
$opt = 'user-messages';
 
$opt = 'user-messages';
 
$wgUser->setOption( $opt, $wgUser->getOption( $opt, '' ) . "||$type|$msg" );
 
$wgUser->setOption( $opt, $wgUser->getOption( $opt, '' ) . "||$type|$msg" );
 
$wgUser->saveSettings();
 
$wgUser->saveSettings();
}</php>}}
+
}
 +
</source>
  
  
 
This second method is called by the rendering method which would usually be placed in a late hook or directly into the skin template code. An array of the messages of each type is returned. If one type has accumulated more than one message they are simply concatenated. Each individual message is placed inside a paragraph element. All the messages are removed from the user's queue when this method is called so that they're only displayed once.
 
This second method is called by the rendering method which would usually be placed in a late hook or directly into the skin template code. An array of the messages of each type is returned. If one type has accumulated more than one message they are simply concatenated. Each individual message is placed inside a paragraph element. All the messages are removed from the user's queue when this method is called so that they're only displayed once.
{{code|<php>static function popMessages() {
+
<source lang="php">
 +
static function popMessages() {
 
global $wgUser;
 
global $wgUser;
 
$opt = 'user-messages';
 
$opt = 'user-messages';
Line 272: Line 282:
 
$wgUser->saveSettings();
 
$wgUser->saveSettings();
 
return $messages;
 
return $messages;
}</php>}}
+
}
 +
</source>
  
 
== TemplateProperties extension ==
 
== TemplateProperties extension ==

Latest revision as of 18:11, 22 May 2015

This extension was created in early 2012 and is available from our extensions repository here. The extension which creates a new ArticleProperties class that is a sub-class of Article. As the name indicates, the extensions main idea was to provide a mechanism by which different classes of articles could be created that each have their own collection of persistent properties.

The ArticleProperties class is an abstract class (one that's designed to be sub-classed, and not to be instantiated directly). It provides the common functionality allowing all its sub-classes to declare the names and types of properties that should be associated with each article of that type. This common class also offers a simple means by which the page can render its MediaWiki actions in their own specific ways.

Each ArticleProperties sub-class has three static variables, table which is the name of the database table the class will store its instances data in, columns which determine the property names and their database data-types, and an option prefix to use for column names so they don't conflict with column names of other tables.

A special page called Special:ArticleProperties allows the creation of the tables for the sub-classes or to add new columns to them.

Why?

MediaWiki makes an excellent web site development framework even for sites that have nothing to do with wiki functionality. The MediaWiki framework takes care of many features common to most websites which can save hundreds of hours of work. There are of course many other fully-featured frameworks available, but I feel very comfortable with the MediaWiki environment and most of my work tends to involve MediaWiki in some way or another, so for me this extension is a natural progression. So in a nutshell, this extension allows non-wiki sites to be made easily while taking advantage of many common website features such as the following:

  • User account registration with all the issues such as forgotten passwords, user-preferences, email confirmation etc.
  • URL to database content mapping system (used in the wiki context as page titles, but this can be used for any kind of data structure)
  • User-groups, page-actions and permissions structure
  • Excellent internationalisation support
  • User timezone management
  • Watchlist and notification system
  • Standardised object model for users, web-requests, pages and skin/output
  • Database agnostic abstraction layer
  • Skin and template mechanism
  • Inherently scalable design with strong caching and load-balancing support
  • Excellent JavaScript resource framework with compression and lazy-loading including most of the commonly required JS components such as jQuery and jQueryUI.
  • URL-based image thumbnailer

Simple example

Here's a basic example ArticleProperties sub-class definition.

class LocationExample extends ArticleProperties {

	public static $table = 'locations';
	public static $prefix = 'example_';
	public static $columns = array(
		'Lat'      => 'FLOAT',
		'Lon'      => 'FLOAT',
		'StreetNo' => 'VARCHAR(8)',
		'Street'   => 'VARCHAR(32)',
		'City'     => 'VARCHAR(32)',
		'Postal'   => 'VARCHAR(16)',
		'Country'  => 'VARCHAR(32)',
		'Notes'    => 'TEXT'
	);

	function __construct( $title ) {
		parent::__construct( $title );
	}
}


A column called "page" is automatically added which will contain the page ID of the MediaWiki article that the properties apply to. There is only a single row for any given page. Here's the "locations" table created by the ArticleProperties class for the sub-class shown above.

+------------------+-------------+------+-----+---------+-------+
| Field            | Type        | Null | Key | Default | Extra |
+------------------+-------------+------+-----+---------+-------+
| example_page     | int(11)     | NO   |     | NULL    |       |
| example_lat      | float       | YES  |     | NULL    |       |
| example_lon      | float       | YES  |     | NULL    |       |
| example_streetno | varchar(8)  | YES  |     | NULL    |       |
| example_street   | varchar(32) | YES  |     | NULL    |       |
| example_city     | varchar(32) | YES  |     | NULL    |       |
| example_postal   | varchar(16) | YES  |     | NULL    |       |
| example_country  | varchar(32) | YES  |     | NULL    |       |
| example_notes    | text        | YES  |     | NULL    |       |
+------------------+-------------+------+-----+---------+-------+


Determining which class a page should instantiate as

The extension provides a hook called "ArticlePropertiesClassFromTitle" which allows its sub-classes to easily define rules by which they should become the class used for a MediaWiki page, for example a specified namespace could use a specific ArticleProperties sub-class instead of the standard Article. In this example, the LocationExample class defined above will be used if the article is in the NS_FOO namespace:

$wgHooks['ArticlePropertiesClassFromTitle'][] = 'wfUseLocationExampleClass';
function wfUseLocationExampleClass( $title, &$class ) {
	if( $title->getNamespace() == NS_FOO ) $class = 'LocationExample';
	return true;
}

Overriding default MediaWiki actions

Not only does the ArticleProperties class allow its sub-classes to have persistent properties, but it also allows the default rendering, editing and saving actions to be overridden so that they can incorporate their properties appropriately. A number of other methods are provided for making interaction with the properties easier which are described in more detail below.

The sub-classes can use view(), edit() and save() methods to override the default article functionality. The edit method allows new form fields to be added to the edit form, and the save method allows these to be processed and written into the page_props table.

Note that these method are only called in the sub-class for non-passive instances since it does not make any sense for them to be called outside of the rendered-page context. For example, if a new LocationExample instance were created and edited programatically the save() method would not be called.

To continue the example above, here's an edit and save method added to integrate the page with one of the persistent properties (the Notes property).

class LocationExample extends ArticleProperties {

	public static $table = 'locations';
	public static $prefix = 'example_';
	public static $columns = array(
		'Lat'      => 'FLOAT',
		'Lon'      => 'FLOAT',
		'StreetNo' => 'VARCHAR(8)',
		'Street'   => 'VARCHAR(32)',
		'City'     => 'VARCHAR(32)',
		'Postal'   => 'VARCHAR(16)',
		'Country'  => 'VARCHAR(32)',
		'Notes'    => 'TEXT'
	);

	function __construct( $title ) {
		parent::__construct( $title );
	}

	function edit( &$editpage, $out ) {
		$out->addHTML( $this->label( 'notes-description', 'Notes' ) . $this->input( 'Notes' ) );
	}

	function save( $request = false ) {
		$this->properties( array( 'Notes' => $request->getText( 'wpNotes' ) ) );
	}

}

The input and label methods are supplied by the ArticleProperties class (along with a number of other useful methods for constructing forms and dealing with page properties). It creates an input named wpNotes and a label for it using an i18n message. The save method then writes this value into the page properties. Any existing value will automatically be shown in the input field.

The ArticleProperties class also provides a query method which allows conditions and options to be sent in the same format used by the SQL Database::select method, and it returns a list of Title objects for matching articles.

And it also provides a table method that allows a list of titles to be rendered as an HTML table. The method takes three parameters, the second two are optional. The first is the title-list, the second an array of HTML attributes such as class and id that the resulting table should have. And the third parameter is an array of columns that the table should use. If the columns are not provided, they will be extracted from the properties of the first article from the title list.

Passive and non-passive instantiations

Another aspect that has been found to be required is a way for the sub-classes constructors to differentiate between a "passive" instantiation used for example to call methods on ArticleProperties instances in a search result, and "non-passive" instantiation which occurs when the instance represents a wiki article being currently viewed. The constructor needs to know because the class may make adjustments to the skin or other global elements of the system in the latter case, but should leave them alone for passive instantiations.

When the ArticleProperties class creates one of the sub-classes via the ArticleFromTitle hook (which is only ever called when a full page render is occurring) it calls the constructor along with the extra $passive parameter set to false. The state of passive will also be stored in the instance property called $mPassive so that methods other than the constructor also have access to it. The following example demonstrates how a sub-class defines its constructor to make adjustments to the skin in the non-passive case.

function __construct( $title, $passive = true ) {
	if( !$passive ) {
		global $wgOut;
		$wgOut->getSkin()->specialisedSkinAdjustments();
	}
	parent::__construct( $title );
	return $this;
}

JavaScript integration

Often ArticleProperties sub-classes have JavaScript aspects to their view and edit methods and these client-side code needs to have access to some of the instance's properties. Another common requirement is for some of the MediaWiki system messages to be available to the JavaScript code.

The ArticleProperties class provides a simple solution to this, which is to add arrays to the sub-class that defines the object properties and messages which should be made available to the client side. These will be made available to the client side via the mw.config method along with the other existing globals such as wgTitle and wgUserName. If the $jsObj property is set then the specified array of instance properties will be made available in the named JavaScript object instead of from the mw.config method.

Here's an example continuing with the "ExampleLocation" class defined above.

class LocationExample extends ArticleProperties {

	var $jsObj = 'locexample';
	var $jsProp = array( 'Lat', 'Lon' );
	var $jsI18n = array( 'welcomecreation', 'userlogin', 'logouttext' );

This makes two of the persistent properties available to the JavaScript as window.locexample.Lat and window.locxample.Lon, and also supplies three of the MediaWiki system messages which can be accessed by mw.config.get('userlogin') for example.

Note that using mw.config is more efficient, so only use a custom object name if you need to use the object for storing more than just the automatically added instance properties, or if you need to adjust some of the values in the JavaScript at run-time. Since the messages are read-only they're always accessed via mw.config regardless of the setting of $jsObj.

Main ArticleProperties methods

getClass

This returns (and if necessary declares) an ArticleProperties sub-class associated with the passed title. It determines the class name by calling the ArticlePropertiesClassFromTitle hook. The returned class name could then be used to create an instance as in the following example.

$class = ArticleProperties::getClass( $title );
$obj = new $class( $title );

Note that the Article::newFromTitle method should not be used because this will be regarded as a non-passive instantiation and may have global side-effects such as skin changes etc depending on the nature of the sub-class being instantiated.

properties

The central method of the ArticleProperties class is the properties method, which is the basic interface to an article's persistent properties. It takes just one parameter which is an hash of properties to read or write. The hash key is the property name, and the value is either null which indicates a request for the value to be read from the database, or anything else which indicates a value to write to the database. In this way a number of properties can be read and written with a single function call. If no parameter is passed at all, or the array is empty, then all the current properties will be returned.

dbGetValue

This properties method above sends any value retrieved form the database via this method first. This method takes a property name and value as parameters and by default simply returns the value unchanged, but its presence allows sub-classes to process the values that are retrieved from the database by the properties method. This is useful for exotic types of properties such as dates, currencies, lists or objects that require some kind of standard formatting or de-serialisation after the read from the database as plain-text.

dbSetValue

This is the corresponding method to dbGetValue but does the processing before the data is stored such as "unformatting" or serialising. Note that dbGetValue and dbSetValue are internal methods that should only ever be called by the ArticleProperties::properties method under normal circumstances. Their purpose is to be overridden by the sub-classes, but not directly called by those sub-classes.

getValue

This is a simpler method than properties to call if the value of just one property is wanted, the name of the property is passed and its current value is returned.

setValue

This is a simpler method than properties to call if the value of just one property needs to be updated the name of the property is passed as the first parameter, and its new value as the second parameter.

updatePropertiesFromRequest

Many classes of page based on ArticleProperties will need to allow the wiki save function to some of the properties. Many of these values will come from the $wgRequest object after a form in the edit view of the page has been submitted. This method allows takes a single parameter listing the names of properties to be extracted from the form submission request and stored into the properties. The method expects the form's values to have the same names as their corresponding property names but with the convention of a preceding wp.

This method returns an integer value indicating the number of properties that were changed.

query

Most of the database queries done that involve an article's properties are a request of a matching list of pages (instances of ArticleProperties sub-classes) which can all be referred to by a normal Title instance. This method allows such queries to be simplified by accepting as parameters just the name of the ArticleProperties sub-class that the resulting titles should be of, an optional array of the SQL conditions for the query and an optional array of the SQL options for the query. The result is an array of Title objects referring to the pages that match the query, or an empty array if there were no matches.

Here's an example of performing a query on the above example class, it prints the latitude and longitude properties for each item in the city "new York":

foreach( ArticleProperties::query( 'LocationExample', array( "example_city = 'New York'" ) ) as $title ) {
	$article = new LocationExample( $title );
	$lat = $article->getValue( 'Lat' );
	$lon = $article->getValue( 'Lon' );
	$wgOut->addHTML( "Location: $lat, $lon" );
}

This example shows a new ArticleProperties sub-class (of the class LocationExample) called $article being created for each of the resulting Title objects. The called to the query method defines the class of article that the resulting Title objects should refer to, and sends an array of SQL conditions that the articles properties must match, in this case just the single condition that the City property should be "New York".

  • Note that the full SQL column name must be used in the conditions array ("example_city") rather than just the property name of "City".
  • Note also that $article is an example of a passive instantiation of an ArticleProperties sub-class, i.e. an instantiation that is not a page render so would not cause any global effects like skin or HTML-title changes etc.

HTML rendering helpers

table

One of the most common things that need to be done with a list of titles of ArticleProperties pages such as is returned by the ArticleProperties::query method is to format selected properties of the results as an HTML table to be sent to the client. This method does this job and takes three parameters, first the array of Title objects, second an optional array of HTML attributes that the table should have, and third an optional list of property names required for the table columns (all are used if no list is supplied).

label

input

inputRow

select

options

textarea

Hooks

This section is a description of the MediaWiki hooks that the extension creates and uses.

ArticlePropertiesChanged

The extension executes this hook whenever the properties of an article are updated. This allows events to be tied in with specific properties, or the property values to be reflected in the article content in some way. One simple use of the hook is to maintain the current property values in the wikitext in standard template syntax so they can be rendered in-wiki and their changes included in the revision history for auditing purposes.

public static function onArticlePropertiesChanged( $article, &$change ) {
	$summary = 'Article properties updated: ' . join( ', ', array_keys( $change ) );
	$content = '{{' . get_class( $article ) . "\n";
	foreach( $article->properties() as $k => $v ) $content .= " | $k = $v\n";
	$content .= "\n}}";
	$article->doEdit( $content, $summary, EDIT_UPDATE );
	return true;
}

ArticlePropertiesClassFromTitle

This hook is called by the extension in order that any articles being created from a title can determine what specific ArticleProperties sub-class they should be instantiated as.

ArticleFromTitle

The ArticleFromTitle hook is called from Article::newFromTitle() which in a normal page render is called from MediaWiki::initializeArticle(). This method's job is to execute another new hook called ArticlePropertiesClassFromTitle which the sub-classes or their management environment adds a callback function which determines based on the Title object which sub-class the new Article::ArticleProperties instance should be created as. For example, the specific application may require certain namespaces, categories or evens specific pages to have their own classes.

EditFormPreloadText

Many ArticleProperties classes of page do not use the normal MediaWiki article text at all, but without a textual revision initially being created, no MediaWiki page will be created in the database for the persistent properties to be associated with. So the EditFormPreloadText hook allows a newly created ArticleProperties instance to have some default content added in the background to ensure the existence of a page in the database. This means that Title instances can refer to pages that are pure property collections with no textual wiki-page aspect to them.

EditPage::showEditForm:fields

The EditPage::showEditForm:fields hook allows ArticleProperties sub-classes to add inputs to the article's edit form so the user can interact with the article properties. The sub-class returns the HTML of the new elements in its edit method.

ArticleSaveComplete

The ArticleSaveComplete hook is used to allow ArticleProperties sub-classes to update any properties that have changed after a page edit of one of its instances has been submitted. This hooks calls the sub-classes save method. This hook must ensure that the save method is only called once in case they in turn do more article-edits programatically which would in turn call the ArticleSaveComplete hook again.

ArticleDeleteComplete

The ArticleDeleteComplete hook is used to clean up any rows in the properties tables that have become redundant due to their page being deleted.

Persistent user messages

Another useful website feature is the ability to queue user messages such as for notification of errors or successful completion of operations. This can be more complicated than it seems because often such messages occur during a form submission which then redirects somewhere else. Rather than come up with a complicated method of encoding the message into the query-string of the redirect which needs to be dealt with specifically in every case, a general persistent message-queue solution is better.

This feature will likely not be incorporated into the ArticleProperties extension because it's not quite related enough to the properties mechanism to justify it, but I've included two static methods here which can be used in an appropriate class of a website project to achieve this persistent messaging idea. These methods use the user's properties to store the message queue which is the same mechanism used to store user preferences.

This first method is called whenever some part of the project needs to display a message for the user. The first parameter is the message content, and the second is an optional message type which can be used by the rendering code to include appropriate CSS classes to the message container. The method appends the type and message data to any current messages in the user's queue.

static function message( $msg, $type = 'success' ) {
	global $wgUser;
	$opt = 'user-messages';
	$wgUser->setOption( $opt, $wgUser->getOption( $opt, '' ) . "||$type|$msg" );
	$wgUser->saveSettings();
}


This second method is called by the rendering method which would usually be placed in a late hook or directly into the skin template code. An array of the messages of each type is returned. If one type has accumulated more than one message they are simply concatenated. Each individual message is placed inside a paragraph element. All the messages are removed from the user's queue when this method is called so that they're only displayed once.

static function popMessages() {
	global $wgUser;
	$opt = 'user-messages';
	$messages = array();
	foreach( explode( '||', $wgUser->getOption( $opt, '' ) ) as $msg ) {
		if( $msg ) {
			list( $type, $msg ) = explode( '|', $msg );
			$msg = "<p>$msg</p>";
			if( array_key_exists( $type, $messages ) ) $messages[$type] .= $msg;
			else $messages[$type] = $msg;
		}
	}
	$wgUser->setOption( $opt, '' );
	$wgUser->saveSettings();
	return $messages;
}

TemplateProperties extension

Another extension which is still in development called TemplateProperties extends the ArticleProperties concept by tying in to the events of a specified list of templates (similar to the concept of "record" articles in RecordAdmin). It adds events so that the named parameters in these specified templates are synchronised with the article's properties.

The extension also adds three parser-functions to allow access to the property information from within article text. The three parser-functions allow for the retrieval of a single property, a query resulting in a simple list, and a query resulting in a sortable table respectively.

These queries retrieve their data from the article properties so that no text parsing is necessary. But if any value being requested doesn't exist in the page_props database table, then it is extracted from the text (if it exists in the text) and added to the table before being returned.

When the template values are changed by an article's text being edited, the new values are extracted (or possibly obtained by hooking in to the parser's information on the current values, and the page_prop table updated.

It would also make sense for this extension replace RecordAdmin by making it add fields to the edit form for editing the article properties and removing the template syntax from the article.

See also