Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:25:50 +08:00
commit 73ee9697a5
34 changed files with 8885 additions and 0 deletions

View File

@@ -0,0 +1,337 @@
<?php
/**
* Example: AJAX handlers
*
* This example shows how to:
* - Register AJAX handlers for logged-in and non-logged-in users
* - Verify nonces for CSRF protection
* - Check user capabilities
* - Sanitize input and return JSON responses
* - Enqueue scripts with localized data
*
* @package YourPlugin
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Enqueue AJAX script
*/
function yourprefix_enqueue_ajax_script() {
wp_enqueue_script(
'yourprefix-ajax',
plugins_url( 'js/ajax-example.js', __FILE__ ),
array( 'jquery' ),
'1.0.0',
true
);
// Localize script with AJAX URL and nonce
wp_localize_script(
'yourprefix-ajax',
'yourprefixAjax',
array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'yourprefix_ajax_nonce' ),
)
);
}
add_action( 'wp_enqueue_scripts', 'yourprefix_enqueue_ajax_script' );
/**
* AJAX handler for logged-in users
*/
function yourprefix_ajax_save_data() {
// Verify nonce
check_ajax_referer( 'yourprefix_ajax_nonce', 'nonce' );
// Check user capability
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array(
'message' => __( 'Permission denied', 'your-plugin' ),
) );
}
// Get and sanitize input
$name = isset( $_POST['name'] ) ? sanitize_text_field( $_POST['name'] ) : '';
$email = isset( $_POST['email'] ) ? sanitize_email( $_POST['email'] ) : '';
$age = isset( $_POST['age'] ) ? absint( $_POST['age'] ) : 0;
// Validate input
if ( empty( $name ) ) {
wp_send_json_error( array(
'message' => __( 'Name is required', 'your-plugin' ),
) );
}
if ( ! is_email( $email ) ) {
wp_send_json_error( array(
'message' => __( 'Invalid email address', 'your-plugin' ),
) );
}
// Process data (example: save to database)
$result = yourprefix_save_user_data( $name, $email, $age );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array(
'message' => $result->get_error_message(),
) );
}
// Return success response
wp_send_json_success( array(
'message' => __( 'Data saved successfully', 'your-plugin' ),
'data' => array(
'name' => $name,
'email' => $email,
'age' => $age,
),
) );
}
add_action( 'wp_ajax_yourprefix_save_data', 'yourprefix_ajax_save_data' );
/**
* AJAX handler for non-logged-in users
*/
function yourprefix_ajax_public_action() {
// Verify nonce (still required for public AJAX)
check_ajax_referer( 'yourprefix_ajax_nonce', 'nonce' );
// Get and sanitize input
$query = isset( $_POST['query'] ) ? sanitize_text_field( $_POST['query'] ) : '';
if ( empty( $query ) ) {
wp_send_json_error( array(
'message' => __( 'Query is required', 'your-plugin' ),
) );
}
// Process query (example: search posts)
$results = yourprefix_search_posts( $query );
wp_send_json_success( array(
'message' => __( 'Search completed', 'your-plugin' ),
'results' => $results,
) );
}
add_action( 'wp_ajax_yourprefix_public_action', 'yourprefix_ajax_public_action' );
add_action( 'wp_ajax_nopriv_yourprefix_public_action', 'yourprefix_ajax_public_action' );
/**
* AJAX handler for fetching posts
*/
function yourprefix_ajax_load_posts() {
// Verify nonce
check_ajax_referer( 'yourprefix_ajax_nonce', 'nonce' );
// Get parameters
$page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
$per_page = isset( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 10;
$category = isset( $_POST['category'] ) ? absint( $_POST['category'] ) : 0;
// Query posts
$args = array(
'post_type' => 'post',
'posts_per_page' => $per_page,
'paged' => $page,
'post_status' => 'publish',
);
if ( $category > 0 ) {
$args['cat'] = $category;
}
$query = new WP_Query( $args );
$posts = array();
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
$posts[] = array(
'id' => get_the_ID(),
'title' => get_the_title(),
'excerpt' => get_the_excerpt(),
'url' => get_permalink(),
'date' => get_the_date(),
);
}
wp_reset_postdata();
}
wp_send_json_success( array(
'posts' => $posts,
'total_pages' => $query->max_num_pages,
'found_posts' => $query->found_posts,
) );
}
add_action( 'wp_ajax_yourprefix_load_posts', 'yourprefix_ajax_load_posts' );
add_action( 'wp_ajax_nopriv_yourprefix_load_posts', 'yourprefix_ajax_load_posts' );
/**
* AJAX handler for deleting item
*/
function yourprefix_ajax_delete_item() {
// Verify nonce
check_ajax_referer( 'yourprefix_ajax_nonce', 'nonce' );
// Check user capability
if ( ! current_user_can( 'delete_posts' ) ) {
wp_send_json_error( array(
'message' => __( 'Permission denied', 'your-plugin' ),
) );
}
// Get item ID
$item_id = isset( $_POST['item_id'] ) ? absint( $_POST['item_id'] ) : 0;
if ( $item_id === 0 ) {
wp_send_json_error( array(
'message' => __( 'Invalid item ID', 'your-plugin' ),
) );
}
// Check if item exists
$post = get_post( $item_id );
if ( ! $post ) {
wp_send_json_error( array(
'message' => __( 'Item not found', 'your-plugin' ),
) );
}
// Delete item
$result = wp_trash_post( $item_id );
if ( ! $result ) {
wp_send_json_error( array(
'message' => __( 'Failed to delete item', 'your-plugin' ),
) );
}
wp_send_json_success( array(
'message' => __( 'Item deleted successfully', 'your-plugin' ),
'item_id' => $item_id,
) );
}
add_action( 'wp_ajax_yourprefix_delete_item', 'yourprefix_ajax_delete_item' );
/**
* AJAX handler for uploading file
*/
function yourprefix_ajax_upload_file() {
// Verify nonce
check_ajax_referer( 'yourprefix_ajax_nonce', 'nonce' );
// Check user capability
if ( ! current_user_can( 'upload_files' ) ) {
wp_send_json_error( array(
'message' => __( 'Permission denied', 'your-plugin' ),
) );
}
// Check if file was uploaded
if ( empty( $_FILES['file'] ) ) {
wp_send_json_error( array(
'message' => __( 'No file uploaded', 'your-plugin' ),
) );
}
// Handle file upload
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$file = $_FILES['file'];
// Validate file type
$allowed_types = array( 'image/jpeg', 'image/png', 'image/gif' );
if ( ! in_array( $file['type'], $allowed_types, true ) ) {
wp_send_json_error( array(
'message' => __( 'Invalid file type. Only JPG, PNG, and GIF are allowed.', 'your-plugin' ),
) );
}
// Upload file
$attachment_id = media_handle_upload( 'file', 0 );
if ( is_wp_error( $attachment_id ) ) {
wp_send_json_error( array(
'message' => $attachment_id->get_error_message(),
) );
}
// Get attachment data
$attachment_url = wp_get_attachment_url( $attachment_id );
wp_send_json_success( array(
'message' => __( 'File uploaded successfully', 'your-plugin' ),
'attachment_id' => $attachment_id,
'attachment_url' => $attachment_url,
) );
}
add_action( 'wp_ajax_yourprefix_upload_file', 'yourprefix_ajax_upload_file' );
/**
* Helper function: Save user data
*
* @param string $name User name.
* @param string $email User email.
* @param int $age User age.
* @return bool|WP_Error
*/
function yourprefix_save_user_data( $name, $email, $age ) {
global $wpdb;
$table_name = $wpdb->prefix . 'yourprefix_users';
$result = $wpdb->insert(
$table_name,
array(
'name' => $name,
'email' => $email,
'age' => $age,
),
array( '%s', '%s', '%d' )
);
if ( false === $result ) {
return new WP_Error( 'db_error', __( 'Database error', 'your-plugin' ) );
}
return true;
}
/**
* Helper function: Search posts
*
* @param string $query Search query.
* @return array
*/
function yourprefix_search_posts( $query ) {
$args = array(
'post_type' => 'post',
's' => $query,
'posts_per_page' => 10,
'post_status' => 'publish',
);
$search_query = new WP_Query( $args );
$results = array();
if ( $search_query->have_posts() ) {
while ( $search_query->have_posts() ) {
$search_query->the_post();
$results[] = array(
'id' => get_the_ID(),
'title' => get_the_title(),
'url' => get_permalink(),
);
}
wp_reset_postdata();
}
return $results;
}

View File

@@ -0,0 +1,274 @@
<?php
/**
* Example: Register custom post type and taxonomy
*
* This example shows how to:
* - Register a custom post type
* - Register a custom taxonomy
* - Link taxonomy to post type
* - Flush rewrite rules on activation
* - Use proper labels and arguments
*
* @package YourPlugin
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Register custom post type
*/
function yourprefix_register_book_post_type() {
$labels = array(
'name' => _x( 'Books', 'Post type general name', 'your-plugin' ),
'singular_name' => _x( 'Book', 'Post type singular name', 'your-plugin' ),
'menu_name' => _x( 'Books', 'Admin Menu text', 'your-plugin' ),
'name_admin_bar' => _x( 'Book', 'Add New on Toolbar', 'your-plugin' ),
'add_new' => __( 'Add New', 'your-plugin' ),
'add_new_item' => __( 'Add New Book', 'your-plugin' ),
'new_item' => __( 'New Book', 'your-plugin' ),
'edit_item' => __( 'Edit Book', 'your-plugin' ),
'view_item' => __( 'View Book', 'your-plugin' ),
'all_items' => __( 'All Books', 'your-plugin' ),
'search_items' => __( 'Search Books', 'your-plugin' ),
'parent_item_colon' => __( 'Parent Books:', 'your-plugin' ),
'not_found' => __( 'No books found.', 'your-plugin' ),
'not_found_in_trash' => __( 'No books found in Trash.', 'your-plugin' ),
'featured_image' => _x( 'Book Cover Image', 'Overrides the "Featured Image" phrase', 'your-plugin' ),
'set_featured_image' => _x( 'Set cover image', 'Overrides the "Set featured image" phrase', 'your-plugin' ),
'remove_featured_image' => _x( 'Remove cover image', 'Overrides the "Remove featured image" phrase', 'your-plugin' ),
'use_featured_image' => _x( 'Use as cover image', 'Overrides the "Use as featured image" phrase', 'your-plugin' ),
'archives' => _x( 'Book archives', 'The post type archive label', 'your-plugin' ),
'insert_into_item' => _x( 'Insert into book', 'Overrides the "Insert into post"/"Insert into page" phrase', 'your-plugin' ),
'uploaded_to_this_item' => _x( 'Uploaded to this book', 'Overrides the "Uploaded to this post"/"Uploaded to this page" phrase', 'your-plugin' ),
'filter_items_list' => _x( 'Filter books list', 'Screen reader text for the filter links', 'your-plugin' ),
'items_list_navigation' => _x( 'Books list navigation', 'Screen reader text for the pagination', 'your-plugin' ),
'items_list' => _x( 'Books list', 'Screen reader text for the items list', 'your-plugin' ),
);
$args = array(
'labels' => $labels,
'description' => __( 'Books managed by your plugin', 'your-plugin' ),
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'books' ),
'capability_type' => 'post',
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 20,
'menu_icon' => 'dashicons-book',
'show_in_rest' => true, // Enable Gutenberg editor
'rest_base' => 'books',
'rest_controller_class' => 'WP_REST_Posts_Controller',
'supports' => array(
'title',
'editor',
'author',
'thumbnail',
'excerpt',
'comments',
'custom-fields',
'revisions',
),
);
register_post_type( 'book', $args );
}
add_action( 'init', 'yourprefix_register_book_post_type' );
/**
* Register custom taxonomy (Hierarchical - like Categories)
*/
function yourprefix_register_genre_taxonomy() {
$labels = array(
'name' => _x( 'Genres', 'taxonomy general name', 'your-plugin' ),
'singular_name' => _x( 'Genre', 'taxonomy singular name', 'your-plugin' ),
'search_items' => __( 'Search Genres', 'your-plugin' ),
'all_items' => __( 'All Genres', 'your-plugin' ),
'parent_item' => __( 'Parent Genre', 'your-plugin' ),
'parent_item_colon' => __( 'Parent Genre:', 'your-plugin' ),
'edit_item' => __( 'Edit Genre', 'your-plugin' ),
'update_item' => __( 'Update Genre', 'your-plugin' ),
'add_new_item' => __( 'Add New Genre', 'your-plugin' ),
'new_item_name' => __( 'New Genre Name', 'your-plugin' ),
'menu_name' => __( 'Genre', 'your-plugin' ),
);
$args = array(
'hierarchical' => true, // true = like categories, false = like tags
'labels' => $labels,
'show_ui' => true,
'show_admin_column' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'genre' ),
'show_in_rest' => true,
);
register_taxonomy( 'genre', array( 'book' ), $args );
}
add_action( 'init', 'yourprefix_register_genre_taxonomy' );
/**
* Register non-hierarchical taxonomy (like Tags)
*/
function yourprefix_register_book_tag_taxonomy() {
$labels = array(
'name' => _x( 'Book Tags', 'taxonomy general name', 'your-plugin' ),
'singular_name' => _x( 'Book Tag', 'taxonomy singular name', 'your-plugin' ),
'search_items' => __( 'Search Book Tags', 'your-plugin' ),
'popular_items' => __( 'Popular Book Tags', 'your-plugin' ),
'all_items' => __( 'All Book Tags', 'your-plugin' ),
'edit_item' => __( 'Edit Book Tag', 'your-plugin' ),
'update_item' => __( 'Update Book Tag', 'your-plugin' ),
'add_new_item' => __( 'Add New Book Tag', 'your-plugin' ),
'new_item_name' => __( 'New Book Tag Name', 'your-plugin' ),
'separate_items_with_commas' => __( 'Separate book tags with commas', 'your-plugin' ),
'add_or_remove_items' => __( 'Add or remove book tags', 'your-plugin' ),
'choose_from_most_used' => __( 'Choose from the most used book tags', 'your-plugin' ),
'not_found' => __( 'No book tags found.', 'your-plugin' ),
'menu_name' => __( 'Book Tags', 'your-plugin' ),
);
$args = array(
'hierarchical' => false, // Non-hierarchical (like tags)
'labels' => $labels,
'show_ui' => true,
'show_admin_column' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'book-tag' ),
'show_in_rest' => true,
);
register_taxonomy( 'book_tag', array( 'book' ), $args );
}
add_action( 'init', 'yourprefix_register_book_tag_taxonomy' );
/**
* Customize archive page query for books
*/
function yourprefix_modify_book_archive_query( $query ) {
// Only modify main query on frontend for book archives
if ( ! is_admin() && $query->is_main_query() && is_post_type_archive( 'book' ) ) {
// Show 12 books per page
$query->set( 'posts_per_page', 12 );
// Order by title
$query->set( 'orderby', 'title' );
$query->set( 'order', 'ASC' );
}
}
add_action( 'pre_get_posts', 'yourprefix_modify_book_archive_query' );
/**
* Add custom columns to book admin list
*/
function yourprefix_add_book_columns( $columns ) {
$new_columns = array();
foreach ( $columns as $key => $value ) {
$new_columns[ $key ] = $value;
// Add genre column after title
if ( 'title' === $key ) {
$new_columns['genre'] = __( 'Genres', 'your-plugin' );
$new_columns['book_tags'] = __( 'Tags', 'your-plugin' );
}
}
return $new_columns;
}
add_filter( 'manage_book_posts_columns', 'yourprefix_add_book_columns' );
/**
* Populate custom columns
*/
function yourprefix_populate_book_columns( $column, $post_id ) {
if ( 'genre' === $column ) {
$genres = get_the_terms( $post_id, 'genre' );
if ( $genres && ! is_wp_error( $genres ) ) {
$genre_list = array();
foreach ( $genres as $genre ) {
$genre_list[] = sprintf(
'<a href="%s">%s</a>',
esc_url( add_query_arg( array( 'genre' => $genre->slug ), admin_url( 'edit.php?post_type=book' ) ) ),
esc_html( $genre->name )
);
}
echo implode( ', ', $genre_list );
} else {
echo '—';
}
}
if ( 'book_tags' === $column ) {
$tags = get_the_terms( $post_id, 'book_tag' );
if ( $tags && ! is_wp_error( $tags ) ) {
$tag_list = array();
foreach ( $tags as $tag ) {
$tag_list[] = esc_html( $tag->name );
}
echo implode( ', ', $tag_list );
} else {
echo '—';
}
}
}
add_action( 'manage_book_posts_custom_column', 'yourprefix_populate_book_columns', 10, 2 );
/**
* Make columns sortable
*/
function yourprefix_sortable_book_columns( $columns ) {
$columns['genre'] = 'genre';
return $columns;
}
add_filter( 'manage_edit-book_sortable_columns', 'yourprefix_sortable_book_columns' );
/**
* Flush rewrite rules on plugin activation
*/
function yourprefix_activate() {
// Register post types and taxonomies
yourprefix_register_book_post_type();
yourprefix_register_genre_taxonomy();
yourprefix_register_book_tag_taxonomy();
// CRITICAL: Flush rewrite rules to prevent 404 errors
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'yourprefix_activate' );
/**
* Flush rewrite rules on plugin deactivation
*/
function yourprefix_deactivate() {
// Flush rewrite rules
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'yourprefix_deactivate' );
/**
* Query books by genre
*
* Example usage in theme templates
*/
function yourprefix_get_books_by_genre( $genre_slug, $posts_per_page = 10 ) {
$args = array(
'post_type' => 'book',
'posts_per_page' => $posts_per_page,
'tax_query' => array(
array(
'taxonomy' => 'genre',
'field' => 'slug',
'terms' => $genre_slug,
),
),
);
return new WP_Query( $args );
}

