最佳实践
以下是一些帮助组织代码的最佳实践,使其能够与 WordPress 核心和其他 WordPress 插件一起良好运行。
避免命名冲突
当您的插件对变量、函数或类使用与另一个插件相同的名称时,就会发生命名冲突。
幸运的是,您可以使用以下方法来避免命名冲突。
程序编码方法
默认情况下,所有变量、函数和类都在全局命名空间中定义,这意味着您的插件可以覆盖另一个插件设置的变量、函数和类,反之亦然。在函数或类内部定义的变量不受此影响。
给所有东西加上前缀
所有全局可访问的代码都应以唯一标识符作为前缀。前缀可以防止其他插件覆盖您的变量并意外调用您的函数和类。它还会阻止您做同样的事情。
为了防止与其他插件冲突,您的前缀长度应至少为 5 个字母。您应该避免使用常见的英语单词,而应选择您的插件特有的单词。
应加前缀的代码包括:
- 函数(除非命名空间)
- 类、接口和特征(除非命名空间)
- 命名空间
- 全局变量
- 选项和瞬态
检查现有实施
PHP 提供了许多函数来验证变量、函数、类和常量是否存在。如果实体存在,所有这些都将返回 true。
- 变量: isset() (包括数组、对象等)
- 函数: function_exists()
- 类: class_exists()
- 常量: defined()
例子:
// Create a function called "wporg_init" if it doesn't already exist
if ( ! function_exists( 'wporg_init' ) ) {
function wporg_init() {
register_setting( 'wporg_settings', 'wporg_option_foo' );
}
}
// Create a function called "wporg_get_foo" if it doesn't already exist
if ( ! function_exists( 'wporg_get_foo' ) ) {
function wporg_get_foo() {
return get_option( 'wporg_option_foo' );
}
}
面向对象的编程方法
解决命名冲突问题的一种更简单的方法是使用插件代码的类。
您仍然需要检查您想要的类的名称是否已被占用,但其余的将由 PHP 处理。
例子
if ( ! class_exists( 'WPOrg_Plugin' ) ) {
class WPOrg_Plugin {
public static function init() {
register_setting( 'wporg_settings', 'wporg_option_foo' );
}
public static function get_foo() {
return get_option( 'wporg_option_foo' );
}
}
WPOrg_Plugin::init();
WPOrg_Plugin::get_foo();
}
文件组织
插件目录的根级别应包含您的plugin-name.php
文件和(可选)您的uninstall.php文件。所有其他文件应尽可能组织到子文件夹中。
文件夹结构
清晰的文件夹结构可以帮助您和其他开发插件的人将相似的文件保存在一起。
以下是供参考的示例文件夹结构:
/plugin-name
plugin-name.php
uninstall.php
/languages
/includes
/admin
/js
/css
/images
/public
/js
/css
/images
插件架构
您为插件选择的架构或代码组织可能取决于插件的大小。
对于与 WordPress 核心、主题或其他插件交互有限的小型单一用途插件,设计复杂的类几乎没有什么好处;除非你知道这个插件以后会大大扩展。
对于包含大量代码的大型插件,请从类开始。单独的样式和脚本文件,甚至与构建相关的文件。这将有助于代码组织和插件的长期维护。
条件加载
将管理代码与公共代码分开很有帮助。使用条件式is_admin()。您仍必须执行功能检查,因为这并不表明用户已通过身份验证或具有管理员级别访问权限。请参阅检查用户能力。
例如:
if ( is_admin() ) {
// we are in admin mode
require_once __DIR__ . '/admin/plugin-name-admin.php';
}
架构模式
虽然有许多可能的架构模式,但它们大致可以分为三种变体:
单文件包含函数:
<?php
/**
* Move Floating Social Bar in Genesis
*
* @package Move_Floating_Social_Bar_In_Genesis
* @author Gary Jones <gary@garyjones.co.uk>
* @license GPL-2.0+
* @link https://github.com/GaryJones/move-floating-social-bar-in-genesis
* @copyright 2013 Gary Jones, Gamajo Tech
*
* @wordpress-plugin
* Plugin Name: Move Floating Social Bar in Genesis
* Plugin URI: https://github.com/GaryJones/move-floating-social-bar-in-genesis
* Description: Moves the Floating Social Bar plugin output from just inside the entry content to just before it.
* Version: 1.0.0
* Author: Gary Jones
* Author URI: https://github.com/GaryJones/move-floating-social-bar-in-genesis
* Text Domain: move-floating-social-bar-in-genesis
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* Domain Path: /languages
* GitHub Plugin URI: https://github.com/GaryJones/move-floating-social-bar-in-genesis
* GitHub Branch: master
*/
// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
die;
}
add_action( 'pre_get_posts', 'mfsbig_remove_floating_social_bar', 15 );
/**
* Remove Floating Social Bar from outputting at the top of the content.
*
* FSB adds these filters at pre_get_posts, priorty 10, so we'll remove them
* just after that.
*
* @since 1.0.0
*/
function mfsbig_remove_floating_social_bar() {
if ( class_exists( 'floating_social_bar' ) ) {
remove_filter( 'the_excerpt', array( floating_social_bar::get_instance(), 'fsb' ), apply_filters( 'fsb_social_bar_priority', 10 ) );
remove_filter( 'the_content', array( floating_social_bar::get_instance(), 'fsb' ), apply_filters( 'fsb_social_bar_priority', 10 ) );
}
}
add_action( 'genesis_before_post_content', 'mfsbig_add_floating_social_bar' ); // XHTML themes
add_action( 'genesis_before_entry_content', 'mfsbig_add_floating_social_bar' ); // HTML5 themes
/**
* Echo the Floating Social Bar in the right place, just before the entry
* content (after the header and entry meta) in Genesis child themes.
*
* As fsb() is really a function for filtering, it requires a single argument
* (the content or excerpt), so we fool it by passing in an empty string instead.
*
* @since 1.0.0
*/
function mfsbig_add_floating_social_bar() {
if ( class_exists( 'floating_social_bar' ) ) {
echo floating_social_bar::get_instance()->fsb('');
}
}
单个插件文件,包含类、实例化对象和可选函数
<?php
/*
Plugin Name: WP Comment Notes
Plugin URI: http://andrewnorcross.com/plugins/
Description: Add custom notes before or after the comment form.
Version: 1.0.0
Author: Andrew Norcross
Author URI: http://andrewnorcross.com
Copyright 2013 Andrew Norcross
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License, version 2, as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
if( !defined( 'WPCMN_VER' ) )
define( 'WPCMN_VER', '1.0.0' );
// Start up the engine
class WP_Comment_Notes
{
/**
* Static property to hold our singleton instance
*
*/
static $instance = false;
/**
* This is our constructor
*
* @return void
*/
private function __construct() {
// back end
add_action ( 'plugins_loaded', array( $this, 'textdomain' ) );
add_action ( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );
add_action ( 'do_meta_boxes', array( $this, 'create_metaboxes' ), 10, 2 );
add_action ( 'save_post', array( $this, 'save_custom_meta' ), 1 );
// front end
add_action ( 'wp_enqueue_scripts', array( $this, 'front_scripts' ), 10 );
add_filter ( 'comment_form_defaults', array( $this, 'custom_notes_filter' ) );
}
/**
* If an instance exists, this returns it. If not, it creates one and
* retuns it.
*
* @return WP_Comment_Notes
*/
public static function getInstance() {
if ( !self::$instance )
self::$instance = new self;
return self::$instance;
}
/**
* load textdomain
*
* @return void
*/
public function textdomain() {
load_plugin_textdomain( 'wpcmn', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
}
/**
* Admin styles
*
* @return void
*/
public function admin_scripts() {
$types = $this->get_post_types();
$screen = get_current_screen();
if ( in_array( $screen->post_type , $types ) ) :
wp_enqueue_style( 'wpcmn-admin', plugins_url('lib/css/admin.css', __FILE__), array(), WPCMN_VER, 'all' );
endif;
}
/**
* call metabox
*
* @return void
*/
public function create_metaboxes( $page, $context ) {
$types = $this->get_post_types();
if ( in_array( $page, $types ) )
add_meta_box( 'wp-comment-notes', __( 'Comment Notes', 'wpcmn' ), array( $this, 'wpcmn_notes_meta' ), $page, 'advanced', 'high' );
}
/**
* display meta fields for notes meta
*
* @return void
*/
public function wpcmn_notes_meta( $post ) {
// Use nonce for verification
wp_nonce_field( 'wpcmn_meta_nonce', 'wpcmn_meta_nonce' );
$post_id = $post->ID;
// get postmeta, and our initial settings
$notes = get_post_meta( $post_id, '_wpcmn_notes', true );
$before_text = isset( $notes['before-text'] ) ? $notes['before-text'] : '';
$before_type = isset( $notes['before-type'] ) ? $notes['before-type'] : 'wpcmn-notes-standard';
$after_text = isset( $notes['after-text'] ) ? $notes['after-text'] : '';
$after_type = isset( $notes['after-type'] ) ? $notes['after-type'] : 'wpcmn-notes-standard';
echo '<script type="text/javascript">jQuery(document).ready(function($){$("#comment_status").click(function(){$(".wpcmn-notes-table tr").toggle();})});</script>';
$disabled_display = comments_open( $post_id ) ? ' style="display:none;"' : '';
$enabled_display = ! comments_open( $post_id ) ? ' style="display:none;"' : '';
echo '<table class="form-table wpcmn-notes-table">';
echo '<tr class="wpcmn-notes-disabled"' . $disabled_display . '>';
echo '<th>' . __( 'Enable comments in order to use Comment Notes', 'wpcmn' ) . '</th>';
echo '</tr>';
echo '<tr class="wpcmn-notes-title"' . $enabled_display . '>';
echo '<td colspan="2"><h5>'.__( 'Before Notes Area', 'wpcmn' ) . '</h5></td>';
echo '</tr>';
echo '<tr class="wpcmn-notes-data wpcmn-notes-before-text"' . $enabled_display . '>';
echo '<th>'.__( 'Message Text', 'wpcmn' ) . '</th>';
echo '<td>';
echo '<textarea class="widefat" name="wpcmn-notes[before-text]" id="wpcmn-before">'.esc_attr( $before_text ) . '</textarea>';
echo '<p class="description">'.__( 'Note: This will not appear to users who are logged in.', 'wpcmn' ) . '</p>';
echo '</td>';
echo '</tr>';
echo '<tr class="wpcmn-notes-data wpcmn-notes-before-type"' . $enabled_display . '>';
echo '<th>'.__( 'Message Type', 'wpcmn' ) . '</th>';
echo '<td>';
echo '<select id="wpcmn-before-type" name="wpcmn-notes[before-type]">';
echo '<option value="wpcmn-notes-standard"' . selected( $before_type, 'wpcmn-notes-standard', false ) . '>' . __( 'Standard', 'wpcmn' ) . '</option>';
echo '<option value="wpcmn-notes-warning"' . selected( $before_type, 'wpcmn-notes-warning', false ) . '>' . __( 'Warning', 'wpcmn' ) . '</option>';
echo '<option value="wpcmn-notes-alert"' . selected( $before_type, 'wpcmn-notes-alert', false ) . '>' . __( 'Alert', 'wpcmn' ) . '</option>';
do_action( 'wpcmn_before_types', $before_type );
echo '</select>';
echo '</td>';
echo '</tr>';
echo '<tr class="wpcmn-notes-title"' . $enabled_display . '>';
echo '<td colspan="2"><h5>'.__( 'After Notes Area', 'wpcmn' ) . '</h5></td>';
echo '</tr>';
echo '<tr class="wpcmn-notes-data wpcmn-notes-after-text"' . $enabled_display . '>';
echo '<th>'.__( 'Message Text', 'wpcmn' ) . '</th>';
echo '<td>';
echo '<textarea class="widefat" name="wpcmn-notes[after-text]" id="wpcmn-after">'.esc_attr( $after_text ) . '</textarea>';
echo '</td>';
echo '</tr>';
echo '<tr class="wpcmn-notes-data wpcmn-notes-after-type"' . $enabled_display . '>';
echo '<th>'.__( 'Message Type', 'wpcmn' ) . '</th>';
echo '<td>';
echo '<select id="wpcmn-after-type" name="wpcmn-notes[after-type]">';
echo '<option value="wpcmn-notes-standard"' . selected( $after_type, 'wpcmn-notes-standard', false ) . '>' . __( 'Standard', 'wpcmn' ) . '</option>';
echo '<option value="wpcmn-notes-warning"' . selected( $after_type, 'wpcmn-notes-warning', false ) . '>' . __( 'Warning', 'wpcmn' ) . '</option>';
echo '<option value="wpcmn-notes-alert"' . selected( $after_type, 'wpcmn-notes-alert', false ) . '>' . __( 'Alert', 'wpcmn' ) . '</option>';
do_action( 'wpcmn_after_types', $after_type );
echo '</select>';
echo '</td>';
echo '</tr>';
echo '</table>';
}
/**
* save post metadata
*
* @return void
*/
public function save_custom_meta( $post_id ) {
// make sure we aren't using autosave
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
return $post_id;
// do our nonce check. ALWAYS A NONCE CHECK
if ( ! isset( $_POST['wpcmn_meta_nonce'] ) || ! wp_verify_nonce( $_POST['wpcmn_meta_nonce'], 'wpcmn_meta_nonce' ) )
return $post_id;
$types = $this->get_post_types();
if ( !in_array ( $_POST['post_type'], $types ) )
return $post_id;
// and make sure the user has the ability to do shit
if ( 'page' == $_POST['post_type'] ) {
if ( ! current_user_can( 'edit_page', $post_id ) ) {
return $post_id;
}
} else {
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return $post_id;
}
}
// all clear. get data via $_POST and store it
$notes = ! empty( $_POST['wpcmn-notes'] ) ? $_POST['wpcmn-notes'] : false;
// update side meta data
if ( $notes ) {
$allowed_html = array(
'a' => array(
'href' => array(),
'title' => array(),
'class' => array(),
'id' => array()
),
'br' => array(),
'em' => array(),
'strong' => array(),
'span' => array(
'class' => array(),
'id' => array()
)
);
update_post_meta( $post_id, '_wpcmn_notes', wp_kses( $notes, $allowed_html ) );
do_action( 'wpcmn_notes_save', $post_id, $notes );
} else {
delete_post_meta( $post_id, '_wpcmn_notes' );
}
}
/**
* call front-end CSS
*
* @return void
*/
public function front_scripts() {
// check for killswitch first
$killswitch = apply_filters( 'wpcmn_killswitch', false );
if ( $killswitch )
return false;
$types = $this->get_post_types();
if ( is_singular( $types ) )
wp_enqueue_style( 'wpcmn-notes', plugins_url( 'lib/css/wpcmn-notes.css', __FILE__ ), array(), WPCMN_VER, 'all' );
}
/**
* The actual filter for adding the notes.
*
* @return array
*/
public function custom_notes_filter( $fields ) {
global $post;
// get the possible meta fields
$notes = get_post_meta( $post->ID, '_wpcmn_notes', true );
if ( empty( $notes ) )
return $fields;
if ( isset( $notes['before-text'] ) ) :
// grab the variables
$text = $notes['before-text'];
$class = isset( $notes['before-type'] ) ? $notes['before-type'] : 'wpcmn-notes-standard';
// build the string
$before = '<p class="wpcmn-notes wpcmn-notes-before' . esc_attr( $class ) . '">' . $text . '</p>';
// output
$fields['comment_notes_before'] = $before;
endif;
if ( isset( $notes['after-text'] ) ) :
// grab the variables
$text = $notes['after-text'];
$class = isset( $notes['after-type'] ) ? $notes['after-type'] : 'wpcmn-notes-standard';
// build the string
$after = '<p class="wpcmn-notes wpcmn-notes-after' . esc_attr( $class ) . '">' . $text . '</p>';
// output
$fields['comment_notes_after'] = $after;
endif;
return $fields;
}
/**
* Return the list of post types that support Comment Notes
*
* @return array
*/
public function get_post_types() {
$types = get_post_types( array( 'public' => true, 'show_ui' => true ) );
foreach( $types as $type ) {
if( ! post_type_supports( $type, 'comments' ) ) {
unset( $types[ $type ] );
}
}
return apply_filters( 'wpcmn_type_support', $types );
}
/// end class
}
// Instantiate our class
$WP_Comment_Notes = WP_Comment_Notes::getInstance();
主插件文件,然后是一个或多个类文件
https://github.com/DevinVinson/WordPress-Plugin-Boilerplate
架构模式解释
上述更复杂的代码组织的具体实现已经写成教程和幻灯片:
样板起点
您可能希望从样板文件开始,而不是从头开始编写您编写的每个新插件。使用样板的优点之一是在您自己的插件之间保持一致性。如果您使用其他人已经熟悉的样板文件,样板文件还可以让其他人更轻松地为您的代码做出贡献。
这些也可以作为不同但可比较的架构的进一步示例。
- WordPress 插件样板:WordPress 插件开发的基础,旨在为构建插件提供清晰一致的指南。
- WordPress Plugin Bootstrap:使用 Grunt、Compass、GIT 和 SVN 开发 WordPress 插件的基本引导程序。
- WP Skeleton Plugin:专注于单元测试和使用 Composer 进行开发的骨架插件。
- WP CLI Scaffold:WP CLI 的 Scaffold 命令创建一个骨架插件,其中包含 CI 配置文件等选项
当然,您可以利用这些和其他方面的不同方面来创建您自己的自定义样板。