The Ultimate Developer's Guide to Customizing Osclass: Hooks, Themes, and Plugins

Osclass stands apart in the world of classifieds software due to its deliberate simplicity and raw power. As a standalone application built with pure, framework-free PHP, it offers developers an unparalleled level of control and performance, unburdened by the overhead of a multi-purpose CMS. This direct access, however, is governed by a single, unbreakable rule: Never, ever hack the core.

This in-depth technical guide is for PHP developers ready to master the Osclass platform the right way. We will dissect the three pillars of professional Osclass development: leveraging the powerful Hooks system for seamless interaction, building Child Themes for complete visual control, and engineering robust Plugins to introduce new functionality. By mastering these concepts, you can build any type of marketplace imaginable while ensuring your platform remains secure, stable, and easily upgradeable.

The Golden Rule: Why Hacking the Core is a Dead End

Before you write a single line of code, this principle must be understood. Modifying Osclass core files (any file within the oc-admin or oc-includes directories) is a critical error that leads to three disastrous outcomes:

  1. Updates Will Destroy Your Work: The moment you update to a new version of Osclass to get security patches or new features, all your custom modifications will be permanently overwritten.
  2. You Create Security Vulnerabilities: Modifying core files can inadvertently introduce security holes and makes your site incompatible with official security patches.
  3. Debugging Becomes Impossible: When issues arise, you won't be able to determine if the problem is a bug in the Osclass core or in your own custom code, making support from the community impossible.

The entire Osclass architecture is designed to be extended safely and efficiently through its APIs, primarily using the powerful system of **Hooks**.

Part 1: Mastering Osclass Hooks (The Core Interaction API)

The Hooks system is the central nervous system of Osclass development. It allows your custom code to interact with the Osclass core at hundreds of specific points without ever modifying a core file. It's directly inspired by the WordPress Hooks system and consists of two distinct types: **Actions** and **Filters**.

A. Actions: Executing Your Code at Specific Events

Actions are specific events that occur during the Osclass execution lifecycle (e.g., header, posted_item, user_register_completed). When Osclass reaches one of these points, it triggers the hook, checks if any functions are "registered" to it, and executes them in order of priority.

You use the function osc_add_hook() to attach your custom function to an action.

osc_add_hook(string $hook_name, callable $function_name, int $priority = 10);

Action Example 1: Adding a Tracking Script to the Footer

This is a classic "Hello World" for hooks. Instead of editing footer.php, you hook into the footer action.

// In your plugin's main file or your theme's functions.php
function my_custom_tracking_script() {
    echo '<!-- Custom Google Tag Manager Code -->' . PHP_EOL;
    echo "<script>console.log('Page loaded, tracking initialized.');</script>" . PHP_EOL;
}
osc_add_hook('footer', 'my_custom_tracking_script');

Action Example 2: Performing an Action After a Listing is Published

Let's create a more advanced function that logs the title of every new listing to a text file. We'll use the posted_item hook, which conveniently passes the complete item data array to our function.

function log_new_listing_title($item) {
    // The $item array contains all data for the newly posted item.
    // Let's get the title and the ID.
    $itemTitle = $item['s_title'];
    $itemId = $item['pk_i_id'];
    $logMessage = date('Y-m-d H:i:s') . " - New Listing Published (ID: " . $itemId . "): " . $itemTitle . PHP_EOL;

    // Define the path to our log file in a writable directory
    $logFile = osc_content_path() . 'logs/new_listings.log';

    // Use file_put_contents with the FILE_APPEND flag to add to the log
    @file_put_contents($logFile, $logMessage, FILE_APPEND);
}
osc_add_hook('posted_item', 'log_new_listing_title');

Comprehensive Action Hook Reference

While there are hundreds of hooks, here are some of the most critical for developers:

  • System Hooks: init (runs early), before_html (before any HTML output), after_html (after all HTML output).
  • Header & Footer Hooks: header (in <head>), admin_header, footer (before </body>), admin_footer.
  • Item (Listing) Hooks: pre_item_add (before adding to DB), posted_item (after adding), pre_item_edit (before editing), edited_item (after editing), delete_item (passes $itemId), item_form (to add fields to the publish form), item_detail (to add content to the listing page).
  • User Hooks: before_user_register (before registration), user_register_completed (after registration, passes $userId), delete_user (passes $userId), profile_form (to add fields to the user profile).
  • Search Hooks: search_form (to add fields to the search form).

B. Filters: Intercepting and Modifying Data

Filters are even more powerful than actions. They give you the ability to intercept data, modify it, and then return it before it's used by Osclass (either for display or for saving to the database). The most important rule of filters is that your hooked function must always return a value.

You use osc_add_filter() to attach your function to a filter.

osc_add_filter(string $filter_name, callable $function_name, int $priority = 10);

Filter Example 1: Appending Text to Every Listing Title

A simple example to illustrate the concept. We intercept the title, add our text, and return it.

function append_text_to_title($title) {
    // IMPORTANT: Always return the modified (or original) variable
    return $title . ' - For Sale';
}
osc_add_filter('item_title', 'append_text_to_title');

Filter Example 2: Enforcing a Minimum Price

The correct way to validate or modify input before it's saved to the database is to use an early action hook like pre_item_add or pre_item_edit to check the submitted parameters.

function validate_minimum_price() {
    $price = Params::getParam('price');
    $min_price = 5000000; // Price is stored in millionths ($5.00)
    
    // Check if a price was submitted and if it's below the minimum
    if ($price !== '' && $price < $min_price) {
        // Add a flash message to inform the user
        osc_add_flash_error_message('The minimum price is $5. Please enter a higher value.');
        
        // Redirect back to the form. For the publish form:
        osc_redirect_to(osc_item_post_url());
    }
}
osc_add_hook('pre_item_add', 'validate_minimum_price');
osc_add_hook('pre_item_edit', 'validate_minimum_price');

