200 lines
7.0 KiB
PHP
Executable File
200 lines
7.0 KiB
PHP
Executable File
<?php
|
|
|
|
// Example component definition file used by the components system:
|
|
/*<?php return [
|
|
|
|
'begin' => function($prop, &$context) {
|
|
$context['current_component_id'] = $prop['id'];
|
|
return('<div class="my-component">');
|
|
},
|
|
|
|
'end' => function($prop) {
|
|
?><script>
|
|
console.log('Component <?= safe($prop['id']) ?> finalized');
|
|
</script><?php
|
|
return('</div>');
|
|
},
|
|
|
|
'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 '<div class="banner">'.safe($s).'</div>';
|
|
}
|
|
|
|
function component_caller_dir()
|
|
{
|
|
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
|
|
return isset($trace[1]['file']) ? dirname($trace[1]['file']) : 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($search_path && !str_starts_with($component_name, 'components/'))
|
|
$candidates[] = rtrim($search_path, '/').'/'.$component_name.'.php';
|
|
if(!str_starts_with($component_name, 'components/'))
|
|
$candidates[] = 'components/'.$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. caller-relative path (for shorthand component names)
|
|
* 3. components/{component_name}.php
|
|
*
|
|
* Component files should return an array with render functions like:
|
|
* return ['render' => function($prop) { ... }];
|
|
*/
|
|
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'])
|
|
foreach(require($result['component_file']) as $k => $v)
|
|
$result[$k] = $v;
|
|
// 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 "<button>{$prop['text']}</button>"; }
|
|
* ]);
|
|
*/
|
|
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($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']);
|
|
|
|
Profiler::log(false, -1);
|
|
return($result);
|
|
}
|
|
|