<< All versions
Skill v1.0.1
currentAutomated scan100/100ksfraser/ksf_fa_quickbudget/ksf-fa-quickbudget
+5 new
──Details
PublishedJune 29, 2026 at 07:42 AM
Content Hashsha256:36acf11182cf8a4b...
Git SHAcfb965da6a1f
Bump Typepatch
──Files
Files (1 file, 6.8 KB)
SKILL.md6.8 KBactive
SKILL.md · 175 lines · 6.8 KB
version: "1.0.1"
FA Module Development — Patterns & Conventions
Module Bootstrap (quickbudget.php entry point)
chdir(__DIR__); // resolve includes from module dir$page_security = 'SA_KSF_QUICKBUDGETVIEW'; // set BEFORE session.incinclude_once($path_to_root . "/includes/session.inc"); // boot FA: $db, TB_PREF, page()add_access_extensions(); // register extension security areasrequire_once __DIR__ . '/FA_QuickBudget_Module.php'; // load module class + Composer autoload
Session & Security
$_SESSION['wa_current_user']->user— current user ID (int)$_SESSION['wa_current_user']->can_access('SA_xxx')— permission check (FA 2.4.3; nothas_access)$_SESSION['wa_current_user']->email,->real_name— user details$page_securitymust be set beforesession.incis included- Extension security areas:
SS_ksf_FA_QuickBudget(section),SA_ksf_FA_QuickBudgetVIEW,SA_ksf_FA_QuickBudgetMANAGE add_access_extensions()must be called aftersession.incfor extension areas to work
Module Page Pattern
chdir(__DIR__)- Set
$page_security include session.inc(boots FA framework)add_access_extensions()page(_("Title"), false, false, '', $js);...end_page();
AJAX / API Pattern
Session-Expiry Guard
AJAX requests MUST set a session-expiry interceptor BEFORE session.inc:
php
$_ksf_quickbudget_is_ajax = isset($_GET['action']) && $_GET['action'] !== '';if ($_ksf_quickbudget_is_ajax) {ob_start(function ($html) {if (strpos($html, 'user_name_entry_field') !== false) {if (!headers_sent()) {http_response_code(401);header('Content-Type: application/json');}return json_encode(array('error' => 'session_expired'));}return $html;});}
FA's Inner Buffer (fmt_errors discard)
After session.inc, discard FA's level-2 output_html buffer so PHP warnings don't corrupt JSON:
php
if ($_ksf_quickbudget_is_ajax) {while (ob_get_level() > 1) {ob_end_clean();}}
quickbudget_api_run() Wrapper
All AJAX handlers should be wrapped by quickbudget_api_run() which:
- Converts PHP warnings/notices to exceptions
- Captures output buffer
- Validates JSON before sending
- Discards FA footer via shutdown function
- Returns
{"error":"..."}on failure
Request Payload
php
$data = quickbudget_request_payload(); // reads php://input for JSON, $_POST for form
JSON Response
php
\Ksfraser\FA\QuickBudget\Support\JsonResponse::send($result); // outputs JSON + exits
Hook System
hook_invoke_first vs hook_invoke_all
hook_invoke_first("quickbudget_entry_create", $data)calls each registered hook until one returns non-nullhook_invoke_firstis an FA framework function (defined in FA core, not in module code)- Other modules can register hooks to intercept module operations
Module Hook Registration (hooks.php)
The hooks_ksf_FA_QuickBudget class (extends hooks) defines hook methods. These are auto-discovered by FA:
install_tabs($app)— register application tabinstall_access()— return[$security_areas, $security_sections]activate_extension($company, $check_only)— SQL install/upgrade
Hook Methods (convention)
Named by operation, called by hook_invoke_first:
quickbudget_budget_generate(&$data, $opts = [])→ budget generationquickbudget_budget_compare(&$data, $opts = [])→ comparison queryquickbudget_factor_import(&$data, $opts = [])→ CSV import- Return null = "not handled" (hook_invoke_first continues), non-null = stops iteration
Hook Callback Convention
Methods receive &$data by reference (mutated for results) and $opts (optional context). Return non-null to claim the hook.
Module Class Pattern (FA_QuickBudget_Module)
Singleton
php
class FA_QuickBudget_Module {private static $instance = null;public static function instance(): self { ... }}
Event Controller Pattern
php
$controller = new \Ksfraser\FA\QuickBudget\Controller\EventController(module: \FA_QuickBudget_Module::instance(),userId: (int) $_SESSION['wa_current_user']->user,hookInvoker: fn($name, $payload) => hook_invoke_first($name, $payload) // optional);$result = $controller->create($data);
EventController methods
create(array $data): array— sanitizes, validates, generates budgetupdate(int $id, array $data): array— permission check, update budgetdelete(int $id): array— delete budget entry
Permission Model
php
// Module-level (FA security areas):$_SESSION['wa_current_user']->can_access('SA_KSF_QUICKBUDGETMANAGE')// Entry-level (quickbudget.php):function quickbudget_can_edit_entry(array $entry, int $userId): boolfunction quickbudget_can_delete_entry(array $entry, int $userId): bool// Allowed if: MANAGE access OR assigned_to == userId OR user_id == userId
Budget Service
php
$service = FA_QuickBudget_Module::instance()->getBudgetService();
DB / SQL Patterns
- Tables:
0_ksf_quickbudget_factors,0_ksf_quickbudget_scenarios,0_ksf_quickbudget_budget,0_ksf_quickbudget_approvals db_query($sql, 'Error message')— second arg is error context; passnullto suppress display_db_error exitTB_PREF— table prefix constant (usually empty string in modern FA)
Integration Tests (curl-based)
php
// Cookie-based auth via exec curl (PHP libcurl cookie handling is incompatible with FA):$resp = $this->execCurl('POST', '/index.php','user_name_entry_field=user&password=pass&company_login_name=0&Submit=Login');// Use -c / -b cookie jar, -D header file, --data for POST// FA_INTEGRATION_BASE_URL env var (default http://localhost:8080)
Namespace / Class Loader
- Composer autoloader booted from
hooks.phpandFA_QuickBudget_Module.phpviarequire_once __DIR__ . '/vendor/autoload.php' - Module class:
FA_QuickBudget_Module - Namespaces:
Ksfraser\FA\QuickBudget\Controller,Ksfraser\FA\QuickBudget\View,Ksfraser\FA\QuickBudget\Support - Third-party:
Ksfraser\Traits\HookQueryProviderTrait,Ksfraser\Traits\CrudEventEmitterTrait
Key "Gotchas"
- Buffer ordering: session.inc calls
ob_start('output_html')(level 2). For AJAX, discard this inner buffer BEFORE returning JSON, or warnings/notices will be rendered as HTML and corrupt the response. - PHP 7.4: target FA container runs 7.4 — no match expressions, no readonly properties,
: mixedreturn type only from PHP 8.0 - `db_query` error message: passing a non-null second arg triggers
display_db_error()which callsexitwith HTML output. Passnullto getfalsereturn on failure. - `hook_invoke_first` vs direct module call: hooks allow other modules to intercept operations. Use hooks for inter-module integration.