WordPress Backend Design Study – An Alternative to MP6?

Our designer and usability expert Nick at marketpress.com did a design study on the coming WordPress backend design MP6 for version 3.7 and tried to analyse what can be done better and how!

wpbackendhigh

Also interesting question he asks:

The dashboard is the least used and yet most needed page in the back-end. Why?

More screens and his full analysis can be read on at MarketPress.com

What's your opinion?

WordPress Dropins

Plugins are pretty popular, since it is easy to adjust your blog or website to your liking with Plugins. Less popular are Dropins.

WordPress alows to replace some functions with Dropins. The list of possibilities isn't big and not well documented. But I like to use Dropins to enhance the possibilties of caching, especially APC and Memcache. But also to adjust uploads, permalinks and similar things are a typical scenario for Dropins.

Most of the Plugins are in the WP Content Folder, normally wp-content or via constant WP_CONTENT_DIR or the URL in WP_CONTENT_URL is defined.
Example:

// Custom content directory
define( 'WP_CONTENT_DIR',  dirname( __FILE__ ) . '/wp-content' );
define( 'WP_CONTENT_URL',  'http://' . $_SERVER['HTTP_HOST'] . '/wp-content' );

I use this in my dev environment or at my clients project for example, since I can access Plugins via FTP in different ways. See my WordPress Starter Repo on Github for more solutions and hints about this topic.

Only Dropins, which get loaded with a language key, aren't in this folder, instead they are in /wp-content/languages. Alternatively you can define the folder via the constant WP_LANG_DIR.
The Dropin for the German key de_DE takes that path. See the repo for read the source or find more information.

In Backend are Dropins listed since version 3.0, but only the following in the table and not the Dropin to the language key. Also you can not just deactivate or activate it via backend, that's why Plugins are preferred for some solutions.

Below is a table with all Dropins I know or I have found. The possible Dropins are echoing via the function _get_dropins() .

File Type of Dropin Active since Context
advanced-cache.php Advanced or Alternative Cache Constant WP_CACHE is TRUE Single Installation
db.php Own database class Always Single installation
db-error.php Own database error alert At DB Error Single installation
install.php Own installation routine At installation Single installation
maintenance.php Own maintenance message At updates, maintenance Single installation
object-cache.php External Object Cache Always Single installation
sunrise.php Before the WP Multisite Installation gets loaded If constant SUNRISE set Multisite installation
blog-deleted.php Own message, when a blog gets deleted If a blog gets deleted Multisite installation
blog-inactive.php Own message, if a blog is set inactive If a blog becomes inactive Multisite installation
blog-suspended.php Own message, if a blog got suspended When a blog gets marked as archive or spam Multisite installation
$locale.php functions at active language key When language key set in constant WP_LANG Single & multisite installation

Update: see the current solution for a custom maintenance mode on the post from Christopher.

Hide Welcome Panel for WordPress Multisite

With a major release update of WordPress, everytime all new functions will be displayed after updating. Basically a good idea, which displays all new features on one page. However, this is not always intentional. In the corporate environment, large groups of users aren't interested in it, so that this screen only provides additional work for the support team.
WordPress has implemented an option for every user. Therefore, a small function that suppresses the info screen and adjust the user settings, so this screen won't appear.

Continue reading …

artikelbild

WP_List_Table – a step by step guide

Throughout WordPress the class WP_List_Table is used to display data, e.g. users, plugins, comments, or posts. The class contains almost all necessary methods for displaying, sorting, paginating, and searching data and and what is more obvious than to use it for your own plugins?

This article tries to give you a comprehensive walk-through all necessary steps to create a table tailored to your needs.

  1. Preliminary work
  2. Basics
  3. Sorting
  4. Actions
  5. Bulk actions
  6. Pagination
  7. Searching
  8. Screen options
  9. Styling the table
  10. Other customizations
  11. Weblinks

Preliminary work

For testing purposes we create a small plugin which adds a menu item:

<?php
/*
Plugin Name: My List Table Example
*/
?>
<div class="wrap">
<div id="icon-users" class="icon32"></div>
<h2>My List Table Test/h2>
</div>
<?php

Basics

For a start we're creating a list table with only the basic functionality. First we have to make sure that the necessary class is available since the WP_List_Table isn't loaded automatically:

if( ! class_exists( 'WP_List_Table' ) ) {
    require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
}

To create a table to your needs you have to derive a class from WP_List_Table:

class My_List_Table extends WP_List_Table {
}

$myListTable = new My__List_Table();