Filter Example 3 (Advanced): Excluding a Category from Search

The search_conditions filter is extremely powerful. It lets you modify the array of WHERE conditions for the item search SQL query. Let's exclude category ID 99 (e.g., "Archived Items") from all public searches.

function exclude_category_from_search($conditions) {
    // Add our custom WHERE clause to the array of conditions
    $conditions[] = DB_TABLE_PREFIX . 't_item.fk_i_category_id <> 99';
    
    return $conditions;
}
osc_add_filter('search_conditions', 'exclude_category_from_search');

Comprehensive Filter Hook Reference

  • Meta Data Filters: meta_title, meta_description, canonical_url.
  • Item Data Filters: item_title, item_description, item_price.
  • Form & Input Filters: item_edit_prepare (modify item data before it populates the edit form).
  • Search Filters: search_conditions (modify WHERE clause), search_order (modify ORDER BY), search_sql (modify the entire SQL query).
  • Email Filters: email_user_registration_subject, email_user_registration_description (and many others for every email template).

Part 2: Theme Development (The Presentation Layer)

The theme controls 100% of the HTML output of your Osclass site. A deep understanding of its structure is key to creating a unique user experience.

A. The Child Theme: The Professional Standard

As with the core, you should never directly edit a theme you didn't create. Always create a Child Theme. This allows you to update the parent theme (to get bug fixes and new features) without losing your customizations.

To create a child theme for the default "Bender" theme:

  1. Create a new folder: oc-content/themes/bender-child/.
  2. Inside it, create index.php with this header:
    <?php
    /*
    Theme Name: Bender Child
    Template: bender
    */
    ?>
    
    The Template: bender line is the magic that links it to the parent.
  3. Create a functions.php file in your child theme folder. This is where you can add your custom hooks and functions.
  4. Activate your "Bender Child" theme in the admin panel.

To modify a template file (e.g., item.php), simply copy it from the parent (bender/item.php) to your child theme (bender-child/item.php) and edit it there. Osclass will automatically use your child theme's version.

B. Understanding The Osclass Loop

The "Loop" is the standard mechanism Osclass uses to display a list of items on a search page. Understanding it is fundamental to theme development.

<?php if (osc_has_items()) { ?>
    <div class="listings">
        <?php while (osc_has_items()) { ?>
            <div class="listing-item">
                <h3><a href="<?php echo osc_item_url(); ?>"><?php echo osc_item_title(); ?></a></h3>
                <p class="price"><?php echo osc_item_formatted_price(); ?></p>
                <p class="location"><?php echo osc_item_city(); ?>, <?php echo osc_item_region(); ?></p>
                <?php if(osc_count_item_resources() > 0) { ?>
                    <a href="<?php echo osc_item_url(); ?>">
                        <img src="<?php echo osc_resource_thumbnail_url(); ?>" alt="<?php echo osc_esc_html(osc_item_title()); ?>" loading="lazy">
                    </a>
                <?php } ?>
            </div>
        <?php } ?>
    </div>
    <?php if (osc_search_total_pages() > 1) { ?>
        <div class="pagination">
            <?php echo osc_search_pagination(); ?>
        </div>
    <?php } ?>
<?php } else { ?>
    <p class="empty-search">No listings found.</p>
<?php } ?>

Part 3: Building a Plugin (The Functionality Layer)

When you need to add new, self-contained functionality that is independent of the visual theme, a plugin is the correct tool.

A. Plugin Activation, Deactivation, and Uninstallation

A professional plugin cleans up after itself. Osclass provides hooks to manage the plugin's lifecycle.

// This code runs when the user activates the plugin
function my_plugin_activate() {
    // Example: Create a new database table
    $conn = DBConnectionClass::newInstance();
    $conn->dao->query('CREATE TABLE IF NOT EXISTS ' . DB_TABLE_PREFIX . 't_my_plugin_log (...)');
    
    // Example: Set a default preference
    osc_set_preference('my_plugin_version', '1.0.0', 'my_plugin_settings', 'STRING');
}
osc_register_plugin(osc_plugin_path(__FILE__), 'my_plugin_activate');

// This code runs when the user deactivates the plugin
function my_plugin_deactivate() {
    // Example: Delete the preference
    osc_delete_preference('my_plugin_version', 'my_plugin_settings');
}
osc_add_hook(osc_plugin_path(__FILE__) . '_disable', 'my_plugin_deactivate');

// This code runs when the user uninstalls the plugin
function my_plugin_uninstall() {
    // Example: Drop the custom database table
    $conn = DBConnectionClass::newInstance();
    $conn->dao->query('DROP TABLE IF EXISTS ' . DB_TABLE_PREFIX . 't_my_plugin_log');
}
osc_add_hook(osc_plugin_path(__FILE__) . '_uninstall', 'my_plugin_uninstall');

B. Creating an Admin Settings Page

A plugin with options needs a settings page in the admin panel.

// In your plugin's main file, hooked to 'admin_menu_init'
function my_plugin_admin_menu() {
    osc_add_admin_menu_page(
        'My Plugin Settings',                           // Page Title
        osc_admin_render_plugin_url(osc_plugin_folder(__FILE__) . 'admin.php'), // URL to your settings file
        'my_plugin_settings',                           // Unique ID
        'plugins'                                       // Parent Menu (plugins, settings, etc.)
    );
}
osc_add_hook('admin_menu_init', 'my_plugin_admin_menu');