View File

@@ -0,0 +1,243 @@
<?php
/**
* Example: Add a meta box to post editor
*
* This example shows how to:
* - Register a meta box
* - Render meta box fields
* - Save meta box data securely
* - Use nonces for CSRF protection
* - Sanitize input and escape output
*
* @package YourPlugin
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Add meta box
*/
function yourprefix_add_meta_box() {
add_meta_box(
'yourprefix_details', // Meta box ID
__( 'Additional Details', 'your-plugin' ), // Title
'yourprefix_render_meta_box', // Callback function
'post', // Post type (or array of post types)
'normal', // Context (normal, side, advanced)
'high' // Priority
);
}
add_action( 'add_meta_boxes', 'yourprefix_add_meta_box' );
/**
* Render meta box content
*
* @param WP_Post $post Post object.
*/
function yourprefix_render_meta_box( $post ) {
// Add nonce for security
wp_nonce_field( 'yourprefix_save_meta', 'yourprefix_meta_nonce' );
// Get current values
$text_value = get_post_meta( $post->ID, '_yourprefix_text', true );
$number_value = get_post_meta( $post->ID, '_yourprefix_number', true );
$select_value = get_post_meta( $post->ID, '_yourprefix_select', true );
$checkbox = get_post_meta( $post->ID, '_yourprefix_checkbox', true );
?>
<table class="form-table">
<tr>
<th>
<label for="yourprefix_text">
<?php esc_html_e( 'Text Field', 'your-plugin' ); ?>
</label>
</th>
<td>
<input
type="text"
id="yourprefix_text"
name="yourprefix_text"
value="<?php echo esc_attr( $text_value ); ?>"
class="regular-text"
/>
<p class="description">
<?php esc_html_e( 'Enter some text here', 'your-plugin' ); ?>
</p>
</td>
</tr>
<tr>
<th>
<label for="yourprefix_number">
<?php esc_html_e( 'Number Field', 'your-plugin' ); ?>
</label>
</th>
<td>
<input
type="number"
id="yourprefix_number"
name="yourprefix_number"
value="<?php echo esc_attr( $number_value ); ?>"
min="0"
max="100"
class="small-text"
/>
</td>
</tr>
<tr>
<th>
<label for="yourprefix_select">
<?php esc_html_e( 'Select Field', 'your-plugin' ); ?>
</label>
</th>
<td>
<select id="yourprefix_select" name="yourprefix_select">
<option value=""><?php esc_html_e( '-- Select --', 'your-plugin' ); ?></option>
<option value="option1" <?php selected( $select_value, 'option1' ); ?>>
<?php esc_html_e( 'Option 1', 'your-plugin' ); ?>
</option>
<option value="option2" <?php selected( $select_value, 'option2' ); ?>>
<?php esc_html_e( 'Option 2', 'your-plugin' ); ?>
</option>
<option value="option3" <?php selected( $select_value, 'option3' ); ?>>
<?php esc_html_e( 'Option 3', 'your-plugin' ); ?>
</option>
</select>
</td>
</tr>
<tr>
<th>
<?php esc_html_e( 'Checkbox', 'your-plugin' ); ?>
</th>
<td>
<label>
<input
type="checkbox"
id="yourprefix_checkbox"
name="yourprefix_checkbox"
value="1"
<?php checked( $checkbox, '1' ); ?>
/>
<?php esc_html_e( 'Enable this option', 'your-plugin' ); ?>
</label>
</td>
</tr>
</table>
<?php
}
/**
* Save meta box data
*
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
*/
function yourprefix_save_meta( $post_id, $post ) {
// Verify nonce
if ( ! isset( $_POST['yourprefix_meta_nonce'] ) ||
! wp_verify_nonce( $_POST['yourprefix_meta_nonce'], 'yourprefix_save_meta' ) ) {
return;
}
// Check autosave
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// Check user permissions
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// Save text field
if ( isset( $_POST['yourprefix_text'] ) ) {
update_post_meta(
$post_id,
'_yourprefix_text',
sanitize_text_field( $_POST['yourprefix_text'] )
);
}
// Save number field
if ( isset( $_POST['yourprefix_number'] ) ) {
update_post_meta(
$post_id,
'_yourprefix_number',
absint( $_POST['yourprefix_number'] )
);
}
// Save select field
if ( isset( $_POST['yourprefix_select'] ) ) {
$allowed_values = array( 'option1', 'option2', 'option3' );
$select_value = sanitize_text_field( $_POST['yourprefix_select'] );
// Validate against allowed values
if ( in_array( $select_value, $allowed_values, true ) ) {
update_post_meta( $post_id, '_yourprefix_select', $select_value );
} else {
delete_post_meta( $post_id, '_yourprefix_select' );
}
}
// Save checkbox
if ( isset( $_POST['yourprefix_checkbox'] ) ) {
update_post_meta( $post_id, '_yourprefix_checkbox', '1' );
} else {
delete_post_meta( $post_id, '_yourprefix_checkbox' );
}
}
add_action( 'save_post', 'yourprefix_save_meta', 10, 2 );
/**
* Display meta box data on frontend
*
* Add this to your theme's single.php or use a filter
*/
function yourprefix_display_meta_data( $content ) {
if ( ! is_singular( 'post' ) ) {
return $content;
}
global $post;
$text_value = get_post_meta( $post->ID, '_yourprefix_text', true );
$number_value = get_post_meta( $post->ID, '_yourprefix_number', true );
$select_value = get_post_meta( $post->ID, '_yourprefix_select', true );
$checkbox = get_post_meta( $post->ID, '_yourprefix_checkbox', true );
if ( $text_value || $number_value || $select_value || $checkbox ) {
$meta_html = '<div class="custom-meta-data">';
$meta_html .= '<h3>' . esc_html__( 'Additional Details', 'your-plugin' ) . '</h3>';
$meta_html .= '<ul>';
if ( $text_value ) {
$meta_html .= '<li><strong>' . esc_html__( 'Text:', 'your-plugin' ) . '</strong> ' . esc_html( $text_value ) . '</li>';
}
if ( $number_value ) {
$meta_html .= '<li><strong>' . esc_html__( 'Number:', 'your-plugin' ) . '</strong> ' . esc_html( $number_value ) . '</li>';
}
if ( $select_value ) {
$meta_html .= '<li><strong>' . esc_html__( 'Selection:', 'your-plugin' ) . '</strong> ' . esc_html( $select_value ) . '</li>';
}
if ( $checkbox ) {
$meta_html .= '<li><strong>' . esc_html__( 'Option enabled', 'your-plugin' ) . '</strong></li>';
}
$meta_html .= '</ul>';
$meta_html .= '</div>';
$content .= $meta_html;
}
return $content;
}
add_filter( 'the_content', 'yourprefix_display_meta_data' );

View File

