Get Your Free Brand/Project Consultation Today and Save 10% on Your First Purchase

Ep 5: Databases, Admin Menus & Secure Data Handling (Mastering WordPress Plugin Development)

<?php

/**
 * CHAPTER 7: DATABASE OPERATIONS
 * 
 * This chapter demonstrates how to interact with the WordPress database safely and efficiently.
 * WordPress provides the $wpdb class for database operations, which offers security and convenience.
 * 
 * Key concepts covered:
 * - Creating custom database tables
 * - CRUD operations (Create, Read, Update, Delete)
 * - Data sanitization and validation
 * - Proper use of WordPress database functions
 * - Security best practices
 * - Admin interface for data management
 * 
 * IMPORTANT: WordPress Database Class ($wpdb)
 * The global $wpdb object provides access to WordPress database functions:
 * - $wpdb->prefix: WordPress table prefix (usually 'wp_')
 * - $wpdb->insert(): Insert data into tables
 * - $wpdb->update(): Update existing data
 * - $wpdb->delete(): Delete data from tables
 * - $wpdb->get_results(): Get multiple rows
 * - $wpdb->get_row(): Get single row
 * - $wpdb->get_var(): Get single value
 * - $wpdb->query(): Execute raw SQL queries
 * - $wpdb->prepare(): Prepare SQL statements (prevents SQL injection)
 * - $wpdb->get_charset_collate(): Get proper charset and collation
 * 
 * WordPress Database Table Types:
 * - Core tables: posts, users, options, etc.
 * - Custom tables: Created by plugins for specific needs
 * - Meta tables: Store additional data (postmeta, usermeta, etc.)
 * 
 * Data Types for MySQL:
 * - TINYINT: -128 to 127 (or 0 to 255 unsigned)
 * - SMALLINT: -32,768 to 32,767
 * - MEDIUMINT: -8,388,608 to 8,388,607
 * - INT: -2,147,483,648 to 2,147,483,647
 * - BIGINT: Large integer values
 * - TINYTEXT: Up to 255 characters
 * - TEXT: Up to 65,535 characters
 * - MEDIUMTEXT: Up to 16,777,215 characters
 * - LONGTEXT: Up to 4,294,967,295 characters
 * - VARCHAR(n): Variable-length string, up to n characters
 * - CHAR(n): Fixed-length string, exactly n characters
 * - DATE: Date values (YYYY-MM-DD)
 * - DATETIME: Date and time values (YYYY-MM-DD HH:MM:SS)
 * - TIMESTAMP: Timestamp values
 * - DECIMAL(m,d): Fixed-point decimal numbers
 * - FLOAT: Floating-point numbers
 * - DOUBLE: Double-precision floating-point numbers
 */

// SECTION 1: TABLE CREATION
// Creating custom database tables in WordPress

/**
 * Function to create a custom table
 * 
 * This function demonstrates the proper way to create custom tables in WordPress:
 * 1. Use global $wpdb object for database access
 * 2. Use $wpdb->prefix to ensure proper table naming
 * 3. Use $wpdb->get_charset_collate() for proper character set
 * 4. Use dbDelta() for table creation (handles updates automatically)
 * 5. Include the upgrade.php file for dbDelta() function
 * 
 * Table Structure Explanation:
 * - id: Primary key, auto-increment, medium integer (up to 8 million records)
 * - name: Short text field using TINYTEXT (up to 255 characters)
 * - value: Longer text field using TEXT (up to 65,535 characters)
 * - PRIMARY KEY: Defines the unique identifier for each row
 * 
 * Alternative Column Types You Can Use:
 * - BIGINT for larger ID numbers
 * - VARCHAR(255) for fixed-length strings
 * - LONGTEXT for very long text content
 * - DATETIME for date/time fields
 * - DECIMAL(10,2) for monetary values
 * - BOOLEAN for true/false values
 * - JSON for structured data (MySQL 5.7+)
 */
function hth_create_custom_table()
{
    global $wpdb; // Access WordPress database object

    // Create table name with WordPress prefix
    // This ensures compatibility with multisite and custom prefixes
    $table_name = $wpdb->prefix . 'hth_custom_table';
    
    // Get the proper charset and collation for the site
    // This ensures proper character encoding for international characters
    $charset_collate = $wpdb->get_charset_collate();

    // SQL query to create the table
    // Note: dbDelta() is very particular about format - spaces and capitalization matter!
    $sql = "CREATE TABLE IF NOT EXISTS $table_name (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        name tinytext NOT NULL,
        value text NOT NULL,
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY  (id)
    ) $charset_collate;";

    // Include the upgrade script that contains dbDelta()
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    
    // Execute the table creation
    // dbDelta() is WordPress's preferred method for table creation
    // It can handle both creation and updates to existing tables
    dbDelta($sql);
}