// Then, create admin.php in your plugin folder to render the HTML form.
// In that file, you would use osc_set_preference() to save form data.

Part 4: Interacting with the Osclass Core & Data

Beyond hooks and presentation, a powerful plugin or theme often needs to directly interact with the Osclass database, retrieve specific information, handle user input, and communicate back to the user. This section covers the essential APIs and helper functions you'll use every day to build dynamic and interactive features.

A. Database Interaction: The Data Access Object (DAO)

Osclass provides a database abstraction layer to ensure that all database queries are handled securely and consistently. You should **never** use raw mysqli_* or PDO functions. Instead, you must use the Osclass Data Access Object (DAO). This provides a simple way to build queries and automatically handles prepared statements to prevent SQL injection.

To get started, you first need to get an instance of the connection object.

$conn = DBConnectionClass::newInstance();
$dao = $conn->getDao();

SELECT Queries: Fetching Data

The DAO provides several methods to retrieve data. The most common is query() for custom selects, and findByPrimaryKey() for getting a single record by its ID.

Example: Get the details of the 5 most recent listings.

// Get the DAO instance
$dao = new DAO();

// Use the Item DAO to get the specific model for items
// This provides useful constants and table names
$itemDao = Item::newInstance();

// Build and execute the query
$dao->select('i.*, d.*');
$dao->from($itemDao->getTableName() . ' as i');
$dao->join($itemDao->getTableDescription() . ' as d', 'i.pk_i_id = d.fk_i_item_id');
$dao->where('d.fk_c_locale_code', osc_current_user_locale());
$dao->where('i.b_enabled', 1);
$dao->where('i.b_active', 1);
$dao->orderBy('i.dt_pub_date', 'DESC');
$dao->limit(5);
$result = $dao->get();

// The result is an object. To get an array of items:
$items = $result->result();

if (!empty($items)) {
    echo '<ul>';
    foreach ($items as $item) {
        // The item array contains all database fields for that listing
        echo '<li>(' . $item['pk_i_id'] . ') ' . $item['s_title'] . '</li>';
    }
    echo '</ul>';
}

INSERT Queries: Adding New Data

When your plugin needs its own database table, you'll use the insert() method. It takes the table name and an array of key-value pairs ('column_name' => 'value').

Example: Log a search query to a custom plugin table.

// Assume you created a table named 't_my_plugin_searches' during plugin activation
$searchLogTable = DB_TABLE_PREFIX . 't_my_plugin_searches';

$dataToInsert = array(
    's_query'      => Params::getParam('sPattern'), // Get search query safely
    'dt_date'      => date('Y-m-d H:i:s'),
    'fk_i_user_id' => osc_logged_user_id() // Returns user ID or null
);

// Get the DAO and perform the insert
$dao = new DAO();
$success = $dao->insert($searchLogTable, $dataToInsert);

if ($success) {
    // The query was successful
} else {
    // There was an error
}

UPDATE Queries: Modifying Existing Data

The update() method is used to modify existing records. It requires the table name, an array of data to update, and an array specifying the WHERE clause.

Example: Add a "view count" to your custom search log table.

$searchLogTable = DB_TABLE_PREFIX . 't_my_plugin_searches';
$logIdToUpdate = 123; // The primary key of the record to update

// We need to increment the existing view count
$dao = new DAO();
$dao->update($searchLogTable, array('i_views = i_views + 1'), array('pk_i_id' => $logIdToUpdate));

DELETE Queries: Removing Data

The delete() method removes records. It takes the table name and a WHERE clause array.

Example: Delete old log entries from your custom table.

$searchLogTable = DB_TABLE_PREFIX . 't_my_plugin_searches';

// Delete all logs older than 30 days
$dao = new DAO();
$success = $dao->delete($searchLogTable, "dt_date < DATE_SUB(NOW(), INTERVAL 30 DAY)");

B. Using Osclass Global Helpers & The View API

Osclass provides hundreds of global "helper" functions that handle the logic of retrieving and formatting data for display. You should **always** use these helpers in your themes and plugins instead of querying the database directly and formatting the data yourself. This ensures your code remains compatible with future Osclass updates.

These functions are only available within the "View" context. This means they work automatically in theme files. If you need to use them inside a function in a plugin, you must first get the View object.

$view = View::newInstance();

Item Helpers (Inside the Loop)

These functions work when you are inside the Osclass Loop (while (osc_has_items()) { ... }) on a search page, or on an item page. They automatically refer to the current listing being displayed.

  • osc_item_id(): Returns the integer ID of the item.
  • osc_item_title(): Returns the sanitized title of the item.
  • osc_item_description(): Returns the sanitized description.
  • osc_item_formatted_price(): Returns the price, correctly formatted with currency symbol and decimal places.
  • osc_item_pub_date(): Returns the publication date, formatted according to your site's settings.
  • osc_item_city(), osc_item_region(), osc_item_country(): Returns location details.
  • osc_item_url(): Returns the full, SEO-friendly URL to the listing.
  • osc_count_item_resources(): Returns the number of images/media attached to the listing.

User Helpers

These functions help you retrieve information about the currently logged-in user or the user who posted a listing.

  • osc_is_web_user_logged_in(): Returns true or false. Essential for conditional logic.
  • osc_logged_user_id(): Returns the integer ID of the logged-in user.
  • osc_logged_user_name(): Returns the name of the logged-in user.
  • osc_logged_user_email(): Returns the email of the logged-in user.
  • osc_user_id(): Inside the loop or on an item page, returns the ID of the item's author.
  • osc_user_name(): Inside the loop or on an item page, returns the name of the item's author.

