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:
223
app/RSpade/CodeQuality/Rules/Blade/LayoutLocalAssets_CodeQualityRule.php
Executable file
223
app/RSpade/CodeQuality/Rules/Blade/LayoutLocalAssets_CodeQualityRule.php
Executable 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)";
|
||||
}
|
||||
}
|
||||
@@ -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])) {
|
||||
|
||||
@@ -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'])) {
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() !!}
|
||||
|
||||
@@ -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
|
||||
");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user