Back to overview

Extending the Application

Developer Tutorials Published on 01.11.2025

Professional Developer Guide: Extending the Application

This guide is for professional developers who want to build custom extensions, integrations, or complex business logic on top of the core application. The primary goal is to create these extensions in a way that minimizes modifications to the core files, ensuring that future updates to the application can be applied with minimal friction.


1. Core Philosophy: Separation of Concerns

To ensure your extensions are update-proof, adhere to this principle: Do not modify core application files. Core files include init.php, dbfunctions.php, login.php, custom_table_view.php, etc.

Instead, leverage the built-in extension points and place all your custom code in a dedicated, separate directory.

Recommended Project Structure

We recommend creating a /custom directory in the application root to house all your extensions.

/
├── admin_customtables.php
├── custom/
│   ├── my_crm_module/
│   │   ├── actions/
│   │   │   ├── approve_order.php
│   │   │   └── generate_invoice.php
│   │   ├── events/
│   │   │   └── on_order_update.php
│   │   ├── includes/
│   │   │   ├── crm_helpers.php
│   │   │   └── lib/ (e.g., for Composer packages)
│   │   └── custom_init.php
│   └── my_other_module/
│       └── ...
├── dbfunctions.php
├── index.php
└── init.php
  • custom_init.php: A central file for your module that can be included by all your scripts. It's perfect for require_once calls to your helper files or Composer's autoload.php.
  • actions/: Contains the server-side scripts that will be called by your Custom Buttons.
  • events/: Contains scripts triggered by Custom Events.
  • includes/: For helper functions, classes, or third-party libraries.

By keeping your code isolated, you can update the core application by simply replacing the original files, leaving your /custom directory untouched.


2. Key Extension Points

The application provides several "hooks" where you can inject your custom logic.

2.1. Custom Buttons

This is the most powerful entry point for custom server-side code.

  1. Create your script: Write a PHP script (e.g., /custom/my_crm_module/actions/approve_order.php).
  2. Create a Custom Button: In the admin area, create a button and set its URL to point to your script. You can pass context using placeholders:
    • URL: custom/my_crm_module/actions/approve_order.php?table_id={TABLE_ID}&id={id}
  3. Secure your script: Your script must be self-contained and secure. See the "Secure Scripting Guide" below.

2.2. Custom Events

These allow you to run server-side logic automatically in response to database events (Create, Update, Delete).

  1. Create your script: Write a PHP script (e.g., /custom/my_crm_module/events/on_order_update.php). This script will have access to global variables like $table_id, $record_id, and $record_data.
  2. Create a Custom Event: In the admin area, create an event, select the trigger (e.g., After saving a record), and point it to your script's path.

2.3. The initialisierung.php Script

For branded, multi-app setups, the setup_<name>/initialisierung.php script is a crucial one-time provisioning tool.

  • Purpose: It runs once when the first user of a new team logs in.
  • Use Cases:
    • Provisioning Tables: Use the duplicate_table_for_user() function to copy pre-configured "template" tables into the new team's workspace.
    • Seeding Data: Programmatically create initial value_lists entries or default records for the new team.
    • Setting Default Rights: Create user_groups and user_rights entries to establish a default permission scheme for the new team.

Example initialisierung.php:

<?php
// This script is included by the core logic.
// Do not include init.php here.
// Globals $user_id and $team are available.

require_once __DIR__ . '/../../dbfunctions.php';

// Find the ID of the master address template table
$template_table = DBGet('custom_tables', null, ['id'], "table_name = ? AND is_template = 1", ['addresses']);
if ($template_table) {
    $template_table_id = $template_table[0]['id'];
    // Create a copy of the 'addresses' table for the new team
    duplicate_table_for_user($template_table_id, $user_id, $team);
}

// Add a default value list entry for the new team
DBInsert('value_lists', 
    ['team', 'list_name', 'value'], 
    [$team, 'order_status', 'Pending']
);
?>

2.4. Custom REST API Endpoints

While the core app has its own API, you can create your own endpoints for integrations.

  1. Create a script (e.g., /custom/my_crm_module/api/v1/get_orders.php).
  2. This script should handle its own authentication (e.g., by checking for an API key passed in headers), perform permission checks, fetch data using DBGet(), and output it as JSON.
// custom/my_crm_module/api/v1/get_orders.php
header('Content-Type: application/json');
require_once '../../../../init.php'; // Go up to the root

// 1. Authenticate API Key (implement your own logic)
$api_key = $_SERVER['HTTP_X_API_KEY'] ?? '';
$user = get_user_by_api_key($api_key); // You need to write this function
if (!$user) {
    http_response_code(401);
    echo json_encode(['error' => 'Unauthorized']);
    exit();
}

// 2. Fetch and return data
$orders = DBGet('data_team_mycompany_orders', ...);
echo json_encode($orders);

3. Secure Scripting Guide for Extensions

Any script you write that is triggered by a button or event must follow these security rules.

Rule 1: Always Initialize and Validate

Every script must start by including the core init.php file. This sets up the session, database connection, and security functions.

<?php
// custom/my_crm_module/actions/approve_order.php

// The path must be relative from your script to the root
require_once __DIR__ . '/../../../init.php'; 
?>

Rule 2: Never Trust Input

Always sanitize and validate any data from $_GET, $_POST, or other external sources. Cast IDs to integers.

$table_id = (int)($_GET['table_id'] ?? 0);
$record_id = (int)($_GET['id'] ?? 0);
$approval_notes = htmlspecialchars($_POST['notes'] ?? '');

if ($table_id === 0 || $record_id === 0) {
    die('Invalid parameters.');
}

Rule 3: Check Permissions Religiously

Before performing any action, verify that the current user has the right to do so. The check_user_permission_for_table_id() function is your primary tool for this.

if (!check_user_permission_for_table_id($table_id)) {
    // You can redirect, show an error, or just silently fail.
    log_user_action($_SESSION['user']['id'], 'permission_denied', 'my_crm_module', "Access denied to approve_order for table {$table_id}");
    redirect_with_error('list', $table_id, 'Access Denied.');
}

Rule 4: Use the Database Abstraction Layer

Never build SQL strings manually with user input. Always use the provided DB* functions (DBGet, DBInsert, DBUpdate, DBDelete). They use prepared statements to prevent SQL injection.

// Get table metadata
$table = get_table_by_id($table_id);
$physical_table = get_physical_table_name($table['table_name'], $table['team']);

// GOOD: Safe update using prepared statements
DBUpdate($physical_table, ['status', 'approved_by'], ['Approved', $_SESSION['user']['id']], $record_id);

// BAD: Vulnerable to SQL Injection
// $sql = "UPDATE {$physical_table} SET status = 'Approved' WHERE id = " . $record_id;
// $pdo->query($sql);

Rule 5: Provide User Feedback

After your script runs, redirect the user back to a relevant page and provide feedback using session flash messages.

// Redirect back to the record with a success message
redirect_with_success('record', $table_id, $record_id, 'Order has been approved!');
By following these principles, you can build powerful, complex, and secure extensions that enhance the application's functionality without compromising its stability or your ability to receive future updates.