URL Helpers

Never hardcode URLs in your themes or plugins. Always use URL helpers to generate them dynamically. This ensures your links will work even if you change your site's URL structure.

  • osc_base_url(): Returns the base URL of your site.
  • osc_contact_url(): Returns the URL of the contact page.
  • osc_user_dashboard_url(): Returns the URL to the logged-in user's dashboard.
  • osc_user_login_url(): Returns the URL of the login page.
  • osc_user_register_url(): Returns the URL of the registration page.

C. Handling Forms & Secure User Input

When creating a settings page for a plugin or a custom form for users, you must handle the input securely. Osclass provides tools to help with this.

Step 1: Creating the Form with CSRF Protection

Cross-Site Request Forgery (CSRF) is a common vulnerability. Osclass has a built-in system to prevent it. You must include a CSRF token in all your forms.

<form action="<?php echo osc_admin_render_plugin_url(osc_plugin_folder(__FILE__) . 'admin.php'); ?>" method="post">
    <!-- IMPORTANT: This hidden input is for security -->
    <input type="hidden" name="action_specific" value="save_my_settings" />
    <?php AdminForm::generate_csrf_token(); ?>

    <h2>My Plugin Settings</h2>
    <label for="apiKey">API Key</label>
    <input type="text" name="apiKey" id="apiKey" value="<?php echo osc_esc_html(osc_get_preference('api_key', 'my_plugin_settings')); ?>" />
    
    <button type="submit">Save Settings</button>
</form>

Step 2: Processing the Form Data Safely

In your processing file (my_plugin/admin.php in this case), you must verify the CSRF token and use the Params class to retrieve the submitted data. **Never use $_POST or $_GET directly.** The Params class automatically runs sanitization routines on the input.

<?php
// First, check if the form was submitted with our specific action
if (Params::getParam('action_specific') == 'save_my_settings') {
    
    // SECOND, verify the CSRF token to prevent unauthorized submissions
    AdminForm::is_csrf_token_valid();

    // THIRD, get the submitted data using the Params class
    $apiKey = Params::getParam('apiKey', false, false); // Params::getParam(key, xss_check, quotes_check)

    // FOURTH, save the data using the Preferences API
    osc_set_preference('api_key', $apiKey, 'my_plugin_settings', 'STRING');

    // FIFTH, provide feedback to the user and redirect
    osc_add_flash_ok_message('Your settings have been saved successfully.', 'admin');
    osc_redirect_to(osc_admin_render_plugin_url(osc_plugin_folder(__FILE__) . 'admin.php'));
}
?>

D. Communicating with the User via Flash Messages

Flash messages are temporary notifications displayed to the user after they perform an action (e.g., "Your listing has been published," "Settings saved," "Invalid email address").

Setting a Flash Message

You can set flash messages from anywhere in your plugin or theme's functions. They are stored in the session and displayed on the next page load.

  • osc_add_flash_ok_message('Success! Your profile was updated.') - For success (green).
  • osc_add_flash_info_message('Your subscription is expiring in 7 days.') - For information (blue).
  • osc_add_flash_warning_message('Please review your listing before publishing.') - For warnings (yellow).
  • osc_add_flash_error_message('The password you entered was incorrect.') - For errors (red).

Displaying Flash Messages in a Theme

To actually show the messages to the user, you need to include the generic message template in your theme files (e.g., in header.php or main.php).

<?php osc_show_flash_message(); ?>

This single function will render any pending flash messages with the correct styling and then clear them from the session so they don't appear again.

Part 5: Advanced Plugin & Theme Development Techniques

With the fundamentals of hooks, themes, and database interaction covered, we can now explore more advanced techniques that are essential for building a modern, dynamic, and professional Osclass platform. This section will cover implementing AJAX, making your extensions translatable (Internationalization), working with custom fields, and creating unique URL routes.

A. Implementing AJAX in Osclass for Dynamic Content

Asynchronous JavaScript and XML (AJAX) allows you to update parts of a webpage without needing to reload the entire page. This is crucial for features like live search, contact forms, or adding an item to a "favorites" list. Osclass has a built-in AJAX handler that makes this process secure and standardized.

The process involves three key steps: the JavaScript request, hooking into the Osclass AJAX API, and the PHP handler function.

Step 1: The JavaScript Request (Client-Side)

First, you need to write the JavaScript that sends the request. We'll use jQuery, which is included with Osclass. The critical part is passing a security token to verify the request is legitimate.

Example: A "Favorite this Item" button in item.php.

<!-- In your theme's item.php file -->
<button class="add-to-favorites" data-item-id="<?php echo osc_item_id(); ?>">Add to Favorites</button>
// In your theme's main javascript file
$(document).ready(function(){
    $('.add-to-favorites').on('click', function(e){
        e.preventDefault();
        
        var button = $(this);
        var itemId = button.data('item-id');
        
        // The AJAX URL needs a custom action name
        var ajaxUrl = '<?php echo osc_ajax_hook_url('my_plugin_favorite_item'); ?>';
        
        $.ajax({
            url: ajaxUrl,
            type: 'POST',
            data: {
                itemId: itemId
            },
            dataType: 'json',
            success: function(response) {
                if(response.success) {
                    button.text('Favorited!').prop('disabled', true);
                    alert(response.message);
                } else {
                    alert('Error: ' + response.message);
                }
            },
            error: function() {
                alert('An unexpected error occurred. Please try again.');
            }
        });
    });
});

Step 2: Hooking into the Osclass AJAX API (Server-Side)