// Hook to create table when theme is switched (for demonstration)
// In production, you'd typically use plugin activation hooks
add_action('after_switch_theme', 'hth_create_custom_table');

// SECTION 2: INSERT OPERATIONS
// Adding new data to the database

/**
 * Function to insert data into the custom table
 * 
 * This function demonstrates the proper way to insert data:
 * 1. Use global $wpdb object
 * 2. Sanitize all input data before insertion
 * 3. Use $wpdb->insert() method for type-safe insertion
 * 4. Handle return values to check for success/failure
 * 
 * WordPress Sanitization Functions:
 * - sanitize_text_field(): For single-line text
 * - sanitize_textarea_field(): For multi-line text
 * - sanitize_email(): For email addresses
 * - sanitize_url(): For URLs
 * - sanitize_key(): For database keys
 * - sanitize_html_class(): For CSS classes
 * - sanitize_file_name(): For file names
 * - absint(): For positive integers
 * - intval(): For integers
 * - floatval(): For floating point numbers
 * - wp_kses(): For HTML content (allows specific tags)
 * - wp_strip_all_tags(): Remove all HTML tags
 * 
 * @param string $name The name to insert
 * @param string $value The value to insert
 * @return int|false The row ID of the inserted row, or false on failure
 */
function hth_insert_custom_data($name, $value)
{
    global $wpdb; // Access WordPress database object

    // Get table name with prefix
    $table_name = $wpdb->prefix . 'hth_custom_table';

    // Prepare the data for insertion with proper sanitization
    // This prevents SQL injection and ensures data integrity
    $data = array(
        'name' => sanitize_text_field($name),           // Sanitize single-line text
        'value' => sanitize_textarea_field($value),     // Sanitize multi-line text
        'created_at' => current_time('mysql'),          // WordPress function for current time
        'updated_at' => current_time('mysql')           // WordPress function for current time
    );

    // Optional: Define data formats for better type safety
    // %s = string, %d = integer, %f = float
    $formats = array(
        '%s', // name
        '%s', // value
        '%s', // created_at
        '%s'  // updated_at
    );

    // Insert the data into the table
    // $wpdb->insert() returns the number of rows inserted, or false on failure
    $result = $wpdb->insert($table_name, $data, $formats);
    
    // Return the inserted row ID or false
    if ($result !== false) {
        return $wpdb->insert_id; // Get the ID of the inserted row
    }
    
    return false; // Insertion failed
}

/**
 * Function to check if the custom table exists and insert initial data
 * 
 * This demonstrates:
 * - Checking if a table exists
 * - Conditional data insertion
 * - Using WordPress database methods safely
 * 
 * Table Existence Check Methods:
 * - SHOW TABLES LIKE: Check if table exists
 * - DESCRIBE table_name: Get table structure
 * - SELECT 1 FROM table_name LIMIT 1: Check if table has data
 */
function hth_check_and_insert_initial_data()
{
    global $wpdb;

    $table_name = $wpdb->prefix . 'hth_custom_table';

    // Check if the table exists using SHOW TABLES
    // $wpdb->get_var() returns a single value
    $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") == $table_name;
    
    if ($table_exists) {
        // Check if table already has data to avoid duplicates
        $count = $wpdb->get_var("SELECT COUNT(*) FROM $table_name");
        
        if ($count == 0) {
            // Insert initial sample data
            hth_insert_custom_data('Sample Name', 'Sample Value');
            hth_insert_custom_data('WordPress', 'Content Management System');
            hth_insert_custom_data('PHP', 'Server-side scripting language');
        }
    }
}

// SECTION 3: READ OPERATIONS
// Retrieving data from the database

/**
 * Function to retrieve data from the custom table
 * 
 * This function demonstrates various ways to retrieve data:
 * - Using $wpdb->get_results() for multiple rows
 * - Using different output formats (ARRAY_A, OBJECT, ARRAY_N)
 * - Basic error handling
 * 
 * WordPress Database Retrieval Methods:
 * - get_results(): Returns array of rows
 * - get_row(): Returns single row
 * - get_col(): Returns single column as array
 * - get_var(): Returns single value
 * 
 * Output Formats:
 * - OBJECT (default): Returns objects with property names
 * - ARRAY_A: Returns associative arrays
 * - ARRAY_N: Returns numerical arrays
 * 
 * @param int $limit Optional. Number of records to retrieve
 * @param string $order_by Optional. Column to order by
 * @param string $order Optional. ASC or DESC
 * @return array|null Array of results or null on failure
 */
