Add model constant export to JS, rsxapp hydration, on_stop lifecycle
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -127,21 +127,8 @@ class Database_BundleIntegration extends BundleIntegration_Abstract
|
||||
// Only regenerate if source is newer than stub
|
||||
if ($stub_mtime >= $source_mtime) {
|
||||
// Also check if the model metadata has changed
|
||||
// by comparing a hash of enums, relationships, and columns
|
||||
$model_metadata = [];
|
||||
|
||||
// Get relationships
|
||||
$model_metadata['rel'] = $fqcn::get_relationships();
|
||||
|
||||
// Get enums
|
||||
if (property_exists($fqcn, 'enums')) {
|
||||
$model_metadata['enums'] = $fqcn::$enums ?? [];
|
||||
}
|
||||
|
||||
// Get columns from models metadata if available
|
||||
if (isset($manifest_data['data']['models'][$class_name]['columns'])) {
|
||||
$model_metadata['columns'] = $manifest_data['data']['models'][$class_name]['columns'];
|
||||
}
|
||||
// by comparing a hash of enums, relationships, columns, and constants
|
||||
$model_metadata = static::_get_model_metadata_for_hash($fqcn, $class_name, $manifest_data);
|
||||
|
||||
$model_metadata_hash = md5(json_encode($model_metadata));
|
||||
$old_metadata_hash = $metadata['model_metadata_hash'] ?? '';
|
||||
@@ -164,21 +151,7 @@ class Database_BundleIntegration extends BundleIntegration_Abstract
|
||||
|
||||
// Store the metadata hash for future comparisons if not already done
|
||||
if (!isset($manifest_data['data']['files'][$file_path]['model_metadata_hash'])) {
|
||||
$model_metadata = [];
|
||||
|
||||
// Get relationships
|
||||
$model_metadata['rel'] = $fqcn::get_relationships();
|
||||
|
||||
// Get enums
|
||||
if (property_exists($fqcn, 'enums')) {
|
||||
$model_metadata['enums'] = $fqcn::$enums ?? [];
|
||||
}
|
||||
|
||||
// Get columns from models metadata if available
|
||||
if (isset($manifest_data['data']['models'][$class_name]['columns'])) {
|
||||
$model_metadata['columns'] = $manifest_data['data']['models'][$class_name]['columns'];
|
||||
}
|
||||
|
||||
$model_metadata = static::_get_model_metadata_for_hash($fqcn, $class_name, $manifest_data);
|
||||
$manifest_data['data']['files'][$file_path]['model_metadata_hash'] = md5(json_encode($model_metadata));
|
||||
}
|
||||
}
|
||||
@@ -262,6 +235,46 @@ class Database_BundleIntegration extends BundleIntegration_Abstract
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get model metadata for hash comparison (detects when stubs need regeneration)
|
||||
*
|
||||
* @param string $fqcn Fully qualified class name
|
||||
* @param string $class_name Simple class name
|
||||
* @param array $manifest_data The manifest data array
|
||||
* @return array Metadata array for hashing
|
||||
*/
|
||||
private static function _get_model_metadata_for_hash(string $fqcn, string $class_name, array $manifest_data): array
|
||||
{
|
||||
$model_metadata = [];
|
||||
|
||||
// Get relationships
|
||||
$model_metadata['rel'] = $fqcn::get_relationships();
|
||||
|
||||
// Get enums
|
||||
if (property_exists($fqcn, 'enums')) {
|
||||
$model_metadata['enums'] = $fqcn::$enums ?? [];
|
||||
}
|
||||
|
||||
// Get columns from models metadata if available
|
||||
if (isset($manifest_data['data']['models'][$class_name]['columns'])) {
|
||||
$model_metadata['columns'] = $manifest_data['data']['models'][$class_name]['columns'];
|
||||
}
|
||||
|
||||
// Get public constants defined directly on this class
|
||||
$reflection = new \ReflectionClass($fqcn);
|
||||
$constants = [];
|
||||
foreach ($reflection->getReflectionConstants(\ReflectionClassConstant::IS_PUBLIC) as $const) {
|
||||
if ($const->getDeclaringClass()->getName() === $fqcn) {
|
||||
$constants[$const->getName()] = $const->getValue();
|
||||
}
|
||||
}
|
||||
if (!empty($constants)) {
|
||||
$model_metadata['constants'] = $constants;
|
||||
}
|
||||
|
||||
return $model_metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize model name for use as filename
|
||||
*/
|
||||
@@ -314,6 +327,32 @@ class Database_BundleIntegration extends BundleIntegration_Abstract
|
||||
$js_model_base_class = config('rsx.js_model_base_class');
|
||||
$extends_class = $js_model_base_class ?: 'Rsx_Js_Model';
|
||||
|
||||
// Collect enum constant names to avoid duplicating them
|
||||
$enum_constant_names = [];
|
||||
foreach ($enums as $column => $enum_values) {
|
||||
foreach ($enum_values as $value => $props) {
|
||||
if (!empty($props['constant'])) {
|
||||
$enum_constant_names[] = $props['constant'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get all public constants defined directly on this model class (not inherited)
|
||||
$reflection = new \ReflectionClass($fqcn);
|
||||
$non_enum_constants = [];
|
||||
foreach ($reflection->getReflectionConstants(\ReflectionClassConstant::IS_PUBLIC) as $const) {
|
||||
// Only include constants defined directly on this class
|
||||
if ($const->getDeclaringClass()->getName() !== $fqcn) {
|
||||
continue;
|
||||
}
|
||||
$const_name = $const->getName();
|
||||
// Skip constants already generated from enums
|
||||
if (in_array($const_name, $enum_constant_names)) {
|
||||
continue;
|
||||
}
|
||||
$non_enum_constants[$const_name] = $const->getValue();
|
||||
}
|
||||
|
||||
// Start building the stub content
|
||||
$content = "/**\n";
|
||||
$content .= " * Auto-generated JavaScript stub for {$class_name}\n";
|
||||
@@ -326,6 +365,16 @@ class Database_BundleIntegration extends BundleIntegration_Abstract
|
||||
// Add static __MODEL property for PHP model name resolution
|
||||
$content .= " static __MODEL = '{$class_name}';\n\n";
|
||||
|
||||
// Generate non-enum constants first (static properties)
|
||||
if (!empty($non_enum_constants)) {
|
||||
$content .= " // Non-enum constants\n";
|
||||
foreach ($non_enum_constants as $const_name => $const_value) {
|
||||
$value_json = json_encode($const_value);
|
||||
$content .= " static {$const_name} = {$value_json};\n";
|
||||
}
|
||||
$content .= "\n";
|
||||
}
|
||||
|
||||
// Generate enum constants and methods
|
||||
foreach ($enums as $column => $enum_values) {
|
||||
// Sort enum values by order property first, then by key
|
||||
|
||||
@@ -222,6 +222,24 @@ class Rsx {
|
||||
return !window.rsxapp.debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current logged-in user model instance
|
||||
* Returns the hydrated ORM model if available, or the raw data object
|
||||
* @returns {Rsx_Js_Model|Object|null} User model instance or null if not logged in
|
||||
*/
|
||||
static user() {
|
||||
return window.rsxapp?.user || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current site model instance
|
||||
* Returns the hydrated ORM model if available, or the raw data object
|
||||
* @returns {Rsx_Js_Model|Object|null} Site model instance or null if not set
|
||||
*/
|
||||
static site() {
|
||||
return window.rsxapp?.site || null;
|
||||
}
|
||||
|
||||
// Generates a unique number for the application instance
|
||||
static uid() {
|
||||
if (typeof Rsx._uid == undef) {
|
||||
@@ -601,6 +619,43 @@ class Rsx {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate rsxapp.user and rsxapp.site into ORM model instances
|
||||
*
|
||||
* Checks if window.rsxapp.user and window.rsxapp.site contain raw data objects
|
||||
* with __MODEL markers, and if the corresponding model classes are available,
|
||||
* replaces them with proper ORM instances.
|
||||
*
|
||||
* This enables code like:
|
||||
* const user = Rsx.user();
|
||||
* await user.some_relationship(); // Works because user is a proper model instance
|
||||
*/
|
||||
static _hydrate_rsxapp_models() {
|
||||
if (!window.rsxapp) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hydrate user if present and has __MODEL marker
|
||||
if (window.rsxapp.user && window.rsxapp.user.__MODEL) {
|
||||
const UserClass = Manifest.get_class_by_name(window.rsxapp.user.__MODEL);
|
||||
// Check class exists and extends Rsx_Js_Model - @JS-DEFENSIVE-01-EXCEPTION - dynamic model resolution
|
||||
if (UserClass && Manifest.js_is_subclass_of(UserClass, Rsx_Js_Model)) {
|
||||
window.rsxapp.user = new UserClass(window.rsxapp.user);
|
||||
console_debug('RSX_INIT', `Hydrated rsxapp.user as ${window.rsxapp.user.__MODEL}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Hydrate site if present and has __MODEL marker
|
||||
if (window.rsxapp.site && window.rsxapp.site.__MODEL) {
|
||||
const SiteClass = Manifest.get_class_by_name(window.rsxapp.site.__MODEL);
|
||||
// Check class exists and extends Rsx_Js_Model - @JS-DEFENSIVE-01-EXCEPTION - dynamic model resolution
|
||||
if (SiteClass && Manifest.js_is_subclass_of(SiteClass, Rsx_Js_Model)) {
|
||||
window.rsxapp.site = new SiteClass(window.rsxapp.site);
|
||||
console_debug('RSX_INIT', `Hydrated rsxapp.site as ${window.rsxapp.site.__MODEL}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Execute multi-phase initialization for all registered classes
|
||||
* This runs various initialization phases in order to properly set up the application
|
||||
@@ -617,6 +672,10 @@ class Rsx {
|
||||
// Setup exception handlers first, before any initialization phases
|
||||
Rsx._setup_exception_handlers();
|
||||
|
||||
// Hydrate rsxapp.user and rsxapp.site into ORM model instances
|
||||
// This must happen early, before any code tries to use these objects
|
||||
Rsx._hydrate_rsxapp_models();
|
||||
|
||||
// Get all registered classes from the manifest
|
||||
const all_classes = Manifest.get_all_classes();
|
||||
|
||||
@@ -697,7 +756,6 @@ class Rsx {
|
||||
y: window.scrollY
|
||||
};
|
||||
sessionStorage.setItem(Rsx._SCROLL_STORAGE_KEY, JSON.stringify(scroll_data));
|
||||
console.log('[Rsx Scroll] Saved:', scroll_data.x, scroll_data.y, 'for', scroll_data.url);
|
||||
}, 100); // 100ms debounce
|
||||
}
|
||||
|
||||
@@ -707,61 +765,46 @@ class Rsx {
|
||||
* @private
|
||||
*/
|
||||
static _restore_scroll_on_refresh() {
|
||||
console.log('[Rsx Scroll] _restore_scroll_on_refresh called');
|
||||
|
||||
// Set up scroll listener to continuously save position
|
||||
window.addEventListener('scroll', Rsx._save_scroll_position, { passive: true });
|
||||
console.log('[Rsx Scroll] Scroll listener attached');
|
||||
|
||||
// Check if this is a page refresh using Performance API
|
||||
const nav_entries = performance.getEntriesByType('navigation');
|
||||
console.log('[Rsx Scroll] Navigation entries:', nav_entries.length);
|
||||
if (nav_entries.length === 0) {
|
||||
console.log('[Rsx Scroll] No navigation entries found, skipping restore');
|
||||
return;
|
||||
}
|
||||
|
||||
const nav_type = nav_entries[0].type;
|
||||
console.log('[Rsx Scroll] Navigation type:', nav_type);
|
||||
if (nav_type !== 'reload') {
|
||||
console.log('[Rsx Scroll] Not a reload (type=' + nav_type + '), skipping restore');
|
||||
return;
|
||||
}
|
||||
|
||||
// This is a refresh - try to restore scroll position
|
||||
const stored = sessionStorage.getItem(Rsx._SCROLL_STORAGE_KEY);
|
||||
console.log('[Rsx Scroll] Stored scroll data:', stored);
|
||||
if (!stored) {
|
||||
console.log('[Rsx Scroll] No stored scroll position found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const scroll_data = JSON.parse(stored);
|
||||
const current_url = window.location.pathname + window.location.search;
|
||||
console.log('[Rsx Scroll] Stored URL:', scroll_data.url, 'Current URL:', current_url);
|
||||
|
||||
// Only restore if URL matches
|
||||
if (scroll_data.url !== current_url) {
|
||||
console.log('[Rsx Scroll] URL mismatch, skipping restore');
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore scroll position instantly
|
||||
console.log('[Rsx Scroll] Restoring scroll to:', scroll_data.x, scroll_data.y);
|
||||
window.scrollTo({
|
||||
left: scroll_data.x,
|
||||
top: scroll_data.y,
|
||||
behavior: 'instant'
|
||||
});
|
||||
|
||||
console.log('[Rsx Scroll] Restored scroll position on refresh:', scroll_data.x, scroll_data.y);
|
||||
|
||||
// Clear stored position after successful restore
|
||||
sessionStorage.removeItem(Rsx._SCROLL_STORAGE_KEY);
|
||||
} catch (e) {
|
||||
// Invalid JSON or other error - ignore
|
||||
console.log('[Rsx Scroll] Error restoring scroll:', e.message);
|
||||
sessionStorage.removeItem(Rsx._SCROLL_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user