Osclass listens for AJAX calls using a specific action hook format: osc_ajax_{your_action_name}. The your_action_name must match what you used to generate the AJAX URL in your JavaScript.

// In your plugin's main file or your theme's functions.php
osc_add_hook('osc_ajax_my_plugin_favorite_item', 'my_plugin_handle_favorite_request');

Step 3: The PHP Handler Function (Server-Side)

This is the PHP function that will process the request. It must perform its logic and then echo a JSON-encoded response before terminating the script.

function my_plugin_handle_favorite_request() {
    // Get the data from the request
    $itemId = Params::getParam('itemId');
    $userId = osc_logged_user_id();

    // Perform your business logic
    if (!$userId) {
        echo json_encode(['success' => false, 'message' => 'You must be logged in to favorite items.']);
        die();
    }
    
    if ($itemId > 0) {
        // Here, you would add your database logic to save the favorite.
        // For example: Favorites::newInstance()->add($userId, $itemId);
        
        $response = [
            'success' => true,
            'message' => 'Item successfully added to your favorites!'
        ];
    } else {
        $response = [
            'success' => false,
            'message' => 'Invalid Item ID provided.'
        ];
    }

    // Echo the response and terminate the script
    header('Content-Type: application/json');
    echo json_encode($response);
    die();
}

B. Internationalization (i18n): Making Your Extensions Translatable

If you plan to share your plugin or theme, it is essential to make it translatable. This process, called Internationalization (i18n), involves wrapping all human-readable strings in your code with special Gettext functions. This allows other users to create language files (.po and .mo) to translate your extension into their own language.

The Core Gettext Functions

Osclass provides several helper functions that are wrappers for the standard PHP Gettext extension.

  • __(): Use this when you need to **return** a translatable string (e.g., assign it to a variable).
  • _e(): Use this when you need to **echo** a translatable string directly to the browser.

Example: Making a simple string translatable.

<?php
// --- INCORRECT (Hardcoded string) ---
$my_variable = 'Hello World';
echo '<h2>My Plugin Settings</h2>';

// --- CORRECT (Translatable strings) ---
// Use __() to return the string to a variable
$my_variable = __('Hello World', 'my_plugin_domain');

// Use _e() to echo the string directly
echo '<h2>';
_e('My Plugin Settings', 'my_plugin_domain');
echo '</h2>';
?>

The second argument, 'my_plugin_domain', is the "text domain." It's a unique identifier for your plugin or theme that tells Osclass which language file to load the translation from.

Creating the Language Files

Once your code is prepared with Gettext functions, you can use a program like Poedit to scan your plugin's folder. It will find all the translatable strings and generate a .pot (Portable Object Template) file. You can then translate this file into other languages, creating .po and .mo files for each one, which should be placed in your plugin's languages subfolder.

C. Working with Custom Fields (Item Meta)

Custom fields are one of Osclass's most powerful features for creating a niche marketplace. Once you've created a custom field in the admin panel (e.g., a "Mileage" field for cars), you need to know how to display its value in your theme.

Displaying Custom Field Values on the Item Page

Osclass provides a simple loop to iterate through all the custom fields associated with a listing. This should be used within your theme's item.php file.

<?php if (osc_has_custom_fields()) { ?>
    <div class="item-custom-fields">
        <h3>Additional Details</h3>
        <ul>
            <?php while (osc_has_custom_fields()) { ?>
                <?php if (osc_field_value() != '') { ?>
                    <li>
                        <strong><?php echo osc_field_name(); ?>:</strong>
                        <?php echo osc_field_value(); ?>
                    </li>
                <?php } ?>
            <?php } ?>
        </ul>
    </div>
<?php } ?>

D. Creating Custom URL Routes

Sometimes a plugin needs its own custom, user-facing URL that doesn't follow the standard Osclass structure (e.g., your-site.com/my-plugin/dashboard/). Osclass has a routing system that lets you define custom URL rules and map them to a specific PHP file in your plugin.

Registering a New Route

You register a new route using osc_add_route(). This is typically done from your plugin's main file.

function my_plugin_register_routes() {
    osc_add_route(
        'my_plugin_dashboard',               // Unique Route Name
        'my-plugin/dashboard/?',             // The URL Regex Rule
        'my-plugin/dashboard/',              // The "pretty" URL to rewrite to
        osc_plugin_folder(__FILE__) . 'views/user_dashboard.php' // The plugin file to load
    );
}
// You can hook this into 'init'
osc_add_hook('init', 'my_plugin_register_routes');

Now, when a user visits https://your-site.com/my-plugin/dashboard/, Osclass will load the content from the user_dashboard.php file located in your plugin's views folder. This allows you to create complex, multi-page plugins with clean, SEO-friendly URLs.

Part 6: Deeper Integration & Advanced Administration

A truly professional Osclass plugin or theme doesn't just add front-end features; it integrates seamlessly into the Osclass administration panel. This provides a polished user experience for the site owner and elevates your extension from a simple script to a complete solution. This section covers advanced techniques for creating admin dashboard widgets, extending the core "Manage Items" table, leveraging Osclass's data storage APIs, and creating custom email notifications.

A. Creating Admin Dashboard Widgets

The Osclass admin dashboard is widget-based, allowing users to customize the information they see at a glance. Your plugin can register its own widgets to display important statistics, recent activity, or quick action links. This is achieved by creating a widget class and registering it with Osclass.

Step 1: Registering the Widget

You must tell Osclass about your new widget. This is done by hooking into the widgets_init action and calling osc_register_widget() from your plugin's main file.