@@ -0,0 +1,384 @@
<?php
/**
* Example: REST API endpoints
*
* This example shows how to:
* - Register custom REST API routes
* - Implement GET, POST, PUT, DELETE endpoints
* - Use permission callbacks for authentication
* - Validate and sanitize request parameters
* - Return proper REST responses and errors
*
* @package YourPlugin
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Register REST API routes
*/
function yourprefix_register_rest_routes() {
// Namespace: yourplugin/v1
$namespace = 'yourplugin/v1';
// GET /wp-json/yourplugin/v1/items
register_rest_route(
$namespace,
'/items',
array(
'methods' => 'GET',
'callback' => 'yourprefix_get_items',
'permission_callback' => '__return_true', // Public endpoint
)
);
// GET /wp-json/yourplugin/v1/items/{id}
register_rest_route(
$namespace,
'/items/(?P<id>\d+)',
array(
'methods' => 'GET',
'callback' => 'yourprefix_get_item',
'permission_callback' => '__return_true',
'args' => array(
'id' => array(
'required' => true,
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
'sanitize_callback' => 'absint',
),
),
)
);
// POST /wp-json/yourplugin/v1/items
register_rest_route(
$namespace,
'/items',
array(
'methods' => 'POST',
'callback' => 'yourprefix_create_item',
'permission_callback' => 'yourprefix_check_permission',
'args' => yourprefix_get_item_args(),
)
);
// PUT /wp-json/yourplugin/v1/items/{id}
register_rest_route(
$namespace,
'/items/(?P<id>\d+)',
array(
'methods' => 'PUT',
'callback' => 'yourprefix_update_item',
'permission_callback' => 'yourprefix_check_permission',
'args' => array_merge(
array(
'id' => array(
'required' => true,
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
'sanitize_callback' => 'absint',
),
),
yourprefix_get_item_args()
),
)
);
// DELETE /wp-json/yourplugin/v1/items/{id}
register_rest_route(
$namespace,
'/items/(?P<id>\d+)',
array(
'methods' => 'DELETE',
'callback' => 'yourprefix_delete_item',
'permission_callback' => 'yourprefix_check_permission',
'args' => array(
'id' => array(
'required' => true,
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
'sanitize_callback' => 'absint',
),
),
)
);
}
add_action( 'rest_api_init', 'yourprefix_register_rest_routes' );
/**
* Get all items
*
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error
*/
function yourprefix_get_items( $request ) {
// Get query parameters
$per_page = $request->get_param( 'per_page' );
$page = $request->get_param( 'page' );
// Set defaults
$per_page = $per_page ? absint( $per_page ) : 10;
$page = $page ? absint( $page ) : 1;
// Query posts
$args = array(
'post_type' => 'post',
'posts_per_page' => $per_page,
'paged' => $page,
'post_status' => 'publish',
);
$query = new WP_Query( $args );
$items = array();
foreach ( $query->posts as $post ) {
$items[] = array(
'id' => $post->ID,
'title' => $post->post_title,
'content' => $post->post_content,
'excerpt' => $post->post_excerpt,
'date' => $post->post_date,
'author' => get_the_author_meta( 'display_name', $post->post_author ),
);
}
// Return response with pagination headers
$response = rest_ensure_response( $items );
$response->header( 'X-WP-Total', $query->found_posts );
$response->header( 'X-WP-TotalPages', $query->max_num_pages );
return $response;
}
/**
* Get single item
*
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error
*/
function yourprefix_get_item( $request ) {
$item_id = $request->get_param( 'id' );
$post = get_post( $item_id );
if ( ! $post ) {
return new WP_Error(
'not_found',
__( 'Item not found', 'your-plugin' ),
array( 'status' => 404 )
);
}
$data = array(
'id' => $post->ID,
'title' => $post->post_title,
'content' => $post->post_content,
'excerpt' => $post->post_excerpt,
'date' => $post->post_date,
'author' => get_the_author_meta( 'display_name', $post->post_author ),
);
return rest_ensure_response( $data );
}
/**
* Create item
*
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error
*/
function yourprefix_create_item( $request ) {
$title = $request->get_param( 'title' );
$content = $request->get_param( 'content' );
$status = $request->get_param( 'status' );
// Create post
$post_id = wp_insert_post( array(
'post_title' => $title,
'post_content' => $content,
'post_status' => $status ? $status : 'draft',
'post_type' => 'post',
) );
if ( is_wp_error( $post_id ) ) {
return $post_id;
}
$post = get_post( $post_id );
$data = array(
'id' => $post->ID,
'title' => $post->post_title,
'content' => $post->post_content,
'status' => $post->post_status,
'date' => $post->post_date,
);
return rest_ensure_response( $data );
}
/**
* Update item
*
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error
*/
function yourprefix_update_item( $request ) {
$item_id = $request->get_param( 'id' );
$post = get_post( $item_id );
if ( ! $post ) {
return new WP_Error(
'not_found',
__( 'Item not found', 'your-plugin' ),
array( 'status' => 404 )
);
}
// Update post
$update_data = array(
'ID' => $item_id,
);
if ( $request->has_param( 'title' ) ) {
$update_data['post_title'] = $request->get_param( 'title' );
}
if ( $request->has_param( 'content' ) ) {
$update_data['post_content'] = $request->get_param( 'content' );
}
if ( $request->has_param( 'status' ) ) {
$update_data['post_status'] = $request->get_param( 'status' );
}
$result = wp_update_post( $update_data, true );
if ( is_wp_error( $result ) ) {
return $result;
}
$post = get_post( $item_id );
$data = array(
'id' => $post->ID,
'title' => $post->post_title,
'content' => $post->post_content,
'status' => $post->post_status,
'date' => $post->post_date,
);
return rest_ensure_response( $data );
}
/**
* Delete item
*
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error
*/
function yourprefix_delete_item( $request ) {
$item_id = $request->get_param( 'id' );
$post = get_post( $item_id );
if ( ! $post ) {
return new WP_Error(
'not_found',
__( 'Item not found', 'your-plugin' ),
array( 'status' => 404 )
);
}
// Delete post (move to trash)
$result = wp_trash_post( $item_id );
if ( ! $result ) {
return new WP_Error(
'cannot_delete',
__( 'Could not delete item', 'your-plugin' ),
array( 'status' => 500 )
);
}
return rest_ensure_response( array(
'deleted' => true,
'id' => $item_id,
) );
}
/**
* Check permission
*
* @return bool
*/
function yourprefix_check_permission() {
return current_user_can( 'edit_posts' );
}
/**
* Get item validation arguments
*
* @return array
*/
function yourprefix_get_item_args() {
return array(
'title' => array(
'required' => true,
'type' => 'string',
'description' => __( 'Item title', 'your-plugin' ),
'sanitize_callback' => 'sanitize_text_field',
),
'content' => array(
'required' => false,
'type' => 'string',
'description' => __( 'Item content', 'your-plugin' ),
'sanitize_callback' => 'wp_kses_post',
),
'status' => array(
'required' => false,
'type' => 'string',
'description' => __( 'Item status', 'your-plugin' ),
'enum' => array( 'publish', 'draft', 'pending', 'private' ),
'sanitize_callback' => 'sanitize_text_field',
),
);
}
/**
* Example AJAX endpoint (alternative to REST API for simpler use cases)
*/
function yourprefix_ajax_get_items() {
// Verify nonce
check_ajax_referer( 'yourprefix_nonce', 'nonce' );
// Check permission
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => __( 'Permission denied', 'your-plugin' ) ) );
}
// Get items
$args = array(
'post_type' => 'post',
'posts_per_page' => 10,
'post_status' => 'publish',
);
$query = new WP_Query( $args );
$items = array();
foreach ( $query->posts as $post ) {
$items[] = array(
'id' => $post->ID,
'title' => $post->post_title,
);
}
wp_send_json_success( array( 'items' => $items ) );
}
add_action( 'wp_ajax_yourprefix_get_items', 'yourprefix_ajax_get_items' );

View File

@@ -0,0 +1,308 @@
<?php
/**
* Example: Settings page using WordPress Settings API
*
* This example shows how to:
* - Add a settings page to WordPress admin
* - Register settings with Settings API
* - Add settings sections and fields
* - Sanitize and validate settings
* - Use different field types
*
* @package YourPlugin
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Add settings page to admin menu
*/
function yourprefix_add_settings_page() {
add_options_page(
__( 'Your Plugin Settings', 'your-plugin' ), // Page title
__( 'Your Plugin', 'your-plugin' ), // Menu title
'manage_options', // Capability
'your-plugin-settings', // Menu slug
'yourprefix_render_settings_page' // Callback function
);
}
add_action( 'admin_menu', 'yourprefix_add_settings_page' );
/**
* Register settings
*/
function yourprefix_register_settings() {
// Register setting
register_setting(
'yourprefix_settings_group', // Option group
'yourprefix_settings', // Option name
'yourprefix_sanitize_settings' // Sanitize callback
);
// Add settings section
add_settings_section(
'yourprefix_general_section', // Section ID
__( 'General Settings', 'your-plugin' ), // Section title
'yourprefix_general_section_callback', // Callback
'your-plugin-settings' // Page slug
);
// Add settings fields
add_settings_field(
'yourprefix_text_field',
__( 'Text Field', 'your-plugin' ),
'yourprefix_text_field_callback',
'your-plugin-settings',
'yourprefix_general_section'
);
add_settings_field(
'yourprefix_number_field',
__( 'Number Field', 'your-plugin' ),
'yourprefix_number_field_callback',
'your-plugin-settings',
'yourprefix_general_section'
);
add_settings_field(
'yourprefix_checkbox_field',
__( 'Checkbox Field', 'your-plugin' ),
'yourprefix_checkbox_field_callback',
'your-plugin-settings',
'yourprefix_general_section'
);
add_settings_field(
'yourprefix_select_field',
__( 'Select Field', 'your-plugin' ),
'yourprefix_select_field_callback',
'your-plugin-settings',
'yourprefix_general_section'
);
add_settings_field(
'yourprefix_textarea_field',
__( 'Textarea Field', 'your-plugin' ),
'yourprefix_textarea_field_callback',
'your-plugin-settings',
'yourprefix_general_section'
);
}
add_action( 'admin_init', 'yourprefix_register_settings' );
/**
* Render settings page
*/
function yourprefix_render_settings_page() {
// Check user capabilities
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Show success message if settings were saved
if ( isset( $_GET['settings-updated'] ) ) {
add_settings_error(
'yourprefix_messages',
'yourprefix_message',
__( 'Settings Saved', 'your-plugin' ),
'updated'
);
}
// Show error/update messages
settings_errors( 'yourprefix_messages' );
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form method="post" action="options.php">
<?php
settings_fields( 'yourprefix_settings_group' );
do_settings_sections( 'your-plugin-settings' );
submit_button( __( 'Save Settings', 'your-plugin' ) );
?>
</form>
</div>
<?php
}
/**
* Section callback
*/
function yourprefix_general_section_callback() {
echo '<p>' . esc_html__( 'Configure your plugin settings below.', 'your-plugin' ) . '</p>';
}
/**
* Get settings with defaults
*
* @return array
*/
function yourprefix_get_settings() {
$defaults = array(
'text_field' => '',
'number_field' => 0,
'checkbox_field' => false,
'select_field' => '',
'textarea_field' => '',
);
$settings = get_option( 'yourprefix_settings', $defaults );
return wp_parse_args( $settings, $defaults );
}
/**
* Text field callback
*/
function yourprefix_text_field_callback() {
$settings = yourprefix_get_settings();
$value = $settings['text_field'];
printf(
'<input type="text" id="yourprefix_text_field" name="yourprefix_settings[text_field]" value="%s" class="regular-text" />',
esc_attr( $value )
);
echo '<p class="description">' . esc_html__( 'Enter some text here', 'your-plugin' ) . '</p>';
}
/**
* Number field callback
*/
function yourprefix_number_field_callback() {
$settings = yourprefix_get_settings();
$value = $settings['number_field'];
printf(
'<input type="number" id="yourprefix_number_field" name="yourprefix_settings[number_field]" value="%s" min="0" max="100" class="small-text" />',
esc_attr( $value )
);
echo '<p class="description">' . esc_html__( 'Enter a number between 0 and 100', 'your-plugin' ) . '</p>';
}
/**
* Checkbox field callback
*/
function yourprefix_checkbox_field_callback() {
$settings = yourprefix_get_settings();
$value = $settings['checkbox_field'];
printf(
'<input type="checkbox" id="yourprefix_checkbox_field" name="yourprefix_settings[checkbox_field]" value="1" %s />',
checked( $value, true, false )
);
echo '<label for="yourprefix_checkbox_field">' . esc_html__( 'Enable this feature', 'your-plugin' ) . '</label>';
}
/**
* Select field callback
*/
function yourprefix_select_field_callback() {
$settings = yourprefix_get_settings();
$value = $settings['select_field'];
?>
<select id="yourprefix_select_field" name="yourprefix_settings[select_field]">
<option value=""><?php esc_html_e( '-- Select --', 'your-plugin' ); ?></option>
<option value="option1" <?php selected( $value, 'option1' ); ?>>
<?php esc_html_e( 'Option 1', 'your-plugin' ); ?>
</option>
<option value="option2" <?php selected( $value, 'option2' ); ?>>
<?php esc_html_e( 'Option 2', 'your-plugin' ); ?>
</option>
<option value="option3" <?php selected( $value, 'option3' ); ?>>
<?php esc_html_e( 'Option 3', 'your-plugin' ); ?>
</option>
</select>
<?php
}
/**
* Textarea field callback
*/
function yourprefix_textarea_field_callback() {
$settings = yourprefix_get_settings();
$value = $settings['textarea_field'];
printf(
'<textarea id="yourprefix_textarea_field" name="yourprefix_settings[textarea_field]" rows="5" cols="50" class="large-text">%s</textarea>',
esc_textarea( $value )
);
echo '<p class="description">' . esc_html__( 'Enter some longer text here', 'your-plugin' ) . '</p>';
}
/**
* Sanitize settings
*
* @param array $input Input array.
* @return array Sanitized array.
*/
function yourprefix_sanitize_settings( $input ) {
$sanitized = array();
// Text field
if ( isset( $input['text_field'] ) ) {
$sanitized['text_field'] = sanitize_text_field( $input['text_field'] );
}
// Number field
if ( isset( $input['number_field'] ) ) {
$number = absint( $input['number_field'] );
// Validate range
if ( $number < 0 || $number > 100 ) {
add_settings_error(
'yourprefix_messages',
'yourprefix_message',
__( 'Number must be between 0 and 100', 'your-plugin' ),
'error'
);
$sanitized['number_field'] = 0;
} else {
$sanitized['number_field'] = $number;
}
}
// Checkbox field
if ( isset( $input['checkbox_field'] ) ) {
$sanitized['checkbox_field'] = true;
} else {
$sanitized['checkbox_field'] = false;
}
// Select field
if ( isset( $input['select_field'] ) ) {
$allowed_values = array( 'option1', 'option2', 'option3' );
$select_value = sanitize_text_field( $input['select_field'] );
// Validate against allowed values
if ( in_array( $select_value, $allowed_values, true ) ) {
$sanitized['select_field'] = $select_value;
} else {
$sanitized['select_field'] = '';
}
}
// Textarea field
if ( isset( $input['textarea_field'] ) ) {
$sanitized['textarea_field'] = sanitize_textarea_field( $input['textarea_field'] );
}
return $sanitized;
}
/**
* Get a specific setting value
*
* @param string $key Setting key.
* @param mixed $default Default value.
* @return mixed Setting value or default.
*/
function yourprefix_get_setting( $key, $default = '' ) {
$settings = yourprefix_get_settings();
return isset( $settings[ $key ] ) ? $settings[ $key ] : $default;
}

View File

