Creating Reusable PHP Components
Learning Objectives
- Master PHP programming concepts
- Write clean, maintainable code
- Apply best practices
- Build dynamic applications
Understanding Reusable Components
Think of reusable PHP components as LEGO blocks for your web applications. Just as you can build complex structures with the same LEGO pieces, reusable components let you construct sophisticated websites while writing code only once. This approach is the foundation of modern web development, especially in WordPress theme and plugin creation.
The DRY Principle: Don't Repeat Yourself
Reusable components embody the DRY principle - a fundamental concept in software engineering that emphasizes reducing repetition in code.
By creating components that can be reused throughout your application, you improve:
- Maintainability: Fix bugs or make improvements in one place
- Consistency: Ensure uniform behavior and appearance
- Development speed: Build new features by assembling existing components
- Code organization: Create cleaner, more modular architecture
Types of Reusable PHP Components
Include Files
The simplest form of reusable components in PHP is the include file - a separate PHP file that can be included in multiple pages.
Example: Header Include
// File: header.php
<?php
function display_site_header($page_title) {
echo '<!DOCTYPE html>';
echo '<html lang="en">';
echo '<head>';
echo ' <meta charset="UTF-8">';
echo ' <title>' . htmlspecialchars($page_title) . ' - My Website</title>';
echo ' <link rel="stylesheet" href="/css/style.css">';
echo '</head>';
echo '<body>';
echo ' <header>';
echo ' <nav>';
echo ' <ul>';
echo ' <li><a href="index.php">Home</a></li>';
echo ' <li><a href="about.php">About</a></li>';
echo ' <li><a href="contact.php">Contact</a></li>';
echo ' </ul>';
echo ' </nav>';
echo ' </header>';
echo ' <main>';
}
?>
Usage in a Page
<?php
// File: index.php
require_once 'header.php';
display_site_header('Home Page');
// Page content goes here
echo '<h1>Welcome to our website!</h1>';
echo '<p>This is the home page content.</p>';
require_once 'footer.php';
?>
Real-World Application
In WordPress themes, the get_header() and get_footer() functions work exactly this way, loading standard components across all pages. This pattern ensures consistency while simplifying maintenance.
Functions and Function Libraries
Functions are the workhorses of reusable code. Think of them as specialized tools in your toolbox - each designed for a specific purpose but usable in many situations.
Example: Function Library for Form Validation
// File: validation_functions.php
<?php
/**
* Validates an email address
*
* @param string $email The email to validate
* @return bool True if valid, false otherwise
*/
function is_valid_email($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
/**
* Validates that a string has minimum length
*
* @param string $string The string to check
* @param int $min_length The minimum required length
* @return bool True if valid, false otherwise
*/
function has_min_length($string, $min_length) {
return strlen(trim($string)) >= $min_length;
}
/**
* Validates that a password meets security requirements
*
* @param string $password The password to validate
* @return bool True if valid, false otherwise
*/
function is_secure_password($password) {
// At least 8 characters, containing uppercase, lowercase, and number
$has_min_length = strlen($password) >= 8;
$has_uppercase = preg_match('/[A-Z]/', $password);
$has_lowercase = preg_match('/[a-z]/', $password);
$has_number = preg_match('/[0-9]/', $password);
return $has_min_length && $has_uppercase && $has_lowercase && $has_number;
}
?>
Usage in a Registration Form
<?php
// File: register.php
require_once 'validation_functions.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $_POST['email'] ?? '';
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$errors = [];
// Validate input using our reusable functions
if (!is_valid_email($email)) {
$errors[] = 'Please enter a valid email address.';
}
if (!has_min_length($username, 4)) {
$errors[] = 'Username must be at least 4 characters long.';
}
if (!is_secure_password($password)) {
$errors[] = 'Password must be at least 8 characters long and contain uppercase, lowercase, and numbers.';
}
// Process form if no errors
if (empty($errors)) {
// Register the user
// ...
}
}
?>
Real-World Application
WordPress uses extensive function libraries like sanitize_text_field(), wp_kses(), and esc_url() for data validation and sanitization across the entire platform. These functions ensure consistent security practices throughout WordPress.
Classes and Object-Oriented Components
Classes are the most sophisticated form of reusable components, allowing you to package related functions and data together. Think of a class as a blueprint for creating objects with specific behaviors and properties.
Example: Form Field Component Class
// File: form_components.php
<?php
/**
* Base class for all form field components
*/
abstract class FormField {
protected $name;
protected $label;
protected $value;
protected $error;
protected $required;
/**
* Constructor
*
* @param string $name Field name
* @param string $label Field label
* @param bool $required Whether field is required
*/
public function __construct($name, $label, $required = false) {
$this->name = $name;
$this->label = $label;
$this->required = $required;
$this->value = '';
$this->error = '';
}
/**
* Set the field value
*
* @param mixed $value The value to set
* @return self For method chaining
*/
public function setValue($value) {
$this->value = $value;
return $this;
}
/**
* Get the field value
*
* @return mixed The field value
*/
public function getValue() {
return $this->value;
}
/**
* Set an error message
*
* @param string $error The error message
* @return self For method chaining
*/
public function setError($error) {
$this->error = $error;
return $this;
}
/**
* Check if field has an error
*
* @return bool True if there's an error
*/
public function hasError() {
return !empty($this->error);
}
/**
* Validate the field
*
* @return bool True if valid, false otherwise
*/
abstract public function validate();
/**
* Render the field HTML
*
* @return string The field HTML
*/
abstract public function render();
}
/**
* Text input field component
*/
class TextField extends FormField {
protected $minLength;
protected $maxLength;
protected $placeholder;
/**
* Constructor
*
* @param string $name Field name
* @param string $label Field label
* @param bool $required Whether field is required
* @param int $minLength Minimum length (0 for no minimum)
* @param int $maxLength Maximum length (0 for no maximum)
* @param string $placeholder Placeholder text
*/
public function __construct($name, $label, $required = false, $minLength = 0, $maxLength = 0, $placeholder = '') {
parent::__construct($name, $label, $required);
$this->minLength = $minLength;
$this->maxLength = $maxLength;
$this->placeholder = $placeholder;
}
/**
* Validate the field
*
* @return bool True if valid, false otherwise
*/
public function validate() {
if ($this->required && $this->value === '') {
$this->error = "The {$this->label} field is required.";
return false;
}
if ($this->minLength > 0 && strlen($this->value) < $this->minLength) {
$this->error = "The {$this->label} field must be at least {$this->minLength} characters.";
return false;
}
if ($this->maxLength > 0 && strlen($this->value) > $this->maxLength) {
$this->error = "The {$this->label} field cannot exceed {$this->maxLength} characters.";
return false;
}
return true;
}
/**
* Render the field HTML
*
* @return string The field HTML
*/
public function render() {
$html = '<div class="form-field' . ($this->hasError() ? ' has-error' : '') . '">';
$html .= '<label for="' . htmlspecialchars($this->name) . '">' . htmlspecialchars($this->label);
if ($this->required) {
$html .= ' <span class="required">*</span>';
}
$html .= '</label>';
$html .= '<input type="text" id="' . htmlspecialchars($this->name) . '" name="' . htmlspecialchars($this->name) . '" value="' . htmlspecialchars($this->value) . '"';
if (!empty($this->placeholder)) {
$html .= ' placeholder="' . htmlspecialchars($this->placeholder) . '"';
}
if ($this->maxLength > 0) {
$html .= ' maxlength="' . $this->maxLength . '"';
}
if ($this->required) {
$html .= ' required';
}
$html .= '>';
if ($this->hasError()) {
$html .= '<p class="error-message">' . htmlspecialchars($this->error) . '</p>';
}
$html .= '</div>';
return $html;
}
}
/**
* Email input field component
*/
class EmailField extends FormField {
/**
* Validate the field
*
* @return bool True if valid, false otherwise
*/
public function validate() {
if ($this->required && $this->value === '') {
$this->error = "The {$this->label} field is required.";
return false;
}
if ($this->value !== '' && !filter_var($this->value, FILTER_VALIDATE_EMAIL)) {
$this->error = "Please enter a valid email address.";
return false;
}
return true;
}
/**
* Render the field HTML
*
* @return string The field HTML
*/
public function render() {
$html = '<div class="form-field' . ($this->hasError() ? ' has-error' : '') . '">';
$html .= '<label for="' . htmlspecialchars($this->name) . '">' . htmlspecialchars($this->label);
if ($this->required) {
$html .= ' <span class="required">*</span>';
}
$html .= '</label>';
$html .= '<input type="email" id="' . htmlspecialchars($this->name) . '" name="' . htmlspecialchars($this->name) . '" value="' . htmlspecialchars($this->value) . '"';
if ($this->required) {
$html .= ' required';
}
$html .= '>';
if ($this->hasError()) {
$html .= '<p class="error-message">' . htmlspecialchars($this->error) . '</p>';
}
$html .= '</div>';
return $html;
}
}
?>
Usage in a Form
<?php
// File: contact_form.php
require_once 'form_components.php';
// Create form fields
$nameField = new TextField('name', 'Your Name', true, 2, 50);
$emailField = new EmailField('email', 'Email Address', true);
$messageField = new TextField('message', 'Your Message', true, 10, 1000);
// Process form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Set values from POST data
$nameField->setValue($_POST['name'] ?? '');
$emailField->setValue($_POST['email'] ?? '');
$messageField->setValue($_POST['message'] ?? '');
// Validate all fields
$valid = $nameField->validate() &&
$emailField->validate() &&
$messageField->validate();
if ($valid) {
// Process the form data
// ...
$success = true;
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Contact Us</title>
</head>
<body>
<h1>Contact Us</h1>
<?php if (isset($success)): ?>
<div class="success-message">
Thank you for your message! We'll get back to you soon.
</div>
<?php else: ?>
<form method="post">
<?php
echo $nameField->render();
echo $emailField->render();
echo $messageField->render();
?>
<button type="submit">Send Message</button>
</form>
<?php endif; ?>
</body>
</html>
Real-World Application
WordPress uses this pattern extensively in its core. For example, the WP_Widget class is a base component that developers extend to create custom widgets. Similarly, the WP_Block class serves as the foundation for Gutenberg blocks, allowing developers to create reusable content blocks with consistent behavior.
Common Component Design Patterns
The Template Pattern
Imagine a restaurant menu where the layout is consistent but the content changes. The template pattern works the same way - providing a consistent structure while allowing variable content.
Example: Template Component with Content Slots
// File: page_template.php
<?php
/**
* Renders a complete page using template components
*
* @param string $title The page title
* @param string $content The main content HTML
* @param array $options Additional options (sidebar, meta tags, etc.)
* @return string The complete page HTML
*/
function render_page($title, $content, $options = []) {
// Start output buffering to capture HTML
ob_start();
// Default options
$defaults = [
'show_sidebar' => true,
'meta_description' => '',
'active_menu' => '',
];
// Merge with provided options
$options = array_merge($defaults, $options);
// Include header
include 'templates/header.php';
// Main content area with optional sidebar
if ($options['show_sidebar']) {
echo '<div class="container with-sidebar">';
echo ' <main class="content">';
echo $content;
echo ' </main>';
// Include sidebar
include 'templates/sidebar.php';
echo '</div>';
} else {
echo '<div class="container full-width">';
echo ' <main class="content">';
echo $content;
echo ' </main>';
echo '</div>';
}
// Include footer
include 'templates/footer.php';
// Return the captured HTML
return ob_get_clean();
}
?>
Template Components
// File: templates/header.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><?php echo htmlspecialchars($title); ?> - My Website</title>
<?php if (!empty($options['meta_description'])): ?>
<meta name="description" content="<?php echo htmlspecialchars($options['meta_description']); ?>">
<?php endif; ?>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<div class="logo">My Website</div>
<nav>
<ul>
<li<?php echo $options['active_menu'] === 'home' ? ' class="active"' : ''; ?>>
<a href="index.php">Home</a>
</li>
<li<?php echo $options['active_menu'] === 'about' ? ' class="active"' : ''; ?>>
<a href="about.php">About</a>
</li>
<li<?php echo $options['active_menu'] === 'services' ? ' class="active"' : ''; ?>>
<a href="services.php">Services</a>
</li>
<li<?php echo $options['active_menu'] === 'contact' ? ' class="active"' : ''; ?>>
<a href="contact.php">Contact</a>
</li>
</ul>
</nav>
</header>
// File: templates/sidebar.php
<aside class="sidebar">
<div class="widget">
<h3>Recent Posts</h3>
<ul>
<li><a href="#">Post Title 1</a></li>
<li><a href="#">Post Title 2</a></li>
<li><a href="#">Post Title 3</a></li>
</ul>
</div>
<div class="widget">
<h3>Categories</h3>
<ul>
<li><a href="#">Category 1</a></li>
<li><a href="#">Category 2</a></li>
<li><a href="#">Category 3</a></li>
</ul>
</div>
</aside>
// File: templates/footer.php
<footer>
<div class="footer-content">
<div class="footer-col">
<h4>About Us</h4>
<p>A brief description of your company or website.</p>
</div>
<div class="footer-col">
<h4>Quick Links</h4>
<ul>
<li><a href="privacy.php">Privacy Policy</a></li>
<li><a href="terms.php">Terms of Service</a></li>
<li><a href="sitemap.php">Sitemap</a></li>
</ul>
</div>
<div class="footer-col">
<h4>Contact Us</h4>
<p>Email: info@example.com<br>
Phone: (123) 456-7890</p>
</div>
</div>
<div class="copyright">
<p>© <?php echo date('Y'); ?> My Website. All rights reserved.</p>
</div>
</footer>
</body>
</html>
Usage in a Page
<?php
// File: about.php
require_once 'page_template.php';
// Prepare the content
$content = '
<h1>About Us</h1>
<p>Welcome to our company page! We are dedicated to providing the best service possible.</p>
<p>Our team of experts has years of experience in the industry.</p>
<h2>Our Mission</h2>
<p>To deliver high-quality products and services to our customers.</p>
';
// Render the page with options
echo render_page('About Us', $content, [
'active_menu' => 'about',
'meta_description' => 'Learn about our company, mission, and team.',
]);
?>
Real-World Application: WordPress Theme System
WordPress's theme system is built entirely on the template pattern. Functions like get_header(), get_footer(), and the template hierarchy (index.php, single.php, page.php, etc.) all work together to provide a consistent site structure while allowing for customized content based on context.
The Factory Pattern
Like a factory producing different products using the same machinery, the factory pattern creates different objects using a common interface. This is particularly useful when creating complex objects with similar structures but different behaviors.
Example: Component Factory
// File: component_factory.php
<?php
/**
* Interface for all content components
*/
interface Component {
/**
* Render the component
*
* @return string HTML representation of the component
*/
public function render();
/**
* Validate the component data
*
* @return bool True if valid, false otherwise
*/
public function validate();
/**
* Get component type
*
* @return string Component type identifier
*/
public function getType();
}
/**
* Text component implementation
*/
class TextComponent implements Component {
protected $content;
protected $heading;
/**
* Constructor
*
* @param array $config Component configuration
*/
public function __construct($config) {
$this->content = $config['content'] ?? '';
$this->heading = $config['heading'] ?? '';
}
/**
* Render the component
*
* @return string HTML representation of the component
*/
public function render() {
$html = '<div class="component text-component">';
if (!empty($this->heading)) {
$html .= '<h2>' . htmlspecialchars($this->heading) . '</h2>';
}
$html .= '<div class="content">' . nl2br(htmlspecialchars($this->content)) . '</div>';
$html .= '</div>';
return $html;
}
/**
* Validate the component data
*
* @return bool True if valid, false otherwise
*/
public function validate() {
return !empty($this->content);
}
/**
* Get component type
*
* @return string Component type identifier
*/
public function getType() {
return 'text';
}
}
/**
* Image component implementation
*/
class ImageComponent implements Component {
protected $src;
protected $alt;
protected $caption;
/**
* Constructor
*
* @param array $config Component configuration
*/
public function __construct($config) {
$this->src = $config['src'] ?? '';
$this->alt = $config['alt'] ?? '';
$this->caption = $config['caption'] ?? '';
}
/**
* Render the component
*
* @return string HTML representation of the component
*/
public function render() {
$html = '<div class="component image-component">';
$html .= '<figure>';
$html .= '<img src="' . htmlspecialchars($this->src) . '" alt="' . htmlspecialchars($this->alt) . '">';
if (!empty($this->caption)) {
$html .= '<figcaption>' . htmlspecialchars($this->caption) . '</figcaption>';
}
$html .= '</figure>';
$html .= '</div>';
return $html;
}
/**
* Validate the component data
*
* @return bool True if valid, false otherwise
*/
public function validate() {
return !empty($this->src);
}
/**
* Get component type
*
* @return string Component type identifier
*/
public function getType() {
return 'image';
}
}
/**
* Component factory class
*/
class ComponentFactory {
/**
* Create a component of the specified type
*
* @param string $type Component type
* @param array $config Component configuration
* @return Component|null The created component or null if type is invalid
*/
public static function createComponent($type, $config) {
switch ($type) {
case 'text':
return new TextComponent($config);
case 'image':
return new ImageComponent($config);
default:
return null;
}
}
}
?>
Usage in a Page Builder
<?php
// File: page_builder.php
require_once 'component_factory.php';
/**
* Build a page from component data
*
* @param array $components Array of component definitions
* @return string The composed page HTML
*/
function build_page($components) {
$html = '';
foreach ($components as $component_data) {
$type = $component_data['type'] ?? '';
$config = $component_data['config'] ?? [];
$component = ComponentFactory::createComponent($type, $config);
if ($component && $component->validate()) {
$html .= $component->render();
}
}
return $html;
}
// Example usage
$page_components = [
[
'type' => 'text',
'config' => [
'heading' => 'Welcome to Our Website',
'content' => 'This is the introduction to our company. We provide quality services to our customers.'
]
],
[
'type' => 'image',
'config' => [
'src' => '/images/office.jpg',
'alt' => 'Our office building',
'caption' => 'Our headquarters in downtown'
]
],
[
'type' => 'text',
'config' => [
'heading' => 'Our Services',
'content' => 'We offer a wide range of services including web development, design, and consulting.'
]
]
];
// Build the page
$page_content = build_page($page_components);
// Include in a template
require_once 'page_template.php';
echo render_page('Home', $page_content);
?>
Real-World Application: WordPress Blocks and Shortcodes
WordPress uses the factory pattern for both shortcodes and Gutenberg blocks. When you register a shortcode with add_shortcode() or a block with register_block_type(), you're essentially adding to a factory that creates the appropriate component when needed. This is why WordPress can render different shortcodes and blocks with consistent behavior.
Best Practices for Reusable Components
Single Responsibility Principle
Each component should do one thing and do it well - like a specialized tool. A hammer is for driving nails, not cutting wood. Similarly, a component should have a single, clear purpose.
Good Example
// A component with a single responsibility - formatting dates
class DateFormatter {
public function formatDate($timestamp, $format = 'Y-m-d') {
return date($format, $timestamp);
}
public function formatDateDiff($timestamp) {
$diff = time() - $timestamp;
if ($diff < 60) {
return "Just now";
} elseif ($diff < 3600) {
return floor($diff / 60) . " minutes ago";
} elseif ($diff < 86400) {
return floor($diff / 3600) . " hours ago";
} else {
return floor($diff / 86400) . " days ago";
}
}
}
Bad Example
// A component trying to do too many things
class Utility {
public function formatDate($timestamp) {
return date('Y-m-d', $timestamp);
}
public function validateEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL);
}
public function generatePassword($length = 8) {
// Password generation logic
}
public function uploadImage($file) {
// Image upload logic
}
}
Encapsulation and Configuration
Like adjustable tools that can be configured for different tasks, components should be encapsulated and configurable.
Good Example
// A well-encapsulated pagination component
class Pagination {
private $totalItems;
private $itemsPerPage;
private $currentPage;
private $baseUrl;
public function __construct($totalItems, $itemsPerPage = 10, $currentPage = 1, $baseUrl = '?page=') {
$this->totalItems = $totalItems;
$this->itemsPerPage = $itemsPerPage;
$this->currentPage = $currentPage;
$this->baseUrl = $baseUrl;
}
public function getTotalPages() {
return ceil($this->totalItems / $this->itemsPerPage);
}
public function render() {
$html = '<div class="pagination">';
$totalPages = $this->getTotalPages();
// Previous button
if ($this->currentPage > 1) {
$html .= '<a href="' . $this->baseUrl . ($this->currentPage - 1) . '" class="prev">« Previous</a>';
} else {
$html .= '<span class="prev disabled">« Previous</span>';
}
// Page numbers
for ($i = 1; $i <= $totalPages; $i++) {
if ($i == $this->currentPage) {
$html .= '<span class="current">' . $i . '</span>';
} else {
$html .= '<a href="' . $this->baseUrl . $i . '">' . $i . '</a>';
}
}
// Next button
if ($this->currentPage < $totalPages) {
$html .= '<a href="' . $this->baseUrl . ($this->currentPage + 1) . '" class="next">Next »</a>';
} else {
$html .= '<span class="next disabled">Next »</span>';
}
$html .= '</div>';
return $html;
}
}
// Usage
$pagination = new Pagination(100, 10, 2, '/blog/page/');
echo $pagination->render();
Documentation and Naming
Well-named and documented components are like tools with clear labels and instructions. They're easier to use correctly and consistently.
Good Example
/**
* Notification Component
*
* Displays various types of notification messages with consistent styling.
*
* @example
* $notification = new Notification();
* echo $notification->success('Your profile has been updated successfully.');
*/
class Notification {
/**
* Display a success message
*
* @param string $message The message to display
* @param bool $dismissible Whether the notification can be dismissed
* @return string HTML for the notification
*/
public function success($message, $dismissible = true) {
return $this->render($message, 'success', $dismissible);
}
/**
* Display an error message
*
* @param string $message The message to display
* @param bool $dismissible Whether the notification can be dismissed
* @return string HTML for the notification
*/
public function error($message, $dismissible = true) {
return $this->render($message, 'error', $dismissible);
}
/**
* Display a warning message
*
* @param string $message The message to display
* @param bool $dismissible Whether the notification can be dismissed
* @return string HTML for the notification
*/
public function warning($message, $dismissible = true) {
return $this->render($message, 'warning', $dismissible);
}
/**
* Display an info message
*
* @param string $message The message to display
* @param bool $dismissible Whether the notification can be dismissed
* @return string HTML for the notification
*/
public function info($message, $dismissible = true) {
return $this->render($message, 'info', $dismissible);
}
/**
* Render the notification
*
* @param string $message The message to display
* @param string $type The notification type (success, error, warning, info)
* @param bool $dismissible Whether the notification can be dismissed
* @return string HTML for the notification
*/
private function render($message, $type, $dismissible) {
$html = '<div class="notification ' . $type . '">';
$html .= '<p>' . htmlspecialchars($message) . '</p>';
if ($dismissible) {
$html .= '<button class="dismiss" aria-label="Dismiss">×</button>';
}
$html .= '</div>';
return $html;
}
}
Integrating Components with WordPress
Creating Reusable Template Parts
WordPress provides several ways to create and use reusable component templates, similar to how a car manufacturer uses interchangeable parts across different models.
Example: WordPress Template Part
// File: template-parts/content-card.php
<?php
/**
* Template part for displaying a content card
*
* @param array $args {
* Optional. Arguments to customize the card display.
*
* @type string $title Card title.
* @type string $content Card content.
* @type string $image_url URL to the card image.
* @type string $button_text Button text.
* @type string $button_url Button URL.
* }
*/
// Extract variables from arguments
$title = $args['title'] ?? '';
$content = $args['content'] ?? '';
$image_url = $args['image_url'] ?? '';
$button_text = $args['button_text'] ?? 'Read More';
$button_url = $args['button_url'] ?? '#';
?>
<div class="content-card">
<?php if (!empty($image_url)): ?>
<div class="card-image">
<img src="<?php echo esc_url($image_url); ?>" alt="<?php echo esc_attr($title); ?>">
</div>
<?php endif; ?>
<div class="card-content">
<?php if (!empty($title)): ?>
<h3 class="card-title"><?php echo esc_html($title); ?></h3>
<?php endif; ?>
<?php if (!empty($content)): ?>
<div class="card-text"><?php echo wpautop(esc_html($content)); ?></div>
<?php endif; ?>
<a href="<?php echo esc_url($button_url); ?>" class="button"><?php echo esc_html($button_text); ?></a>
</div>
</div>
Usage in a WordPress Page Template
// File: page-features.php
<?php
/**
* Template Name: Features Page
*/
get_header();
?>
<div class="container">
<h1 class="page-title"><?php the_title(); ?></h1>
<div class="features-grid">
<?php
// Feature 1
get_template_part('template-parts/content', 'card', [
'title' => 'Responsive Design',
'content' => 'Our websites work beautifully on all devices, from desktops to smartphones.',
'image_url' => get_template_directory_uri() . '/images/responsive.jpg',
'button_url' => '/services/responsive-design/'
]);
// Feature 2
get_template_part('template-parts/content', 'card', [
'title' => 'SEO Optimization',
'content' => 'Improve your search engine rankings with our advanced SEO techniques.',
'image_url' => get_template_directory_uri() . '/images/seo.jpg',
'button_text' => 'Learn More',
'button_url' => '/services/seo-optimization/'
]);
// Feature 3
get_template_part('template-parts/content', 'card', [
'title' => 'E-commerce Solutions',
'content' => 'Sell your products online with our secure and user-friendly e-commerce platforms.',
'image_url' => get_template_directory_uri() . '/images/ecommerce.jpg',
'button_url' => '/services/ecommerce/'
]);
?>
</div>
</div>
<?php
get_footer();
?>
Creating Reusable Shortcodes
Shortcodes allow you to create components that can be easily inserted into any WordPress content area, similar to how you might insert a USB drive into any computer with a USB port.
Example: Custom Alert Shortcode
// File: custom-shortcodes.php (included in your theme's functions.php)
<?php
/**
* Register custom shortcodes
*/
function register_custom_shortcodes() {
add_shortcode('alert', 'alert_shortcode');
}
add_action('init', 'register_custom_shortcodes');
/**
* Alert box shortcode implementation
*
* @param array $atts Shortcode attributes
* @param string $content Shortcode content
* @return string HTML output
*/
function alert_shortcode($atts, $content = null) {
// Default attributes
$attributes = shortcode_atts([
'type' => 'info', // Possible values: info, success, warning, error
'title' => '',
'dismissible' => 'no',
], $atts);
// Validate alert type
$valid_types = ['info', 'success', 'warning', 'error'];
if (!in_array($attributes['type'], $valid_types)) {
$attributes['type'] = 'info';
}
// Convert dismissible to boolean
$is_dismissible = $attributes['dismissible'] === 'yes';
// Generate unique ID for dismissible alerts
$alert_id = '';
if ($is_dismissible) {
$alert_id = 'alert-' . uniqid();
}
// Start building the HTML
$html = '<div class="custom-alert alert-' . esc_attr($attributes['type']) . '"';
if ($is_dismissible) {
$html .= ' id="' . esc_attr($alert_id) . '"';
}
$html .= '>';
// Add title if present
if (!empty($attributes['title'])) {
$html .= '<h4 class="alert-title">' . esc_html($attributes['title']) . '</h4>';
}
// Add content
$html .= '<div class="alert-content">' . wpautop(do_shortcode($content)) . '</div>';
// Add dismiss button if dismissible
if ($is_dismissible) {
$html .= '<button class="alert-dismiss" aria-label="Dismiss" onclick="document.getElementById('' . esc_js($alert_id) . '').style.display='none';">×</button>';
}
$html .= '</div>';
return $html;
}
?>
Usage in WordPress Content
// In WordPress editor
[alert type="warning" title="Important Notice" dismissible="yes"]
Please backup your database before updating to the latest version.
[/alert]
[alert type="success"]
Your registration was successful! You should receive a confirmation email shortly.
[/alert]
Creating Custom WordPress Blocks
Gutenberg blocks are the modern way to create reusable components in WordPress, operating like modular building blocks that users can arrange however they want.
WordPress Block Registration
// File: blocks/testimonial/index.php
<?php
/**
* Register the testimonial block
*/
function register_testimonial_block() {
register_block_type(
'my-theme/testimonial',
[
'editor_script' => 'testimonial-block-editor',
'editor_style' => 'testimonial-block-editor',
'style' => 'testimonial-block',
'render_callback' => 'render_testimonial_block',
'attributes' => [
'quote' => [
'type' => 'string',
'default' => '',
],
'author' => [
'type' => 'string',
'default' => '',
],
'role' => [
'type' => 'string',
'default' => '',
],
'imageUrl' => [
'type' => 'string',
'default' => '',
],
'backgroundColor' => [
'type' => 'string',
'default' => '#f5f5f5',
],
],
]
);
}
add_action('init', 'register_testimonial_block');
/**
* Render the testimonial block on the front-end
*
* @param array $attributes Block attributes
* @return string HTML output
*/
function render_testimonial_block($attributes) {
// Extract block attributes
$quote = $attributes['quote'] ?? '';
$author = $attributes['author'] ?? '';
$role = $attributes['role'] ?? '';
$image_url = $attributes['imageUrl'] ?? '';
$background_color = $attributes['backgroundColor'] ?? '#f5f5f5';
// Build the HTML
$html = sprintf(
'<div class="wp-block-my-theme-testimonial" style="background-color: %s;">',
esc_attr($background_color)
);
$html .= '<div class="testimonial-content">';
// Quote
if (!empty($quote)) {
$html .= sprintf(
'<blockquote class="testimonial-quote">%s</blockquote>',
wpautop(esc_html($quote))
);
}
// Author info
$html .= '<div class="testimonial-author">';
if (!empty($image_url)) {
$html .= sprintf(
'<div class="author-image"><img src="%s" alt="%s"></div>',
esc_url($image_url),
esc_attr($author)
);
}
$html .= '<div class="author-details">';
if (!empty($author)) {
$html .= sprintf(
'<div class="author-name">%s</div>',
esc_html($author)
);
}
if (!empty($role)) {
$html .= sprintf(
'<div class="author-role">%s</div>',
esc_html($role)
);
}
$html .= '</div>'; // End author-details
$html .= '</div>'; // End testimonial-author
$html .= '</div>'; // End testimonial-content
$html .= '</div>'; // End testimonial block
return $html;
}
?>
Real-World WordPress Component Examples
- WooCommerce Product Cards: Reusable components for displaying products consistently across the site
- Contact Form 7: A plugin that provides reusable form components through shortcodes
- The Events Calendar: Uses component-based architecture to display events in different views
- Yoast SEO: Implements reusable meta box components across different post types
Advanced Component Concepts
Component Caching
Caching components is like preparing meals in advance - it saves time when you need to serve them. This technique significantly improves performance for complex or frequently used components.
Example: Cached Navigation Component
// File: cached_nav.php
<?php
/**
* Renders a cached navigation menu
*
* @param string $menu_location The menu location to render
* @param int $cache_time Cache lifetime in seconds (default: 1 hour)
* @return string The navigation HTML
*/
function get_cached_navigation($menu_location = 'primary', $cache_time = 3600) {
// Create a unique cache key
$cache_key = 'nav_' . $menu_location . '_' . md5(serialize(wp_get_current_user()));
// Check if we have cached content
$cached_html = get_transient($cache_key);
if ($cached_html !== false) {
return $cached_html;
}
// If no cache or expired, generate the navigation
$html = '';
// Build the navigation HTML
$menu_args = [
'theme_location' => $menu_location,
'container' => 'nav',
'container_class' => 'site-navigation',
'menu_class' => 'nav-menu',
'echo' => false,
];
$html = wp_nav_menu($menu_args);
// Store in cache
set_transient($cache_key, $html, $cache_time);
return $html;
}
/**
* Clear navigation cache when menus are updated
*/
function clear_navigation_cache() {
global $wpdb;
// Delete all transients that start with 'nav_'
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_nav_%'");
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_nav_%'");
}
// Hook into menu updates to clear cache
add_action('wp_update_nav_menu', 'clear_navigation_cache');
?>
Dependency Injection
Dependency injection is like ordering components for assembly rather than building everything yourself. You provide a component with its dependencies rather than having it create them internally.
Example: Database Component with Dependency Injection
// File: database_service.php
<?php
/**
* Database configuration interface
*/
interface DatabaseConfig {
/**
* Get database host
*
* @return string The database host
*/
public function getHost();
/**
* Get database name
*
* @return string The database name
*/
public function getName();
/**
* Get database user
*
* @return string The database user
*/
public function getUser();
/**
* Get database password
*
* @return string The database password
*/
public function getPassword();
}
/**
* WordPress database configuration implementation
*/
class WordPressDatabaseConfig implements DatabaseConfig {
/**
* Get database host
*
* @return string The database host
*/
public function getHost() {
global $wpdb;
return DB_HOST;
}
/**
* Get database name
*
* @return string The database name
*/
public function getName() {
return DB_NAME;
}
/**
* Get database user
*
* @return string The database user
*/
public function getUser() {
return DB_USER;
}
/**
* Get database password
*
* @return string The database password
*/
public function getPassword() {
return DB_PASSWORD;
}
}
/**
* Test environment database configuration
*/
class TestDatabaseConfig implements DatabaseConfig {
/**
* Get database host
*
* @return string The database host
*/
public function getHost() {
return 'localhost';
}
/**
* Get database name
*
* @return string The database name
*/
public function getName() {
return 'test_db';
}
/**
* Get database user
*
* @return string The database user
*/
public function getUser() {
return 'test_user';
}
/**
* Get database password
*
* @return string The database password
*/
public function getPassword() {
return 'test_password';
}
}
/**
* Database service that uses dependency injection
*/
class DatabaseService {
private $config;
private $connection;
/**
* Constructor with injected dependency
*
* @param DatabaseConfig $config The database configuration
*/
public function __construct(DatabaseConfig $config) {
$this->config = $config;
}
/**
* Connect to the database
*
* @return bool True if connected successfully
*/
public function connect() {
try {
$this->connection = new PDO(
'mysql:host=' . $this->config->getHost() . ';dbname=' . $this->config->getName(),
$this->config->getUser(),
$this->config->getPassword()
);
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return true;
} catch (PDOException $e) {
error_log('Database connection failed: ' . $e->getMessage());
return false;
}
}
/**
* Execute a query
*
* @param string $query The SQL query
* @param array $params Query parameters
* @return array|false Results or false on failure
*/
public function query($query, $params = []) {
if (!$this->connection) {
if (!$this->connect()) {
return false;
}
}
try {
$statement = $this->connection->prepare($query);
$statement->execute($params);
return $statement->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log('Query failed: ' . $e->getMessage());
return false;
}
}
}
?>
Usage with Different Configurations
<?php
require_once 'database_service.php';
// Production environment
$productionConfig = new WordPressDatabaseConfig();
$productionDb = new DatabaseService($productionConfig);
$users = $productionDb->query('SELECT * FROM users WHERE active = ?', [1]);
// Test environment
$testConfig = new TestDatabaseConfig();
$testDb = new DatabaseService($testConfig);
$testUsers = $testDb->query('SELECT * FROM users WHERE active = ?', [1]);
?>
Benefits of Dependency Injection
- Testability: Easy to swap real dependencies with test doubles
- Flexibility: Change implementations without modifying the component
- Decoupling: Components don't need to know how their dependencies are created
- Reusability: Components can be used in different contexts
Event-Driven Components
Event-driven components are like smart home devices that react to triggers. Instead of direct communication, components listen for events and respond accordingly.
Example: Simple Event System
// File: event_system.php
<?php
/**
* Simple event management system
*/
class EventManager {
private static $instance = null;
private $listeners = [];
/**
* Get the singleton instance
*
* @return EventManager The event manager instance
*/
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Add an event listener
*
* @param string $event Event name
* @param callable $callback Function to call when event is triggered
* @param int $priority Priority (higher numbers run first)
* @return void
*/
public function addListener($event, $callback, $priority = 10) {
if (!isset($this->listeners[$event])) {
$this->listeners[$event] = [];
}
$this->listeners[$event][] = [
'callback' => $callback,
'priority' => $priority
];
// Sort listeners by priority
usort($this->listeners[$event], function($a, $b) {
return $b['priority'] - $a['priority'];
});
}
/**
* Remove an event listener
*
* @param string $event Event name
* @param callable $callback Function to remove
* @return bool True if listener was removed
*/
public function removeListener($event, $callback) {
if (!isset($this->listeners[$event])) {
return false;
}
foreach ($this->listeners[$event] as $key => $listener) {
if ($listener['callback'] === $callback) {
unset($this->listeners[$event][$key]);
$this->listeners[$event] = array_values($this->listeners[$event]);
return true;
}
}
return false;
}
/**
* Trigger an event
*
* @param string $event Event name
* @param mixed $data Data to pass to listeners
* @return void
*/
public function trigger($event, $data = null) {
if (!isset($this->listeners[$event])) {
return;
}
foreach ($this->listeners[$event] as $listener) {
call_user_func($listener['callback'], $data);
}
}
}
/**
* Helper functions for working with events
*/
/**
* Add an event listener
*
* @param string $event Event name
* @param callable $callback Function to call when event is triggered
* @param int $priority Priority (higher numbers run first)
* @return void
*/
function on_event($event, $callback, $priority = 10) {
EventManager::getInstance()->addListener($event, $callback, $priority);
}
/**
* Remove an event listener
*
* @param string $event Event name
* @param callable $callback Function to remove
* @return bool True if listener was removed
*/
function off_event($event, $callback) {
return EventManager::getInstance()->removeListener($event, $callback);
}
/**
* Trigger an event
*
* @param string $event Event name
* @param mixed $data Data to pass to listeners
* @return void
*/
function trigger_event($event, $data = null) {
EventManager::getInstance()->trigger($event, $data);
}
?>
Example: Form Processing with Events
<?php
require_once 'event_system.php';
// Component 1: Form Handler
class UserRegistrationForm {
public function process($formData) {
// Basic validation
if (empty($formData['email']) || empty($formData['password'])) {
return false;
}
// Trigger form.processed event with the form data
trigger_event('user.register.submitted', $formData);
return true;
}
}
// Component 2: Email Validator
class EmailValidator {
public function __construct() {
// Register to listen for the form.processed event
on_event('user.register.submitted', [$this, 'validateEmail']);
}
public function validateEmail($data) {
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
trigger_event('user.register.invalid_email', $data);
return;
}
trigger_event('user.register.email_valid', $data);
}
}
// Component 3: Password Validator
class PasswordValidator {
public function __construct() {
// Register to listen for the form.processed event
on_event('user.register.submitted', [$this, 'validatePassword']);
}
public function validatePassword($data) {
if (strlen($data['password']) < 8) {
trigger_event('user.register.invalid_password', $data);
return;
}
trigger_event('user.register.password_valid', $data);
}
}
// Component 4: User Creator
class UserCreator {
private $valid_email = false;
private $valid_password = false;
private $current_data = null;
public function __construct() {
// Register for validation events
on_event('user.register.email_valid', [$this, 'markEmailValid']);
on_event('user.register.password_valid', [$this, 'markPasswordValid']);
// Register for invalid events to reset flags
on_event('user.register.invalid_email', [$this, 'markEmailInvalid']);
on_event('user.register.invalid_password', [$this, 'markPasswordInvalid']);
}
public function markEmailValid($data) {
$this->valid_email = true;
$this->current_data = $data;
$this->attemptUserCreation();
}
public function markPasswordValid($data) {
$this->valid_password = true;
$this->current_data = $data;
$this->attemptUserCreation();
}
public function markEmailInvalid($data) {
$this->valid_email = false;
trigger_event('user.register.failed', [
'reason' => 'Invalid email address'
]);
}
public function markPasswordInvalid($data) {
$this->valid_password = false;
trigger_event('user.register.failed', [
'reason' => 'Password too short (minimum 8 characters)'
]);
}
private function attemptUserCreation() {
if ($this->valid_email && $this->valid_password && $this->current_data) {
// All validations passed, create the user
$userId = 123; // In a real app, this would insert to database and get ID
trigger_event('user.register.success', [
'user_id' => $userId,
'email' => $this->current_data['email']
]);
// Reset flags
$this->valid_email = false;
$this->valid_password = false;
$this->current_data = null;
}
}
}
// Component 5: Email Notifier
class EmailNotifier {
public function __construct() {
on_event('user.register.success', [$this, 'sendWelcomeEmail']);
}
public function sendWelcomeEmail($data) {
// In a real app, this would send an actual email
echo "Sending welcome email to: " . htmlspecialchars($data['email']) . "
";
}
}
// Initialize components
$form = new UserRegistrationForm();
$emailValidator = new EmailValidator();
$passwordValidator = new PasswordValidator();
$userCreator = new UserCreator();
$emailNotifier = new EmailNotifier();
// Listen for success and failure events for UI feedback
on_event('user.register.success', function($data) {
echo "Registration successful! User ID: " . $data['user_id'] . "";
});
on_event('user.register.failed', function($data) {
echo "Registration failed: " . $data['reason'] . "";
});
// Process a form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$form->process($_POST);
}
?>
<!-- Registration Form -->
<form method="post">
<div>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Register</button>
</form>
Benefits of Event-Driven Components
- Loose Coupling: Components don't need direct references to each other
- Extensibility: New components can listen for events without modifying existing code
- Flexibility: Event handlers can be added or removed dynamically
- Parallelism: Multiple components can respond to the same event independently
WordPress Hooks as Component System
WordPress's actions and filters system is a sophisticated event-driven component architecture. It's like a building's electrical system - components can "plug in" anywhere.
Example: WordPress Hook-Based Component
// File: social_share_component.php
<?php
/**
* Social Share Component
*
* A reusable component for adding social sharing to WordPress content
*/
class SocialShareComponent {
/**
* Default settings
*
* @var array
*/
private $defaults = [
'networks' => ['facebook', 'twitter', 'linkedin'],
'position' => 'after',
'show_count' => true,
'custom_class' => '',
];
/**
* Current settings
*
* @var array
*/
private $settings;
/**
* Initialize the component
*
* @param array $settings Custom settings
*/
public function __construct($settings = []) {
// Merge with defaults
$this->settings = wp_parse_args($settings, $this->defaults);
// Register hooks
add_filter('the_content', [$this, 'addShareButtons']);
add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']);
}
/**
* Add share buttons to content
*
* @param string $content The post content
* @return string Modified content
*/
public function addShareButtons($content) {
// Only on single posts/pages
if (!is_singular()) {
return $content;
}
$buttons = $this->renderButtons();
// Add buttons based on position setting
if ($this->settings['position'] === 'before') {
return $buttons . $content;
} elseif ($this->settings['position'] === 'both') {
return $buttons . $content . $buttons;
} else {
return $content . $buttons;
}
}
/**
* Enqueue required CSS and JavaScript
*/
public function enqueueAssets() {
wp_enqueue_style(
'social-share-component',
plugin_dir_url(__FILE__) . 'css/social-share.css',
[],
'1.0.0'
);
wp_enqueue_script(
'social-share-component',
plugin_dir_url(__FILE__) . 'js/social-share.js',
['jquery'],
'1.0.0',
true
);
}
/**
* Render the share buttons HTML
*
* @return string The buttons HTML
*/
private function renderButtons() {
$post_url = urlencode(get_permalink());
$post_title = urlencode(get_the_title());
$html = '<div class="social-share-component ' . esc_attr($this->settings['custom_class']) . '">';
$html .= '<h4>Share this post:</h4>';
$html .= '<ul class="share-buttons">';
foreach ($this->settings['networks'] as $network) {
switch ($network) {
case 'facebook':
$html .= '<li class="facebook">';
$html .= '<a href="https://www.facebook.com/sharer/sharer.php?u=' . $post_url . '" target="_blank">';
$html .= '<span class="icon">Facebook</span>';
if ($this->settings['show_count']) {
$html .= '<span class="count">0</span>';
}
$html .= '</a></li>';
break;
case 'twitter':
$html .= '<li class="twitter">';
$html .= '<a href="https://twitter.com/intent/tweet?url=' . $post_url . '&text=' . $post_title . '" target="_blank">';
$html .= '<span class="icon">Twitter</span>';
if ($this->settings['show_count']) {
$html .= '<span class="count">0</span>';
}
$html .= '</a></li>';
break;
case 'linkedin':
$html .= '<li class="linkedin">';
$html .= '<a href="https://www.linkedin.com/shareArticle?mini=true&url=' . $post_url . '&title=' . $post_title . '" target="_blank">';
$html .= '<span class="icon">LinkedIn</span>';
if ($this->settings['show_count']) {
$html .= '<span class="count">0</span>';
}
$html .= '</a></li>';
break;
}
}
$html .= '</ul>';
$html .= '</div>';
return $html;
?>
Benefits of WordPress Hook-Based Components
- Non-Intrusive: Components can add functionality without modifying core files
- Extensible: Multiple components can hook into the same point
- Priority Control: Control the order of execution with priority values
- Removable: Components can be removed or replaced without breaking other components
Practical Reusable Component Examples
Building a Modal Dialog Component
A modal dialog is a perfect candidate for a reusable component - it has a consistent structure but variable content.
Modal Component Class
// File: modal_component.php
<?php
/**
* Modal Dialog Component
*/
class ModalComponent {
private $id;
private $title;
private $content;
private $footer;
private $options;
/**
* Constructor
*
* @param string $id Unique modal ID
* @param string $title Modal title
* @param string $content Modal content HTML
* @param string $footer Modal footer HTML (optional)
* @param array $options Additional options
*/
public function __construct($id, $title, $content, $footer = '', $options = []) {
$this->id = $id;
$this->title = $title;
$this->content = $content;
$this->footer = $footer;
// Default options
$defaults = [
'size' => 'medium', // small, medium, large
'closable' => true,
'backdrop_close' => true,
'animation' => 'fade', // fade, slide, none
'classes' => '',
];
$this->options = array_merge($defaults, $options);
}
/**
* Render the modal HTML
*
* @return string The modal HTML
*/
public function render() {
$modalClasses = 'modal';
$modalClasses .= ' modal-' . $this->options['size'];
$modalClasses .= ' modal-animation-' . $this->options['animation'];
if (!empty($this->options['classes'])) {
$modalClasses .= ' ' . $this->options['classes'];
}
$backdropAttr = $this->options['backdrop_close'] ? 'data-close-on-backdrop="true"' : '';
$html = ''; // End modal
// Include the backdrop
$html .= '';
return $html;
}
/**
* Get the JavaScript to initialize the modal
*
* @return string The initialization JavaScript
*/
public function getInitScript() {
$js = '<script>
document.addEventListener("DOMContentLoaded", function() {
var modal = document.getElementById("' . esc_js($this->id) . '");
var backdrop = document.getElementById("' . esc_js($this->id) . '-backdrop");
var closeButtons = modal.querySelectorAll("[data-dismiss='modal']");
// Close button handlers
closeButtons.forEach(function(button) {
button.addEventListener("click", function() {
modal.style.display = "none";
backdrop.style.display = "none";
document.body.classList.remove("modal-open");
});
});
// Backdrop click handler
if (modal.getAttribute("data-close-on-backdrop") === "true") {
backdrop.addEventListener("click", function() {
modal.style.display = "none";
backdrop.style.display = "none";
document.body.classList.remove("modal-open");
});
}
});
// Helper function to open the modal
function openModal_' . esc_js($this->id) . '() {
var modal = document.getElementById("' . esc_js($this->id) . '");
var backdrop = document.getElementById("' . esc_js($this->id) . '-backdrop");
modal.style.display = "block";
backdrop.style.display = "block";
document.body.classList.add("modal-open");
}
</script>';
return $js;
}
/**
* Get an HTML trigger button for the modal
*
* @param string $text Button text
* @param string $type Button type (primary, secondary, etc.)
* @param array $attributes Additional button attributes
* @return string The button HTML
*/
public function getTriggerButton($text, $type = 'primary', $attributes = []) {
$attrs = '';
foreach ($attributes as $key => $value) {
$attrs .= ' ' . esc_attr($key) . '="' . esc_attr($value) . '"';
}
return '<button type="button" class="button button-' . esc_attr($type) . '" onclick="openModal_' . esc_js($this->id) . '()"' . $attrs . '>' . esc_html($text) . '</button>';
}
}
?>
Usage Example
<?php
require_once 'modal_component.php';
// Create a contact form modal
$contactFormContent = '
<form id="contact-form" method="post">
<div class="form-group">
<label for="name">Your Name</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
</div>
</form>
';
$contactFormFooter = '
<button type="button" class="button button-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="button button-primary" onclick="document.getElementById(\'contact-form\').submit()">Send Message</button>
';
$contactModal = new ModalComponent(
'contact-modal',
'Contact Us',
$contactFormContent,
$contactFormFooter,
[
'size' => 'medium',
'animation' => 'slide'
]
);
// Create a terms and conditions modal
$termsContent = '
<h4>1. Acceptance of Terms</h4>
<p>By accessing this website, you agree to be bound by these Terms and Conditions.</p>
<h4>2. Use License</h4>
<p>Permission is granted to temporarily download one copy of the materials on this website for personal, non-commercial transitory viewing only.</p>
<h4>3. Disclaimer</h4>
<p>The materials on this website are provided on an \'as is\' basis.</p>
';
$termsFooter = '
<button type="button" class="button button-primary" data-dismiss="modal">I Understand</button>
';
$termsModal = new ModalComponent(
'terms-modal',
'Terms and Conditions',
$termsContent,
$termsFooter,
[
'size' => 'large',
'backdrop_close' => false
]
);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Modal Example</title>
<style>
/* Basic modal styling */
.modal { position: fixed; z-index: 1000; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; box-shadow: 0 5px 15px rgba(0,0,0,0.3); border-radius: 5px; }
.modal-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 999; }
.modal-small { width: 300px; }
.modal-medium { width: 500px; }
.modal-large { width: 800px; }
.modal-header { padding: 15px; border-bottom: 1px solid #eee; position: relative; }
.modal-body { padding: 15px; max-height: 70vh; overflow-y: auto; }
.modal-footer { padding: 15px; border-top: 1px solid #eee; text-align: right; }
.modal-close { position: absolute; right: 15px; top: 15px; font-size: 24px; font-weight: bold; border: none; background: none; cursor: pointer; }
.button { padding: 8px 16px; border-radius: 4px; border: none; cursor: pointer; margin-left: 10px; }
.button-primary { background: #007bff; color: white; }
.button-secondary { background: #6c757d; color: white; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; margin-bottom: 5px; }
.form-group input, .form-group textarea { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
body.modal-open { overflow: hidden; }
</style>
</head>
<body>
<h1>Modal Component Example</h1>
<p>This page demonstrates the reusable modal component.</p>
<div class="buttons">
<?php echo $contactModal->getTriggerButton('Contact Us', 'primary'); ?>
<?php echo $termsModal->getTriggerButton('Terms and Conditions', 'secondary'); ?>
</div>
<?php
// Render the modals
echo $contactModal->render();
echo $termsModal->render();
// Include the initialization scripts
echo $contactModal->getInitScript();
echo $termsModal->getInitScript();
?>
</body>
</html>
Building a Pagination Component
Pagination is used across many pages and applications, making it an ideal candidate for a reusable component.
Pagination Component Class
// File: pagination_component.php
<?php
/**
* Pagination Component
*
* A reusable component for creating pagination controls
*/
class PaginationComponent {
private $totalItems;
private $itemsPerPage;
private $currentPage;
private $totalPages;
private $options;
/**
* Constructor
*
* @param int $totalItems Total number of items
* @param int $itemsPerPage Number of items per page
* @param int $currentPage Current page number
* @param array $options Pagination options
*/
public function __construct($totalItems, $itemsPerPage = 10, $currentPage = 1, $options = []) {
$this->totalItems = max(0, intval($totalItems));
$this->itemsPerPage = max(1, intval($itemsPerPage));
$this->totalPages = ceil($this->totalItems / $this->itemsPerPage);
$this->currentPage = max(1, min($this->totalPages, intval($currentPage)));
// Default options
$defaults = [
'baseUrl' => '?page=', // URL pattern for page links
'showFirstLast' => true, // Show first/last page links
'showPrevNext' => true, // Show previous/next links
'maxPageLinks' => 5, // Maximum number of page links to display
'classes' => [
'container' => 'pagination',
'item' => 'page-item',
'link' => 'page-link',
'current' => 'current',
'disabled' => 'disabled',
],
'labels' => [
'first' => '« First',
'last' => 'Last »',
'previous' => '‹ Previous',
'next' => 'Next ›',
],
];
$this->options = array_merge($defaults, $options);
}
/**
* Get the starting and ending page numbers for display
*
* @return array [startPage, endPage]
*/
private function getPageRange() {
$maxLinks = $this->options['maxPageLinks'];
// If we have fewer pages than max, show all
if ($this->totalPages <= $maxLinks) {
return [1, $this->totalPages];
}
// Calculate how many links to show on each side of current page
$sideLinks = floor(($maxLinks - 1) / 2);
// Initial range
$startPage = $this->currentPage - $sideLinks;
$endPage = $this->currentPage + $sideLinks;
// Adjust if we're near the beginning
if ($startPage < 1) {
$endPage += (1 - $startPage);
$startPage = 1;
}
// Adjust if we're near the end
if ($endPage > $this->totalPages) {
$startPage -= ($endPage - $this->totalPages);
$endPage = $this->totalPages;
}
// Final check to ensure values are valid
$startPage = max(1, $startPage);
$endPage = min($this->totalPages, $endPage);
return [$startPage, $endPage];
}
/**
* Render the pagination
*
* @return string Pagination HTML
*/
public function render() {
// If there's only one page, don't show pagination
if ($this->totalPages <= 1) {
return '';
}
$html = '';
return $html;
}
/**
* Render a single page link
*
* @param int $page Page number
* @param string $label Link label
* @param bool $disabled Whether the link is disabled
* @param bool $isCurrent Whether this is the current page
* @return string Link HTML
*/
private function renderPageLink($page, $label, $disabled = false, $isCurrent = false) {
$classes = $this->options['classes']['item'];
if ($isCurrent) {
$classes .= ' ' . $this->options['classes']['current'];
}
if ($disabled) {
$classes .= ' ' . $this->options['classes']['disabled'];
}
$html = '';
if ($disabled || $isCurrent) {
$html .= '';
} else {
$url = $this->options['baseUrl'] . $page;
$html .= '' . $label . '';
}
$html .= ' ';
return $html;
}
/**
* Get the offset for database queries
*
* @return int Query offset
*/
public function getOffset() {
return ($this->currentPage - 1) * $this->itemsPerPage;
}
/**
* Get current page number
*
* @return int Current page
*/
public function getCurrentPage() {
return $this->currentPage;
}
/**
* Get total pages
*
* @return int Total pages
*/
public function getTotalPages() {
return $this->totalPages;
}
}
?>
Usage Example
<?php
require_once 'pagination_component.php';
// Sample database query
function get_posts_paginated($page = 1, $per_page = 10) {
global $wpdb;
// Count total posts
$total_query = "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish'";
$total = $wpdb->get_var($total_query);
// Create pagination component
$pagination = new PaginationComponent($total, $per_page, $page, [
'baseUrl' => '/blog/page/',
'maxPageLinks' => 7
]);
// Get the posts for this page
$offset = $pagination->getOffset();
$posts_query = "SELECT * FROM {$wpdb->posts}
WHERE post_type = 'post' AND post_status = 'publish'
ORDER BY post_date DESC
LIMIT {$per_page} OFFSET {$offset}";
$posts = $wpdb->get_results($posts_query);
return [
'posts' => $posts,
'pagination' => $pagination->render(),
'total_pages' => $pagination->getTotalPages(),
'current_page' => $pagination->getCurrentPage()
];
}
// Get the current page from the URL
$current_page = isset($_GET['page']) ? intval($_GET['page']) : 1;
// Get paginated posts
$result = get_posts_paginated($current_page, 5);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Blog Posts</title>
<style>
/* Basic pagination styling */
.pagination { display: flex; list-style: none; padding: 0; margin: 20px 0; }
.page-item { margin: 0 2px; }
.page-link { display: block; padding: 8px 12px; text-decoration: none; color: #007bff; background-color: #fff; border: 1px solid #ddd; border-radius: 4px; }
.current .page-link { background-color: #007bff; color: white; border-color: #007bff; }
.disabled .page-link { color: #6c757d; pointer-events: none; }
.page-link:hover { background-color: #f8f9fa; }
</style>
</head>
<body>
<h1>Blog Posts</h1>
<div class="posts">
<?php if (empty($result['posts'])): ?>
<p>No posts found.</p>
<?php else: ?>
<?php foreach ($result['posts'] as $post): ?>
<article class="post">
<h2><?php echo esc_html($post->post_title); ?></h2>
<div class="post-meta">
<?php echo esc_html(date('F j, Y', strtotime($post->post_date))); ?>
</div>
<div class="post-excerpt">
<?php echo wp_trim_words($post->post_content, 30); ?>
</div>
<a href="<?php echo get_permalink($post->ID); ?>" class="read-more">Read More</a>
</article>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php echo $result['pagination']; ?>
<div class="pagination-info">
Page <?php echo $result['current_page']; ?> of <?php echo $result['total_pages']; ?>
</div>
</body>
</html>
Conclusion: Building Your Component Library
Creating reusable PHP components is like building a personal toolbox for web development. As you expand this toolbox, your development speed increases, your code quality improves, and your applications become more maintainable.
When you approach development with a component-based mindset, you'll find yourself naturally creating more modular, reusable code. Over time, these components form a powerful library that makes each new project faster and more consistent than the last.
Key Takeaways
- Follow the DRY principle: Write code once, use it everywhere
- Design for reusability: Think about how components can be used in different contexts
- Create clear interfaces: Well-defined inputs and outputs make components easier to use
- Document your components: Good documentation makes your component library more valuable
- Build a coherent system: Components should work together like pieces of a puzzle
As you build WordPress themes and plugins, remember that the most successful ones are built on a foundation of well-designed, reusable components. Whether you're creating a simple blog or a complex application, this component-based approach will serve you well throughout your development career.
Additional Resources
- PHP Object-Oriented Programming - Official PHP documentation
- WordPress Hooks API - Essential for WordPress component development
- WordPress Template Hierarchy - Understanding WordPress templating
- WordPress Block Editor Handbook - Building WordPress Gutenberg blocks
- PHP The Right Way - Modern PHP best practices