// In your plugin's main file
function my_plugin_register_widgets() {
    require_once osc_plugin_path(__FILE__) . 'widgets/LatestUnapprovedWidget.php';
    osc_register_widget('LatestUnapprovedWidget');
}
osc_add_hook('widgets_init', 'my_plugin_register_widgets');

Step 2: Building the Widget Class

Now, create the actual widget file (widgets/LatestUnapprovedWidget.php). The class must extend the AdminWidget base class and implement a render() method. This method contains the logic to fetch and display the widget's content.

<?php
class LatestUnapprovedWidget extends AdminWidget {
    
    public function __construct() {
        parent::__construct(
            'latest_unapproved_widget', // Unique widget ID
            __('Latest Unapproved Items', 'my_plugin_domain'), // Widget Name
            __('Displays a list of the 5 most recent listings awaiting approval.', 'my_plugin_domain') // Widget Description
        );
    }

    /**
     * The main function that renders the widget's HTML content.
     */
    public function render() {
        // Use the Item DAO to find items that are not active and not enabled
        $items = Item::newInstance()->find(
            array(
                'b_active'  => false,
                'b_enabled' => false
            ),
            0, // Start from the first record
            5, // Limit to 5 results
            'dt_pub_date', // Order by
            'DESC' // Order direction
        );

        echo '<div class="widget-latest-unapproved">';
        if (count($items) > 0) {
            echo '<ul>';
            foreach ($items as $item) {
                $url = osc_admin_base_url(true) . '?page=items&action=item_edit&id=' . $item['pk_i_id'];
                echo '<li>';
                echo '<a href="' . $url . '" target="_blank">' . osc_esc_html($item['s_title']) . '</a>';
                echo ' <span style="color:#999;">(' . osc_format_date($item['dt_pub_date']) . ')</span>';
                echo '</li>';
            }
            echo '</ul>';
        } else {
            echo '<p>' . __('No items are currently awaiting approval.', 'my_plugin_domain') . '</p>';
        }
        echo '</div>';
    }
}
?>

Once this is done, the site administrator can go to the dashboard, click "Add Widget," and find "Latest Unapproved Items" in the list of available widgets.

B. Extending the Admin "Manage Items" Table

One of the most powerful ways to improve an admin's workflow is to add custom, relevant information directly to the main listings table at Tools > Items. You can add new columns with custom data and even add new bulk actions.

Adding a Custom Column

This is a two-step process: first, you add the column header, and second, you populate the column's content for each row.

// In your plugin's main file

// Step 1: Add the column header
function my_plugin_add_item_table_header($columns) {
    // Add our new column at the 3rd position (index 2)
    array_splice($columns, 2, 0, array('my_custom_column' => __('My Custom Data', 'my_plugin_domain')));
    return $columns;
}
osc_add_filter('manage_items_columns', 'my_plugin_add_item_table_header');

// Step 2: Populate the column for each item row
function my_plugin_add_item_table_content($item) {
    // The $item array contains all data for the current row
    // Check if we are in our custom column
    if (osc_current_admin_column() === 'my_custom_column') {
        // Example: Get a custom meta field value for this item
        $my_data = osc_get_item_meta('my_plugin_custom_field');
        echo ($my_data != '') ? osc_esc_html($my_data) : 'N/A';
    }
}
osc_add_hook('items_processing_row', 'my_plugin_add_item_table_content');

Adding a New Bulk Action

This allows an admin to select multiple listings and apply a custom action to all of them at once.

// In your plugin's main file

// Step 1: Add the option to the dropdown menu
function my_plugin_add_bulk_action($actions) {
    $actions['my_custom_action'] = __('Mark as Special', 'my_plugin_domain');
    return $actions;
}
osc_add_filter('item_bulk_actions', 'my_plugin_add_bulk_action');

// Step 2: Process the action when the form is submitted
function my_plugin_handle_bulk_action($action, $itemIds) {
    if ($action === 'my_custom_action') {
        $count = 0;
        foreach ($itemIds as $id) {
            // Your logic here: update a custom field, send an API call, etc.
            // For example, let's update a meta field for each item
            Item::newInstance()->update(array('b_special' => 1), array('pk_i_id' => $id));
            $count++;
        }
        osc_add_flash_ok_message($count . ' ' . __('items have been marked as special.', 'my_plugin_domain'), 'admin');
    }
}
// Note: This hook receives the action name and an array of selected item IDs
osc_add_hook('item_bulk_action', 'my_plugin_handle_bulk_action', 10, 2);

C. The Session and Preferences APIs: Storing Data Correctly

Osclass provides two primary mechanisms for storing data: the **Session** for temporary, user-specific data, and **Preferences** for permanent, site-wide settings.

Working with the Session API

The Session is for data that should only persist for a single user's visit. It's perfect for multi-step forms or storing temporary user choices. Always use the Session class wrapper.

// Get the Session instance
$session = Session::newInstance();

// Store a value in the session
$session->_set('my_plugin_user_choice', 'blue');

// Retrieve a value from the session
$userColor = $session->_get('my_plugin_user_choice'); // Returns 'blue'

// Check if a session variable exists
if ($session->_is_set('my_plugin_user_choice')) {
    // ...
}

// Remove a value from the session
$session->_drop('my_plugin_user_choice');

Working with the Preferences API

Preferences are stored permanently in the t_preference database table. This is the correct way to store your plugin's settings. The API handles serialization and caching automatically.

// To save a preference
// osc_set_preference(key, value, section, type)
// 'section' should be a unique name for your plugin to avoid conflicts
osc_set_preference('api_key', 'xyz123abc', 'my_plugin_settings', 'STRING');
osc_set_preference('enable_feature', true, 'my_plugin_settings', 'BOOLEAN');
osc_set_preference('item_limit', 50, 'my_plugin_settings', 'INTEGER');