@@ -0,0 +1,285 @@
# OOP WordPress Plugin Template
This is an object-oriented singleton pattern for WordPress plugins. Best for medium to large plugins that need organized, maintainable architecture.
## Features
✅ Singleton pattern (single instance)
✅ Complete plugin header
✅ ABSPATH security check
✅ Class-based organization
✅ Custom post type and taxonomy
✅ Admin settings page with separate view template
✅ AJAX handlers (logged-in and public)
✅ REST API endpoints
✅ Proper activation/deactivation hooks
✅ Uninstall script for cleanup
✅ Internationalization ready
✅ Conditional asset loading
✅ Security best practices
## Installation
1. Copy this folder to `wp-content/plugins/`
2. Rename folder and files to match your plugin name
3. Find and replace the following:
- `My_OOP_Plugin` → Your_Plugin_Class_Name
- `My OOP Plugin` → Your plugin name
- `my-oop-plugin` → your-plugin-slug
- `myop_` → yourprefix_
- `MYOP_` → YOURPREFIX_
- `https://example.com` → Your website
- `Your Name` → Your name
4. Activate in WordPress admin
## Structure
```
my-oop-plugin/
├── my-oop-plugin.php # Main plugin file with class
├── uninstall.php # Cleanup on uninstall
├── README.md # This file
├── views/ # Template files
│ └── admin-settings.php # Settings page template
├── assets/ # CSS/JS files (create as needed)
│ ├── css/
│ │ ├── admin-style.css
│ │ └── style.css
│ └── js/
│ ├── admin-script.js
│ └── script.js
└── languages/ # Translation files (create as needed)
```
## Class Structure
### Singleton Pattern
```php
My_OOP_Plugin::get_instance(); // Get single instance
```
### Key Methods
**Initialization**:
- `__construct()` - Private constructor, sets up plugin
- `define_constants()` - Define plugin constants
- `init_hooks()` - Register WordPress hooks
- `init()` - Initialize functionality on `init` hook
**Post Types & Taxonomies**:
- `register_post_types()` - Register custom post types
- `register_taxonomies()` - Register custom taxonomies
**Admin**:
- `add_admin_menu()` - Add admin menu pages
- `render_settings_page()` - Render settings page
- `save_settings()` - Handle form submission
- `get_settings()` - Get plugin settings
**Assets**:
- `admin_enqueue_scripts()` - Load admin assets
- `enqueue_scripts()` - Load frontend assets
**AJAX**:
- `ajax_handler()` - Handle logged-in AJAX requests
- `ajax_handler_nopriv()` - Handle public AJAX requests
- `process_ajax_data()` - Process AJAX data
**REST API**:
- `register_rest_routes()` - Register REST endpoints
- `rest_permission_check()` - Check permissions
- `rest_get_books()` - GET /wp-json/myop/v1/books
- `rest_get_book()` - GET /wp-json/myop/v1/books/{id}
**Lifecycle**:
- `activate()` - Run on plugin activation
- `deactivate()` - Run on plugin deactivation
## Included Examples
### Custom Post Type & Taxonomy
- "Books" post type with Gutenberg support
- "Genres" hierarchical taxonomy
### Settings Page
- Located in Settings → OOP Plugin
- Separate view template (views/admin-settings.php)
- Multiple field types (text, number, checkbox)
- Nonce verification and sanitization
### AJAX Handlers
- Logged-in: `wp_ajax_myop_action`
- Public: `wp_ajax_nopriv_myop_action`
- Nonce verification and capability checking
### REST API
- `GET /wp-json/myop/v1/books` - List all books
- `GET /wp-json/myop/v1/books/{id}` - Get single book
- Permission callbacks
- Validation and sanitization
## Extending the Plugin
### Add a New Method
```php
/**
* Your custom method
*
* @param string $param Parameter description
* @return mixed
*/
private function custom_method( $param ) {
// Your code here
}
```
### Add a New Hook
Add to `init_hooks()`:
```php
add_action( 'hook_name', array( $this, 'method_name' ) );
```
### Add a New REST Endpoint
Add to `register_rest_routes()`:
```php
register_rest_route(
'myop/v1',
'/endpoint',
array(
'methods' => 'GET',
'callback' => array( $this, 'rest_endpoint_handler' ),
'permission_callback' => array( $this, 'rest_permission_check' ),
)
);
```
## Security Checklist
- [x] ABSPATH check at top of file
- [x] Private constructor (singleton)
- [x] Prevent cloning and unserializing
- [x] Nonces for all forms
- [x] Capability checks (current_user_can)
- [x] Input sanitization (sanitize_text_field, absint)
- [x] Output escaping (esc_html, esc_attr)
- [x] AJAX nonce verification (check_ajax_referer)
- [x] REST API permission callbacks
- [x] REST API argument validation
- [x] Conditional asset loading
## Advantages of OOP Pattern
**Organization** - Related code grouped in methods
**Maintainability** - Easy to find and modify functionality
**Encapsulation** - Private methods protect internal logic
**Singleton** - Prevents multiple instances
**Scalability** - Easy to extend with new methods
**Testing** - Methods can be tested individually
## When to Use OOP vs Simple
**Use OOP when**:
- Plugin has 10+ functions
- Need organized, maintainable code
- Multiple developers working on plugin
- Plugin will grow over time
- Need private/protected methods
**Use Simple when**:
- Plugin has <10 functions
- Simple, focused functionality
- One-person project
- Unlikely to grow significantly
## Distribution & Auto-Updates
### Enabling GitHub Auto-Updates
You can provide automatic updates from GitHub without submitting to WordPress.org:
**1. Install Plugin Update Checker library:**
```bash
cd your-plugin/
git submodule add https://github.com/YahnisElsts/plugin-update-checker.git
```
**2. Add to `init_hooks()` method:**
```php
/**
* Include and initialize update checker
*/
private function init_updater() {
$updater_path = plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php';
if ( ! file_exists( $updater_path ) ) {
return;
}
require $updater_path;
$updateChecker = YahnisElsts\PluginUpdateChecker\v5\PucFactory::buildUpdateChecker(
'https://github.com/yourusername/your-plugin/',
__FILE__,
'your-plugin-slug'
);
$updateChecker->setBranch( 'main' );
$updateChecker->getVcsApi()->enableReleaseAssets();
}
```
Then call it in `init_hooks()`:
```php
private function init_hooks() {
// Existing hooks...
// Initialize auto-updates
$this->init_updater();
}
```
**3. For private repos, add token to wp-config.php:**
```php
define( 'YOUR_PLUGIN_GITHUB_TOKEN', 'ghp_xxxxxxxxxxxxx' );
```
Then update `init_updater()`:
```php
if ( defined( 'MYOP_GITHUB_TOKEN' ) ) {
$updateChecker->setAuthentication( MYOP_GITHUB_TOKEN );
}
```
**4. Create releases:**
```bash
# Update version in plugin header
git add my-oop-plugin.php
git commit -m "Bump version to 1.0.1"
git tag 1.0.1
git push origin main
git push origin 1.0.1
# Create GitHub Release with pre-built ZIP
```
### Resources
- **Complete Guide**: See `references/github-auto-updates.md`
- **Implementation Examples**: See `examples/github-updater.php`
- **Plugin Update Checker**: https://github.com/YahnisElsts/plugin-update-checker
## Resources
- [WordPress Plugin Handbook](https://developer.wordpress.org/plugins/)
- [WordPress OOP Best Practices](https://developer.wordpress.org/plugins/plugin-basics/best-practices/)
- [Singleton Pattern](https://en.wikipedia.org/wiki/Singleton_pattern)
## License
GPL v2 or later

View File

@@ -0,0 +1,526 @@
<?php
/**
* Plugin Name: My OOP Plugin
* Plugin URI: https://example.com/my-oop-plugin/
* Description: An object-oriented WordPress plugin using singleton pattern with security best practices.
* Version: 1.0.0
* Requires at least: 5.9
* Requires PHP: 7.4
* Author: Your Name
* Author URI: https://example.com/
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: my-oop-plugin
* Domain Path: /languages
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Main plugin class using singleton pattern
*/
class My_OOP_Plugin {
/**
* Single instance of the class
*
* @var My_OOP_Plugin
*/
private static $instance = null;
/**
* Plugin version
*
* @var string
*/
const VERSION = '1.0.0';
/**
* Plugin directory path
*
* @var string
*/
private $plugin_dir;
/**
* Plugin directory URL
*
* @var string
*/
private $plugin_url;
/**
* Get singleton instance
*
* @return My_OOP_Plugin
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor - Initialize plugin
*/
private function __construct() {
$this->plugin_dir = plugin_dir_path( __FILE__ );
$this->plugin_url = plugin_dir_url( __FILE__ );
$this->define_constants();
$this->init_hooks();
}
/**
* Prevent cloning
*/
private function __clone() {}
/**
* Prevent unserializing
*/
public function __wakeup() {
throw new Exception( 'Cannot unserialize singleton' );
}
/**
* Define plugin constants
*/
private function define_constants() {
define( 'MYOP_VERSION', self::VERSION );
define( 'MYOP_PLUGIN_DIR', $this->plugin_dir );
define( 'MYOP_PLUGIN_URL', $this->plugin_url );
define( 'MYOP_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
}
/**
* Initialize WordPress hooks
*/
private function init_hooks() {
// Core hooks
add_action( 'init', array( $this, 'init' ) );
add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
// AJAX hooks
add_action( 'wp_ajax_myop_action', array( $this, 'ajax_handler' ) );
add_action( 'wp_ajax_nopriv_myop_action', array( $this, 'ajax_handler_nopriv' ) );
// REST API hooks
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
// Activation/Deactivation
register_activation_hook( __FILE__, array( $this, 'activate' ) );
register_deactivation_hook( __FILE__, array( $this, 'deactivate' ) );
}
/**
* Initialize plugin functionality
*/
public function init() {
// Load text domain
load_plugin_textdomain(
'my-oop-plugin',
false,
dirname( MYOP_PLUGIN_BASENAME ) . '/languages'
);
// Register custom post type
$this->register_post_types();
// Register taxonomies
$this->register_taxonomies();
}
/**
* Register custom post types
*/
private function register_post_types() {
$labels = array(
'name' => _x( 'Books', 'post type general name', 'my-oop-plugin' ),
'singular_name' => _x( 'Book', 'post type singular name', 'my-oop-plugin' ),
'menu_name' => _x( 'Books', 'admin menu', 'my-oop-plugin' ),
'add_new' => _x( 'Add New', 'book', 'my-oop-plugin' ),
'add_new_item' => __( 'Add New Book', 'my-oop-plugin' ),
'edit_item' => __( 'Edit Book', 'my-oop-plugin' ),
'new_item' => __( 'New Book', 'my-oop-plugin' ),
'view_item' => __( 'View Book', 'my-oop-plugin' ),
'search_items' => __( 'Search Books', 'my-oop-plugin' ),
'not_found' => __( 'No books found', 'my-oop-plugin' ),
'not_found_in_trash' => __( 'No books found in Trash', 'my-oop-plugin' ),
);
$args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'books' ),
'capability_type' => 'post',
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 5,
'menu_icon' => 'dashicons-book',
'show_in_rest' => true,
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
);
register_post_type( 'book', $args );
}
/**
* Register taxonomies
*/
private function register_taxonomies() {
$labels = array(
'name' => _x( 'Genres', 'taxonomy general name', 'my-oop-plugin' ),
'singular_name' => _x( 'Genre', 'taxonomy singular name', 'my-oop-plugin' ),
'search_items' => __( 'Search Genres', 'my-oop-plugin' ),
'all_items' => __( 'All Genres', 'my-oop-plugin' ),
'parent_item' => __( 'Parent Genre', 'my-oop-plugin' ),
'parent_item_colon' => __( 'Parent Genre:', 'my-oop-plugin' ),
'edit_item' => __( 'Edit Genre', 'my-oop-plugin' ),
'update_item' => __( 'Update Genre', 'my-oop-plugin' ),
'add_new_item' => __( 'Add New Genre', 'my-oop-plugin' ),
'new_item_name' => __( 'New Genre Name', 'my-oop-plugin' ),
'menu_name' => __( 'Genres', 'my-oop-plugin' ),
);
$args = array(
'hierarchical' => true,
'labels' => $labels,
'show_ui' => true,
'show_admin_column' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'genre' ),
'show_in_rest' => true,
);
register_taxonomy( 'genre', array( 'book' ), $args );
}
/**
* Add admin menu pages
*/
public function add_admin_menu() {
add_options_page(
__( 'My OOP Plugin Settings', 'my-oop-plugin' ),
__( 'OOP Plugin', 'my-oop-plugin' ),
'manage_options',
'my-oop-plugin',
array( $this, 'render_settings_page' )
);
}
/**
* Render settings page
*/
public function render_settings_page() {
// Check user capabilities
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Handle form submission
if ( isset( $_POST['myop_settings_submit'] ) ) {
$this->save_settings();
}
// Get current settings
$settings = $this->get_settings();
// Render page
include $this->plugin_dir . 'views/admin-settings.php';
}
/**
* Save plugin settings
*/
private function save_settings() {
// Verify nonce
if ( ! isset( $_POST['myop_settings_nonce'] ) ||
! wp_verify_nonce( $_POST['myop_settings_nonce'], 'myop_settings_action' ) ) {
wp_die( __( 'Security check failed', 'my-oop-plugin' ) );
}
// Sanitize and save settings
$settings = array(
'option1' => isset( $_POST['myop_option1'] ) ? sanitize_text_field( $_POST['myop_option1'] ) : '',
'option2' => isset( $_POST['myop_option2'] ) ? absint( $_POST['myop_option2'] ) : 0,
'option3' => isset( $_POST['myop_option3'] ) ? (bool) $_POST['myop_option3'] : false,
);
update_option( 'myop_settings', $settings );
add_settings_error(
'myop_messages',
'myop_message',
__( 'Settings Saved', 'my-oop-plugin' ),
'updated'
);
}
/**
* Get plugin settings
*
* @return array
*/
private function get_settings() {
$defaults = array(
'option1' => '',
'option2' => 0,
'option3' => false,
);
$settings = get_option( 'myop_settings', $defaults );
return wp_parse_args( $settings, $defaults );
}
/**
* Enqueue admin scripts and styles
*
* @param string $hook Current admin page hook
*/
public function admin_enqueue_scripts( $hook ) {
// Only load on specific pages
if ( 'edit.php' !== $hook && 'post.php' !== $hook && 'post-new.php' !== $hook ) {
return;
}
$screen = get_current_screen();
if ( $screen && 'book' === $screen->post_type ) {
wp_enqueue_style(
'myop-admin-style',
$this->plugin_url . 'assets/css/admin-style.css',
array(),
self::VERSION
);
wp_enqueue_script(
'myop-admin-script',
$this->plugin_url . 'assets/js/admin-script.js',
array( 'jquery' ),
self::VERSION,
true
);
wp_localize_script(
'myop-admin-script',
'myopData',
array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'myop_ajax_nonce' ),
)
);
}
}
/**
* Enqueue frontend scripts and styles
*/
public function enqueue_scripts() {
if ( is_singular( 'book' ) ) {
wp_enqueue_style(
'myop-style',
$this->plugin_url . 'assets/css/style.css',
array(),
self::VERSION
);
wp_enqueue_script(
'myop-script',
$this->plugin_url . 'assets/js/script.js',
array( 'jquery' ),
self::VERSION,
true
);
}
}
/**
* AJAX handler (for logged-in users)
*/
public function ajax_handler() {
// Verify nonce
check_ajax_referer( 'myop_ajax_nonce', 'nonce' );
// Check capability
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => __( 'Permission denied', 'my-oop-plugin' ) ) );
}
// Get and sanitize input
$input = isset( $_POST['data'] ) ? sanitize_text_field( $_POST['data'] ) : '';
// Process data
$result = $this->process_ajax_data( $input );
// Return response
wp_send_json_success( array(
'message' => __( 'Success!', 'my-oop-plugin' ),
'data' => $result,
) );
}
/**
* AJAX handler (for non-logged-in users)
*/
public function ajax_handler_nopriv() {
// For public AJAX requests
wp_send_json_error( array( 'message' => __( 'Login required', 'my-oop-plugin' ) ) );
}
/**
* Process AJAX data
*
* @param string $data Input data
* @return mixed
*/
private function process_ajax_data( $data ) {
// Process your data here
return $data;
}
/**
* Register REST API routes
*/
public function register_rest_routes() {
register_rest_route(
'myop/v1',
'/books',
array(
'methods' => 'GET',
'callback' => array( $this, 'rest_get_books' ),
'permission_callback' => array( $this, 'rest_permission_check' ),
)
);
register_rest_route(
'myop/v1',
'/books/(?P<id>\d+)',
array(
'methods' => 'GET',
'callback' => array( $this, 'rest_get_book' ),
'permission_callback' => '__return_true',
'args' => array(
'id' => array(
'required' => true,
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
'sanitize_callback' => 'absint',
),
),
)
);
}
/**
* REST API permission check
*
* @return bool
*/
public function rest_permission_check() {
return current_user_can( 'edit_posts' );
}
/**
* REST API endpoint: Get books
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error
*/
public function rest_get_books( $request ) {
$args = array(
'post_type' => 'book',
'posts_per_page' => 10,
'post_status' => 'publish',
);
$books = get_posts( $args );
$data = array();
foreach ( $books as $book ) {
$data[] = array(
'id' => $book->ID,
'title' => $book->post_title,
'content' => $book->post_content,
'excerpt' => $book->post_excerpt,
);
}
return rest_ensure_response( $data );
}
/**
* REST API endpoint: Get single book
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error
*/
public function rest_get_book( $request ) {
$book_id = $request->get_param( 'id' );
$book = get_post( $book_id );
if ( ! $book || 'book' !== $book->post_type ) {
return new WP_Error( 'not_found', __( 'Book not found', 'my-oop-plugin' ), array( 'status' => 404 ) );
}
$data = array(
'id' => $book->ID,
'title' => $book->post_title,
'content' => $book->post_content,
'excerpt' => $book->post_excerpt,
);
return rest_ensure_response( $data );
}
/**
* Plugin activation
*/
public function activate() {
// Register post types and taxonomies
$this->register_post_types();
$this->register_taxonomies();
// Flush rewrite rules
flush_rewrite_rules();
// Set default settings
if ( false === get_option( 'myop_settings' ) ) {
add_option( 'myop_settings', array(
'option1' => '',
'option2' => 0,
'option3' => false,
) );
}
// Set activation timestamp
if ( false === get_option( 'myop_activated_time' ) ) {
add_option( 'myop_activated_time', current_time( 'timestamp' ) );
}
}
/**
* Plugin deactivation
*/
public function deactivate() {
// Flush rewrite rules
flush_rewrite_rules();
// Clear scheduled events
wp_clear_scheduled_hook( 'myop_cron_event' );
}
}
// Initialize plugin
My_OOP_Plugin::get_instance();