function hth_get_custom_data($limit = null, $order_by = 'id', $order = 'ASC')
{
    global $wpdb;

    $table_name = $wpdb->prefix . 'hth_custom_table';

    // Build the query with optional parameters
    $sql = "SELECT * FROM $table_name ORDER BY $order_by $order";
    
    // Add limit if specified
    if ($limit !== null) {
        $sql .= " LIMIT " . absint($limit);
    }

    // Retrieve all data from the custom table
    // ARRAY_A returns associative arrays instead of objects
    $results = $wpdb->get_results($sql, ARRAY_A);

    return $results;
}

/**
 * Function to get a single record by ID
 * 
 * Demonstrates:
 * - Using $wpdb->prepare() to prevent SQL injection
 * - Using $wpdb->get_row() for single row retrieval
 * - Parameter validation
 * 
 * @param int $id The record ID to retrieve
 * @return array|null Single record or null if not found
 */
function hth_get_custom_data_by_id($id)
{
    global $wpdb;

    // Validate input
    $id = absint($id);
    if ($id <= 0) {
        return null;
    }

    $table_name = $wpdb->prefix . 'hth_custom_table';

    // Use $wpdb->prepare() to safely include variables in SQL
    // %d = integer, %s = string, %f = float
    $sql = $wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $id);
    
    // Get single row
    $result = $wpdb->get_row($sql, ARRAY_A);

    return $result;
}

/**
 * Function to search data by name
 * 
 * Demonstrates:
 * - Using LIKE operator for partial matching
 * - Proper escaping of LIKE wildcards
 * - Case-insensitive searching
 * 
 * @param string $search_term The term to search for
 * @return array Array of matching records
 */
function hth_search_custom_data($search_term)
{
    global $wpdb;

    $table_name = $wpdb->prefix . 'hth_custom_table';

    // Escape the search term for LIKE query
    $search_term = '%' . $wpdb->esc_like($search_term) . '%';

    // Use prepare for safe LIKE queries
    $sql = $wpdb->prepare("SELECT * FROM $table_name WHERE name LIKE %s", $search_term);
    
    $results = $wpdb->get_results($sql, ARRAY_A);

    return $results;
}

// SECTION 4: UPDATE OPERATIONS
// Modifying existing data

/**
 * Function to update data in the custom table
 * 
 * This function demonstrates:
 * - Using $wpdb->update() method
 * - Proper data sanitization for updates
 * - Using WHERE conditions safely
 * - Return value handling
 * 
 * $wpdb->update() Parameters:
 * - $table: Table name
 * - $data: Array of column => value pairs to update
 * - $where: Array of column => value pairs for WHERE clause
 * - $format: Array of formats for $data values
 * - $where_format: Array of formats for $where values
 * 
 * @param int $id The record ID to update
 * @param string $name The new name value
 * @param string $value The new value
 * @return bool True on success, false on failure
 */
function hth_update_custom_data($id, $name, $value)
{
    global $wpdb;

    // Validate input
    $id = absint($id);
    if ($id <= 0) {
        return false;
    }

    $table_name = $wpdb->prefix . 'hth_custom_table';

    // Prepare the data for updating with proper sanitization
    $data = array(
        'name' => sanitize_text_field($name),
        'value' => sanitize_textarea_field($value),
        'updated_at' => current_time('mysql')
    );

    // Define WHERE condition
    $where = array('id' => $id);

    // Define data formats for type safety
    $formats = array('%s', '%s', '%s');        // Data formats
    $where_formats = array('%d');              // Where formats

    // Update the data in the table
    // Returns number of rows updated, or false on failure
    $result = $wpdb->update($table_name, $data, $where, $formats, $where_formats);

    return $result !== false;
}

// SECTION 5: DELETE OPERATIONS
// Removing data from the database

/**
 * Function to delete data from the custom table
 * 
 * This function demonstrates:
 * - Using $wpdb->delete() method
 * - Safe deletion with WHERE conditions
 * - Input validation before deletion
 * - Return value checking
 * 
 * $wpdb->delete() Parameters:
 * - $table: Table name
 * - $where: Array of column => value pairs for WHERE clause
 * - $where_format: Array of formats for $where values
 * 
 * @param int $id The record ID to delete
 * @return bool True on success, false on failure
 */
