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,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 );
}
*/