Logic File Architecture Documentation
Overview
The logic layer (/logic/) provides the business logic and controller functionality in the application's MVC-like architecture. All logic files follow a standardized pattern using the LogicResult class for consistent return handling.
Critical rule: Logic files must never call exit(), die(), or throw exceptions. Every code path must return a LogicResult object. This makes logic files testable, composable, and safe to call from any context.
Directory Structure
/logic/ # Core logic files
/plugins/*/logic/ # Plugin-specific logic files
/theme/*/logic/ # Theme-specific logic overridesLogic File Pattern
Every logic file follows this naming convention and structure:
File naming: [page_name]_logic.php
Function naming: [page_name]_logic(array $input): LogicResult
Every logic file takes a single $input array (the merged $_GET/$_POST/route params from the caller) and returns a LogicResult. There are no extra positional parameters and no per-route variants — the calling convention is identical for page handlers, action surfaces, and API entry points.
Basic Structure
<?php
function page_name_logic(array $input): LogicResult {
// PathHelper, Globalvars, SessionControl are always available
require_once(PathHelper::getIncludePath('includes/LibraryFunctions.php'));
// Include required data classes
require_once(PathHelper::getIncludePath('data/users_class.php'));
// Get singletons
$settings = Globalvars::get_instance();
$session = SessionControl::get_instance();
// Business logic here
$page_vars = array();
$page_vars['settings'] = $settings;
$page_vars['session'] = $session;
// Return using LogicResult
return LogicResult::render($page_vars);
}
?>LogicResult Class
The LogicResult class provides a standardized return format for all logic functions, enabling consistent handling of renders, redirects, and errors.
Class Definition
class LogicResult {
public $redirect = null;
public $data = [];
public $error = null;
// Static factory methods
public static function redirect($url, $data = []);
public static function render($data = []);
public static function error($message, $data = []);
}Three Return Patterns
1. Render Pattern
Used when the logic prepares data for a view to render:function product_logic($get_vars, $post_vars) {
$product = new Product($get_vars['id'], TRUE);
$page_vars = array();
$page_vars['product'] = $product;
$page_vars['title'] = $product->get('pro_name');
return LogicResult::render($page_vars);
}2. Redirect Pattern
Used when the logic needs to redirect to another page:function logout_logic($get_vars, $post_vars) {
$session = SessionControl::get_instance();
$session->log_out();
return LogicResult::redirect('/login');
}3. Error Pattern
Used when an error occurs that should be displayed to the user:function secure_page_logic($get_vars, $post_vars) {
$session = SessionControl::get_instance();
if (!$session->is_logged_in()) {
return LogicResult::error('You must be logged in to access this page');
}
// Continue with normal logic...
return LogicResult::render($page_vars);
}View Integration
Views should always use process_logic() to call logic functions. This handles redirects, errors, and data extraction automatically:
// ✅ CORRECT - Always use process_logic()
$page_vars = process_logic(product_logic($_GET, $_POST));
$product = $page_vars['product'];process_logic() handles:
LogicResult::redirect()— performs the redirectLogicResult::error()— adds error message to session, returns data for re-displayLogicResult::render()— returns the data array- Legacy array returns — passes through unchanged for backward compatibility
// ❌ WRONG - Don't manually handle LogicResult in views
$result = product_logic($_GET, $_POST);
if ($result instanceof LogicResult) {
if ($result->redirect) {
LibraryFunctions::redirect($result->redirect);
exit();
}
$page_vars = $result->data;
}
// ✅ CORRECT - One line
$page_vars = process_logic(product_logic($_GET, $_POST));Common Patterns
Feature Toggle Pattern
function feature_logic($get_vars, $post_vars) {
$settings = Globalvars::get_instance();
if (!$settings->get_setting('feature_active')) {
return LogicResult::error('This feature is not available');
}
// Feature logic continues...
return LogicResult::render($page_vars);
}Permission Check Pattern
function admin_page_logic($get_vars, $post_vars) {
$session = SessionControl::get_instance();
if (!$session->is_logged_in()) {
return LogicResult::redirect('/login');
}
if ($session->get_permission_level() < 5) {
return LogicResult::error('You do not have permission to access this page');
}
// Admin logic continues...
return LogicResult::render($page_vars);
}Form Processing Pattern
function form_logic($get_vars, $post_vars) {
if ($post_vars) {
// Process form
$user = new User(NULL);
$user->set('usr_name', $post_vars['name']);
$user->save();
// Redirect after POST
return LogicResult::redirect('/success');
}
// Display form
return LogicResult::render($page_vars);
}Edit Form Pattern
When editing existing records with FormWriterV2, check edit_primary_key_value from POST first:
function admin_item_edit_logic($get_vars, $post_vars) {
// CRITICAL: Check edit_primary_key_value (form submission) first, fallback to GET
if (isset($post_vars['edit_primary_key_value'])) {
$item = new Item($post_vars['edit_primary_key_value'], TRUE);
} elseif (isset($get_vars['itm_item_id'])) {
$item = new Item($get_vars['itm_item_id'], TRUE);
} else {
$item = new Item(NULL);
}
if ($post_vars) {
// Process form...
$item->save();
return LogicResult::redirect('/admin/admin_item?itm_item_id=' . $item->key);
}
return LogicResult::render(['item' => $item]);
}See FormWriter Documentation - Edit Forms for complete details on why this pattern is required.
Error Handling Pattern
When calling code that might throw exceptions (e.g., Stripe, external APIs), catch them and return LogicResult::error():
function checkout_logic($get_vars, $post_vars) {
if ($post_vars) {
try {
$cart = $session->get_shopping_cart();
$cart->process_payment($post_vars);
return LogicResult::redirect('/order-confirmation');
} catch (Exception $e) {
return LogicResult::error($e->getMessage(), $post_vars);
}
}
return LogicResult::render($page_vars);
}Missing/Invalid Parameter Pattern
function event_logic($get_vars, $post_vars) {
if (empty($get_vars['event_id'])) {
return LogicResult::error('Event ID is required');
}
$event = new Event($get_vars['event_id'], TRUE);
if (!$event->get('evt_id')) {
return LogicResult::error('Event not found');
}
// Continue with valid event...
return LogicResult::render(['event' => $event]);
}Rules for Logic Files
Never do these in logic files:
// ❌ WRONG - Never call exit()
LibraryFunctions::redirect('/page');
exit();
// ❌ WRONG - Never throw exceptions
throw new SystemDisplayableError('Something went wrong');
// ❌ WRONG - Never set headers directly
header("HTTP/1.0 404 Not Found");
exit();
// ❌ WRONG - Never echo output directly
echo json_encode(['success' => true]);
exit();
// ❌ WRONG - Never return raw arrays in new code
return $page_vars;Always do these:
// ✅ CORRECT - Return LogicResult for redirects
return LogicResult::redirect('/page');
// ✅ CORRECT - Return LogicResult for errors
return LogicResult::error('Something went wrong');
// ✅ CORRECT - Return LogicResult for page renders
return LogicResult::render($page_vars);
// ✅ CORRECT - Catch exceptions from services and wrap them
try {
$stripe->charge($amount);
} catch (Exception $e) {
return LogicResult::error($e->getMessage(), $post_vars);
}Migration from Legacy Patterns
Converting Old Logic Files
When updating legacy logic files, convert all throw, exit(), and raw array returns:
Redirect conversion:
// Before:
LibraryFunctions::redirect('/some-page');
exit();
// After:
return LogicResult::redirect('/some-page');Error conversion:
// Before:
throw new SystemDisplayableError('Email is required');
// After:
return LogicResult::error('Email is required');Array return conversion:
// Before:
return $page_vars;
// After:
return LogicResult::render($page_vars);Backward Compatibility
process_logic() handles both old and new return formats, so views don't need to change when logic files are migrated:
// This works whether the logic returns LogicResult or a raw array
$page_vars = process_logic(some_logic($_GET, $_POST));Testing Logic Files
Because logic files return LogicResult objects and never exit() or throw, they can be tested directly:
// tests/logic/test_product_logic.php
function test_product_logic() {
// Test render case
$result = product_logic(['id' => 1], []);
assert($result instanceof LogicResult);
assert($result->redirect === null);
assert(!empty($result->data['product']));
// Test redirect case
$result = product_logic([], ['delete' => 1]);
assert($result instanceof LogicResult);
assert($result->redirect === '/products');
// Test error case
$result = product_logic(['id' => 999999], []);
assert($result instanceof LogicResult);
assert($result->error !== null);
}Plugin Logic Files
Plugins can provide their own logic files following the same patterns:
// plugins/bookings/logic/booking_logic.php
function booking_logic($get_vars, $post_vars) {
require_once(PathHelper::getIncludePath('plugins/bookings/data/bookings_class.php'));
// Plugin-specific logic
$booking = new Booking($get_vars['id'], TRUE);
return LogicResult::render(['booking' => $booking]);
}Theme Override Pattern
Themes can override logic files to customize behavior:
/logic/product_logic.php # Base logic
/theme/canvas/logic/product_logic.php # Theme override
The theme version will be loaded when using:
require_once(PathHelper::getThemeFilePath('product_logic.php', 'logic'));Best Practice: Extending Base Logic Without Modifying Core
Instead of completely replacing core logic, themes can create focused logic files that provide additional data to core views. This approach:
- Keeps core logic untouched
- Allows multiple themes to coexist with different data needs
- Makes maintenance easier
- Follows single responsibility principle
// /theme/phillyzouk/logic/index_logic.php
<?php
function index_logic($get_vars, $post_vars) {
require_once(PathHelper::getIncludePath('data/posts_class.php'));
require_once(PathHelper::getIncludePath('data/events_class.php'));
$page_vars = array();
// Load recent blog posts (4 posts for homepage)
$recent_posts = new MultiPost(
array('published' => TRUE, 'deleted' => false),
array('pst_published_time' => 'DESC'),
4, 0
);
$recent_posts->load();
$page_vars['recent_posts'] = $recent_posts;
// Load upcoming events (6 events for sidebar)
$upcoming_events = new MultiEvent(
array('deleted' => false, 'upcoming' => true),
array('evt_start_time' => 'ASC'),
6, 0
);
$upcoming_events->load();
$page_vars['upcoming_events'] = $upcoming_events;
return LogicResult::render($page_vars);
}
?>Using the Theme Logic in Views
// /theme/phillyzouk/views/index.php
<?php
require_once(PathHelper::getThemeFilePath('PublicPage.php', 'includes'));
require_once(PathHelper::getThemeFilePath('index_logic.php', 'logic'));
$page_vars = process_logic(index_logic($_GET, $_POST));
$page = new PublicPage();
$page->public_header(array(
'title' => 'Home',
'showheader' => true
));
?>
<!-- Use $page_vars['recent_posts'] and $page_vars['upcoming_events'] in template -->
<?php foreach ($page_vars['recent_posts'] as $post): ?>
<!-- Render post -->
<?php endforeach; ?>Key Advantages of This Pattern
- No Core Modification - Base logic files remain unchanged
- Theme-Specific Data - Each theme can load different data sets
- Clear Separation - Logic layer stays independent from view layer
- Easy Debugging - Can inspect
$page_varsto see what data is available - Reusable - Other themes can use similar patterns for their needs
Common Issues and Solutions
Issue: "Cannot use object of type LogicResult as array"
Cause: View is calling a logic function directly without process_logic()
Solution: Wrap the call with process_logic():
// ❌ Causes error
$page_vars = product_logic($_GET, $_POST);
echo $page_vars['product'];
// ✅ Works correctly
$page_vars = process_logic(product_logic($_GET, $_POST));
echo $page_vars['product'];Issue: Logic file not found
Cause: Incorrect path or naming convention
Solution: Ensure file follows [name]_logic.php pattern and use correct include:
require_once(PathHelper::getIncludePath('logic/product_logic.php')); // Core
require_once(PathHelper::getThemeFilePath('product_logic.php', 'logic')); // Theme-awareIssue: Redirect not working
Cause: Output sent before redirect, or not using process_logic()
Solution: Ensure no echo/print before process_logic() call, and check for PHP errors/warnings
Consistent Variable Naming
Always use these standard variable names:
$page_vars- Array of variables to pass to view$settings- Globalvars singleton instance$session- SessionControl singleton instance
Related Documentation
- Plugin Developer Guide - For plugin-specific logic patterns
- Admin Pages Documentation - For admin interface logic
- Main Architecture Guide - For overall system architecture
Specifications
/specs/implemented/logic_result_minimal_spec.md- Phase 1 implementation (redirect/render/error)/specs/logic_result_with_validation_spec.md- Phase 2: complete migration and validation support