function hth_delete_custom_data($id)
{
    global $wpdb;

    // Validate input
    $id = absint($id);
    if ($id <= 0) {
        return false;
    }

    $table_name = $wpdb->prefix . 'hth_custom_table';

    // Define WHERE condition for deletion
    $where = array('id' => $id);
    
    // Define format for WHERE condition
    $where_format = array('%d');

    // Delete the data from the table
    // Returns number of rows deleted, or false on failure
    $result = $wpdb->delete($table_name, $where, $where_format);

    return $result !== false;
}

/**
 * Function to delete all data from the custom table
 * 
 * Demonstrates:
 * - Using $wpdb->query() for custom SQL
 * - TRUNCATE vs DELETE differences
 * - Bulk operations
 * 
 * TRUNCATE vs DELETE:
 * - TRUNCATE: Faster, resets auto-increment, can't use WHERE
 * - DELETE: Slower, preserves auto-increment, can use WHERE
 * 
 * @param bool $reset_auto_increment Whether to reset the auto-increment counter
 * @return bool True on success, false on failure
 */
function hth_delete_all_custom_data($reset_auto_increment = false)
{
    global $wpdb;

    $table_name = $wpdb->prefix . 'hth_custom_table';

    if ($reset_auto_increment) {
        // Use TRUNCATE to reset auto-increment
        $result = $wpdb->query("TRUNCATE TABLE $table_name");
    } else {
        // Use DELETE to preserve auto-increment
        $result = $wpdb->query("DELETE FROM $table_name");
    }

    return $result !== false;
}

// SECTION 6: ADMIN INTERFACE
// Creating admin interface for data management

/**
 * Function to display custom data in the admin area
 * 
 * This function demonstrates:
 * - HTML table creation for data display
 * - Proper data escaping for output
 * - Handling empty data sets
 * - Using WordPress admin styles
 * 
 * WordPress Output Escaping Functions:
 * - esc_html(): Escape HTML entities
 * - esc_attr(): Escape HTML attributes
 * - esc_url(): Escape URLs
 * - esc_js(): Escape JavaScript
 * - esc_textarea(): Escape textarea content
 * - wp_kses(): Allow specific HTML tags
 * - wp_kses_post(): Allow post content HTML tags
 */
function hth_display_custom_data()
{
    // Get all data from the custom table
    $data = hth_get_custom_data();

    if (!empty($data)) {
        echo '<div class="wrap">';
        echo '<h2>' . __('Custom Data', 'hth-sample-plugin') . '</h2>';
        
        // Use WordPress admin table styling
        echo '<table class="wp-list-table widefat fixed striped">';
        echo '<thead>';
        echo '<tr>';
        echo '<th scope="col" class="manage-column">' . __('ID', 'hth-sample-plugin') . '</th>';
        echo '<th scope="col" class="manage-column">' . __('Name', 'hth-sample-plugin') . '</th>';
        echo '<th scope="col" class="manage-column">' . __('Value', 'hth-sample-plugin') . '</th>';
        echo '<th scope="col" class="manage-column">' . __('Created', 'hth-sample-plugin') . '</th>';
        echo '<th scope="col" class="manage-column">' . __('Actions', 'hth-sample-plugin') . '</th>';
        echo '</tr>';
        echo '</thead>';
        echo '<tbody>';

        foreach ($data as $row) {
            echo '<tr>';
            echo '<td>' . esc_html($row['id']) . '</td>';
            echo '<td>' . esc_html($row['name']) . '</td>';
            echo '<td>' . esc_html($row['value']) . '</td>';
            echo '<td>' . esc_html($row['created_at']) . '</td>';
            echo '<td>';
            echo '<a href="#" class="button button-small" onclick="editRecord(' . esc_attr($row['id']) . ')">' . __('Edit', 'hth-sample-plugin') . '</a> ';
            echo '<a href="#" class="button button-small button-link-delete" onclick="deleteRecord(' . esc_attr($row['id']) . ')">' . __('Delete', 'hth-sample-plugin') . '</a>';
            echo '</td>';
            echo '</tr>';
        }

        echo '</tbody>';
        echo '</table>';
        echo '</div>';
    } else {
        echo '<div class="wrap">';
        echo '<h2>' . __('Custom Data', 'hth-sample-plugin') . '</h2>';
        echo '<p>' . __('No data found.', 'hth-sample-plugin') . '</p>';
        echo '</div>';
    }
}