For demonstration purposes we create some sample data. Usually this data would be read from the database:

var $example_data = array(
  array('ID' => 1,'booktitle' => 'Quarter Share', 'author' => 'Nathan Lowell',
        'isbn' => '978-0982514542'),
  array('ID' => 2, 'booktitle' => '7th Son: Descent','author' => 'J. C. Hutchins',
        'isbn' => '0312384378'),
  array('ID' => 3, 'booktitle' => 'Shadowmagic', 'author' => 'John Lenahan',
        'isbn' => '978-1905548927'),
  array('ID' => 4, 'booktitle' => 'The Crown Conspiracy', 'author' => 'Michael J. Sullivan',
        'isbn' => '978-0979621130'),
  array('ID' => 5, 'booktitle'     => 'Max Quick: The Pocket and the Pendant', 'author'    => 'Mark Jeffrey',
        'isbn' => '978-0061988929'),
  array('ID' => 6, 'booktitle' => 'Jack Wakes Up: A Novel', 'author' => 'Seth Harwood',
        'isbn' => '978-0307454355')
);

Before we can display the data in the table we have to define some methods and variables:

function get_columns(){
  $columns = array(
    'booktitle' => 'Title',
    'author'    => 'Author',
    'isbn'      => 'ISBN'
  );
  return $columns;
}

function prepare_items() {
  $columns = $this->get_columns();
  $hidden = array();
  $sortable = array();
  $this->_column_headers = array($columns, $hidden, $sortable);
  $this->items = $this->example_data;;
}

The method get_columns() is needed to label the columns on the top and bottom of the table. The keys in the array have to be the same as in the data array otherwise the respective columns aren't displayed.

prepare_items defines two arrays controlling the behaviour of the table:

  • $hidden defines the hidden columns (see Screen Options),
  • $sortable defines if the table can be sorted by this column.

Finally the method assigns the example data to the class' data representation variable items.

Before actually displaying each column WordPress looks for methods called column_{key_name}, e.g. function column_booktitle. There has to be such a method for every defined column. To avoid the need to create a method for each column there is column_default that will process any column for which no special method is defined:

function column_default( $item, $column_name ) {
  switch( $column_name ) { 
    case 'booktitle':
    case 'author':
    case 'isbn':
      return $item[ $column_name ];
    default:
      return print_r( $item, true ) ; //Show the whole array for troubleshooting purposes
  }
}

In our example the method will return the title for every column and if the column is not found it displays the content of the $item array for debugging purposes.

These are the essential ingredients to define a custom list table class. All you have to do now is to add an admin page to the backend, create an instance of our class, prepare the items and call display() to actually display the table:

function my_add_menu_items(){
    add_menu_page( 'My Plugin List Table', 'My List Table Example', 'activate_plugins', 'my_list_test', 'my_render_list_page' );
}
add_action( 'admin_menu', 'my_add_menu_items' );

function my_render_list_page(){
  $myListTable = new My_Example_List_Table();
  echo '<div class="wrap"><h2>My List Table Test</h2>'; 
  $myListTable->prepare_items(); 
  $myListTable->display(); 
  echo '</div>'; 
}

This is the minimal version of a WP_List_Table possible:


Download minimal WP_List_Table example (gist)

Sorting

At the moment the items appear in the order they are defined in the code since the WP_List_Table class does not contain any code for sorting. What it does contain is some code to mark certain columns as sortable. In section "Basics" there already was a line $sortable = array(); which now will be changed to:

$sortable = $this->get_sortable_columns();

Additionally we need the method:

function get_sortable_columns() {
  $sortable_columns = array(
    'booktitle'  => array('booktitle',false),
    'author' => array('author',false),
    'isbn'   => array('isbn',false)
  );
  return $sortable_columns;
}

This way the above mentioned column headers are changed to links and display small triangles if the mouse hovers over them. The second parameter in the value array of $sortable_columns takes care of a possible pre-ordered column. If the value is true the column is assumed to be ordered ascending, if the value is false the column is assumed descending or unordered. This is needed for the small triangle beside the column name indicating the sort order to show in the correct direction:

If you click on the column header the page is reloaded and $_GET contains something like this:

array
  'page' => string 'my_list_test' (length=12)
  'orderby' => string 'booktitle' (length=5)
  'order' => string 'asc' (length=3)

With this information you can write a method for sorting our example data:

function usort_reorder( $a, $b ) {
  // If no sort, default to title
  $orderby = ( ! empty( $_GET['orderby'] ) ) ? $_GET['orderby'] : 'booktitle';
  // If no order, default to asc
  $order = ( ! empty($_GET['order'] ) ) ? $_GET['order'] : 'asc';
  // Determine sort order
  $result = strcmp( $a[$orderby], $b[$orderby] );
  // Send final sort direction to usort
  return ( $order === 'asc' ) ? $result : -$result;
}

To actually sort the data we have to extend prepare_items():

function prepare_items() {
  [..]
  usort( $this->example_data, array( &$this, 'usort_reorder' ) );
  $this->items = $this->example_data;
}

If you're retrieving the data from the database (which is most likely) it's of course best to use SQL's ORDERBY directly.

Actions

If you not only want to display the items but also want to manipulate them you have to define some actions:

function column_booktitle($item) {
  $actions = array(
            'edit'      => sprintf('<a href="?page=%s&action=%s&book=%s">Edit</a>',$_REQUEST['page'],'edit',$item['ID']),
            'delete'    => sprintf('<a href="?page=%s&action=%s&book=%s">Delete</a>',$_REQUEST['page'],'delete',$item['ID']),
        );

  return sprintf('%1$s %2$s', $item['booktitle'], $this->row_actions($actions) );
}

These actions will appear if the user hovers the mouse cursor over the table:

If you click on one of the action links the form will return for example the following data in $_GET:

array
  'page' => string 'my_list_test' (length=12)
  'action' => string 'delete' (length=6)
  'book' => string '2' (length=1)

Bulk actions

Bulk action are implemented by overwriting the method get_bulk_actions() and returning an associated array:

function get_bulk_actions() {
  $actions = array(
    'delete'    => 'Delete'
  );
  return $actions;
}

This only puts the dropdown menu and the apply button above and below the table:

The checkboxes for the rows have to be defined separately. As mentioned above there is a method column_{column} for rendering a column. The cb-column is a special case:

function column_cb($item) {
        return sprintf(
            '<input type="checkbox" name="book[]" value="%s" />', $item['ID']
        );    
    }

This method currently will not be processed because we have to tell the class about the new column by extending the method get_columns():

