Mark PHP version compatibility fallback as legitimate in Php_Fixer

Add public directory asset support to bundle system
Fix PHP Fixer to replace ALL Rsx\ FQCNs with simple class names

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-30 17:18:10 +00:00
parent 6e41df0789
commit 8c8fb8e902
8 changed files with 720 additions and 121 deletions

View File

@@ -0,0 +1,223 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Blade;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class LayoutLocalAssets_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'BLADE-LAYOUT-ASSETS-01';
}
public function get_name(): string
{
return 'Layout Local Asset Includes';
}
public function get_description(): string
{
return 'Enforces that local assets in layout files are included via bundle definitions, not hardcoded link/script tags';
}
public function get_file_patterns(): array
{
return ['*.blade.php'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* This rule should run during manifest scan to provide immediate feedback
*
* IMPORTANT: This method should ALWAYS return false unless explicitly requested
* by the framework developer. Manifest-time checks are reserved for critical
* framework convention violations that need immediate developer attention.
*
* Rules executed during manifest scan will run on every file change in development,
* potentially impacting performance. Only enable this for rules that:
* - Enforce critical framework conventions that would break the application
* - Need to provide immediate feedback before code execution
* - Have been specifically requested to run at manifest-time by framework maintainers
*
* DEFAULT: Always return false unless you have explicit permission to do otherwise.
*
* EXCEPTION: This rule has been explicitly approved to run at manifest-time because
* hardcoded local asset includes in layouts bypass the bundle system and break
* cache-busting and asset management conventions.
*/
public function is_called_during_manifest_scan(): bool
{
return true; // Explicitly approved for manifest-time checking
}
/**
* Process file during manifest update to extract local asset violations
*/
public function on_manifest_file_update(string $file_path, string $contents, array $metadata = []): ?array
{
// Only check files that contain <html> (layouts)
if (!str_contains($contents, '<html')) {
return null;
}
$lines = explode("\n", $contents);
$violations = [];
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Check for <link rel="stylesheet" with local href
if (preg_match('/<link\s+[^>]*href=["\'](\/[^"\']*)["\'][^>]*>/i', $line, $matches)) {
$href = $matches[1];
// Skip if it's a CDN/external URL (contains http)
if (str_contains(strtolower($line), 'http')) {
continue;
}
$violations[] = [
'type' => 'local_css',
'line' => $line_number,
'code' => trim($line),
'path' => $href
];
}
// Check for <script src= with local src
if (preg_match('/<script\s+[^>]*src=["\'](\/[^"\']*)["\'][^>]*>/i', $line, $matches)) {
$src = $matches[1];
// Skip if it's a CDN/external URL (contains http)
if (str_contains(strtolower($line), 'http')) {
continue;
}
$violations[] = [
'type' => 'local_js',
'line' => $line_number,
'code' => trim($line),
'path' => $src
];
}
}
if (!empty($violations)) {
return ['local_asset_violations' => $violations];
}
return null;
}
/**
* Check blade layout file for local asset violations stored in metadata
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check layouts
if (!str_contains($contents, '<html')) {
return;
}
// Check for violations in code quality metadata
if (isset($metadata['code_quality_metadata']['BLADE-LAYOUT-ASSETS-01']['local_asset_violations'])) {
$violations = $metadata['code_quality_metadata']['BLADE-LAYOUT-ASSETS-01']['local_asset_violations'];
// Throw on first violation
foreach ($violations as $violation) {
$asset_type = $violation['type'] === 'local_css' ? 'CSS' : 'JavaScript';
$error_message = "Code Quality Violation (BLADE-LAYOUT-ASSETS-01) - Local {$asset_type} Asset in Layout\n\n";
$error_message .= "Local asset files should be included via bundle definitions, not hardcoded in layout files.\n\n";
$error_message .= "File: {$file_path}\n";
$error_message .= "Line: {$violation['line']}\n";
$error_message .= "Path: {$violation['path']}\n";
$error_message .= "Code: {$violation['code']}\n\n";
$error_message .= $this->get_detailed_remediation($file_path, $violation);
throw new \App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException(
$error_message,
0,
null,
base_path($file_path),
$violation['line']
);
}
}
}
/**
* Get detailed remediation instructions
*/
private function get_detailed_remediation(string $file_path, array $violation): string
{
$path = $violation['path'];
$is_css = $violation['type'] === 'local_css';
$tag_type = $is_css ? '<link>' : '<script>';
// Determine bundle file name from layout path
$path_parts = pathinfo($file_path);
$dir_name = basename(dirname($file_path));
$bundle_name = ucfirst($dir_name) . '_Bundle';
$bundle_file = dirname($file_path) . '/' . strtolower($dir_name) . '_bundle.php';
return "FRAMEWORK CONVENTION: Local assets must be included via bundle definitions.
WHY THIS MATTERS:
- Bundle system provides automatic cache-busting
- Assets are properly ordered with dependencies
- Development/production builds are optimized
- All assets are tracked and validated
REQUIRED STEPS:
1. Remove the hardcoded {$tag_type} tag from {$file_path}:
DELETE: {$violation['code']}
2. Add the asset to your bundle definition in {$bundle_file}:
class {$bundle_name} extends Rsx_Bundle_Abstract
{
public static function define(): array
{
return [
'include' => [
'jquery',
'lodash',
'/public{$path}', // Add this line
'rsx/app/{$dir_name}',
],
];
}
}
3. The bundle system will automatically generate:
" . ($is_css
? "<link rel=\"stylesheet\" href=\"{$path}?v=<?php echo filemtime('FULL_PATH'); ?>\">"
: "<script src=\"{$path}?v=<?php echo filemtime('FULL_PATH'); ?>\" defer></script>") . "
KEY BENEFITS:
- Automatic filemtime() cache-busting on every page load
- Proper asset ordering (CDN assets Public assets Compiled bundles)
- Redis-cached path resolution for performance
- Ambiguity detection prevents multiple files with same path
BUNDLE INCLUDE SYNTAX:
- Prefix with /public/ for static assets from public/ directories
- Path after /public/ is searched across ALL public/ directories in rsx/
- Example: '/public/vendor/css/core.css' resolves to 'rsx/public/vendor/css/core.css'
CACHE-BUSTING:
- Bundle generates tags with <?php echo filemtime('...'); ?> for fresh timestamps
- No need to manually manage version parameters
- Updates automatically when file changes
For complete documentation:
php artisan rsx:man bundle_api
(See PUBLIC ASSET INCLUDES section)";
}
}