/**
 * Function to add a menu item in the admin area
 * 
 * This function demonstrates:
 * - Adding top-level admin menu pages
 * - Using WordPress dashicons
 * - Setting menu positions
 * - Proper capability checking
 * 
 * add_menu_page() Parameters:
 * - $page_title: Page title in browser tab
 * - $menu_title: Menu title in sidebar
 * - $capability: Required user capability
 * - $menu_slug: Unique menu slug
 * - $callback: Function to display page content
 * - $icon_url: Menu icon (dashicon or URL)
 * - $position: Menu position (lower numbers = higher position)
 * 
 * Common Menu Positions:
 * - 2: Dashboard
 * - 5: Posts
 * - 10: Media
 * - 15: Links
 * - 20: Pages
 * - 25: Comments
 * - 59: Separator
 * - 60: Appearance
 * - 65: Plugins
 * - 70: Users
 * - 75: Tools
 * - 80: Settings
 * - 99: Separator
 */
function hth_add_custom_menu_item()
{
    add_menu_page(
        __('Custom Data', 'hth-sample-plugin'),    // Page title
        __('Custom Data', 'hth-sample-plugin'),    // Menu title
        'manage_options',                          // Capability required
        'hth-custom-data',                         // Menu slug
        'hth_display_custom_data',                 // Callback function
        'dashicons-database',                      // Icon (dashicon)
        6                                          // Position (after Posts)
    );
}

// Hook to add menu item to admin
add_action('admin_menu', 'hth_add_custom_menu_item');

// SECTION 7: PLUGIN LIFECYCLE HOOKS
// Functions for plugin activation and deactivation

/**
 * Function to register a custom database table on plugin activation
 * 
 * This function runs when the plugin is activated and:
 * - Creates the custom table
 * - Inserts initial data
 * - Sets up any necessary options
 * 
 * Plugin activation is the proper time to:
 * - Create database tables
 * - Set default options
 * - Create necessary directories
 * - Schedule cron jobs
 * - Clear caches
 */
function hth_activate_plugin()
{
    // Create the custom table
    hth_create_custom_table();
    
    // Insert initial data into the custom table
    // This is optional - remove if you don't want sample data
    hth_check_and_insert_initial_data();
    
    // Set plugin version for future updates
    add_option('hth_plugin_version', '1.0.0');
    
    // Set activation timestamp
    add_option('hth_plugin_activated', current_time('mysql'));
}

/**
 * Function to deactivate the plugin and remove the custom table
 * 
 * This function runs when the plugin is deactivated and:
 * - Removes the custom table
 * - Cleans up plugin options
 * - Removes scheduled cron jobs
 * 
 * Plugin deactivation cleanup:
 * - Remove custom tables (if desired)
 * - Delete plugin options
 * - Clear scheduled events
 * - Remove temporary files
 * 
 * Note: Some plugins preserve data on deactivation
 * and only remove it on uninstall
 */
function hth_deactivate_plugin()
{
    global $wpdb;

    $table_name = $wpdb->prefix . 'hth_custom_table';

    // SQL query to drop the table
    $sql = "DROP TABLE IF EXISTS $table_name;";

    // Use $wpdb->query() for DROP TABLE operations
    // dbDelta() is not suitable for dropping tables
    $wpdb->query($sql);
    
    // Clean up plugin options
    delete_option('hth_plugin_version');
    delete_option('hth_plugin_activated');
}

