Build WordPress plugins from scratch: actions and filters, shortcodes, settings pages, custom post types, meta boxes, AJAX, REST endpoints, custom tables, cron and blocks.
Lessons
As a plugin grows past a few hundred lines, organising it into namespaced classes — each responsible for one area, loaded by an autoloader and wired together by a single bootstrap — keeps it understandable, testable and free of naming collisions.
Everything so far used the gdc_books_ prefix to avoid collisions. A PHP namespace does the same job for classes, more cleanly. GDC\Books\PostTypes can never clash with another plugin's PostTypes.
gdc-books/
├── gdc-books.php Header + bootstrap only
├── composer.json
└── src/
├── Plugin.php Wires every component together
├── PostTypes.php Book post type + taxonomies
├── Admin/
│ ├── SettingsPage.php
│ └── MetaBoxes.php
├── Rest/
│ └── BooksController.php
└── Shortcodes.php{
"name": "gizdevcraft/gdc-books",
"autoload": {
"psr-4": {
"GDC\\Books\\": "src/"
}
}
}Run composer dump-autoload. Now new GDC\Books\PostTypes() loads src/PostTypes.php automatically — no more lists of require_once.
<?php
namespace GDC\Books;
defined( 'ABSPATH' ) || exit;
/**
* Registers the Book post type and its taxonomies.
*/
final class PostTypes {
public const POST_TYPE = 'gdc_book';
public function register(): void {
add_action( 'init', array( $this, 'register_post_type' ) );
add_action( 'init', array( $this, 'register_taxonomies' ) );
}
public function register_post_type(): void {
register_post_type(
self::POST_TYPE,
array(
'labels' => array( 'name' => __( 'Books', 'gdc-books' ) ),
'public' => true,
'has_archive' => 'books',
'show_in_rest' => true,
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
)
);
}
public function register_taxonomies(): void {
register_taxonomy(
'gdc_genre',
self::POST_TYPE,
array(
'hierarchical' => true,
'show_in_rest' => true,
)
);
}
}The constructor does nothing. All hooking happens in register(), which the bootstrap calls. That separation means a test can create the object without side effects.
<?php
namespace GDC\Books;
final class Plugin {
/** @var object[] */
private array $components;
public function __construct() {
$this->components = array(
new PostTypes(),
new Shortcodes(),
new Rest\BooksController(),
);
if ( is_admin() ) {
$this->components[] = new Admin\SettingsPage();
$this->components[] = new Admin\MetaBoxes();
}
}
public function run(): void {
foreach ( $this->components as $component ) {
$component->register();
}
}
}<?php
/**
* Plugin Name: GDC Books
* Version: 2.0.0
* Requires PHP: 7.4
* Text Domain: gdc-books
*/
defined( 'ABSPATH' ) || exit;
require_once __DIR__ . '/vendor/autoload.php';
add_action(
'plugins_loaded',
static function () {
( new GDC\Books\Plugin() )->run();
}
);vendor/** in the release zip (or use a simple spl_autoload_register() loader), because users do not run Composer.GDC\Books\PostTypes with a register() method.Plugin class that runs every component.Admin\SettingsPage and load it only in the admin.In accordance with Section 6 & Section 7 of the DPDP Act 2023, third-party analytics and marketing trackers are strictly gated behind your affirmative consent. You may withdraw consent at any time under Section 6(4).
Essential for session authentication (ci_session), Cross-Site Request Forgery security (csrf_cookie_name), and Cloudflare DDoS/bot prevention. Cannot be disabled.
Gated under DPDP Section 7. Allows Google Analytics 4 (Google Consent Mode v2) to measure aggregate, anonymized tool usage.
Enables browser local storage to preserve your theme selection (dark/light mode) and code editor layout between visits.
Used to display developer sponsorships and native tool partner notices.