// To retrieve a preference
$apiKey = osc_get_preference('api_key', 'my_plugin_settings');
$isFeatureEnabled = (bool) osc_get_preference('enable_feature', 'my_plugin_settings');

// To delete a preference
osc_delete_preference('api_key', 'my_plugin_settings');

D. Creating and Sending Custom Email Notifications

A professional plugin often needs to send its own unique email notifications. Osclass has a robust, template-based email system that you should always use instead of calling PHP's mail() function directly. This ensures emails are themed correctly and can be translated by the user.

Step 1: Register Your Email Template (on Plugin Activation)

First, you need to add your email's content to the database so the admin can edit it.

function my_plugin_activate() {
    $email_data = array(
        's_name' => 'My Plugin Notification',
        's_internal_name' => 'my_plugin_custom_email', // Unique internal name
        's_title' => 'Hello, {CONTACT_NAME}! A special event has occurred.',
        's_text' => '<p>Dear {CONTACT_NAME},</p><p>We are writing to inform you that the listing "{ITEM_TITLE}" has been flagged for review.</p><p>Thank you,<br>{SITE_TITLE}</p>'
    );
    
    // Use the EmailTemplates model to insert it
    EmailTemplates::newInstance()->insert($email_data);
}
osc_register_plugin(osc_plugin_path(__FILE__), 'my_plugin_activate');

Step 2: Triggering the Email Send

To send the email, you gather your data, prepare your placeholders (like {CONTACT_NAME}), and then use osc_send_mail().

function send_my_custom_notification($itemId) {
    // Get the item and user data
    $item = Item::newInstance()->findByPrimaryKey($itemId);
    $user = User::newInstance()->findByPrimaryKey($item['fk_i_user_id']);

    // Get the email template from the database
    $email_template = EmailTemplates::newInstance()->findByInternalName('my_plugin_custom_email');
    
    // Prepare the placeholders to be replaced
    $placeholders = array(
        '{CONTACT_NAME}' => $user['s_name'],
        '{ITEM_TITLE}'   => $item['s_title'],
        '{SITE_TITLE}'   => osc_page_title(),
        '{SITE_URL}'     => osc_base_url()
    );

    // Replace placeholders in the subject and body
    $subject = osc_apply_placeholders($email_template['s_title'], $placeholders);
    $body = osc_apply_placeholders($email_template['s_text'], $placeholders);

    // Create the email parameters array
    $email_params = array(
        'to'       => $user['s_email'],
        'to_name'  => $user['s_name'],
        'subject'  => $subject,
        'body'     => $body
    );

    // Send the email using the Osclass mailer
    osc_send_mail($email_params);
}

Part 7: Advanced Development Patterns & Best Practices

Mastering the APIs is the first step; becoming a professional Osclass developer requires adopting patterns that ensure your extensions are robust, secure, and seamlessly integrated. This final section moves beyond individual functions to explore higher-level concepts: leveraging the Osclass Model layer for elegant data manipulation, implementing advanced security practices, automating tasks with the Command Line Interface (CLI), and creating front-end widgets that empower site administrators.

A. Leveraging the Osclass Model Layer for Cleaner Code

While the Data Access Object (DAO) is excellent for custom SQL queries, Osclass is built on a Model-View-Controller (MVC) architecture. The **Models** (e.g., Item, User, Category) are the heart of this pattern. They contain the business logic and pre-built methods for common data operations. Using Models instead of the DAO for standard tasks leads to cleaner, more readable, and more maintainable code.

DAO vs. Model: A Practical Comparison

Imagine you need to fetch all active listings for a specific user (ID 123).

The DAO approach (more verbose):

$dao = new DAO();
$dao->select();
$dao->from(DB_TABLE_PREFIX . 't_item');
$dao->where('fk_i_user_id', 123);
$dao->where('b_active', 1);
$dao->where('b_enabled', 1);
$result = $dao->get();
$items = $result->result();

The Model approach (cleaner and more abstract):

// The Item model has a built-in method for this exact task
$items = Item::newInstance()->findByUserID(123);

The Model approach is not only shorter but also less prone to error, as the complex query logic is handled internally by the Osclass core. You should always check the relevant Model file in oc-includes/osclass/model/ to see if a method already exists for your needs before writing a custom DAO query.

Powerful Model Methods You Should Use:

  • Item::newInstance()->findLatest($count): Gets the $count most recently published items.
  • Item::newInstance()->findPopular($count): Gets the $count most viewed items.
  • User::newInstance()->findByEmail($email): Finds a user by their email address.
  • Category::newInstance()->findSubcategories($categoryId): Gets all direct subcategories of a given category ID.
  • Alerts::newInstance()->findSubscribers($itemId): Finds all users who have an active search alert that matches a newly published item.

B. Advanced Security Practices: A Developer's Responsibility

Writing secure code is non-negotiable. While the Osclass core provides a secure foundation, a poorly written plugin can expose a website to significant risk. Go beyond the basics of CSRF nonces with these critical practices.

1. Input Validation vs. Sanitization

These two concepts are often confused. Sanitization (like using Params::getParam()) cleans data. Validation confirms data is what you expect it to be. You must do both.

Example: Validating a user-submitted age field.

// Get the sanitized input
$age = Params::getParam('user_age');

// Now, VALIDATE it
if (!is_numeric($age) || $age < 18 || $age > 120) {
    // The data is not a valid age, even if it's sanitized.
    // Return an error and do not process it.
    osc_add_flash_error_message('Please enter a valid age between 18 and 120.');
    // Redirect back to the form...
} else {
    // Validation passed, proceed to save the integer value.
    User::newInstance()->update(array('i_age' => (int)$age), array('pk_i_id' => osc_logged_user_id()));
}