function get_columns() {
  $columns = array(
    'cb'        => '<input type="checkbox" />',
[..]
}

This will also put the "select all" checkbox in the title bar:

If you don't want to display the checkbox in the title you simply set the value to an empty string. Nevertheless you still have to define the key/value pair otherwise no checkboxes are shown at all:

If "Apply" is pressed the form will return various variables: action and action2 contain the selected action or -1 if the user chose no action, and if any checkbox was selected the marked rows, in our case books, for example:

'action' => string 'delete' (length=6)
'book' => 
  array
    0 => string '2' (length=1)
    1 => string '6' (length=1)
'action2' => string '-1' (length=2)

action contains the selection from the upper select box, action2 the selection from the lower select box, and book the id of the selected rows, if any. You can use the method current_action() to query action/action2:

$action = $this->current_action();

It will return action if it's set, otherwise action2. If nothing is set the method returns FALSE.

Pagination

First things first: WordPress does not paginate your data in any way. It only contains a method to display a navigation bar on the top and bottom right of the table:

You have to tell the method how many items you have in total, how many items shall be displayed on a page, and most important, the data to be displayed on the page:

function prepare_items() {
  [...]
  $per_page = 5;
  $current_page = $this->get_pagenum();
  $total_items = count($this->example_data);

  // only ncessary because we have sample data
  $this->found_data = array_slice($this->example_data,(($current_page-1)*$per_page),$per_page);

  $this->set_pagination_args( array(
    'total_items' => $total_items,                  //WE have to calculate the total number of items
    'per_page'    => $per_page                     //WE have to determine how many items to show on a page
  ) );
  $this->items = $this->found_data;
}

As pointed out in the comment the array_slice is only necessary because we use sample data. If you're retrieving the data from a database you only need to load the necessary data by using SQL's LIMIT.

Searching

If you have a huge amount of data a search field will simplify accessing certain items:

$myListTable->search_box('search', 'search_id');

The button text search is defined by the first parameter, the id of the input by the second parameter. The method creates the following output:

<p class="search-box">
<label class="screen-reader-text" for="search_id-search-input">
search:</label> 
<input id="search_id-search-input" type="text" name="s" value="" /> 
<input id="search-submit" class="button" type="submit" name="" value="search" />
</p>

The method will place the input field and the search button on the right side and style it correctly. The <form> element is not generated. You have to add it manually, in our case this would be:

<form method="post">
  <input type="hidden" name="page" value="my_list_test" />
  <?php $this->search_box('search', 'search_id'); ?>
</form>

(The hidden element is needed to load the right page.)
To react to the search command you need to check the content of $_POST['s'] and filter your data accordingly before displaying the table.

Screen options

All core backend pages containing a WP_List_Table provide a "Screen Options" slide-in where the user can adjust the columns to be shown and the number of rows to be displayed.
To add options to your plugin you need to change your current code. First you have to make sure that the screen options are displayed only on the current page:

$hook = add_menu_page('My Plugin List Table', 'My List Table Example', 'activate_plugins', 'my_list_test', 'my_render_list_page');
add_action( "load-$hook", 'add_options' );

function add_options() {
  $option = 'per_page';
  $args = array(
         'label' => 'Books',
         'default' => 10,
         'option' => 'books_per_page'
         );
  add_screen_option( $option, $args );
}

This only displays the option field and apply button, saving and loading the data has to be defined separately. WordPress provides a filter called set-screen-option to take care of this:

add_filter('set-screen-option', 'test_table_set_option', 10, 3);
function test_table_set_option($status, $option, $value) {
  return $value;
}

The option is stored in the table usermeta in the database so each user has his own setting. To retrieve the option and adjust the table display accordingly the method prepare_items has to be altered (excerpt):

function prepare_items() {
[..]

  //paging
  $per_page = $this->get_items_per_page('books_per_page', 5);
  $current_page = $this->get_pagenum();

  [...]

Instead of simply assigning a number the user specified value is loaded. If the user hasn't changed the value there is no such option stored in the database and a default value is taken.

Adding the checkboxes for hiding/showing the columns is done by WordPress automatically. You just have to make sure that your derived class is instantiated before the screen option panel is rendered so that the parent class can retrieve the column names. To accomplish this the corresponding code is moved into the method add_options():

function add_options() {
    global $myListTable;
 
    $option = 'per_page';
    $args = array(
        'label' => 'Books',
        'default' => 10,
        'option' => 'books_per_page'
    );
    add_screen_option( $option, $args );

    $myListTable = new My_Example_List_Table;
}

The user's selections are automatically saved via Ajax functions. Nevertheless you have take care by yourself that the columns are hidden if the page is loaded initially. The method get_column_info() returns all, the hidden and the sortable columns. In the method prepare_items() instead of

$columns = $this->get_columns();
$hidden = array();
$sortable = $this->get_sortable_columns();
$this->_column_headers = array($columns, $hidden, $sortable);

it's now

$this->_column_headers = $this->get_column_info();

and the columns are set according to the screen options.

Annotation: you should avoid some strings as keynames since they are treated by WordPress specially:

$special = array('_title', 'cb', 'comment', 'media', 'name', 'title', 'username', 'blogname');

Your table would still work, but you won't be able to show/hide the columns.

Styling the table

Currently the table is styled to the WordPress defaults. To change this you have to adapt the CSS classes which are automatically assigned to each column. The class name consists of the string "column-" and the key name of the $columns array, e.g. "column-isbn" or "column-author". As an example the width of the columns will be redefined (for simplicity the style data is written directly into the HTML header):

function _construct() {
  [...]
  add_action( 'admin_head', array( &$this, 'admin_header' ) );
  [...]
}

function admin_header() {
  $page = ( isset($_GET['page'] ) ) ? esc_attr( $_GET['page'] ) : false;
  if( 'my_list_test' != $page )
    return; 

  echo '<style type="text/css">';
  echo '.wp-list-table .column-id { width: 5%; }';
  echo '.wp-list-table .column-booktitle { width: 40%; }';
  echo '.wp-list-table .column-author { width: 35%; }';
  echo '.wp-list-table .column-isbn { width: 20%; }';
  echo '</style>';
}

Other customizations

If there are no items in the list the standard message is "No items found." is displayed. If you want to change this message you can overwrite the method no_items():

function no_items() {
  _e( 'No books found, dude.' );
}

Download complete WP_List_Table example (gist) or see the Gist

WordPress-Christmas-2010-21

Register Settings on WordPress Multisite

The use of WordPress for several blogs in the network can be useful to simplify several steps and is becoming increasingly popular. Whether you want the classical scenarios of blog hosting service or like to create multilingual websites or other ideas. Therefore, it is also important for plugin developers to use the functions and to expand or develop their own Plugins for it specifically.

Much is the same, but not all, and in this small article I would like to briefly explain how you set settings in the database when you activate a Plugin. Continue reading …