Add class override system for framework customization
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1272,8 +1272,9 @@ class Manifest
|
|||||||
{
|
{
|
||||||
manifest_start:
|
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;
|
static::$_needs_manifest_restart = false;
|
||||||
|
self::$__get_rsx_files_cache = null;
|
||||||
|
|
||||||
// Reset manifest structure, retaining only existing files data
|
// Reset manifest structure, retaining only existing files data
|
||||||
$existing_files = static::$data['data']['files'] ?? [];
|
$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
|
// 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();
|
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
|
// PHP FIXER INTEGRATION POINT
|
||||||
// ==================================================================================
|
// ==================================================================================
|
||||||
@@ -1438,6 +1445,12 @@ class Manifest
|
|||||||
// so that abstract property is available for subclass filtering
|
// so that abstract property is available for subclass filtering
|
||||||
static::__collate_files_by_classes();
|
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
|
// Build event handler index from attributes
|
||||||
static::__build_event_handler_index();
|
static::__build_event_handler_index();
|
||||||
|
|
||||||
@@ -1692,7 +1705,11 @@ class Manifest
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for duplicate base class names within the same file type
|
* 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
|
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 ($classes_by_extension as $extension => $base_class_files) {
|
||||||
foreach ($base_class_files as $class_name => $files) {
|
foreach ($base_class_files as $class_name => $files) {
|
||||||
if (count($files) > 1) {
|
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);
|
$file_type = $extension === 'php' ? 'PHP' : ($extension === 'js' ? 'JavaScript' : $extension);
|
||||||
|
|
||||||
throw new \RuntimeException(
|
throw new \RuntimeException(
|
||||||
@@ -1762,9 +1804,23 @@ class Manifest
|
|||||||
|
|
||||||
// Step 1: Index files by class name for quick lookups
|
// Step 1: Index files by class name for quick lookups
|
||||||
// This creates a map of className => filename (not full metadata to save space)
|
// 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) {
|
foreach (static::$data['data']['files'] as $file => $filedata) {
|
||||||
if ($filedata['extension'] == $ext && !empty($filedata['class'])) {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
183
app/RSpade/man/class_override.txt
Executable file
183
app/RSpade/man/class_override.txt
Executable file
@@ -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
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Rsx\Models;
|
||||||
|
|
||||||
|
use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
|
||||||
|
|
||||||
|
class User_Model extends Rsx_Model_Abstract
|
||||||
|
{
|
||||||
|
protected $table = 'users';
|
||||||
|
|
||||||
|
public static $enums = [
|
||||||
|
'role_id' => [
|
||||||
|
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
|
||||||
@@ -181,6 +181,44 @@ if [ "$SHOW_DIFF" = true ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
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
|
# In project mode, check for uncommitted changes outside ./rsx
|
||||||
if [ "$IS_PROJECT_MODE" = true ]; then
|
if [ "$IS_PROJECT_MODE" = true ]; then
|
||||||
# Get uncommitted changes (modified, staged, deleted) excluding ./rsx
|
# Get uncommitted changes (modified, staged, deleted) excluding ./rsx
|
||||||
|
|||||||
@@ -331,6 +331,11 @@
|
|||||||
"created_at": "2025-12-08T04:22:58+00:00",
|
"created_at": "2025-12-08T04:22:58+00:00",
|
||||||
"created_by": "root",
|
"created_by": "root",
|
||||||
"command": "php artisan make:migration:safe drop_migrations_and_rename_sessions_table"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
35
database/migrations/2025_12_10_020633_set_user_1_invite_accepted.php
Executable file
35
database/migrations/2025_12_10_020633_set_user_1_invite_accepted.php
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* IMPORTANT: Use raw MySQL queries for clarity and auditability
|
||||||
|
* ✅ DB::statement("ALTER TABLE users ADD COLUMN age BIGINT")
|
||||||
|
* ❌ Schema::table() with Blueprint
|
||||||
|
*
|
||||||
|
* REQUIRED: ALL tables MUST have: id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
|
||||||
|
* No exceptions - every table needs this exact ID column (SIGNED for easier migrations)
|
||||||
|
*
|
||||||
|
* Integer types: Use BIGINT for all integers, TINYINT(1) for booleans only
|
||||||
|
* Never use unsigned - all integers should be signed
|
||||||
|
*
|
||||||
|
* Migrations must be self-contained - no Model/Service references
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
DB::statement("UPDATE users SET invite_accepted_at = NOW() WHERE id = 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* down() method is prohibited in RSpade framework
|
||||||
|
* Migrations should only move forward, never backward
|
||||||
|
* You may remove this comment as soon as you see it and understand.
|
||||||
|
*/
|
||||||
|
};
|
||||||
@@ -102,6 +102,22 @@ Classes are namespacing tools. Use static unless instances needed (models, resou
|
|||||||
|
|
||||||
**Commit discipline**: ONLY commit when explicitly asked. Commits are milestones, not individual changes.
|
**Commit discipline**: ONLY commit when explicitly asked. Commits are milestones, not individual changes.
|
||||||
|
|
||||||
|
### Class Overrides
|
||||||
|
|
||||||
|
To customize framework classes without modifying `/system/`, copy them to `rsx/` with the same class name. The manifest automatically uses your version and renames the framework file to `.upstream`.
|
||||||
|
|
||||||
|
**Common override targets** (copy from `system/app/RSpade/Core/Models/`):
|
||||||
|
- `User_Model` - Add custom fields, relationships, methods
|
||||||
|
- `User_Profile_Model` - Extend profile data
|
||||||
|
- `Site_Model` - Add site-specific settings
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp system/app/RSpade/Core/Models/User_Model.php rsx/models/user_model.php
|
||||||
|
# Edit namespace to Rsx\Models, customize as needed
|
||||||
|
```
|
||||||
|
|
||||||
|
Details: `php artisan rsx:man class_override`
|
||||||
|
|
||||||
### DO NOT RUN `rsx:clean`
|
### DO NOT RUN `rsx:clean`
|
||||||
|
|
||||||
**RSpade's cache auto-invalidates on file changes.** Running `rsx:clean` causes 30-60 second rebuilds with zero benefit.
|
**RSpade's cache auto-invalidates on file changes.** Running `rsx:clean` causes 30-60 second rebuilds with zero benefit.
|
||||||
|
|||||||
Reference in New Issue
Block a user