From ecc386301f283eaf9f04e84e950f24fed01affa8 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 10 Dec 2025 02:37:07 +0000 Subject: [PATCH] Add class override system for framework customization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/RSpade/Core/Manifest/Manifest.php | 64 +++++- app/RSpade/man/class_override.txt | 183 ++++++++++++++++++ bin/framework-pull-upstream.sh | 38 ++++ database/migrations/.migration_whitelist | 5 + ...2_10_020633_set_user_1_invite_accepted.php | 35 ++++ docs/CLAUDE.dist.md | 16 ++ 6 files changed, 337 insertions(+), 4 deletions(-) create mode 100755 app/RSpade/man/class_override.txt create mode 100755 database/migrations/2025_12_10_020633_set_user_1_invite_accepted.php diff --git a/app/RSpade/Core/Manifest/Manifest.php b/app/RSpade/Core/Manifest/Manifest.php index 01d4a0f00..2eb1cb307 100755 --- a/app/RSpade/Core/Manifest/Manifest.php +++ b/app/RSpade/Core/Manifest/Manifest.php @@ -1272,8 +1272,9 @@ class Manifest { manifest_start: - // Reset manifest restart flag at the beginning of each pass + // Reset caches at the beginning of each pass (important for restarts) static::$_needs_manifest_restart = false; + self::$__get_rsx_files_cache = null; // Reset manifest structure, retaining only existing files data $existing_files = static::$data['data']['files'] ?? []; @@ -1354,9 +1355,15 @@ class Manifest // but we don't need to log it as it's not an error condition } - // Validate class names are unique. + // Validate class names are unique (also handles rsx/ overriding app/RSpade/ classes) static::__check_unique_base_class_names(); + // If a class override was detected (rsx/ overriding app/RSpade/), restart manifest build + if (static::$_needs_manifest_restart) { + console_debug('MANIFEST', 'Class override detected, restarting manifest build'); + goto manifest_start; + } + // ================================================================================== // PHP FIXER INTEGRATION POINT // ================================================================================== @@ -1438,6 +1445,12 @@ class Manifest // so that abstract property is available for subclass filtering static::__collate_files_by_classes(); + // Check if a class override was detected and framework file renamed + if (static::$_needs_manifest_restart) { + console_debug('MANIFEST', 'Class override detected, restarting manifest build'); + goto manifest_start; + } + // Build event handler index from attributes static::__build_event_handler_index(); @@ -1692,7 +1705,11 @@ class Manifest /** * Check for duplicate base class names within the same file type - * Throws a fatal error if any base class name appears in multiple files of the same type + * + * When a class exists in both rsx/ and app/RSpade/, this is a developer override. + * The framework version is renamed to .upstream and removed from indexing. + * + * Throws a fatal error if duplicates exist within the same area (both rsx/ or both app/RSpade/) */ protected static function __check_unique_base_class_names(): void { @@ -1725,6 +1742,31 @@ class Manifest foreach ($classes_by_extension as $extension => $base_class_files) { foreach ($base_class_files as $class_name => $files) { if (count($files) > 1) { + // Check if this is a valid override (rsx/ vs app/RSpade/) + $rsx_files = array_filter($files, fn($f) => str_starts_with($f, 'rsx/')); + $framework_files = array_filter($files, fn($f) => str_starts_with($f, 'app/RSpade/')); + + // Valid override: exactly one file in rsx/, rest in app/RSpade/ + if (count($rsx_files) === 1 && count($framework_files) >= 1) { + // Rename framework files to .upstream and remove from manifest + foreach ($framework_files as $framework_file) { + $full_framework_path = base_path($framework_file); + $upstream_path = $full_framework_path . '.upstream'; + + if (file_exists($full_framework_path) && !file_exists($upstream_path)) { + rename($full_framework_path, $upstream_path); + console_debug('MANIFEST', "Class override: {$class_name} - moved {$framework_file} to .upstream"); + } + + // Remove from manifest data so it won't be indexed + unset(static::$data['data']['files'][$framework_file]); + } + + static::$_needs_manifest_restart = true; + continue; + } + + // Not a valid override - throw error $file_type = $extension === 'php' ? 'PHP' : ($extension === 'js' ? 'JavaScript' : $extension); throw new \RuntimeException( @@ -1762,9 +1804,23 @@ class Manifest // Step 1: Index files by class name for quick lookups // This creates a map of className => filename (not full metadata to save space) + // NOTE: Class override detection (rsx/ vs app/RSpade/) happens earlier in __check_unique_base_class_names() foreach (static::$data['data']['files'] as $file => $filedata) { if ($filedata['extension'] == $ext && !empty($filedata['class'])) { - static::$data['data'][$ext . '_classes'][$filedata['class']] = $file; + $class_name = $filedata['class']; + + // Duplicates should have been caught by __check_unique_base_class_names() + // but check here as a safety net + if (isset(static::$data['data'][$ext . '_classes'][$class_name])) { + $existing_file = static::$data['data'][$ext . '_classes'][$class_name]; + throw new \RuntimeException( + "Duplicate {$ext} class detected: {$class_name}\n" . + "Found in:\n - {$existing_file}\n - {$file}\n" . + "Class names must be unique across the codebase." + ); + } + + static::$data['data'][$ext . '_classes'][$class_name] = $file; } } diff --git a/app/RSpade/man/class_override.txt b/app/RSpade/man/class_override.txt new file mode 100755 index 000000000..3ac80c1fc --- /dev/null +++ b/app/RSpade/man/class_override.txt @@ -0,0 +1,183 @@ +# Class Override System + +## NAME + +class_override - Override framework classes with application-specific versions + +## SYNOPSIS + +Copy a framework class to your rsx/ directory with the same class name: + + # Override User_Model + cp system/app/RSpade/Core/Models/User_Model.php rsx/models/user_model.php + + # Edit the copy to customize behavior + nano rsx/models/user_model.php + + # Manifest automatically detects and activates override + +## DESCRIPTION + +The class override system allows developers to replace framework classes with +custom implementations. When a class with the same name exists in both rsx/ +and app/RSpade/, the manifest automatically: + +1. Renames the framework file to .upstream (e.g., User_Model.php.upstream) +2. Uses the rsx/ version as the authoritative class +3. Rebuilds to reflect the change + +This enables customization without forking the entire framework. + +## HOW IT WORKS + +During manifest build, when duplicate class names are detected: + + Framework file: app/RSpade/Core/Models/User_Model.php + Override file: rsx/models/user_model.php + + Result: + - app/RSpade/Core/Models/User_Model.php renamed to .upstream + - rsx/models/user_model.php becomes the active class + +The .upstream file preserves the original for reference and framework updates. + +## COMMON OVERRIDE TARGETS + +These framework classes are commonly overridden to add application-specific +fields, relationships, or behavior: + + User_Model - Add custom user fields, relationships, methods + User_Profile_Model - Extend profile with application-specific data + Site_Model - Add site-specific settings or relationships + +Location of originals: + + system/app/RSpade/Core/Models/User_Model.php + system/app/RSpade/Core/Models/User_Profile_Model.php + system/app/RSpade/Core/Models/Site_Model.php + +## CREATING AN OVERRIDE + +1. Copy the framework file to rsx/: + + cp system/app/RSpade/Core/Models/User_Model.php rsx/models/user_model.php + +2. Update the namespace in your copy: + + namespace Rsx\Models; // Was: namespace App\RSpade\Core\Models; + +3. Modify the class as needed (add fields, methods, relationships) + +4. Run manifest build or load any page (triggers automatic rebuild): + + php artisan rsx:manifest:build + +5. Verify the override is active: + + php artisan tinker --execute="echo Manifest::php_find_class('User_Model');" + # Output: rsx/models/user_model.php + +## EXAMPLE: EXTENDING USER_MODEL + + [ + 1 => ['constant' => 'ROLE_ADMIN', 'label' => 'Administrator'], + 2 => ['constant' => 'ROLE_USER', 'label' => 'User'], + ], + ]; + + // Add application-specific relationships + public function projects() + { + return $this->hasMany(Project_Model::class, 'owner_id'); + } + + // Add custom methods + public function can_access_admin(): bool + { + return $this->role_id === self::ROLE_ADMIN; + } + + // Include all original User_Model functionality... + } + +## FRAMEWORK UPDATES + +When updating the framework with `php artisan rsx:framework:pull`: + +1. Before checking for uncommitted changes, the update script automatically: + - Restores any deleted framework files (git checkout) + - Deletes any .upstream files +2. Git pull brings in updated framework files +3. Your rsx/ override remains unchanged +4. On next manifest build, override detection runs again +5. The updated framework file is re-renamed to .upstream + +This automatic cleanup ensures framework updates work seamlessly with overrides. +The next manifest build will re-detect your overrides and rename the (now updated) +framework files back to .upstream. + +To see what changed in the framework version after an update: + + diff rsx/models/user_model.php \ + system/app/RSpade/Core/Models/User_Model.php.upstream + +## REMOVING AN OVERRIDE + +To revert to the framework version: + +1. Delete your override file: + + rm rsx/models/user_model.php + +2. Restore the framework file: + + mv system/app/RSpade/Core/Models/User_Model.php.upstream \ + system/app/RSpade/Core/Models/User_Model.php + +3. Rebuild manifest: + + php artisan rsx:manifest:build + +## LIMITATIONS + +- Only works for classes in rsx/ overriding app/RSpade/ classes +- Both files must define the same class name +- Cannot override multiple framework classes with one file +- JavaScript classes follow the same override pattern + +## TROUBLESHOOTING + +### Override not detected + +Ensure class names match exactly. Check with: + + grep -r "^class " rsx/models/user_model.php + grep -r "^class " system/app/RSpade/Core/Models/User_Model.php + +### Missing methods after override + +Your override replaces the entire class. Copy all needed functionality from +the .upstream file or extend from a different base class. + +### Framework file not renamed + +Run a clean manifest build: + + php artisan rsx:manifest:build --clean + +## SEE ALSO + +rsx_upstream(7) - Framework update management +manifest_api(7) - Manifest system documentation +model(7) - Model documentation diff --git a/bin/framework-pull-upstream.sh b/bin/framework-pull-upstream.sh index 2f9ae720d..5ee8ecf9c 100755 --- a/bin/framework-pull-upstream.sh +++ b/bin/framework-pull-upstream.sh @@ -181,6 +181,44 @@ if [ "$SHOW_DIFF" = true ]; then exit 0 fi +# ============================================================================= +# STEP: Clean up class override artifacts before checking for uncommitted changes +# ============================================================================= +# The manifest's class override system renames framework files to .upstream when +# an rsx/ override exists. Before updating, we need to: +# 1. Restore any deleted files (git checkout) +# 2. Delete any .upstream files (the originals will be restored by git) +# This ensures git sees a clean state, and the next manifest build will +# re-apply any overrides with the updated framework files. +# ============================================================================= + +echo "→ Cleaning up class override artifacts..." + +# Step 1: Restore deleted files +DELETED_FILES=$(git status --porcelain 2>&1 | grep "^ D " | sed 's/^ D //' || true) +if [ -n "$DELETED_FILES" ]; then + echo " Restoring deleted files..." + echo "$DELETED_FILES" | while read -r file; do + if [ -n "$file" ]; then + git checkout HEAD -- "$file" 2>/dev/null && echo " ✓ Restored: $file" || true + fi + done +fi + +# Step 2: Delete .upstream files (framework files renamed by class override system) +UPSTREAM_FILES=$(find . -name "*.upstream" -type f 2>/dev/null | grep -v "./rsx/" || true) +if [ -n "$UPSTREAM_FILES" ]; then + echo " Removing .upstream override markers..." + echo "$UPSTREAM_FILES" | while read -r upstream_file; do + if [ -n "$upstream_file" ] && [ -f "$upstream_file" ]; then + rm -f "$upstream_file" && echo " ✓ Removed: $upstream_file" || true + fi + done +fi + +echo " ✓ Override cleanup complete" +echo "" + # In project mode, check for uncommitted changes outside ./rsx if [ "$IS_PROJECT_MODE" = true ]; then # Get uncommitted changes (modified, staged, deleted) excluding ./rsx diff --git a/database/migrations/.migration_whitelist b/database/migrations/.migration_whitelist index c5514e2f1..3404986b9 100755 --- a/database/migrations/.migration_whitelist +++ b/database/migrations/.migration_whitelist @@ -331,6 +331,11 @@ "created_at": "2025-12-08T04:22:58+00:00", "created_by": "root", "command": "php artisan make:migration:safe drop_migrations_and_rename_sessions_table" + }, + "2025_12_10_020633_set_user_1_invite_accepted.php": { + "created_at": "2025-12-10T02:06:33+00:00", + "created_by": "root", + "command": "php artisan make:migration:safe set_user_1_invite_accepted" } } } \ No newline at end of file diff --git a/database/migrations/2025_12_10_020633_set_user_1_invite_accepted.php b/database/migrations/2025_12_10_020633_set_user_1_invite_accepted.php new file mode 100755 index 000000000..4995fa7bf --- /dev/null +++ b/database/migrations/2025_12_10_020633_set_user_1_invite_accepted.php @@ -0,0 +1,35 @@ +