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' => []];
|
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
|
* Cache keys for vendor/app
|
||||||
*/
|
*/
|
||||||
@@ -175,6 +181,14 @@ class BundleCompiler
|
|||||||
$result['cdn_css'] = $this->cdn_assets['css'];
|
$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
|
// Add bundle file paths for development
|
||||||
if (!$this->is_production) {
|
if (!$this->is_production) {
|
||||||
if (isset($outputs['vendor_js'])) {
|
if (isset($outputs['vendor_js'])) {
|
||||||
@@ -501,6 +515,43 @@ class BundleCompiler
|
|||||||
}
|
}
|
||||||
$this->resolved_includes[$include_key] = true;
|
$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
|
// Check bundle aliases and resolve to actual class
|
||||||
$bundle_aliases = config('rsx.bundle_aliases', []);
|
$bundle_aliases = config('rsx.bundle_aliases', []);
|
||||||
if (is_string($item) && isset($bundle_aliases[$item])) {
|
if (is_string($item) && isset($bundle_aliases[$item])) {
|
||||||
|
|||||||
@@ -391,6 +391,12 @@ abstract class Rsx_Bundle_Abstract
|
|||||||
$html[] = $tag;
|
$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
|
// Add JS: jQuery first, then others
|
||||||
foreach (array_merge($jquery_js, $other_js) as $asset) {
|
foreach (array_merge($jquery_js, $other_js) as $asset) {
|
||||||
$tag = '<script src="' . htmlspecialchars($asset['url']) . '" defer';
|
$tag = '<script src="' . htmlspecialchars($asset['url']) . '" defer';
|
||||||
@@ -402,6 +408,12 @@ abstract class Rsx_Bundle_Abstract
|
|||||||
$html[] = $tag;
|
$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
|
// Add CSS bundles
|
||||||
// In development mode with split bundles, add vendor then app
|
// In development mode with split bundles, add vendor then app
|
||||||
if (!empty($compiled['vendor_css_bundle_path']) || !empty($compiled['app_css_bundle_path'])) {
|
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\Http\Request;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
@@ -215,12 +216,86 @@ class AssetHandler
|
|||||||
// Set additional security headers
|
// Set additional security headers
|
||||||
static::__set_security_headers($response, $mime_type);
|
static::__set_security_headers($response, $mime_type);
|
||||||
|
|
||||||
// Enable gzip if supported
|
return $response;
|
||||||
if (static::__should_compress($mime_type)) {
|
}
|
||||||
$response->headers->set('Content-Encoding', 'gzip');
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $response;
|
// 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,52 +369,22 @@ class AssetHandler
|
|||||||
/**
|
/**
|
||||||
* Find asset file in public directories
|
* 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
|
* @param string $path
|
||||||
* @return string|null Full file path or null if not found
|
* @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)
|
protected static function __find_asset_file($path)
|
||||||
{
|
{
|
||||||
// NEVER serve PHP files under any circumstances
|
try {
|
||||||
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
return static::find_public_asset($path);
|
||||||
if ($extension === 'php') {
|
} catch (NotFoundHttpException $e) {
|
||||||
throw new HttpException(403, 'PHP files cannot be served as static assets');
|
// Not found - return null for backward compatibility
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
// Let other exceptions (403, 500) bubble up
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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
|
* Get discovered public directories
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1383,7 +1383,34 @@ class Manifest
|
|||||||
// Validate class names are unique.
|
// Validate class names are unique.
|
||||||
static::__check_unique_base_class_names();
|
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 = [];
|
$php_fixer_modified_files = [];
|
||||||
if (!app()->environment('production')) {
|
if (!app()->environment('production')) {
|
||||||
$php_fixer_modified_files = static::__run_php_fixer($files_to_process);
|
$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/
|
* Run Php_Fixer on all PHP files in rsx/ and app/RSpade/
|
||||||
* Called before Phase 2 parsing to ensure all files are fixed
|
* 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
|
* @param array $changed_files List of changed files from Phase 1
|
||||||
* @return array List of files that were modified by Php_Fixer
|
* @return array List of files that were modified by Php_Fixer
|
||||||
*/
|
*/
|
||||||
@@ -2289,7 +2339,14 @@ class Manifest
|
|||||||
{
|
{
|
||||||
$modified_files = [];
|
$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 = [];
|
$class_structure_hash_data = [];
|
||||||
|
|
||||||
foreach (static::$data['data']['files'] as $file_path => $metadata) {
|
foreach (static::$data['data']['files'] as $file_path => $metadata) {
|
||||||
@@ -2311,12 +2368,24 @@ class Manifest
|
|||||||
// Calculate hash of class structure
|
// Calculate hash of class structure
|
||||||
$new_class_structure_hash = sha1(json_encode($class_structure_hash_data));
|
$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;
|
$previous_hash = static::$data['data']['php_fixer_hash'] ?? null;
|
||||||
$structure_changed = ($previous_hash !== $new_class_structure_hash);
|
$structure_changed = ($previous_hash !== $new_class_structure_hash);
|
||||||
|
|
||||||
if ($structure_changed) {
|
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 = [];
|
$php_files_to_fix = [];
|
||||||
|
|
||||||
foreach (static::$data['data']['files'] as $file_path => $metadata) {
|
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;
|
static::$data['data']['php_fixer_hash'] = $new_class_structure_hash;
|
||||||
} else {
|
} 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 = [];
|
$php_files_to_fix = [];
|
||||||
|
|
||||||
foreach ($changed_files as $file_path) {
|
foreach ($changed_files as $file_path) {
|
||||||
|
|||||||
@@ -15,9 +15,57 @@ use RuntimeException;
|
|||||||
* Performs automatic fixes and enhancements to PHP source files during development:
|
* Performs automatic fixes and enhancements to PHP source files during development:
|
||||||
* - Auto-adds #[Relationship] attributes to model files
|
* - Auto-adds #[Relationship] attributes to model files
|
||||||
* - Auto-updates namespaces based on file location
|
* - 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.
|
* 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
|
class Php_Fixer
|
||||||
{
|
{
|
||||||
@@ -335,6 +383,32 @@ class Php_Fixer
|
|||||||
/**
|
/**
|
||||||
* Fix use statements - remove unnecessary ones, add missing ones
|
* 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 $file_path Relative path from base_path()
|
||||||
* @param string $content Current file content
|
* @param string $content Current file content
|
||||||
* @param array $step_2_manifest_data Manifest state during Phase 2
|
* @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
|
* Replace fully qualified Rsx class names with simple names
|
||||||
*
|
*
|
||||||
* This function finds \Rsx\Namespace\ClassName patterns and replaces them
|
* 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 string $content File content
|
||||||
* @param array $step_2_manifest_data Manifest data
|
* @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
|
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
|
// Build a set of all valid Rsx\ class names in manifest
|
||||||
$class_name_counts = [];
|
$valid_rsx_classes = [];
|
||||||
foreach ($step_2_manifest_data['data']['files'] ?? [] as $manifest_file => $metadata) {
|
foreach ($step_2_manifest_data['data']['files'] ?? [] as $manifest_file => $metadata) {
|
||||||
if (isset($metadata['class']) && !empty($metadata['class'])) {
|
if (isset($metadata['class']) && !empty($metadata['class'])) {
|
||||||
$simple_name = $metadata['class'];
|
// Check if this file is an Rsx\ class (in rsx/ directory)
|
||||||
if (!isset($class_name_counts[$simple_name])) {
|
if (str_starts_with($manifest_file, 'rsx/')) {
|
||||||
$class_name_counts[$simple_name] = 0;
|
$simple_name = $metadata['class'];
|
||||||
|
$valid_rsx_classes[$simple_name] = true;
|
||||||
}
|
}
|
||||||
$class_name_counts[$simple_name]++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,8 +758,18 @@ class Php_Fixer
|
|||||||
$modifications = [];
|
$modifications = [];
|
||||||
|
|
||||||
for ($i = 0; $i < count($tokens); $i++) {
|
for ($i = 0; $i < count($tokens); $i++) {
|
||||||
// Look for namespace separator that starts a fully qualified name
|
$fqcn = null;
|
||||||
if ($tokens[$i] === '\\' || (is_array($tokens[$i]) && $tokens[$i][0] === T_NS_SEPARATOR)) {
|
$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\
|
// Check if this starts \Rsx\
|
||||||
$fqcn_start = $i;
|
$fqcn_start = $i;
|
||||||
$fqcn = '\\';
|
$fqcn = '\\';
|
||||||
@@ -707,40 +794,42 @@ class Php_Fixer
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$fqcn_end = $j;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is an Rsx FQCN (ONLY process \Rsx\ namespaced classes)
|
// Process FQCN if we found one
|
||||||
if (str_starts_with($fqcn, '\\Rsx\\')) {
|
if ($fqcn && str_starts_with($fqcn, '\\Rsx\\')) {
|
||||||
// Extract simple class name (last part after final \)
|
// Extract simple class name (last part after final \)
|
||||||
$parts = explode('\\', trim($fqcn, '\\'));
|
$parts = explode('\\', trim($fqcn, '\\'));
|
||||||
$simple_name = end($parts);
|
$simple_name = end($parts);
|
||||||
|
|
||||||
// Only replace if this class name is UNIQUE (appears only once in manifest)
|
// Replace if this class exists in manifest (even if name is not unique)
|
||||||
if (isset($class_name_counts[$simple_name]) && $class_name_counts[$simple_name] === 1) {
|
// Step 3 will add the correct use statement to disambiguate
|
||||||
// Calculate the byte positions for replacement
|
if (isset($valid_rsx_classes[$simple_name])) {
|
||||||
$start_pos = 0;
|
// Calculate the byte positions for replacement
|
||||||
for ($k = 0; $k < $fqcn_start; $k++) {
|
$start_pos = 0;
|
||||||
if (is_array($tokens[$k])) {
|
for ($k = 0; $k < $fqcn_start; $k++) {
|
||||||
$start_pos += strlen($tokens[$k][1]);
|
if (is_array($tokens[$k])) {
|
||||||
} else {
|
$start_pos += strlen($tokens[$k][1]);
|
||||||
$start_pos += strlen($tokens[$k]);
|
} else {
|
||||||
}
|
$start_pos += strlen($tokens[$k]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$length = 0;
|
|
||||||
for ($k = $fqcn_start; $k < $j; $k++) {
|
|
||||||
if (is_array($tokens[$k])) {
|
|
||||||
$length += strlen($tokens[$k][1]);
|
|
||||||
} else {
|
|
||||||
$length += strlen($tokens[$k]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$modifications[] = [
|
|
||||||
'start' => $start_pos,
|
|
||||||
'length' => $length,
|
|
||||||
'replacement' => $simple_name,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$length = 0;
|
||||||
|
for ($k = $fqcn_start; $k < $fqcn_end; $k++) {
|
||||||
|
if (is_array($tokens[$k])) {
|
||||||
|
$length += strlen($tokens[$k][1]);
|
||||||
|
} else {
|
||||||
|
$length += strlen($tokens[$k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$modifications[] = [
|
||||||
|
'start' => $start_pos,
|
||||||
|
'length' => $length,
|
||||||
|
'replacement' => $simple_name,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,77 @@ INCLUDE TYPES
|
|||||||
External resources:
|
External resources:
|
||||||
'cdn:https://unpkg.com/library.js'
|
'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
|
BUNDLE RENDERING
|
||||||
In Blade layouts/views:
|
In Blade layouts/views:
|
||||||
{!! Dashboard_Bundle::render() !!}
|
{!! Dashboard_Bundle::render() !!}
|
||||||
|
|||||||
@@ -278,12 +278,15 @@ class Frontend_Bundle extends Rsx_Bundle_Abstract
|
|||||||
'rsx/theme/variables.scss', // Order matters
|
'rsx/theme/variables.scss', // Order matters
|
||||||
'rsx/app/frontend', // Directory
|
'rsx/app/frontend', // Directory
|
||||||
'rsx/models', // For JS stubs
|
'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.
|
Auto-compiles on page reload in development.
|
||||||
|
|
||||||
```blade
|
```blade
|
||||||
@@ -517,18 +520,66 @@ User_Model::create(['email' => $email]);
|
|||||||
|
|
||||||
### Enums
|
### 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
|
```php
|
||||||
|
// ✅ CORRECT - Integer keys map to constants
|
||||||
public static $enums = [
|
public static $enums = [
|
||||||
'status_id' => [
|
'status_id' => [
|
||||||
1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active'],
|
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
|
// 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_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
|
### Migrations
|
||||||
|
|
||||||
**Forward-only, no rollbacks.**
|
**Forward-only, no rollbacks.**
|
||||||
@@ -913,9 +964,11 @@ class User_Model extends Rsx_Model_Abstract
|
|||||||
protected $table = 'users';
|
protected $table = 'users';
|
||||||
protected $fillable = []; // Always empty - no mass assignment
|
protected $fillable = []; // Always empty - no mass assignment
|
||||||
|
|
||||||
|
// Enum columns - MUST use integer keys
|
||||||
public static $enums = [
|
public static $enums = [
|
||||||
'status_id' => [
|
'status_id' => [
|
||||||
1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active'],
|
1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active'],
|
||||||
|
2 => ['constant' => 'STATUS_INACTIVE', 'label' => 'Inactive'],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -930,9 +983,12 @@ public function up()
|
|||||||
CREATE TABLE articles (
|
CREATE TABLE articles (
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
title VARCHAR(255) NOT NULL,
|
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,
|
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
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user