View File

@@ -0,0 +1,47 @@
<?php
/**
* Uninstall script
*
* This file is called when the plugin is uninstalled via WordPress admin.
*/
// Exit if not called by WordPress
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
// Delete plugin options
delete_option( 'myop_settings' );
delete_option( 'myop_activated_time' );
// Delete transients
delete_transient( 'myop_cache' );
// For multisite
if ( is_multisite() ) {
global $wpdb;
$blog_ids = $wpdb->get_col( "SELECT blog_id FROM $wpdb->blogs" );
foreach ( $blog_ids as $blog_id ) {
switch_to_blog( $blog_id );
delete_option( 'myop_settings' );
delete_option( 'myop_activated_time' );
delete_transient( 'myop_cache' );
restore_current_blog();
}
}
// Delete custom post type data (optional)
/*
$books = get_posts( array(
'post_type' => 'book',
'posts_per_page' => -1,
'post_status' => 'any',
) );
foreach ( $books as $book ) {
wp_delete_post( $book->ID, true );
}
*/

View File

@@ -0,0 +1,87 @@
<?php
/**
* Admin settings page template
*
* @var array $settings Current plugin settings
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<?php settings_errors( 'myop_messages' ); ?>
<form method="post" action="">
<?php wp_nonce_field( 'myop_settings_action', 'myop_settings_nonce' ); ?>
<table class="form-table">
<tr>
<th scope="row">
<label for="myop_option1"><?php esc_html_e( 'Text Option', 'my-oop-plugin' ); ?></label>
</th>
<td>
<input
type="text"
id="myop_option1"
name="myop_option1"
value="<?php echo esc_attr( $settings['option1'] ); ?>"
class="regular-text"
/>
<p class="description">
<?php esc_html_e( 'Enter some text here', 'my-oop-plugin' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="myop_option2"><?php esc_html_e( 'Number Option', 'my-oop-plugin' ); ?></label>
</th>
<td>
<input
type="number"
id="myop_option2"
name="myop_option2"
value="<?php echo esc_attr( $settings['option2'] ); ?>"
min="0"
max="100"
class="small-text"
/>
<p class="description">
<?php esc_html_e( 'Enter a number between 0 and 100', 'my-oop-plugin' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="myop_option3"><?php esc_html_e( 'Checkbox Option', 'my-oop-plugin' ); ?></label>
</th>
<td>
<fieldset>
<label>
<input
type="checkbox"
id="myop_option3"
name="myop_option3"
value="1"
<?php checked( $settings['option3'], true ); ?>
/>
<?php esc_html_e( 'Enable this feature', 'my-oop-plugin' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Check to enable this feature', 'my-oop-plugin' ); ?>
</p>
</fieldset>
</td>
</tr>
</table>
<?php submit_button( __( 'Save Settings', 'my-oop-plugin' ), 'primary', 'myop_settings_submit' ); ?>
</form>
</div>

View File

@@ -0,0 +1,406 @@
# PSR-4 WordPress Plugin Template
This is a modern PSR-4 autoloading pattern for WordPress plugins using Composer. Best for large, complex plugins that need professional architecture with namespaces and autoloading.
## Features
✅ PSR-4 autoloading with Composer
✅ Namespace organization (MyPSR4Plugin\*)
✅ Singleton pattern
✅ Modular class structure by domain
✅ Custom post type with meta boxes
✅ Custom taxonomy
✅ Admin settings using Settings API
✅ REST API endpoints (GET, POST)
✅ Asset management (admin + frontend)
✅ Proper activation/deactivation hooks
✅ Uninstall script for cleanup
✅ Internationalization ready
✅ WordPress Coding Standards ready (PHPCS)
✅ Security best practices
## Installation
1. Copy this folder to `wp-content/plugins/`
2. Rename folder and files to match your plugin name
3. Find and replace the following:
- `MyPSR4Plugin` → YourPluginNamespace
- `My PSR-4 Plugin` → Your plugin name
- `my-psr4-plugin` → your-plugin-slug
- `mypp_` → yourprefix_
- `MYPP_` → YOURPREFIX_
- `yourname/my-psr4-plugin` → your-composer/package-name
- `https://example.com` → Your website
- `Your Name` → Your name
4. Run `composer install` in the plugin directory
5. Activate in WordPress admin
## Structure
```
my-psr4-plugin/
├── my-psr4-plugin.php # Main plugin file (bootstrapper)
├── composer.json # Composer dependencies and autoloading
├── uninstall.php # Cleanup on uninstall
├── README.md # This file
├── src/ # Source code (PSR-4 autoloaded)
│ ├── Plugin.php # Main plugin class
│ ├── PostTypes/ # Custom post types
│ │ └── Book.php
│ ├── Taxonomies/ # Taxonomies
│ │ └── Genre.php
│ ├── Admin/ # Admin functionality
│ │ └── Settings.php
│ ├── Frontend/ # Frontend functionality
│ │ └── Assets.php
│ └── API/ # REST API endpoints
│ └── BookEndpoints.php
├── assets/ # CSS/JS files (create as needed)
│ ├── css/
│ │ ├── admin-style.css
│ │ └── style.css
│ └── js/
│ ├── admin-script.js
│ └── script.js
├── languages/ # Translation files (create as needed)
└── vendor/ # Composer dependencies (auto-generated)
```
## Composer Setup
### Install Dependencies
```bash
cd wp-content/plugins/my-psr4-plugin
composer install
```
### Development Dependencies
The template includes WordPress Coding Standards (WPCS) for code quality:
```bash
# Check code standards
composer phpcs
# Fix code standards automatically
composer phpcbf
```
### Update Dependencies
```bash
composer update
```
## Namespaces
The plugin uses the following namespace structure:
- `MyPSR4Plugin\` - Root namespace
- `MyPSR4Plugin\PostTypes\` - Custom post types
- `MyPSR4Plugin\Taxonomies\` - Taxonomies
- `MyPSR4Plugin\Admin\` - Admin functionality
- `MyPSR4Plugin\Frontend\` - Frontend functionality
- `MyPSR4Plugin\API\` - REST API endpoints
## Included Examples
### Custom Post Type (src/PostTypes/Book.php)
- "Books" post type with Gutenberg support
- Meta boxes for ISBN, Author, Publication Year
- Nonce verification and sanitization
### Taxonomy (src/Taxonomies/Genre.php)
- "Genres" hierarchical taxonomy
- Linked to Books post type
### Admin Settings (src/Admin/Settings.php)
- Located in Settings → PSR-4 Plugin
- Uses WordPress Settings API
- Multiple field types with validation
### REST API (src/API/BookEndpoints.php)
- `GET /wp-json/mypp/v1/books` - List all books
- `GET /wp-json/mypp/v1/books/{id}` - Get single book
- `POST /wp-json/mypp/v1/books` - Create book
- Permission callbacks and validation
### Assets (src/Frontend/Assets.php)
- Conditional loading (admin + frontend)
- Localized script data
## Adding New Classes
### 1. Create Class File
Create `src/YourNamespace/YourClass.php`:
```php
<?php
namespace MyPSR4Plugin\YourNamespace;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class YourClass {
private static $instance = null;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
// Initialize
}
}
```
### 2. Initialize in Plugin.php
Add to `src/Plugin.php` in the `init()` method:
```php
YourNamespace\YourClass::get_instance();
```
### 3. Composer Will Auto-Load
No need to manually require files! Composer's PSR-4 autoloader handles it.
## Security Checklist
- [x] ABSPATH check in every file
- [x] Namespaces prevent naming conflicts
- [x] Private constructors (singletons)
- [x] Nonces for all forms
- [x] Capability checks (current_user_can)
- [x] Input sanitization (sanitize_text_field, absint)
- [x] Output escaping (esc_html, esc_attr)
- [x] REST API permission callbacks
- [x] REST API argument validation
- [x] Conditional asset loading
- [x] Composer autoloader check
## Advantages of PSR-4 Pattern
**Professional** - Industry-standard autoloading
**No manual requires** - Composer handles class loading
**Organized** - Classes grouped by domain/functionality
**Scalable** - Easy to add new features
**Testable** - Each class is independently testable
**Standards** - Compatible with modern PHP tooling
**Namespaces** - Prevents naming conflicts
**Version control** - Clean git diffs (no huge files)
## When to Use PSR-4
**Use PSR-4 when**:
- Plugin has 20+ classes
- Multiple developers
- Need professional architecture
- Plan to add many features over time
- Want to use Composer packages
- Need unit testing
- Building a commercial plugin
**Use OOP when**:
- Plugin has 10-20 classes
- Single developer
- Don't need Composer dependencies
**Use Simple when**:
- Plugin has <10 classes
- Quick, focused functionality
## Development Workflow
### 1. Create New Feature Class
```bash
# Example: Create a new AJAX handler
touch src/AJAX/BookActions.php
```
### 2. Write Class with Namespace
```php
<?php
namespace MyPSR4Plugin\AJAX;
class BookActions {
// Your code
}
```
### 3. Initialize in Plugin.php
```php
AJAX\BookActions::get_instance();
```
### 4. Test
No need to run `composer dump-autoload` - PSR-4 discovers classes automatically!
## Code Quality
### Check Code Standards
```bash
composer phpcs
```
### Fix Code Standards
```bash
composer phpcbf
```
### Add Custom Rules
Edit `composer.json` scripts:
```json
"scripts": {
"phpcs": "phpcs --standard=WordPress --extensions=php src/",
"phpcbf": "phpcbf --standard=WordPress --extensions=php src/"
}
```
## Distribution & Auto-Updates
### Enabling GitHub Auto-Updates
You can provide automatic updates from GitHub without submitting to WordPress.org:
**1. Install Plugin Update Checker via Composer:**
```bash
composer require yahnis-elsts/plugin-update-checker
```
Or as git submodule:
```bash
git submodule add https://github.com/YahnisElsts/plugin-update-checker.git
```
**2. Create Updater class:**
Create `src/Updater.php`:
```php
<?php
namespace MyPSR4Plugin;
use YahnisElsts\PluginUpdateChecker\v5\PucFactory;
class Updater {
private static $instance = null;
private $updateChecker;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->init_updater();
}
private function init_updater() {
$updater_path = plugin_dir_path( __DIR__ ) . 'vendor/yahnis-elsts/plugin-update-checker/plugin-update-checker.php';
if ( ! file_exists( $updater_path ) ) {
return;
}
require $updater_path;
$this->updateChecker = PucFactory::buildUpdateChecker(
'https://github.com/yourusername/your-plugin/',
plugin_dir_path( __DIR__ ) . 'my-psr4-plugin.php',
'your-plugin-slug'
);
$this->updateChecker->setBranch( 'main' );
$this->updateChecker->getVcsApi()->enableReleaseAssets();
// Private repo authentication
if ( defined( 'MYPP_GITHUB_TOKEN' ) ) {
$this->updateChecker->setAuthentication( MYPP_GITHUB_TOKEN );
}
}
}
```
**3. Initialize in Plugin.php:**
Add to `src/Plugin.php` `init()` method:
```php
Updater::get_instance();
```
**4. For private repos, add token to wp-config.php:**
```php
define( 'MYPP_GITHUB_TOKEN', 'ghp_xxxxxxxxxxxxx' );
```
**5. Create releases:**
```bash
# Update version in plugin header
git add my-psr4-plugin.php
git commit -m "Bump version to 1.0.1"
git tag 1.0.1
git push origin main
git push origin 1.0.1
# Create GitHub Release with pre-built ZIP
```
**6. Build release ZIP:**
```bash
#!/bin/bash
# build-release.sh
VERSION="1.0.1"
PLUGIN_SLUG="my-psr4-plugin"
# Install dependencies (no dev packages)
composer install --no-dev --optimize-autoloader
# Create ZIP
zip -r "${PLUGIN_SLUG}-${VERSION}.zip" \
"${PLUGIN_SLUG}/" \
-x "*.git*" "*.github/*" "tests/*" "node_modules/*"
echo "Built: ${PLUGIN_SLUG}-${VERSION}.zip"
```
### Resources
- **Complete Guide**: See `references/github-auto-updates.md`
- **Implementation Examples**: See `examples/github-updater.php`
- **Plugin Update Checker**: https://github.com/YahnisElsts/plugin-update-checker
## Resources
- [WordPress Plugin Handbook](https://developer.wordpress.org/plugins/)
- [PSR-4 Autoloading](https://www.php-fig.org/psr/psr-4/)
- [Composer Documentation](https://getcomposer.org/doc/)
- [WordPress Coding Standards](https://developer.wordpress.org/coding-standards/)
## License
GPL v2 or later

View File

@@ -0,0 +1,33 @@
{
"name": "yourname/my-psr4-plugin",
"description": "A modern WordPress plugin using PSR-4 autoloading",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"authors": [
{
"name": "Your Name",
"email": "your@email.com"
}
],
"require": {
"php": ">=7.4"
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.7",
"wp-coding-standards/wpcs": "^3.0"
},
"autoload": {
"psr-4": {
"MyPSR4Plugin\\": "src/"
}
},
"scripts": {
"phpcs": "phpcs --standard=WordPress src/",
"phpcbf": "phpcbf --standard=WordPress src/"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* Plugin Name: My PSR-4 Plugin
* Plugin URI: https://example.com/my-psr4-plugin/
* Description: A modern WordPress plugin using PSR-4 autoloading with Composer and namespaces.
* Version: 1.0.0
* Requires at least: 5.9
* Requires PHP: 7.4
* Author: Your Name
* Author URI: https://example.com/
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: my-psr4-plugin
* Domain Path: /languages
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Define plugin constants
define( 'MYPP_VERSION', '1.0.0' );
define( 'MYPP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'MYPP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'MYPP_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
// Require Composer autoloader
if ( file_exists( MYPP_PLUGIN_DIR . 'vendor/autoload.php' ) ) {
require_once MYPP_PLUGIN_DIR . 'vendor/autoload.php';
} else {
// Show admin notice if autoloader is missing
add_action( 'admin_notices', function() {
?>
<div class="notice notice-error">
<p>
<?php
printf(
/* translators: %s: command to run */
esc_html__( 'My PSR-4 Plugin requires Composer dependencies. Please run %s in the plugin directory.', 'my-psr4-plugin' ),
'<code>composer install</code>'
);
?>
</p>
</div>
<?php
} );
return;
}
// Initialize plugin
\MyPSR4Plugin\Plugin::get_instance();
// Activation hook
register_activation_hook( __FILE__, array( \MyPSR4Plugin\Plugin::get_instance(), 'activate' ) );
// Deactivation hook
register_deactivation_hook( __FILE__, array( \MyPSR4Plugin\Plugin::get_instance(), 'deactivate' ) );

