function($prop, &$context) { $context['current_component_id'] = $prop['id']; return('
'); }, 'end' => function($prop) { ?>'); }, 'render' => function($prop, &$context) { return($context['begin']($prop, $context) . $context['end']($prop)); }, 'about' => 'This is an example component that wraps content in a div and logs when it is finalized.', ]; */ $GLOBALS['render_funcs'] = array(); $GLOBALS['id_counter'] = $GLOBALS['id_counter'] ?? 1; function component_error_banner($s) { return ''; } function component_expand_search_paths($search_path = false) { $paths = array(); if(is_array($search_path)) $paths = $search_path; else if(is_string($search_path) && trim($search_path) !== '') $paths = array($search_path); $normalized = array(); foreach($paths as $path) { $base = rtrim((string)$path, '/'); if($base === '') continue; $normalized[] = $base; if(substr($base, -11) !== '/components') $normalized[] = $base.'/components'; } return array_values(array_unique($normalized)); } function component_caller_dir() { $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 8); foreach($trace as $frame) { if(!isset($frame['file']) || !is_string($frame['file'])) continue; if(realpath($frame['file']) === realpath(__FILE__)) continue; return dirname($frame['file']); } return getcwd(); } function component_normalize_name($file_name) { return preg_replace('/\.php$/', '', trim((string)$file_name)); } function component_resolve_file($file_name, $search_path = false) { $component_name = component_normalize_name($file_name); $candidates = array($component_name.'.php'); if(!str_starts_with($component_name, 'components/')) $candidates[] = 'components/'.$component_name.'.php'; if(!str_starts_with($component_name, 'components/')) { foreach(component_expand_search_paths($search_path) as $path) $candidates[] = $path.'/'.$component_name.'.php'; } if(!str_starts_with($component_name, 'interface/')) $candidates[] = 'interface/'.$component_name.'.php'; if(!str_starts_with($component_name, 'packages/')) $candidates[] = 'packages/'.$component_name.'.php'; foreach(array_unique($candidates) as $candidate) { if(file_exists($candidate)) return $candidate; } return false; } /** * Loads a component from file system and registers it in the global registry * * Component files are searched in this order: * 1. exact file path passed to the loader * 2. components/{component_name}.php * 3. caller-relative path and caller/components/{component_name}.php * 4. interface/{component_name}.php and packages/{component_name}.php * * Component files should return an array with render functions like: * return ['render' => function($prop) { ... }]; * ERP-style files that return a single callable are treated as the default render function. */ function component_get_func($file_name, $return_false_if_not_found = false, $search_path = false) { $component_name = component_normalize_name($file_name); $result = []; // Check if component is already loaded in global registry if(!isset($GLOBALS['render_funcs'][$component_name])) { $component_file = component_resolve_file($component_name, $search_path); if($component_file) { $result['component_file'] = $component_file; } else { // Component file not found - create error component if($return_false_if_not_found) return(false); $result['component_file'] = false; $result['error'] = 'Component not found: '.$component_name; $result['render'] = function() use($component_name) { return component_error_banner('component not found: '.$component_name); }; } if($result['component_file']) { $component_definition = require($result['component_file']); if(is_callable($component_definition)) $result['render'] = $component_definition; else if(is_array($component_definition)) { foreach($component_definition as $k => $v) $result[$k] = $v; } else $result['render'] = ''; } // Register component in global registry for future use $GLOBALS['render_funcs'][$component_name] = $result; } return($GLOBALS['render_funcs'][$component_name]); } /** * Checks if a component exists without loading it * Returns true if component file can be found, false otherwise */ function component_exists($file_name) { $caller_dir = component_caller_dir(); return(component_get_func($file_name, true, $caller_dir) !== false); } /** * Loads a component and returns its definition * Same as component_get_func but with caller directory detection */ function component_load($file_name) { $caller_dir = component_caller_dir(); return(component_get_func($file_name, true, $caller_dir)); } function component_call($file_name, $render_call, $prop = array()) { $prop['render_call'] = $render_call; return component($file_name, $prop); } /** * Declares an inline component directly in code (not from file) * * @param string $file_name - Component name for registry * @param array $render_prop - Component definition with render functions * * Example: * component_declare('my-button', [ * 'render' => function($prop) { return ""; } * ]); */ function component_declare($file_name, $render_prop) { // Register the inline component directly in global registry $GLOBALS['render_funcs'][$file_name] = $render_prop; } /** * Main component rendering function - the heart of the component system * * @param string $file_name - Component name or path * @param array $prop - Properties/data to pass to component * @return mixed - Component output from the selected render method * * Component calling examples: * component('components/example/theme-switcher') * component_call('components/workspace/panel', 'render', ['title' => 'Status']) * component('workspace/panel', ['render_call' => 'render']) */ function component($file_name, $prop = array()) { Profiler::log('component '.$file_name, 1); $caller_dir = component_caller_dir(); if($file_name == '') return(component_error_banner('[component name empty]')); $file_name = component_normalize_name($file_name); // Default render method is 'render', but can be overridden $prop['render_call'] = first($prop['render_call'] ?? false, 'render'); // Support for calling specific render methods: 'component:method' if(stristr($file_name, ':')) // you can specify which render function to call { $prop['render_call'] = $file_name; $file_name = nibble(':', $prop['render_call']); } // Get component definition from registry (load if not already loaded) $renderer = $GLOBALS['render_funcs'][$file_name] ?? false; if(!$renderer) { // Component not in registry - try to load from file component_get_func($file_name, false, $caller_dir); $renderer = $GLOBALS['render_funcs'][$file_name] ?? false; } // Generate unique ID for component instance if not provided $prop['id'] = !empty($prop['id']) ? $prop['id'] : 'c'.($GLOBALS['id_counter']++); $prop['filename'] = $file_name; if(isset($prop[':before']) && $prop[':before'] !== '') echo $prop[':before']; ob_start(); if(isset($renderer[$prop['render_call']]) && is_callable($renderer[$prop['render_call']])) $result = ($renderer[$prop['render_call']]($prop, $renderer)); else if(isset($renderer[$prop['render_call']])) $result = ($renderer[$prop['render_call']]); else $result = component_error_banner('component render method not found: '.$file_name.':'.$prop['render_call']); $printed_result = ob_get_clean(); if($result === null) $result = $printed_result; else if(is_string($result)) $result .= $printed_result; else echo $printed_result; if(isset($prop[':after']) && $prop[':after'] !== '') echo $prop[':after']; Profiler::log(false, -1); return($result); }