2. Output Escaping: Preventing Cross-Site Scripting (XSS)

Never trust data, even data from your own database. It could have been compromised or entered maliciously. You must "escape" all data just before you echo it to the browser to prevent malicious scripts from running.

  • osc_esc_html($string): Use this for echoing content inside a standard HTML element (e.g., <div>, <p>, <strong>). This is the most common escaping function.
  • osc_esc_js($string): Use this when echoing a string inside a JavaScript block (e.g., in an alert() or when defining a variable).
  • esc_attr($string): A WordPress-compatible function for echoing content inside an HTML attribute (e.g., title="..." or placeholder="...").
<?php $listingTitle = osc_item_title(); // Gets the raw title ?>

<!-- CORRECT: Escaped for HTML context -->
<h2 title="<?php echo esc_attr($listingTitle); ?>"><?php echo osc_esc_html($listingTitle); ?></h2>

<script>
    // CORRECT: Escaped for JavaScript context
    var itemTitle = '<?php echo osc_esc_js($listingTitle); ?>';
    console.log('The title of this item is: ' + itemTitle);
</script>

3. Checking User Permissions and Capabilities

Never assume a user has the right to perform an action. Always check their permissions, especially for admin-side functionality.

  • osc_is_admin_user_logged_in(): Returns true if the current user is a logged-in administrator.
  • osc_is_web_user_logged_in(): Returns true if the current user is a logged-in front-end user.
  • osc_item_user_id(): Returns the ID of the user who published the current item. You can compare this to osc_logged_user_id() to see if the current user is the owner of the listing.

C. Automating Tasks with the Osclass CLI (cron.php)

Many marketplaces require automated, recurring tasks, such as deactivating expired listings, sending out daily email digests, or cleaning up temporary files. Osclass includes a Command Line Interface (CLI) entry point, cron.php, which can be triggered by a server cron job to run these tasks.

Step 1: Create Your Custom Cron Function

You can create a function that performs your desired task and hook it into one of Osclass's built-in schedules: cron_hourly, cron_daily, or cron_weekly.

// In your plugin's main file

// This function will contain the logic for our automated task
function my_plugin_deactivate_old_listings() {
    $conn = DBConnectionClass::newInstance();
    $dao = $conn->getDao();

    // Deactivate all items older than 90 days that have not been renewed
    $dao->update(
        Item::newInstance()->getTableName(),
        array('b_active' => 0),
        "dt_pub_date < DATE_SUB(NOW(), INTERVAL 90 DAY)"
    );
    
    // You could also log that the cron ran successfully
    error_log('My Plugin: Daily deactivation cron ran successfully.');
}

// Attach our function to the built-in daily cron hook
osc_add_hook('cron_daily', 'my_plugin_deactivate_old_listings');

Step 2: Setting up the Server Cron Job

To run all daily cron hooks, you would set up a cron job on your server (via cPanel or the command line) to execute the following command once per day:

php /path/to/your/osclass/cron.php --cron-type=daily

This command will trigger the cron_daily hook in Osclass, which in turn will execute your my_plugin_deactivate_old_listings function and any other functions attached to that hook.

D. Creating Front-end Widgets for Your Theme

Plugins can provide their own front-end widgets that can be placed in a theme's sidebar or footer. This allows your plugin's functionality to be visually integrated into any theme.

Step 1: Register the Widget File

You must include your widget file and then register it using osc_register_widget().

// In your plugin's main file
function my_plugin_register_frontend_widgets() {
    require_once osc_plugin_path(__FILE__) . 'widgets/TopViewedWidget.php';
    osc_register_widget('TopViewedWidget');
}
// A good place to call this is in the main plugin file or hooked into 'init'.
osc_add_hook('init', 'my_plugin_register_frontend_widgets');

Step 2: Create the Widget Class

The front-end widget class must extend the Widget base class and contain a widget() function. This function is responsible for rendering the widget's HTML.

<?php
class TopViewedWidget extends Widget {

    public function __construct() {
        $this->setAsciiName('top_viewed_widget');
        $this->setName(__('Top 5 Most Viewed Items', 'my_plugin_domain'));
        $this->setDescription(__('Displays a list of the 5 most popular listings on the site.', 'my_plugin_domain'));
    }

    /**
     * The main function that renders the widget's HTML content.
     * @param array $params Contains any options set by the user in the admin panel.
     */
    public function widget($params = array()) {
        // Use the Item model to get the 5 most popular items
        $popularItems = Item::newInstance()->findPopular(5);

        echo '<div class="widget widget-top-viewed">';
        echo '<h3>' . $this->getName() . '</h3>';

        if (count($popularItems) > 0) {
            echo '<ul>';
            foreach ($popularItems as $item) {
                // To use item helpers here, we need to manually set the view context
                View::newInstance()->_exportVariableToView('item', $item);
                
                echo '<li>';
                echo '<a href="' . osc_item_url() . '">' . osc_item_title() . '</a>';
                echo ' <span>(' . osc_item_views() . ' views)</span>';
                echo '</li>';
            }
            echo '</ul>';
        } else {
            echo '<p>' . __('No listings have been viewed yet.', 'my_plugin_domain') . '</p>';
        }
        
        echo '</div>';
    }
}
?>

Once registered, administrators can go to Appearance > Widgets, drag the "Top 5 Most Viewed Items" widget into a sidebar, and it will appear on the front end of their site.