View File

@@ -0,0 +1,251 @@
<?php
/**
* Book REST API Endpoints
*
* @package MyPSR4Plugin\API
*/
namespace MyPSR4Plugin\API;
use MyPSR4Plugin\PostTypes\Book;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Book REST API endpoints class
*/
class BookEndpoints {
/**
* Single instance
*
* @var BookEndpoints
*/
private static $instance = null;
/**
* Namespace
*
* @var string
*/
const NAMESPACE = 'mypp/v1';
/**
* Get instance
*
* @return BookEndpoints
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
}
/**
* Register REST routes
*/
public function register_routes() {
// Get all books
register_rest_route(
self::NAMESPACE,
'/books',
array(
'methods' => 'GET',
'callback' => array( $this, 'get_books' ),
'permission_callback' => '__return_true',
)
);
// Get single book
register_rest_route(
self::NAMESPACE,
'/books/(?P<id>\d+)',
array(
'methods' => 'GET',
'callback' => array( $this, 'get_book' ),
'permission_callback' => '__return_true',
'args' => array(
'id' => array(
'required' => true,
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
'sanitize_callback' => 'absint',
),
),
)
);
// Create book
register_rest_route(
self::NAMESPACE,
'/books',
array(
'methods' => 'POST',
'callback' => array( $this, 'create_book' ),
'permission_callback' => array( $this, 'check_permission' ),
'args' => $this->get_book_args(),
)
);
}
/**
* Get books
*
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response|\WP_Error
*/
public function get_books( $request ) {
$args = array(
'post_type' => Book::POST_TYPE,
'posts_per_page' => 10,
'post_status' => 'publish',
);
$books = get_posts( $args );
$data = array();
foreach ( $books as $book ) {
$data[] = $this->prepare_book_data( $book );
}
return rest_ensure_response( $data );
}
/**
* Get single book
*
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response|\WP_Error
*/
public function get_book( $request ) {
$book_id = $request->get_param( 'id' );
$book = get_post( $book_id );
if ( ! $book || Book::POST_TYPE !== $book->post_type ) {
return new \WP_Error(
'not_found',
__( 'Book not found', 'my-psr4-plugin' ),
array( 'status' => 404 )
);
}
$data = $this->prepare_book_data( $book );
return rest_ensure_response( $data );
}
/**
* Create book
*
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response|\WP_Error
*/
public function create_book( $request ) {
$title = $request->get_param( 'title' );
$content = $request->get_param( 'content' );
$isbn = $request->get_param( 'isbn' );
$author = $request->get_param( 'author' );
$year = $request->get_param( 'year' );
$post_id = wp_insert_post( array(
'post_title' => $title,
'post_content' => $content,
'post_type' => Book::POST_TYPE,
'post_status' => 'draft',
) );
if ( is_wp_error( $post_id ) ) {
return $post_id;
}
// Save meta data
if ( $isbn ) {
update_post_meta( $post_id, '_mypp_isbn', $isbn );
}
if ( $author ) {
update_post_meta( $post_id, '_mypp_author', $author );
}
if ( $year ) {
update_post_meta( $post_id, '_mypp_year', $year );
}
$book = get_post( $post_id );
$data = $this->prepare_book_data( $book );
return rest_ensure_response( $data );
}
/**
* Prepare book data for response
*
* @param \WP_Post $book Post object.
* @return array
*/
private function prepare_book_data( $book ) {
return array(
'id' => $book->ID,
'title' => $book->post_title,
'content' => $book->post_content,
'excerpt' => $book->post_excerpt,
'isbn' => get_post_meta( $book->ID, '_mypp_isbn', true ),
'author' => get_post_meta( $book->ID, '_mypp_author', true ),
'year' => (int) get_post_meta( $book->ID, '_mypp_year', true ),
);
}
/**
* Check permission
*
* @return bool
*/
public function check_permission() {
return current_user_can( 'edit_posts' );
}
/**
* Get book arguments for validation
*
* @return array
*/
private function get_book_args() {
return array(
'title' => array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'content' => array(
'required' => false,
'type' => 'string',
'sanitize_callback' => 'wp_kses_post',
),
'isbn' => array(
'required' => false,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'author' => array(
'required' => false,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'year' => array(
'required' => false,
'type' => 'integer',
'sanitize_callback' => 'absint',
),
);
}
}

View File

@@ -0,0 +1,248 @@
<?php
/**
* Admin Settings
*
* @package MyPSR4Plugin\Admin
*/
namespace MyPSR4Plugin\Admin;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Settings class
*/
class Settings {
/**
* Single instance
*
* @var Settings
*/
private static $instance = null;
/**
* Option name
*
* @var string
*/
const OPTION_NAME = 'mypp_settings';
/**
* Get instance
*
* @return Settings
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
add_action( 'admin_menu', array( $this, 'add_menu_page' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
}
/**
* Add menu page
*/
public function add_menu_page() {
add_options_page(
__( 'My PSR-4 Plugin Settings', 'my-psr4-plugin' ),
__( 'PSR-4 Plugin', 'my-psr4-plugin' ),
'manage_options',
'my-psr4-plugin',
array( $this, 'render_settings_page' )
);
}
/**
* Register settings
*/
public function register_settings() {
register_setting(
'mypp_settings_group',
self::OPTION_NAME,
array( $this, 'sanitize_settings' )
);
add_settings_section(
'mypp_general_section',
__( 'General Settings', 'my-psr4-plugin' ),
array( $this, 'render_section_description' ),
'my-psr4-plugin'
);
add_settings_field(
'mypp_option1',
__( 'Text Option', 'my-psr4-plugin' ),
array( $this, 'render_text_field' ),
'my-psr4-plugin',
'mypp_general_section',
array( 'field_id' => 'option1' )
);
add_settings_field(
'mypp_option2',
__( 'Number Option', 'my-psr4-plugin' ),
array( $this, 'render_number_field' ),
'my-psr4-plugin',
'mypp_general_section',
array( 'field_id' => 'option2' )
);
add_settings_field(
'mypp_option3',
__( 'Checkbox Option', 'my-psr4-plugin' ),
array( $this, 'render_checkbox_field' ),
'my-psr4-plugin',
'mypp_general_section',
array( 'field_id' => 'option3' )
);
}
/**
* Render settings page
*/
public function render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form method="post" action="options.php">
<?php
settings_fields( 'mypp_settings_group' );
do_settings_sections( 'my-psr4-plugin' );
submit_button( __( 'Save Settings', 'my-psr4-plugin' ) );
?>
</form>
</div>
<?php
}
/**
* Render section description
*/
public function render_section_description() {
echo '<p>' . esc_html__( 'Configure your plugin settings below.', 'my-psr4-plugin' ) . '</p>';
}
/**
* Render text field
*
* @param array $args Field arguments.
*/
public function render_text_field( $args ) {
$settings = $this->get_settings();
$field_id = $args['field_id'];
$value = isset( $settings[ $field_id ] ) ? $settings[ $field_id ] : '';
printf(
'<input type="text" id="mypp_%1$s" name="%2$s[%1$s]" value="%3$s" class="regular-text" />',
esc_attr( $field_id ),
esc_attr( self::OPTION_NAME ),
esc_attr( $value )
);
}
/**
* Render number field
*
* @param array $args Field arguments.
*/
public function render_number_field( $args ) {
$settings = $this->get_settings();
$field_id = $args['field_id'];
$value = isset( $settings[ $field_id ] ) ? $settings[ $field_id ] : 0;
printf(
'<input type="number" id="mypp_%1$s" name="%2$s[%1$s]" value="%3$s" min="0" max="100" class="small-text" />',
esc_attr( $field_id ),
esc_attr( self::OPTION_NAME ),
esc_attr( $value )
);
}
/**
* Render checkbox field
*
* @param array $args Field arguments.
*/
public function render_checkbox_field( $args ) {
$settings = $this->get_settings();
$field_id = $args['field_id'];
$value = isset( $settings[ $field_id ] ) ? $settings[ $field_id ] : false;
printf(
'<input type="checkbox" id="mypp_%1$s" name="%2$s[%1$s]" value="1" %3$s />',
esc_attr( $field_id ),
esc_attr( self::OPTION_NAME ),
checked( $value, true, false )
);
}
/**
* Sanitize settings
*
* @param array $input Input array.
* @return array
*/
public function sanitize_settings( $input ) {
$sanitized = array();
if ( isset( $input['option1'] ) ) {
$sanitized['option1'] = sanitize_text_field( $input['option1'] );
}
if ( isset( $input['option2'] ) ) {
$sanitized['option2'] = absint( $input['option2'] );
}
if ( isset( $input['option3'] ) ) {
$sanitized['option3'] = (bool) $input['option3'];
}
return $sanitized;
}
/**
* Get settings
*
* @return array
*/
public function get_settings() {
$defaults = array(
'option1' => '',
'option2' => 0,
'option3' => false,
);
$settings = get_option( self::OPTION_NAME, $defaults );
return wp_parse_args( $settings, $defaults );
}
/**
* Set default settings on activation
*/
public function set_defaults() {
if ( false === get_option( self::OPTION_NAME ) ) {
add_option( self::OPTION_NAME, array(
'option1' => '',
'option2' => 0,
'option3' => false,
) );
}
}
}

View File

@@ -0,0 +1,111 @@
<?php
/**
* Frontend Assets
*
* @package MyPSR4Plugin\Frontend
*/
namespace MyPSR4Plugin\Frontend;
use MyPSR4Plugin\PostTypes\Book;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Assets class
*/
class Assets {
/**
* Single instance
*
* @var Assets
*/
private static $instance = null;
/**
* Get instance
*
* @return Assets
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
}
/**
* Enqueue frontend scripts and styles
*/
public function enqueue_scripts() {
// Only load on single book pages
if ( is_singular( Book::POST_TYPE ) ) {
wp_enqueue_style(
'mypp-style',
MYPP_PLUGIN_URL . 'assets/css/style.css',
array(),
MYPP_VERSION
);
wp_enqueue_script(
'mypp-script',
MYPP_PLUGIN_URL . 'assets/js/script.js',
array( 'jquery' ),
MYPP_VERSION,
true
);
}
}
/**
* Enqueue admin scripts and styles
*
* @param string $hook Current admin page hook.
*/
public function enqueue_admin_scripts( $hook ) {
// Only load on book edit pages
if ( 'post.php' !== $hook && 'post-new.php' !== $hook && 'edit.php' !== $hook ) {
return;
}
$screen = get_current_screen();
if ( $screen && Book::POST_TYPE === $screen->post_type ) {
wp_enqueue_style(
'mypp-admin-style',
MYPP_PLUGIN_URL . 'assets/css/admin-style.css',
array(),
MYPP_VERSION
);
wp_enqueue_script(
'mypp-admin-script',
MYPP_PLUGIN_URL . 'assets/js/admin-script.js',
array( 'jquery' ),
MYPP_VERSION,
true
);
// Localize script
wp_localize_script(
'mypp-admin-script',
'myppData',
array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'mypp_ajax_nonce' ),
)
);
}
}
}