View File

@@ -43,6 +43,12 @@ class BundleCompiler
*/
protected array $cdn_assets = ['js' => [], 'css' => []];
/**
* Public directory assets (served via AssetHandler with filemtime cache-busting)
* Format: ['js' => [['url' => '/path/to/file.js', 'full_path' => '/full/filesystem/path']], ...]
*/
protected array $public_assets = ['js' => [], 'css' => []];
/**
* Cache keys for vendor/app
*/
@@ -175,6 +181,14 @@ class BundleCompiler
$result['cdn_css'] = $this->cdn_assets['css'];
}
// Add public directory assets
if (!empty($this->public_assets['js'])) {
$result['public_js'] = $this->public_assets['js'];
}
if (!empty($this->public_assets['css'])) {
$result['public_css'] = $this->public_assets['css'];
}
// Add bundle file paths for development
if (!$this->is_production) {
if (isset($outputs['vendor_js'])) {
@@ -501,6 +515,43 @@ class BundleCompiler
}
$this->resolved_includes[$include_key] = true;
// Check for /public/ prefix - static assets from public directories
if (is_string($item) && str_starts_with($item, '/public/')) {
$relative_path = substr($item, 8); // Strip '/public/' prefix
try {
// Resolve via AssetHandler (with Redis caching)
$full_path = \App\RSpade\Core\Dispatch\AssetHandler::find_public_asset($relative_path);
// Determine file type
$extension = strtolower(pathinfo($relative_path, PATHINFO_EXTENSION));
if ($extension === 'js') {
$this->public_assets['js'][] = [
'url' => '/' . $relative_path,
'full_path' => $full_path
];
} elseif ($extension === 'css') {
$this->public_assets['css'][] = [
'url' => '/' . $relative_path,
'full_path' => $full_path
];
} else {
throw new RuntimeException(
"Public asset must be .js or .css file: {$item}\n" .
"Only JavaScript and CSS files can be included via /public/ prefix."
);
}
return;
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
throw new RuntimeException(
"Failed to resolve public asset: {$item}\n" .
$e->getMessage()
);
}
}
// Check bundle aliases and resolve to actual class
$bundle_aliases = config('rsx.bundle_aliases', []);
if (is_string($item) && isset($bundle_aliases[$item])) {

View File

@@ -391,6 +391,12 @@ abstract class Rsx_Bundle_Abstract
$html[] = $tag;
}
// Add public directory CSS (with filemtime cache-busting)
$public_css = $compiled['public_css'] ?? [];
foreach ($public_css as $asset) {
$html[] = '<link rel="stylesheet" href="' . htmlspecialchars($asset['url']) . '?v=<?php echo filemtime(\'' . addslashes($asset['full_path']) . '\'); ?>">';
}
// Add JS: jQuery first, then others
foreach (array_merge($jquery_js, $other_js) as $asset) {
$tag = '<script src="' . htmlspecialchars($asset['url']) . '" defer';
@@ -402,6 +408,12 @@ abstract class Rsx_Bundle_Abstract
$html[] = $tag;
}
// Add public directory JS (with filemtime cache-busting and defer)
$public_js = $compiled['public_js'] ?? [];
foreach ($public_js as $asset) {
$html[] = '<script src="' . htmlspecialchars($asset['url']) . '?v=<?php echo filemtime(\'' . addslashes($asset['full_path']) . '\'); ?>" defer></script>';
}
// Add CSS bundles
// In development mode with split bundles, add vendor then app
if (!empty($compiled['vendor_css_bundle_path']) || !empty($compiled['app_css_bundle_path'])) {

View File

@@ -9,6 +9,7 @@ namespace App\RSpade\Core\Dispatch;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Redis;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -215,12 +216,86 @@ class AssetHandler
// Set additional security headers
static::__set_security_headers($response, $mime_type);
// Enable gzip if supported
if (static::__should_compress($mime_type)) {
$response->headers->set('Content-Encoding', 'gzip');
return $response;
}
return $response;
/**
* Find a public asset by relative path with Redis caching
*
* Resolves paths like "sneat/css/demo.css" to full filesystem paths like
* "rsx/public/sneat/css/demo.css" by scanning all public/ directories.
*
* Results are cached in Redis indefinitely. Cached paths are validated
* before use - if file no longer exists, cache is invalidated and re-scan occurs.
*
* @param string $relative_path Relative path like "sneat/css/demo.css"
* @return string Full filesystem path
* @throws \Symfony\Component\HttpKernel\Exception\HttpException If not found or ambiguous
*/
public static function find_public_asset(string $relative_path): string
{
// Ensure directories are discovered
static::__ensure_directories_discovered();
// Sanitize the path
$relative_path = static::__sanitize_path($relative_path);
// Check Redis cache first
$cache_key = 'rspade:public_asset:' . $relative_path;
$cached_path = Redis::get($cache_key);
if ($cached_path) {
// Verify cached file still exists
if (File::exists($cached_path) && File::isFile($cached_path)) {
return $cached_path;
}
// Stale cache - invalidate and re-scan
Redis::del($cache_key);
}
// NEVER serve PHP files under any circumstances
$extension = strtolower(pathinfo($relative_path, PATHINFO_EXTENSION));
if ($extension === 'php') {
throw new HttpException(403, 'PHP files cannot be served as static assets');
}
// Scan all public directories for matches
$matches = [];
foreach (static::$public_directories as $module => $directory) {
$full_path = $directory . '/' . $relative_path;
if (File::exists($full_path) && File::isFile($full_path)) {
// Check exclusion rules
if (static::__is_file_excluded($full_path, $relative_path)) {
throw new HttpException(403, 'Access to this file is forbidden');
}
$matches[] = $full_path;
}
}
// Check for ambiguous matches
if (count($matches) > 1) {
// Show first two matches in error
$first_two = array_slice($matches, 0, 2);
throw new HttpException(
500,
"Ambiguous public asset request: '{$relative_path}' matches multiple files: '" .
implode("', '", $first_two) . "'"
);
}
// Check for no matches
if (count($matches) === 0) {
throw new NotFoundHttpException("Public asset not found: {$relative_path}");
}
// Single match - cache and return
$resolved_path = $matches[0];
Redis::set($cache_key, $resolved_path);
return $resolved_path;
}
/**
@@ -294,53 +369,23 @@ class AssetHandler
/**
* Find asset file in public directories
*
* Wrapper around find_public_asset() that returns null instead of throwing
* NotFoundHttpException for backward compatibility with existing code.
*
* @param string $path
* @return string|null Full file path or null if not found
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
* @throws \Symfony\Component\HttpKernel\Exception\HttpException For PHP files, exclusions, or ambiguous matches
*/
protected static function __find_asset_file($path)
{
// NEVER serve PHP files under any circumstances
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
if ($extension === 'php') {
throw new HttpException(403, 'PHP files cannot be served as static assets');
}
// Try each public directory
foreach (static::$public_directories as $module => $directory) {
$full_path = $directory . '/' . $path;
if (File::exists($full_path) && File::isFile($full_path)) {
// Check exclusion rules before returning
if (static::__is_file_excluded($full_path, $path)) {
throw new HttpException(403, 'Access to this file is forbidden');
}
return $full_path;
}
}
// Check if path includes module prefix (e.g., "admin/css/style.css")
$parts = explode('/', $path, 2);
if (count($parts) === 2) {
$module = $parts[0];
$asset_path = $parts[1];
if (isset(static::$public_directories[$module])) {
$full_path = static::$public_directories[$module] . '/' . $asset_path;
if (File::exists($full_path) && File::isFile($full_path)) {
// Check exclusion rules before returning
if (static::__is_file_excluded($full_path, $asset_path)) {
throw new HttpException(403, 'Access to this file is forbidden');
}
return $full_path;
}
}
}
try {
return static::find_public_asset($path);
} catch (NotFoundHttpException $e) {
// Not found - return null for backward compatibility
return null;
}
// Let other exceptions (403, 500) bubble up
}
/**
* Check if a file should be excluded from serving
@@ -674,32 +719,6 @@ class AssetHandler
}
}
/**
* Check if content should be compressed
*
* @param string $mime_type
* @return bool
*/
protected static function __should_compress($mime_type)
{
// Compress text-based content
$compressible = [
'text/',
'application/javascript',
'application/json',
'application/xml',
'image/svg+xml'
];
foreach ($compressible as $type) {
if (str_starts_with($mime_type, $type)) {
return true;
}
}
return false;
}
/**
* Get discovered public directories
*

View File

@@ -1383,7 +1383,34 @@ class Manifest
// Validate class names are unique.
static::__check_unique_base_class_names();
// Apply Php_Fixer to all PHP files in rsx/ and app/RSpade/ before parsing
// ==================================================================================
// PHP FIXER INTEGRATION POINT
// ==================================================================================
// This is where automatic code fixes are applied before Phase 2 parsing.
//
// WHAT PHP_FIXER DOES:
// 1. Fixes namespaces to match file paths
// 2. Removes/rebuilds use statements (strips Rsx\ and App\RSpade\ prefixes)
// 3. Replaces FQCNs like \Rsx\Models\User_Model with simple names User_Model
// 4. Adds #[Relationship] attributes to model ORM methods
// 5. Removes leading backslashes from attributes: #[\Route] → #[Route]
//
// SMART REBUILDING:
// - Tracks SHA1 hash of all class structures (ClassName:ParentClass)
// - If structure changed: Fixes ALL files (cascading updates needed)
// - If structure unchanged: Fixes ONLY $files_to_process (incremental)
//
// WHY BEFORE PHASE 2:
// - Phase 2 parses metadata from file content
// - If we fix AFTER parsing, manifest would have old/incorrect metadata
// - By fixing BEFORE, we parse the corrected content
//
// RE-PARSING LOOP BELOW:
// - If Php_Fixer modified files, we MUST re-parse them
// - This updates manifest with corrected namespace/class/FQCN data
// - Without this, manifest would reference old class locations
// ==================================================================================
$php_fixer_modified_files = [];
if (!app()->environment('production')) {
$php_fixer_modified_files = static::__run_php_fixer($files_to_process);
@@ -2282,6 +2309,29 @@ class Manifest
* Run Php_Fixer on all PHP files in rsx/ and app/RSpade/
* Called before Phase 2 parsing to ensure all files are fixed
*
* SMART REBUILD STRATEGY:
* This method implements an intelligent rebuild strategy to avoid unnecessary file writes:
*
* 1. STRUCTURE HASH: Creates SHA1 hash of "ClassName:ParentClass" for ALL classes
* - Detects when classes are added, removed, renamed, or inheritance changes
*
* 2. FULL REBUILD TRIGGERS:
* - New class added (may need new use statements elsewhere)
* - Class renamed (all references need updating)
* - Inheritance changed (may affect use statement resolution)
* When triggered: Fix ALL PHP files in rsx/ and app/RSpade/
*
* 3. INCREMENTAL REBUILD:
* - Structure hash unchanged (no new/renamed classes)
* - Only fixes files that actually changed on disk
* More efficient, avoids touching unchanged files
*
* WHY THIS MATTERS:
* - use statement management depends on knowing all available classes
* - FQCN replacement needs to check class name uniqueness
* - When class structure changes, files referencing those classes need updating
* - When structure stable, only changed files need processing
*
* @param array $changed_files List of changed files from Phase 1
* @return array List of files that were modified by Php_Fixer
*/
@@ -2289,7 +2339,14 @@ class Manifest
{
$modified_files = [];
// Build hash array of all PHP classes to detect structural changes
// ==================================================================================
// STEP 1: BUILD CLASS STRUCTURE HASH
// ==================================================================================
// Create a fingerprint of ALL classes in the codebase.
// Format: "path/to/file.php" => "ClassName:ParentClass"
// This lets us detect when the class structure itself changes (not just file contents)
// ==================================================================================
$class_structure_hash_data = [];
foreach (static::$data['data']['files'] as $file_path => $metadata) {
@@ -2311,12 +2368,24 @@ class Manifest
// Calculate hash of class structure
$new_class_structure_hash = sha1(json_encode($class_structure_hash_data));
// Check if class structure has changed
// ==================================================================================
// STEP 2: DECIDE REBUILD STRATEGY
// ==================================================================================
// Compare with previous hash to detect structural changes
// ==================================================================================
$previous_hash = static::$data['data']['php_fixer_hash'] ?? null;
$structure_changed = ($previous_hash !== $new_class_structure_hash);
if ($structure_changed) {
// Class structure changed - fix ALL PHP files in rsx/ and app/RSpade/
// ==================================================================================
// FULL REBUILD: Class structure changed
// ==================================================================================
// When class structure changes, we MUST fix ALL files because:
// - New classes may be referenced in existing files → need new use statements
// - Renamed classes need all references updated
// - Inheritance changes may affect use statement resolution
// ==================================================================================
$php_files_to_fix = [];
foreach (static::$data['data']['files'] as $file_path => $metadata) {
@@ -2340,10 +2409,19 @@ class Manifest
}
}
// Store updated hash
// Store updated hash for next rebuild comparison
static::$data['data']['php_fixer_hash'] = $new_class_structure_hash;
} else {
// Class structure unchanged - only fix changed PHP files with classes
// ==================================================================================
// INCREMENTAL REBUILD: Class structure unchanged
// ==================================================================================
// Only fix files that actually changed on disk.
// Safe because:
// - No new classes = no new use statements needed elsewhere
// - No renamed classes = no references to update
// - No inheritance changes = use statement resolution unchanged
// Result: Much faster, avoids touching 99% of files on typical edits
// ==================================================================================
$php_files_to_fix = [];
foreach ($changed_files as $file_path) {

View File

@@ -15,9 +15,57 @@ use RuntimeException;
* Performs automatic fixes and enhancements to PHP source files during development:
* - Auto-adds #[Relationship] attributes to model files
* - Auto-updates namespaces based on file location
* - Other automatic code improvements
* - Removes/rebuilds use statements for Rsx\ and App\RSpade\ classes
* - Replaces FQCNs like \Rsx\Models\User_Model with simple names User_Model
* - Removes leading backslashes from attributes: #[\Route] → #[Route]
*
* Only runs in non-production environments to avoid modifying deployed code.
*
* ======================================================================================
* HOW TO ADD NEW RULES
* ======================================================================================
*
* TO ADD A NEW FIX:
* 1. Create a new private static method: __fix_your_feature($file_path, $content, $manifest)
* 2. Add it to the fix() method's sequential application list (line ~123)
* 3. Method signature: private static function __fix_*($file_path, $content, &$step_2_manifest_data): string
* 4. Return the modified content (or original if no changes)
*
* EXAMPLE SKELETON:
* ```php
* private static function __fix_rsx_fqcn($file_path, string $content, array &$step_2_manifest_data): string
* {
* // Only process files in rsx/ or app/RSpade/
* if (!str_starts_with($file_path, 'rsx/') && !str_starts_with($file_path, 'app/RSpade/')) {
* return $content;
* }
*
* // Parse tokens for accurate replacement
* $tokens = token_get_all($content);
*
* // Find patterns to fix
* // Build modifications array with positions
*
* // Apply modifications (usually in reverse order to preserve positions)
*
* return $modified_content;
* }
* ```
*
* IMPORTANT PATTERNS:
* - Always work with tokens for PHP syntax awareness
* - Build modifications array, then apply in reverse order (preserves positions)
* - Use step_2_manifest_data to look up class information
* - Return original content if no changes needed
* - Never modify files in production environment (checked by caller)
*
* MANIFEST DATA AVAILABLE:
* - static::$data['data']['files'] - All indexed files with metadata
* - Files processed earlier in Phase 2 have complete metadata
* - Current file NOT in manifest yet (being processed now)
* - See method docblock for fix() for complete manifest structure details
*
* ======================================================================================
*/
class Php_Fixer
{
@@ -335,6 +383,32 @@ class Php_Fixer
/**
* Fix use statements - remove unnecessary ones, add missing ones
*
* THREE-STEP PROCESS:
*
* STEP 1: Remove all Rsx\ and App\RSpade\ use statements
* - We'll rebuild these based on actual usage
* - Protects vendor/Laravel use statements (never removes)
*
* STEP 2: Replace ALL Rsx\ FQCNs with simple names
* - Converts: \Rsx\Models\User_Model::class User_Model::class
* - Works for ALL classes in manifest (even non-unique names)
* - Relies on Step 3 to add disambiguating use statements
*
* STEP 3: Re-add use statements based on actual usage
* - Scans for simple class name references (from Step 2 replacements)
* - Looks up FQCNs in manifest
* - Adds: use Rsx\Models\User_Model;
* - Disambiguates non-unique class names automatically
*
* EXAMPLE TRANSFORMATION:
* Before:
* return $this->belongsTo(\Rsx\Models\User_Model::class, 'team_lead_id');
*
* After:
* use Rsx\Models\User_Model;
* ...
* return $this->belongsTo(User_Model::class, 'team_lead_id');
*
* @param string $file_path Relative path from base_path()
* @param string $content Current file content
* @param array $step_2_manifest_data Manifest state during Phase 2
@@ -648,7 +722,10 @@ class Php_Fixer
* Replace fully qualified Rsx class names with simple names
*
* This function finds \Rsx\Namespace\ClassName patterns and replaces them
* with just ClassName if that class exists uniquely (only one file with that class name)
* with just ClassName for ALL Rsx\ classes in manifest (even non-unique names).
*
* Step 3 of __fix_use_statements() will add the appropriate use statement,
* which disambiguates non-unique class names.
*
* @param string $content File content
* @param array $step_2_manifest_data Manifest data
@@ -656,15 +733,15 @@ class Php_Fixer
*/
private static function __replace_rsx_fqcn_with_simple_names(string $content, array &$step_2_manifest_data): string
{
// First, build a map of simple class names to count occurrences
$class_name_counts = [];
// Build a set of all valid Rsx\ class names in manifest
$valid_rsx_classes = [];
foreach ($step_2_manifest_data['data']['files'] ?? [] as $manifest_file => $metadata) {
if (isset($metadata['class']) && !empty($metadata['class'])) {
// Check if this file is an Rsx\ class (in rsx/ directory)
if (str_starts_with($manifest_file, 'rsx/')) {
$simple_name = $metadata['class'];
if (!isset($class_name_counts[$simple_name])) {
$class_name_counts[$simple_name] = 0;
$valid_rsx_classes[$simple_name] = true;
}
$class_name_counts[$simple_name]++;
}
}
@@ -681,8 +758,18 @@ class Php_Fixer
$modifications = [];
for ($i = 0; $i < count($tokens); $i++) {
// Look for namespace separator that starts a fully qualified name
if ($tokens[$i] === '\\' || (is_array($tokens[$i]) && $tokens[$i][0] === T_NS_SEPARATOR)) {
$fqcn = null;
$fqcn_start = null;
$fqcn_end = null;
// PHP 8+ uses T_NAME_FULLY_QUALIFIED for complete FQCN like \Rsx\Models\User_Model
if (is_array($tokens[$i]) && defined('T_NAME_FULLY_QUALIFIED') && $tokens[$i][0] === T_NAME_FULLY_QUALIFIED) {
$fqcn = $tokens[$i][1];
$fqcn_start = $i;
$fqcn_end = $i + 1;
}
// Fallback* for older PHP or partial namespaces
elseif ($tokens[$i] === '\\' || (is_array($tokens[$i]) && $tokens[$i][0] === T_NS_SEPARATOR)) {
// Check if this starts \Rsx\
$fqcn_start = $i;
$fqcn = '\\';
@@ -707,15 +794,18 @@ class Php_Fixer
break;
}
}
$fqcn_end = $j;
}
// Check if this is an Rsx FQCN (ONLY process \Rsx\ namespaced classes)
if (str_starts_with($fqcn, '\\Rsx\\')) {
// Process FQCN if we found one
if ($fqcn && str_starts_with($fqcn, '\\Rsx\\')) {
// Extract simple class name (last part after final \)
$parts = explode('\\', trim($fqcn, '\\'));
$simple_name = end($parts);
// Only replace if this class name is UNIQUE (appears only once in manifest)
if (isset($class_name_counts[$simple_name]) && $class_name_counts[$simple_name] === 1) {
// Replace if this class exists in manifest (even if name is not unique)
// Step 3 will add the correct use statement to disambiguate
if (isset($valid_rsx_classes[$simple_name])) {
// Calculate the byte positions for replacement
$start_pos = 0;
for ($k = 0; $k < $fqcn_start; $k++) {
@@ -727,7 +817,7 @@ class Php_Fixer
}
$length = 0;
for ($k = $fqcn_start; $k < $j; $k++) {
for ($k = $fqcn_start; $k < $fqcn_end; $k++) {
if (is_array($tokens[$k])) {
$length += strlen($tokens[$k][1]);
} else {
@@ -743,7 +833,6 @@ class Php_Fixer
}
}
}
}
// Apply modifications in reverse order to preserve positions
usort($modifications, function ($a, $b) {

View File

@@ -118,6 +118,77 @@ INCLUDE TYPES
External resources:
'cdn:https://unpkg.com/library.js'
Public Directory Assets
Static assets from public/ directories with automatic cache-busting:
'/public/sneat/css/core.css'
'/public/sneat/js/helpers.js'
These resolve to files in any public/ directory in rsx/. Resolution
cached in Redis for performance. Generates tags with filemtime() for
fresh cache-busting on each page render.
PUBLIC ASSET INCLUDES
Bundles can include static assets from any public/ directory with
automatic cache-busting via filemtime().
SYNTAX
Prefix paths with /public/ in bundle includes:
'include' => [
'/public/sneat/css/core.css',
'/public/sneat/js/helpers.js',
]
RESOLUTION
Path "sneat/css/demo.css" resolves to first match across all public/
directories in manifest. Resolution cached in Redis indefinitely.
Searches:
rsx/public/sneat/css/demo.css
rsx/app/admin/public/sneat/css/demo.css
rsx/theme/public/sneat/css/demo.css
... (all public/ directories)
OUTPUT
CSS: <link rel="stylesheet" href="/sneat/css/demo.css?v={filemtime}">
JS: <script src="/sneat/js/helpers.js?v={filemtime}" defer></script>
The filemtime() call executes on each page render, providing fresh
cache-busting timestamps without rebuilding bundles.
ORDERING
Public assets output with CDN includes, before compiled bundle code.
Order preserved as listed in bundle definition:
1. CDN CSS assets
2. Public directory CSS
3. Compiled bundle CSS
4. CDN JS assets
5. Public directory JS
6. Compiled bundle JS
AMBIGUITY ERRORS
If multiple files match the same path, compilation fails:
RuntimeException: Ambiguous public asset request:
'sneat/css/demo.css' matches multiple files:
'rsx/public/sneat/css/demo.css',
'rsx/theme/public/sneat/css/demo.css'
Solution: Use more specific paths or rename files to avoid conflicts.
CACHING
- Path resolution cached in Redis indefinitely
- Cache validated on each use (file existence check)
- Stale cache automatically re-scanned
- filemtime() executes on each page render for cache-busting
RESTRICTIONS
- Only .js and .css files allowed
- Must start with /public/ prefix
- Files must exist in a public/ directory
- No PHP files allowed (security)
BUNDLE RENDERING
In Blade layouts/views:
{!! Dashboard_Bundle::render() !!}

View File

@@ -278,12 +278,15 @@ class Frontend_Bundle extends Rsx_Bundle_Abstract
'rsx/theme/variables.scss', // Order matters
'rsx/app/frontend', // Directory
'rsx/models', // For JS stubs
'/public/vendor/css/core.css', // Public directory asset (filemtime cache-busting)
],
];
}
}
```
Bundles support `/public/` prefix for including static assets from public directories with automatic cache-busting.
Auto-compiles on page reload in development.
```blade
@@ -517,18 +520,66 @@ User_Model::create(['email' => $email]);
### Enums
**🔴 CRITICAL: Enum columns MUST be integers in both database and model definition**
Enum columns store integer values in the database, NOT strings. The model definition maps those integers to constants and labels.
```php
// ✅ CORRECT - Integer keys map to constants
public static $enums = [
'status_id' => [
1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active'],
2 => ['constant' => 'STATUS_INACTIVE', 'label' => 'Inactive'],
3 => ['constant' => 'STATUS_PENDING', 'label' => 'Pending'],
],
'priority_id' => [
1 => ['constant' => 'PRIORITY_LOW', 'label' => 'Low'],
2 => ['constant' => 'PRIORITY_MEDIUM', 'label' => 'Medium'],
3 => ['constant' => 'PRIORITY_HIGH', 'label' => 'High'],
],
];
// ❌ WRONG - String keys are NOT allowed
public static $enums = [
'status' => [ // ❌ Column name should be status_id
'active' => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active'], // ❌ String key
'inactive' => ['constant' => 'STATUS_INACTIVE', 'label' => 'Inactive'], // ❌ String key
],
];
// Usage
$user->status_id = User_Model::STATUS_ACTIVE;
$user->status_id = User_Model::STATUS_ACTIVE; // Sets to 1
echo $user->status_label; // "Active"
echo $user->status_id; // 1 (integer)
```
**Migration Requirements**: Enum columns must be INT(11), NEVER VARCHAR:
```php
public function up()
{
DB::statement("
CREATE TABLE users (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
status_id INT(11) NOT NULL DEFAULT 1, -- ✅ CORRECT - Enum column
priority_id INT(11) NOT NULL DEFAULT 1, -- ✅ CORRECT - Enum column
is_active TINYINT(1) NOT NULL DEFAULT 1, -- Boolean field (0=false, 1=true)
INDEX idx_status_id (status_id),
INDEX idx_priority_id (priority_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
");
}
// ❌ WRONG - VARCHAR columns are NOT allowed for enums
CREATE TABLE users (
status VARCHAR(20) NOT NULL DEFAULT 'active' -- ❌ WRONG - Use INT(11) instead
);
```
**Column Type Guidelines**:
- **INT(11)** - ALL enum columns use this type
- **TINYINT(1)** - Boolean fields ONLY (stores 0 or 1, treated as true/false in PHP)
### Migrations
**Forward-only, no rollbacks.**
@@ -913,9 +964,11 @@ class User_Model extends Rsx_Model_Abstract
protected $table = 'users';
protected $fillable = []; // Always empty - no mass assignment
// Enum columns - MUST use integer keys
public static $enums = [
'status_id' => [
1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active'],
2 => ['constant' => 'STATUS_INACTIVE', 'label' => 'Inactive'],
],
];
}
@@ -930,9 +983,12 @@ public function up()
CREATE TABLE articles (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
status_id TINYINT(1) NOT NULL DEFAULT 1,
status_id INT(11) NOT NULL DEFAULT 1, -- Enum column
priority_id INT(11) NOT NULL DEFAULT 1, -- Enum column
is_published TINYINT(1) NOT NULL DEFAULT 0, -- Boolean field (0 or 1)
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_status_id (status_id)
INDEX idx_status_id (status_id),
INDEX idx_priority_id (priority_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
");
}