Extending the Application
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 forrequire_oncecalls to your helper files or Composer'sautoload.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.
- Create your script: Write a PHP script (e.g.,
/custom/my_crm_module/actions/approve_order.php). - 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}
- URL:
- 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).
- 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. - 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_listsentries or default records for the new team. - Setting Default Rights: Create
user_groupsanduser_rightsentries to establish a default permission scheme for the new team.
- Provisioning Tables: Use the
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.
- Create a script (e.g.,
/custom/my_crm_module/api/v1/get_orders.php). - 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!');