View File

@@ -0,0 +1,130 @@
<?php
/**
* Main Plugin class
*
* @package MyPSR4Plugin
*/
namespace MyPSR4Plugin;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Main Plugin class
*/
class Plugin {
/**
* Single instance of the class
*
* @var Plugin
*/
private static $instance = null;
/**
* Get singleton instance
*
* @return Plugin
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->init_hooks();
}
/**
* Prevent cloning
*/
private function __clone() {}
/**
* Prevent unserializing
*/
public function __wakeup() {
throw new \Exception( 'Cannot unserialize singleton' );
}
/**
* Initialize WordPress hooks
*/
private function init_hooks() {
add_action( 'init', array( $this, 'init' ) );
add_action( 'admin_menu', array( $this, 'register_admin_pages' ) );
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
}
/**
* Initialize plugin
*/
public function init() {
// Load text domain
load_plugin_textdomain(
'my-psr4-plugin',
false,
dirname( MYPP_PLUGIN_BASENAME ) . '/languages'
);
// Initialize submodules
PostTypes\Book::get_instance();
Taxonomies\Genre::get_instance();
Admin\Settings::get_instance();
Frontend\Assets::get_instance();
API\BookEndpoints::get_instance();
}
/**
* Register admin pages
*/
public function register_admin_pages() {
// Admin pages are handled by Admin\Settings class
}
/**
* Register REST routes
*/
public function register_rest_routes() {
// REST routes are handled by API\BookEndpoints class
}
/**
* Plugin activation
*/
public function activate() {
// Register post types and taxonomies
PostTypes\Book::get_instance()->register();
Taxonomies\Genre::get_instance()->register();
// Flush rewrite rules
flush_rewrite_rules();
// Set default options
Admin\Settings::get_instance()->set_defaults();
// Set activation timestamp
if ( false === get_option( 'mypp_activated_time' ) ) {
add_option( 'mypp_activated_time', current_time( 'timestamp' ) );
}
}
/**
* Plugin deactivation
*/
public function deactivate() {
// Flush rewrite rules
flush_rewrite_rules();
// Clear scheduled events
wp_clear_scheduled_hook( 'mypp_cron_event' );
}
}

View File

@@ -0,0 +1,215 @@
<?php
/**
* Book Custom Post Type
*
* @package MyPSR4Plugin\PostTypes
*/
namespace MyPSR4Plugin\PostTypes;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Book post type class
*/
class Book {
/**
* Single instance
*
* @var Book
*/
private static $instance = null;
/**
* Post type slug
*
* @var string
*/
const POST_TYPE = 'book';
/**
* Get instance
*
* @return Book
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
add_action( 'init', array( $this, 'register' ) );
add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
add_action( 'save_post_' . self::POST_TYPE, array( $this, 'save_meta' ), 10, 2 );
}
/**
* Register post type
*/
public function register() {
$labels = array(
'name' => _x( 'Books', 'post type general name', 'my-psr4-plugin' ),
'singular_name' => _x( 'Book', 'post type singular name', 'my-psr4-plugin' ),
'menu_name' => _x( 'Books', 'admin menu', 'my-psr4-plugin' ),
'add_new' => _x( 'Add New', 'book', 'my-psr4-plugin' ),
'add_new_item' => __( 'Add New Book', 'my-psr4-plugin' ),
'edit_item' => __( 'Edit Book', 'my-psr4-plugin' ),
'new_item' => __( 'New Book', 'my-psr4-plugin' ),
'view_item' => __( 'View Book', 'my-psr4-plugin' ),
'search_items' => __( 'Search Books', 'my-psr4-plugin' ),
'not_found' => __( 'No books found', 'my-psr4-plugin' ),
'not_found_in_trash' => __( 'No books found in Trash', 'my-psr4-plugin' ),
);
$args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'books' ),
'capability_type' => 'post',
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 5,
'menu_icon' => 'dashicons-book',
'show_in_rest' => true,
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
);
register_post_type( self::POST_TYPE, $args );
}
/**
* Add meta boxes
*/
public function add_meta_boxes() {
add_meta_box(
'mypp_book_details',
__( 'Book Details', 'my-psr4-plugin' ),
array( $this, 'render_meta_box' ),
self::POST_TYPE,
'normal',
'high'
);
}
/**
* Render meta box
*
* @param \WP_Post $post Post object.
*/
public function render_meta_box( $post ) {
// Add nonce for security
wp_nonce_field( 'mypp_save_book_meta', 'mypp_book_meta_nonce' );
// Get current values
$isbn = get_post_meta( $post->ID, '_mypp_isbn', true );
$author = get_post_meta( $post->ID, '_mypp_author', true );
$year = get_post_meta( $post->ID, '_mypp_year', true );
?>
<table class="form-table">
<tr>
<th><label for="mypp_isbn"><?php esc_html_e( 'ISBN', 'my-psr4-plugin' ); ?></label></th>
<td>
<input
type="text"
id="mypp_isbn"
name="mypp_isbn"
value="<?php echo esc_attr( $isbn ); ?>"
class="regular-text"
/>
</td>
</tr>
<tr>
<th><label for="mypp_author"><?php esc_html_e( 'Author', 'my-psr4-plugin' ); ?></label></th>
<td>
<input
type="text"
id="mypp_author"
name="mypp_author"
value="<?php echo esc_attr( $author ); ?>"
class="regular-text"
/>
</td>
</tr>
<tr>
<th><label for="mypp_year"><?php esc_html_e( 'Publication Year', 'my-psr4-plugin' ); ?></label></th>
<td>
<input
type="number"
id="mypp_year"
name="mypp_year"
value="<?php echo esc_attr( $year ); ?>"
min="1000"
max="9999"
class="small-text"
/>
</td>
</tr>
</table>
<?php
}
/**
* Save meta box data
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
*/
public function save_meta( $post_id, $post ) {
// Verify nonce
if ( ! isset( $_POST['mypp_book_meta_nonce'] ) ||
! wp_verify_nonce( $_POST['mypp_book_meta_nonce'], 'mypp_save_book_meta' ) ) {
return;
}
// Check autosave
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// Check permissions
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// Save ISBN
if ( isset( $_POST['mypp_isbn'] ) ) {
update_post_meta(
$post_id,
'_mypp_isbn',
sanitize_text_field( $_POST['mypp_isbn'] )
);
}
// Save Author
if ( isset( $_POST['mypp_author'] ) ) {
update_post_meta(
$post_id,
'_mypp_author',
sanitize_text_field( $_POST['mypp_author'] )
);
}
// Save Year
if ( isset( $_POST['mypp_year'] ) ) {
update_post_meta(
$post_id,
'_mypp_year',
absint( $_POST['mypp_year'] )
);
}
}
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* Genre Taxonomy
*
* @package MyPSR4Plugin\Taxonomies
*/
namespace MyPSR4Plugin\Taxonomies;
use MyPSR4Plugin\PostTypes\Book;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Genre taxonomy class
*/
class Genre {
/**
* Single instance
*
* @var Genre
*/
private static $instance = null;
/**
* Taxonomy slug
*
* @var string
*/
const TAXONOMY = 'genre';
/**
* Get instance
*
* @return Genre
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
add_action( 'init', array( $this, 'register' ) );
}
/**
* Register taxonomy
*/
public function register() {
$labels = array(
'name' => _x( 'Genres', 'taxonomy general name', 'my-psr4-plugin' ),
'singular_name' => _x( 'Genre', 'taxonomy singular name', 'my-psr4-plugin' ),
'search_items' => __( 'Search Genres', 'my-psr4-plugin' ),
'all_items' => __( 'All Genres', 'my-psr4-plugin' ),
'parent_item' => __( 'Parent Genre', 'my-psr4-plugin' ),
'parent_item_colon' => __( 'Parent Genre:', 'my-psr4-plugin' ),
'edit_item' => __( 'Edit Genre', 'my-psr4-plugin' ),
'update_item' => __( 'Update Genre', 'my-psr4-plugin' ),
'add_new_item' => __( 'Add New Genre', 'my-psr4-plugin' ),
'new_item_name' => __( 'New Genre Name', 'my-psr4-plugin' ),
'menu_name' => __( 'Genres', 'my-psr4-plugin' ),
);
$args = array(
'hierarchical' => true,
'labels' => $labels,
'show_ui' => true,
'show_admin_column' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'genre' ),
'show_in_rest' => true,
);
register_taxonomy( self::TAXONOMY, array( Book::POST_TYPE ), $args );
}
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* Uninstall script
*
* This file is called when the plugin is uninstalled via WordPress admin.
*/
// Exit if not called by WordPress
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
// Delete plugin options
delete_option( 'mypp_settings' );
delete_option( 'mypp_activated_time' );
// Delete transients
delete_transient( 'mypp_cache' );
// For multisite
if ( is_multisite() ) {
global $wpdb;
$blog_ids = $wpdb->get_col( "SELECT blog_id FROM $wpdb->blogs" );
foreach ( $blog_ids as $blog_id ) {
switch_to_blog( $blog_id );
delete_option( 'mypp_settings' );
delete_option( 'mypp_activated_time' );
delete_transient( 'mypp_cache' );
restore_current_blog();
}
}
// Delete custom post type data (optional)
/*
$books = get_posts( array(
'post_type' => 'book',
'posts_per_page' => -1,
'post_status' => 'any',
) );
foreach ( $books as $book ) {
wp_delete_post( $book->ID, true );
}
*/