/**
 * ADDITIONAL DATABASE OPERATIONS AND BEST PRACTICES:
 * 
 * 1. Advanced Query Examples:
 * 
 * // JOIN queries
 * $sql = "SELECT p.post_title, m.meta_value 
 *         FROM {$wpdb->posts} p 
 *         JOIN {$wpdb->postmeta} m ON p.ID = m.post_id 
 *         WHERE m.meta_key = '_custom_field'";
 * 
 * // COUNT queries
 * $count = $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE name LIKE '%search%'");
 * 
 * // GROUP BY queries
 * $results = $wpdb->get_results("SELECT name, COUNT(*) as count FROM $table_name GROUP BY name");
 * 
 * // Date range queries
 * $sql = $wpdb->prepare("SELECT * FROM $table_name WHERE created_at BETWEEN %s AND %s", 
 *                      '2023-01-01', '2023-12-31');
 * 
 * 2. Transaction Support:
 * 
 * // Start transaction
 * $wpdb->query('START TRANSACTION');
 * 
 * // Perform multiple operations
 * $result1 = $wpdb->insert($table_name, $data1);
 * $result2 = $wpdb->insert($table_name, $data2);
 * 
 * // Commit or rollback
 * if ($result1 && $result2) {
 *     $wpdb->query('COMMIT');
 * } else {
 *     $wpdb->query('ROLLBACK');
 * }
 * 
 * 3. Performance Optimization:
 * 
 * // Use indexes for frequently queried columns
 * $wpdb->query("ALTER TABLE $table_name ADD INDEX idx_name (name)");
 * 
 * // Use LIMIT for large datasets
 * $results = $wpdb->get_results("SELECT * FROM $table_name LIMIT 10 OFFSET 20");
 * 
 * // Use prepared statements for repeated queries
 * $stmt = $wpdb->prepare("SELECT * FROM $table_name WHERE name = %s", $name);
 * 
 * 4. Error Handling:
 * 
 * // Check for errors after database operations
 * if ($wpdb->last_error) {
 *     error_log('Database error: ' . $wpdb->last_error);
 * }
 * 
 * // Enable error reporting during development
 * $wpdb->show_errors();
 * 
 * 5. Security Considerations:
 * 
 * // Always use $wpdb->prepare() for dynamic queries
 * $sql = $wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $id);
 * 
 * // Validate and sanitize all input
 * $name = sanitize_text_field($_POST['name']);
 * $email = sanitize_email($_POST['email']);
 * 
 * // Use appropriate capabilities for admin functions
 * if (!current_user_can('manage_options')) {
 *     wp_die('Access denied');
 * }
 * 
 * 6. Backup and Migration:
 * 
 * // Export data
 * function export_custom_data() {
 *     global $wpdb;
 *     $table_name = $wpdb->prefix . 'hth_custom_table';
 *     $data = $wpdb->get_results("SELECT * FROM $table_name", ARRAY_A);
 *     return json_encode($data);
 * }
 * 
 * // Import data
 * function import_custom_data($json_data) {
 *     $data = json_decode($json_data, true);
 *     foreach ($data as $row) {
 *         hth_insert_custom_data($row['name'], $row['value']);
 *     }
 * }
 * 
 * 7. Database Versioning:
 * 
 * // Track database version for updates
 * function check_database_version() {
 *     $current_version = get_option('hth_db_version', '1.0');
 *     if (version_compare($current_version, '1.1', '<')) {
 *         upgrade_database_to_1_1();
 *         update_option('hth_db_version', '1.1');
 *     }
 * }
 * 
 * 8. Multisite Considerations:
 * 
 * // Use appropriate table prefix for multisite
 * $table_name = $wpdb->base_prefix . 'hth_custom_table'; // Global table
 * $table_name = $wpdb->prefix . 'hth_custom_table';      // Per-site table
 * 
 * // Check if multisite
 * if (is_multisite()) {
 *     // Handle multisite-specific logic
 * }
 */
Influencer, Blogger, Vlogger

Crafting Your Personal Brand: A Comprehensive Guide for Influencers

In the realm of influencers, personal branding is the key to standing out and making a lasting impact

Implementing Scrum in Your Tech Startup: A Step-by-Step Guide

In the dynamic landscape of tech startups, efficient project management is the key to success. Scrum, an agile framework,

Nuhuman’s Jeanré Van Zyl Shows Ways To Hack your Biology

Kyla sits down with Jeanré Van Zyl, a regional biohacker well-versed in the field of wellness and technology to find […]

How much will it cost?

It is often quite a simple question, but as the answer unfolds it becomes evident that it is a lot […]

Apple Gule Unplugged: A Candid Conversation with Apple Gule

Born in  Nhlangano, Swaziland, Aphelele Gule, better known as Apple Gule to his loyal fanbase, reaching various genres including house […]

Escape the 9–5: How to Build a Profitable Freelance or Digital Agency Career

1. Mindset Trumps Strategy No matter how many strategies or tutorials are available, real progress is blocked by psychological limitations. […]

Communication Hacks for Agencies, Freelancers & Startups: Save Time & Wow Your Clients

Key Takeaways Find the Right Communication Balance: Avoid overloading clients with unnecessary details or going radio-silent. Give them the info […]

From Legacy to Leadership: CTO Lessons in Building Tech that Actually Works

This is a company that's moved in from the retail space and is now moving into more areas of merchant and payment gateways, with a...