View File

@@ -0,0 +1,181 @@
# Simple WordPress Plugin Template
This is a functional programming pattern for WordPress plugins. Best for small to medium plugins that don't require complex object-oriented architecture.
## Features
✅ Complete plugin header with all fields
✅ ABSPATH security check
✅ Unique function prefix (mysp_)
✅ Custom post type registration (Books)
✅ Admin settings page with nonce verification
✅ AJAX handler with security checks
✅ Proper activation/deactivation hooks
✅ Uninstall script for cleanup
✅ Internationalization ready
✅ Conditional asset loading
✅ Security best practices (sanitization, escaping, capability checks)
## Installation
1. Copy this folder to `wp-content/plugins/`
2. Rename folder and files to match your plugin name
3. Find and replace the following:
- `My Simple Plugin` → Your plugin name
- `my-simple-plugin` → your-plugin-slug
- `mysp_` → yourprefix_
- `MYSP_` → YOURPREFIX_
- `https://example.com` → Your website
- `Your Name` → Your name
4. Activate in WordPress admin
## Structure
```
my-simple-plugin/
├── my-simple-plugin.php # Main plugin file
├── uninstall.php # Cleanup on uninstall
├── README.md # This file
├── assets/ # CSS/JS files (create as needed)
│ ├── css/
│ │ ├── admin-style.css
│ │ └── style.css
│ └── js/
│ ├── admin-script.js
│ └── script.js
└── languages/ # Translation files (create as needed)
```
## Included Examples
### Custom Post Type
- Registers "Books" post type
- Gutenberg-enabled
- Archive page support
- Custom rewrite slug
### Settings Page
- Located in Settings → Simple Plugin
- Nonce verification
- Sanitization and validation
- Settings error handling
### AJAX Handler
- Action: `wp_ajax_mysp_action`
- Nonce verification
- Capability checking
- JSON response
### Activation/Deactivation
- Flushes rewrite rules
- Sets default options
- Cleans up scheduled events
### Uninstall
- Deletes all plugin options
- Clears transients
- Multisite support
- Optional: Delete custom post type data
## Security Checklist
- [x] ABSPATH check at top of file
- [x] Unique function prefix (mysp_)
- [x] Nonces for all forms
- [x] Capability checks (current_user_can)
- [x] Input sanitization (sanitize_text_field)
- [x] Output escaping (esc_html, esc_attr)
- [x] AJAX nonce verification (check_ajax_referer)
- [x] Conditional asset loading (don't load everywhere)
- [x] Proper uninstall cleanup
## Next Steps
1. Create the `assets/` directory structure
2. Add your CSS and JavaScript files
3. Extend with additional features:
- Meta boxes
- Shortcodes
- Widgets
- REST API endpoints
- Custom taxonomies
- WP-CLI commands
## Distribution & Auto-Updates
### Enabling GitHub Auto-Updates
You can provide automatic updates from GitHub without submitting to WordPress.org:
**1. Install Plugin Update Checker library:**
```bash
cd your-plugin/
git submodule add https://github.com/YahnisElsts/plugin-update-checker.git
```
**2. Add to your main plugin file:**
```php
// Include Plugin Update Checker
require plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php';
use YahnisElsts\PluginUpdateChecker\v5\PucFactory;
// Initialize update checker
$updateChecker = PucFactory::buildUpdateChecker(
'https://github.com/yourusername/your-plugin/',
__FILE__,
'your-plugin-slug'
);
// Set branch (default: main)
$updateChecker->setBranch( 'main' );
// Use GitHub Releases (recommended)
$updateChecker->getVcsApi()->enableReleaseAssets();
```
**3. For private repos, add token to wp-config.php:**
```php
define( 'YOUR_PLUGIN_GITHUB_TOKEN', 'ghp_xxxxxxxxxxxxx' );
```
Then in your plugin:
```php
if ( defined( 'YOUR_PLUGIN_GITHUB_TOKEN' ) ) {
$updateChecker->setAuthentication( YOUR_PLUGIN_GITHUB_TOKEN );
}
```
**4. Create releases:**
```bash
# Update version in plugin header
git add my-simple-plugin.php
git commit -m "Bump version to 1.0.1"
git tag 1.0.1
git push origin main
git push origin 1.0.1
# Create GitHub Release (optional but recommended)
# - Upload pre-built ZIP file
# - Add release notes
```
### Resources
- **Complete Guide**: See `references/github-auto-updates.md`
- **Implementation Examples**: See `examples/github-updater.php`
- **Plugin Update Checker**: https://github.com/YahnisElsts/plugin-update-checker
## Resources
- [WordPress Plugin Handbook](https://developer.wordpress.org/plugins/)
- [WordPress Coding Standards](https://developer.wordpress.org/coding-standards/)
- [Plugin Security](https://developer.wordpress.org/apis/security/)
## License
GPL v2 or later

View File

@@ -0,0 +1,272 @@
<?php
/**
* Plugin Name: My Simple Plugin
* Plugin URI: https://example.com/my-simple-plugin/
* Description: A simple WordPress plugin demonstrating functional programming pattern with security best practices.
* Version: 1.0.0
* Requires at least: 5.9
* Requires PHP: 7.4
* Author: Your Name
* Author URI: https://example.com/
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: my-simple-plugin
* Domain Path: /languages
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Define plugin constants
define( 'MYSP_VERSION', '1.0.0' );
define( 'MYSP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'MYSP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'MYSP_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
/**
* Initialize the plugin
*/
function mysp_init() {
// Load text domain for translations
load_plugin_textdomain( 'my-simple-plugin', false, dirname( MYSP_PLUGIN_BASENAME ) . '/languages' );
// Register custom post type (example)
mysp_register_book_post_type();
}
add_action( 'init', 'mysp_init' );
/**
* Register a custom post type (example)
*/
function mysp_register_book_post_type() {
$labels = array(
'name' => _x( 'Books', 'post type general name', 'my-simple-plugin' ),
'singular_name' => _x( 'Book', 'post type singular name', 'my-simple-plugin' ),
'menu_name' => _x( 'Books', 'admin menu', 'my-simple-plugin' ),
'add_new' => _x( 'Add New', 'book', 'my-simple-plugin' ),
'add_new_item' => __( 'Add New Book', 'my-simple-plugin' ),
'edit_item' => __( 'Edit Book', 'my-simple-plugin' ),
'new_item' => __( 'New Book', 'my-simple-plugin' ),
'view_item' => __( 'View Book', 'my-simple-plugin' ),
'search_items' => __( 'Search Books', 'my-simple-plugin' ),
'not_found' => __( 'No books found', 'my-simple-plugin' ),
'not_found_in_trash' => __( 'No books found in Trash', 'my-simple-plugin' ),
);
$args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'books' ),
'capability_type' => 'post',
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 5,
'menu_icon' => 'dashicons-book',
'show_in_rest' => true, // Enable Gutenberg editor
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
);
register_post_type( 'book', $args );
}
/**
* Enqueue admin scripts and styles
*/
function mysp_admin_enqueue_scripts( $hook ) {
// Only load on specific admin pages
if ( 'edit.php' !== $hook && 'post.php' !== $hook && 'post-new.php' !== $hook ) {
return;
}
$screen = get_current_screen();
if ( $screen && 'book' === $screen->post_type ) {
wp_enqueue_style(
'mysp-admin-style',
MYSP_PLUGIN_URL . 'assets/css/admin-style.css',
array(),
MYSP_VERSION
);
wp_enqueue_script(
'mysp-admin-script',
MYSP_PLUGIN_URL . 'assets/js/admin-script.js',
array( 'jquery' ),
MYSP_VERSION,
true
);
// Localize script with nonce and AJAX URL
wp_localize_script(
'mysp-admin-script',
'myspData',
array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'mysp_ajax_nonce' ),
)
);
}
}
add_action( 'admin_enqueue_scripts', 'mysp_admin_enqueue_scripts' );
/**
* Enqueue frontend scripts and styles
*/
function mysp_enqueue_scripts() {
// Only load on single book pages
if ( is_singular( 'book' ) ) {
wp_enqueue_style(
'mysp-style',
MYSP_PLUGIN_URL . 'assets/css/style.css',
array(),
MYSP_VERSION
);
wp_enqueue_script(
'mysp-script',
MYSP_PLUGIN_URL . 'assets/js/script.js',
array( 'jquery' ),
MYSP_VERSION,
true
);
}
}
add_action( 'wp_enqueue_scripts', 'mysp_enqueue_scripts' );
/**
* Add settings page to admin menu
*/
function mysp_add_settings_page() {
add_options_page(
__( 'My Simple Plugin Settings', 'my-simple-plugin' ),
__( 'Simple Plugin', 'my-simple-plugin' ),
'manage_options',
'my-simple-plugin',
'mysp_render_settings_page'
);
}
add_action( 'admin_menu', 'mysp_add_settings_page' );
/**
* Render settings page
*/
function mysp_render_settings_page() {
// Check user capabilities
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Handle form submission
if ( isset( $_POST['mysp_settings_submit'] ) ) {
// Verify nonce
if ( ! isset( $_POST['mysp_settings_nonce'] ) || ! wp_verify_nonce( $_POST['mysp_settings_nonce'], 'mysp_settings_action' ) ) {
wp_die( __( 'Security check failed', 'my-simple-plugin' ) );
}
// Sanitize and save option
$option_value = isset( $_POST['mysp_option'] ) ? sanitize_text_field( $_POST['mysp_option'] ) : '';
update_option( 'mysp_option', $option_value );
// Show success message
add_settings_error(
'mysp_messages',
'mysp_message',
__( 'Settings Saved', 'my-simple-plugin' ),
'updated'
);
}
// Get current option value
$option_value = get_option( 'mysp_option', '' );
// Display settings page
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<?php settings_errors( 'mysp_messages' ); ?>
<form method="post" action="">
<?php wp_nonce_field( 'mysp_settings_action', 'mysp_settings_nonce' ); ?>
<table class="form-table">
<tr>
<th scope="row">
<label for="mysp_option"><?php esc_html_e( 'Example Option', 'my-simple-plugin' ); ?></label>
</th>
<td>
<input
type="text"
id="mysp_option"
name="mysp_option"
value="<?php echo esc_attr( $option_value ); ?>"
class="regular-text"
/>
<p class="description"><?php esc_html_e( 'Enter some text here', 'my-simple-plugin' ); ?></p>
</td>
</tr>
</table>
<?php submit_button( __( 'Save Settings', 'my-simple-plugin' ), 'primary', 'mysp_settings_submit' ); ?>
</form>
</div>
<?php
}
/**
* Example AJAX handler
*/
function mysp_ajax_handler() {
// Verify nonce
check_ajax_referer( 'mysp_ajax_nonce', 'nonce' );
// Check user capability
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => __( 'Permission denied', 'my-simple-plugin' ) ) );
}
// Get and sanitize input
$input = isset( $_POST['data'] ) ? sanitize_text_field( $_POST['data'] ) : '';
// Process and return response
wp_send_json_success( array(
'message' => __( 'Success!', 'my-simple-plugin' ),
'data' => $input,
) );
}
add_action( 'wp_ajax_mysp_action', 'mysp_ajax_handler' );
/**
* Activation hook
*/
function mysp_activate() {
// Register post type before flushing rewrite rules
mysp_register_book_post_type();
// Flush rewrite rules
flush_rewrite_rules();
// Set default options
if ( false === get_option( 'mysp_option' ) ) {
add_option( 'mysp_option', '' );
}
// Set activation timestamp
if ( false === get_option( 'mysp_activated_time' ) ) {
add_option( 'mysp_activated_time', current_time( 'timestamp' ) );
}
}
register_activation_hook( __FILE__, 'mysp_activate' );
/**
* Deactivation hook
*/
function mysp_deactivate() {
// Flush rewrite rules
flush_rewrite_rules();
// Clear any scheduled events
wp_clear_scheduled_hook( 'mysp_cron_event' );
}
register_deactivation_hook( __FILE__, 'mysp_deactivate' );

View File

@@ -0,0 +1,56 @@
<?php
/**
* Uninstall script
*
* This file is called when the plugin is uninstalled via WordPress admin.
* It should clean up all plugin data.
*/
// Exit if not called by WordPress
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
// Delete plugin options
delete_option( 'mysp_option' );
delete_option( 'mysp_activated_time' );
// Delete transients
delete_transient( 'mysp_cache' );
// For multisite, delete from all sites
if ( is_multisite() ) {
global $wpdb;
$blog_ids = $wpdb->get_col( "SELECT blog_id FROM $wpdb->blogs" );
foreach ( $blog_ids as $blog_id ) {
switch_to_blog( $blog_id );
delete_option( 'mysp_option' );
delete_option( 'mysp_activated_time' );
delete_transient( 'mysp_cache' );
restore_current_blog();
}
}
// Delete custom post type data (optional - consider if users want to keep content)
// Uncomment the following if you want to delete all custom post type posts on uninstall
/*
$books = get_posts( array(
'post_type' => 'book',
'posts_per_page' => -1,
'post_status' => 'any',
) );
foreach ( $books as $book ) {
wp_delete_post( $book->ID, true ); // true = force delete (skip trash)
}
*/
// If you created custom database tables, drop them here
/*
global $wpdb;
$table_name = $wpdb->prefix . 'mysp_custom_table';
$wpdb->query( "DROP TABLE IF EXISTS $table_name" );
*/