Files
rspade_system/app/RSpade/Core/Manifest/Manifest.php
root 37a6183eb4 Fix code quality violations and add VS Code extension features
Fix VS Code extension storage paths for new directory structure
Fix jqhtml compiled files missing from bundle
Fix bundle babel transformation and add rsxrealpath() function

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 00:43:05 +00:00

3957 lines
153 KiB
PHP
Executable File

<?php
namespace App\RSpade\Core\Manifest;
use Exception;
use Illuminate\Support\Facades\File;
use RecursiveCallbackFilterIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
use ReflectionMethod;
use ReflectionNamedType;
use Throwable;
use App\RSpade\CodeQuality\RuntimeChecks\ManifestErrors;
use App\RSpade\Core\ExtensionRegistry;
use App\RSpade\Core\IntegrationRegistry;
use App\RSpade\Core\Kernels\ManifestKernel;
use App\RSpade\Core\Locks\RsxLocks;
use App\RSpade\Core\Manifest\ManifestSupport_Abstract;
/**
* Manifest - RSX File Discovery and Metadata Management System
*
* PURPOSE: Discovers, indexes, and caches metadata about all RSX files for fast lookups
*
* PATH HANDLING:
* - All file paths are stored as RELATIVE paths from base_path()
* - Use base_path($relative) to get absolute paths when needed
* - Laravel's base_path() always returns the project root directory
*
* PROCESSING MODEL - 5-PHASE ARCHITECTURE:
*
* When manifest needs updating (scan() or rebuild()):
* Phase 1: File Discovery - Scan directories and detect changes
* Phase 2: Parse Metadata - Token parsing for PHP/JS structure
* - NO reflection used, NO files loaded
* Phase 3: Load Dependencies - Load PHP classes in dependency order
* - Ensures all classes are available for reflection
* Phase 4: Extract Reflection - PHP reflection data extraction
* - Attributes, methods, parameters, etc.
* Phase 5: Process Modules - Run manifest support modules, build autoloader
* - Model_ManifestSupport extracts database metadata
* Phase 6: Generate Stubs - JavaScript API and model stubs
* Phase 7: Save & Finalize - Write cache, clear views, run quality checks
*
* When loading from valid cache:
* - Only loads cached data, NO file scanning or processing
* - Autoloader handles on-demand class loading
*
* CORE OPERATIONS:
* 1. init() - Ensures manifest is ready (loads cache + scans for updates, or rebuilds if no cache)
* 2. scan() - Incrementally updates manifest for changed files only (called by init())
* 3. rebuild() - Forces complete re-scan of all RSX files
* 4. clear() - Removes all cached manifest data
*
* FILE PROCESSING:
* - Scans /rsx/ directory recursively (excludes vendor, node_modules, etc.)
* - Extracts metadata from PHP files (classes, methods, attributes, namespaces)
* - Parses JavaScript/TypeScript files (imports, exports, classes)
* - Indexes Blade templates and views (sections, extends, view IDs)
* - Uses token parsing for basic info, reflection for detailed metadata
*
* QUERY METHODS:
* - php_find_class() - Locate PHP class by name
* - find_php_fqcn() - Locate PHP class by fully qualified name
* - js_find_class() - Locate JavaScript class
* - find_view() - Locate view template by ID
* - get_extending() - Find all classes extending a parent
* - get_with_attribute() - Find classes/methods with specific attributes
* - get_routes() - Extract all route definitions from attributes
* - get_stats() - Statistical summary of manifest contents
*
* DEBUGGING:
* - Use `php artisan manifest:dump` to view complete manifest contents
* - Supports JSON (default), YAML, and PHP export formats
* - Can filter by path or class name for targeted debugging
*
* DATA STRUCTURE (static::$data):
* [
* 'rsx/path/to/file.php' => [ // Keys are relative to base_path()
* 'file' => 'rsx/path/to/file.php', // Relative to base_path()
* 'hash' => 'sha1_hash_of_file_contents',
* 'mtime' => unix_timestamp,
* 'size' => bytes,
* 'extension' => 'php',
*
* // PHP-specific fields:
* 'namespace' => 'App\\Controllers',
* 'class' => 'UserController',
* 'fqcn' => 'App\\Controllers\\UserController',
* 'extends' => 'BaseController',
* 'implements' => ['Interface1', 'Interface2'],
* 'traits' => ['TraitName'],
* 'attributes' => [
* 'Route' => [['pattern' => '/users', 'methods' => ['GET']]],
* 'Cache' => [['ttl' => 3600]]
* ],
* 'methods' => [
* 'index' => [
* 'name' => 'index',
* 'static' => false,
* 'visibility' => 'public',
* 'attributes' => ['Route' => [['/users', 'GET']]],
* 'parameters' => [
* ['name' => 'request', 'type' => 'Request', 'nullable' => false]
* ]
* ]
* ],
* 'properties' => [
* ['name' => 'prop', 'visibility' => 'private', 'static' => false]
* ],
*
* // JavaScript-specific fields:
* 'imports' => [['from' => 'react', 'imports' => 'React']],
* 'exports' => ['ComponentName'],
* 'default_export' => 'MainComponent',
* 'public_static_methods' => ['getInstance'],
* 'static_properties' => ['instance'],
*
* // View-specific fields:
* 'view_id' => 'user-profile',
* 'sections' => ['content', 'sidebar'],
* 'extends' => 'layouts.main'
* ],
* // ... more files
* ]
*
* CACHING:
* - Stores as PHP array in /storage/rsx-build/manifest_data.php for fast include()
* - Also exports as JSON for JavaScript tooling compatibility
* - Uses file size + mtime for change detection (fast, avoids unnecessary hashing)
*/
class Manifest
{
/**
* Debug options for controlling manifest behavior from commands
* Set by commands like rsx:manifest:build to control processing
*/
public static $_debug_options = [];
/**
* Special directories that are excluded from manifest
* @deprecated Use config('rsx.manifest.excluded_dirs') instead
*/
protected const EXCLUDED_DIRS = ['resource', 'public', 'vendor', 'node_modules', '.git', 'storage'];
/**
* File extensions to process
* @deprecated Use ExtensionRegistry::get_all_extensions() instead
*/
protected const PROCESSABLE_EXTENSIONS = ['php', 'js', 'jsx', 'ts', 'tsx', 'phtml', 'scss', 'less', 'css', 'blade.php'];
/**
* Path to the cache file
*/
protected const CACHE_FILE = '/storage/rsx-build/manifest_data.php';
/**
* The loaded manifest data structure:
* [
* 'generated' => datetime string,
* 'hash' => sha512 hash of data,
* 'data' => ['files' => [...file metadata...]]
* ]
*/
protected static ?array $data = null;
/**
* Whether data has been loaded
*/
protected static bool $_has_init = false;
/**
* Flag to signal manifest needs to restart due to file rename
*/
protected static bool $_needs_manifest_restart = false;
// The manifest kernel instance (cached) (???)
protected static ?ManifestKernel $kernel = null;
protected static $_manifest_compile_lock;
protected static $__get_rsx_files_cache;
protected static bool $__has_changed_cache;
protected static bool $_has_manifest_ready = false;
protected static bool $_manifest_is_bad = false;
// Track if we've already shown the manifest rescan message this page load
protected static bool $__shown_rescan_message = false;
// ========================================
// Query Methods
// ========================================
/**
* Get all manifest data (just the files, not metadata)
*/
public static function get_all(): array
{
static::init();
return static::$data['data']['files'] ?? [];
}
/**
* Get the autoloader class map for simplified class name resolution
* @return array Map of simple class names to arrays of FQCNs
*/
public static function get_autoloader_class_map(): array
{
static::init();
return static::$data['data']['autoloader_class_map'] ?? [];
}
/**
* Get data for a specific file
*/
public static function get_file(string $file_path): array
{
static::init();
// Normalize path to forward slashes
$file_path = str_replace('\\', '/', $file_path);
// Convert to relative path if absolute
$base_path_normalized = str_replace('\\', '/', base_path());
if (str_starts_with($file_path, $base_path_normalized)) {
$file_path = str_replace($base_path_normalized . '/', '', $file_path);
}
// Remove leading slash if present
$file_path = ltrim($file_path, '/');
if (!isset(static::$data['data']['files'][$file_path])) {
throw new \RuntimeException("File not found in manifest: {$file_path}");
}
return static::$data['data']['files'][$file_path];
}
/**
* Find a PHP class by name
*/
public static function php_find_class(string $class_name): string
{
static::init();
if (!isset(static::$data['data']['php_classes'][$class_name])) {
throw new \RuntimeException("PHP class not found in manifest: {$class_name}");
}
return static::$data['data']['php_classes'][$class_name];
}
/**
* Find a PHP class by fully qualified name
*/
public static function find_php_fqcn(string $fqcn): string
{
$files = static::get_all();
foreach ($files as $file => $metadata) {
if (isset($metadata['fqcn']) && $metadata['fqcn'] === $fqcn) {
return $file;
}
}
throw new \RuntimeException("PHP class with FQCN not found in manifest: {$fqcn}");
}
/**
* Get manifest metadata by PHP class name
* This is a convenience method that finds the class and returns its metadata
*/
public static function php_get_metadata_by_class(string $class_name): array
{
$file = static::php_find_class($class_name);
return static::get_file($file);
}
/**
* Get manifest metadata by PHP fully qualified class name
* This is a convenience method that finds the class and returns its metadata
*/
public static function php_get_metadata_by_fqcn(string $fqcn): array
{
$file = static::find_php_fqcn($fqcn);
return static::get_file($file);
}
/**
* Find a JavaScript class
*/
public static function js_find_class(string $class_name): string
{
static::init();
if (!isset(static::$data['data']['js_classes'][$class_name])) {
throw new \RuntimeException("JavaScript class not found in manifest: {$class_name}");
}
return static::$data['data']['js_classes'][$class_name];
}
/**
* Find a view by ID
*/
public static function find_view(string $id): string
{
$files = static::get_all();
$matches = [];
// Find all files with matching ID (only check Blade views)
foreach ($files as $file => $metadata) {
if (isset($metadata['id']) && $metadata['id'] === $id && str_ends_with($file, '.blade.php')) {
$matches[] = $file;
}
}
// Check results
if (count($matches) === 0) {
throw new \RuntimeException("View not found in manifest: {$id}");
}
if (count($matches) > 1) {
$file_list = implode("\n - ", $matches);
throw new \RuntimeException(
"Duplicate view ID detected: {$id}\n" .
"Found in multiple files:\n - {$file_list}\n" .
'View IDs must be unique across all Blade files.'
);
}
return $matches[0];
}
/**
* Find a view by RSX ID (path-agnostic identifier)
*/
public static function find_view_by_rsx_id(string $id): string
{
// This method now properly checks for duplicates
return static::find_view($id);
}
/**
* Get path for a file by its filename only (quick and dirty lookup)
*
* This is a convenience method for finding files when you know the filename is unique.
* Only works for files in the /rsx directory. Fatal errors if:
* - File not found in manifest
* - Multiple files with the same name exist
* - File is outside /rsx directory
*
* @param string $filename Just the filename with extension (e.g., "Counter_Widget.jqhtml")
* @return string The relative path to the file (e.g., "rsx/app/demo/components/Counter_Widget.jqhtml")
* @throws RuntimeException If file not found, multiple matches, or outside /rsx
*/
public static function get_path_by_filename(string $filename): string
{
$files = static::get_all();
$matches = [];
foreach ($files as $path => $metadata) {
// Only consider files in /rsx directory
if (!str_starts_with($path, 'rsx/')) {
continue;
}
// Extract just the filename from the path
$file_basename = basename($path);
if ($file_basename === $filename) {
$matches[] = $path;
}
}
if (empty($matches)) {
throw new \RuntimeException(
"Fatal: File not found in manifest: {$filename}\n" .
'This method only searches files in the /rsx directory.'
);
}
if (count($matches) > 1) {
throw new \RuntimeException(
"Fatal: Multiple files with name '{$filename}' found in manifest:\n" .
' - ' . implode("\n - ", $matches) . "\n" .
'This method requires unique filenames.'
);
}
return $matches[0];
}
/**
* Get all classes extending a parent (filters out abstract classes by default)
* Returns array of class metadata indexed by class name
*/
public static function php_get_extending(string $parentclass): array
{
// Get concrete subclasses only (abstract filtered out by default)
$subclasses = self::php_get_subclasses_of($parentclass, true);
$classpile = [];
foreach ($subclasses as $classname) {
// Get the file path from php_classes index, then get metadata from files
if (isset(static::$data['data']['php_classes'][$classname])) {
$file_path = static::$data['data']['php_classes'][$classname];
$classpile[$classname] = static::$data['data']['files'][$file_path];
}
}
return $classpile;
}
/**
* Get all JavaScript classes extending a parent
* Returns array of class metadata indexed by class name
*/
public static function js_get_extending(string $parentclass): array
{
// Get all subclasses (JavaScript has no abstract concept)
$subclasses = self::js_get_subclasses_of($parentclass);
$classpile = [];
foreach ($subclasses as $classname) {
// Get the file path from js_classes index, then get metadata from files
if (isset(static::$data['data']['js_classes'][$classname])) {
$file_path = static::$data['data']['js_classes'][$classname];
$classpile[$classname] = static::$data['data']['files'][$file_path];
}
}
return $classpile;
}
/**
* Check if a class is a subclass of another by traversing the inheritance chain
*
* @param string $subclass The child class name (simple name, not FQCN)
* @param string $superclass The parent class name to check for (simple name, not FQCN)
* @return bool True if subclass extends superclass (directly or indirectly), false otherwise
*/
public static function php_is_subclass_of(string $subclass, string $superclass): bool
{
// Strip namespace if FQCN was passed (contains backslash)
if (strpos($subclass, '\\') !== false) {
// Get the class name after the last backslash
$parts = explode('\\', $subclass);
$subclass = end($parts);
}
if (strpos($superclass, '\\') !== false) {
// Get the class name after the last backslash
$parts = explode('\\', $superclass);
$superclass = end($parts);
}
$files = static::get_all();
$current_class = $subclass;
$visited = []; // Prevent infinite loops in case of circular inheritance
while ($current_class) {
// Prevent infinite loops
if (in_array($current_class, $visited)) {
return false;
}
$visited[] = $current_class;
// Find the current class in the manifest
if (!isset(static::$data['data']['php_classes'][$current_class])) {
return false;
}
// Get file metadata
$file_path = static::$data['data']['php_classes'][$current_class];
$metadata = static::$data['data']['files'][$file_path];
if (empty($metadata['extends'])) {
return false;
}
if ($metadata['extends'] == $superclass) {
return true;
}
// TODO: Maybe use native reflection if base class does not exist in the manifest past this point>
// Move up the chain to the parent class
$current_class = $metadata['extends'];
}
return false;
}
/**
* Check if a PHP class is abstract
*
* @param string $class_name The class name to check (simple name, not FQCN)
* @return bool True if the class is abstract, false if concrete or not found
*/
public static function php_is_abstract(string $class_name): bool
{
// Ensure manifest is loaded
static::init();
// Strip namespace if FQCN was passed
if (strpos($class_name, '\\') !== false) {
$parts = explode('\\', $class_name);
$class_name = end($parts);
}
// Return false if class not in manifest
if (!isset(static::$data['data']['php_classes'][$class_name])) {
return false;
}
// Get file metadata and check the abstract property
$file_path = static::$data['data']['php_classes'][$class_name];
$metadata = static::$data['data']['files'][$file_path];
return $metadata['abstract'] ?? false;
}
/**
* Get the full inheritance lineage (ancestry) of a PHP class
*
* Returns an array of parent class names from immediate parent to top-level ancestor.
* Example: For class C extends B extends A, returns ['B', 'A']
*
* @param string $class_name The class name (FQCN or simple name)
* @return array Array of parent class simple names in order from immediate parent to root
*/
public static function php_get_lineage(string $class_name): array
{
// Ensure manifest is loaded
static::init();
// Strip namespace if FQCN was passed
if (strpos($class_name, '\\') !== false) {
$parts = explode('\\', $class_name);
$class_name = end($parts);
}
$lineage = [];
$current_class = $class_name;
$visited = []; // Prevent infinite loops
while ($current_class) {
// Prevent infinite loops in circular inheritance
if (in_array($current_class, $visited)) {
break;
}
$visited[] = $current_class;
// Find current class in manifest
if (!isset(static::$data['data']['php_classes'][$current_class])) {
break;
}
$file_path = static::$data['data']['php_classes'][$current_class];
$metadata = static::$data['data']['files'][$file_path];
if (!$metadata) {
break;
}
$extends = $metadata['extends'] ?? null;
if (!$extends) {
break;
}
// Add parent to lineage (simple name)
$lineage[] = $extends;
// Move up the chain
$current_class = $extends;
}
return $lineage;
}
/**
* Check if a class is a subclass of another by traversing the inheritance chain
*
* @param string $subclass The child class name (simple name, not FQCN)
* @param string $superclass The parent class name to check for (simple name, not FQCN)
* @return bool True if subclass extends superclass (directly or indirectly), false otherwise
*/
public static function js_is_subclass_of(string $subclass, string $superclass): bool
{
// Strip namespace if FQCN was passed (contains backslash)
if (strpos($subclass, '\\') !== false) {
// Get the class name after the last backslash
$parts = explode('\\', $subclass);
$subclass = end($parts);
}
if (strpos($superclass, '\\') !== false) {
// Get the class name after the last backslash
$parts = explode('\\', $superclass);
$superclass = end($parts);
}
$files = static::get_all();
$current_class = $subclass;
$visited = []; // Prevent infinite loops in case of circular inheritance
while ($current_class) {
// Prevent infinite loops
if (in_array($current_class, $visited)) {
return false;
}
$visited[] = $current_class;
// Find the current class in the manifest
if (!isset(static::$data['data']['js_classes'][$current_class])) {
return false;
}
// Get file metadata
$file_path = static::$data['data']['js_classes'][$current_class];
$metadata = static::$data['data']['files'][$file_path];
if (empty($metadata['extends'])) {
return false;
}
if ($metadata['extends'] == $superclass) {
return true;
}
// Move up the chain to the parent class
$current_class = $metadata['extends'];
}
return false;
}
/**
* Get the complete inheritance chain for a JavaScript class
* Returns array of parent class names in order from immediate parent to root
*
* @param string $class_name The class name to get lineage for
* @return array Array of parent class names (empty if class not found or has no parents)
* Example: If A extends B and B extends C, js_get_lineage('A') returns ['B', 'C']
*/
public static function js_get_lineage(string $class_name): array
{
// Strip namespace if FQCN was passed
if (strpos($class_name, '\\') !== false) {
$parts = explode('\\', $class_name);
$class_name = end($parts);
}
$lineage = [];
$current_class = $class_name;
$visited = []; // Prevent infinite loops
while ($current_class) {
// Prevent infinite loops in circular inheritance
if (in_array($current_class, $visited)) {
break;
}
$visited[] = $current_class;
// Find the current class in manifest
if (!isset(static::$data['data']['js_classes'][$current_class])) {
break;
}
// Get the file path from js_classes index, then get metadata from files
$file_path = static::$data['data']['js_classes'][$current_class];
$metadata = static::$data['data']['files'][$file_path];
$extends = $metadata['extends'] ?? null;
if (!$extends) {
break;
}
// Add parent to lineage
$lineage[] = $extends;
// Move up the chain
$current_class = $extends;
}
return $lineage;
}
/**
* Get all direct subclasses of a given PHP class using the pre-built index
*
* @param string $class_name The parent class name (simple name, not FQCN)
* @param bool $concrete_only Whether to filter out abstract classes (default: true)
* @return array Array of subclass names, or empty array if class not found or has no children
*/
public static function php_get_subclasses_of(string $class_name, bool $concrete_only = true): array
{
// Strip namespace if FQCN was passed
if (strpos($class_name, '\\') !== false) {
$parts = explode('\\', $class_name);
$class_name = end($parts);
}
// Return empty array if class not in subclass_index
if (!isset(static::$data['data']['php_subclass_index'][$class_name])) {
return [];
}
$subclasses = static::$data['data']['php_subclass_index'][$class_name];
// If not filtering for concrete classes, return all subclasses
if (!$concrete_only) {
return $subclasses;
}
// Filter out abstract classes
$concrete_subclasses = [];
foreach ($subclasses as $subclass) {
// Get file path and metadata
if (!isset(static::$data['data']['php_classes'][$subclass])) {
shouldnt_happen(
"Fatal: PHP class '{$subclass}' found in subclass index but not in php_classes.\n" .
"This indicates a major data integrity issue with the manifest.\n" .
'Try running: php artisan rsx:manifest:build --clean'
);
}
$file_path = static::$data['data']['php_classes'][$subclass];
$metadata = static::$data['data']['files'][$file_path];
// Check if abstract property exists in manifest data
if (!isset($metadata['abstract'])) {
shouldnt_happen(
"Fatal: Abstract property missing for PHP class '{$subclass}' in manifest data.\n" .
"This indicates a major data integrity issue with the manifest.\n" .
'Try running: php artisan rsx:manifest:build --clean'
);
}
// Include only non-abstract classes
if (!$metadata['abstract']) {
$concrete_subclasses[] = $subclass;
}
}
return $concrete_subclasses;
}
/**
* Get all direct subclasses of a given JavaScript class using the pre-built index
*
* @param string $class_name The parent class name
* @return array Array of subclass names, or empty array if class not found or has no children
*/
public static function js_get_subclasses_of(string $class_name): array
{
// Return empty array if class not in subclass_index
if (!isset(static::$data['data']['js_subclass_index'][$class_name])) {
return [];
}
return static::$data['data']['js_subclass_index'][$class_name];
}
/**
* Get all classes with a specific attribute
*/
public static function get_with_attribute(string $attribute_class): array
{
$files = static::get_all();
$results = [];
foreach ($files as $file => $metadata) {
// Check class attributes
if (isset($metadata['attributes'][$attribute_class])) {
$results[] = [
'file' => $file,
'class' => $metadata['class'] ?? null,
'fqcn' => $metadata['fqcn'] ?? null,
'type' => 'class',
'instances' => $metadata['attributes'][$attribute_class],
];
}
// Check public static method attributes (PHP files)
if (isset($metadata['public_static_methods'])) {
foreach ($metadata['public_static_methods'] as $method_name => $method_data) {
if (isset($method_data['attributes'][$attribute_class])) {
$results[] = [
'file' => $file,
'class' => $metadata['class'] ?? null,
'fqcn' => $metadata['fqcn'] ?? null,
'method' => $method_name,
'type' => 'method',
'instances' => $method_data['attributes'][$attribute_class],
];
}
}
}
// Check regular method attributes (JS files may have these)
if (isset($metadata['methods'])) {
foreach ($metadata['methods'] as $method_name => $method_data) {
if (isset($method_data['attributes'][$attribute_class])) {
$results[] = [
'file' => $file,
'class' => $metadata['class'] ?? null,
'fqcn' => $metadata['fqcn'] ?? null,
'method' => $method_name,
'type' => 'method',
'instances' => $method_data['attributes'][$attribute_class],
];
}
}
}
}
// Sort alphabetically by class name to ensure deterministic behavior and prevent race condition bugs
usort($results, function ($a, $b) {
return strcmp($a['class'] ?? '', $b['class'] ?? '');
});
return $results;
}
/**
* Get all routes from the manifest
*/
public static function get_routes(): array
{
static::init();
$routes = [
'controllers' => [],
'api' => [],
];
// Look for Route attributes - must check all namespaces since Route is not a real class
// PHP attributes without an import will use the current namespace
$files = static::get_all();
$route_classes = [];
foreach ($files as $file => $metadata) {
// Check public static method attributes for any attribute ending with 'Route'
if (isset($metadata['public_static_methods'])) {
foreach ($metadata['public_static_methods'] as $method_name => $method_data) {
if (isset($method_data['attributes'])) {
foreach ($method_data['attributes'] as $attr_name => $attr_instances) {
// Check if this is a Route attribute (ends with \Route or is just Route)
if (str_ends_with($attr_name, '\\Route') || $attr_name === 'Route') {
$route_classes[] = [
'file' => $file,
'class' => $metadata['class'] ?? null,
'fqcn' => $metadata['fqcn'] ?? null,
'method' => $method_name,
'type' => 'method',
'instances' => $attr_instances,
];
}
}
}
}
}
}
foreach ($route_classes as $item) {
if ($item['type'] === 'method') {
foreach ($item['instances'] as $route_args) {
$pattern = $route_args[0] ?? ($route_args['pattern'] ?? null);
$methods = $route_args[1] ?? ($route_args['methods'] ?? ['GET']);
$name = $route_args[2] ?? ($route_args['name'] ?? null);
if ($pattern) {
// Ensure pattern starts with /
if ($pattern[0] !== '/') {
$pattern = '/' . $pattern;
}
// Determine type (API or controller)
$type = str_contains($item['file'], '/api/') || str_contains($item['class'] ?? '', 'Api') ? 'api' : 'controllers';
// Initialize route if not exists
if (!isset($routes[$type][$pattern])) {
$routes[$type][$pattern] = [];
}
// Extract Auth attributes for this method from the file metadata
$require_attrs = [];
$file_metadata = $files[$item['file']] ?? null;
if ($file_metadata && isset($file_metadata['public_static_methods'][$item['method']]['attributes']['Auth'])) {
$require_attrs = $file_metadata['public_static_methods'][$item['method']]['attributes']['Auth'];
}
// Add for each HTTP method
foreach ((array) $methods as $method) {
$method_upper = strtoupper($method);
// Initialize method array if not exists
if (!isset($routes[$type][$pattern][$method_upper])) {
$routes[$type][$pattern][$method_upper] = [];
}
// Add handler to array (allows duplicates for dispatch-time detection)
$routes[$type][$pattern][$method_upper][] = [
'class' => $item['fqcn'] ?? $item['class'],
'method' => $item['method'],
'name' => $name,
'file' => $item['file'],
'require' => $require_attrs,
];
}
}
}
}
}
// Sort routes alphabetically by path to ensure deterministic behavior and prevent race condition bugs
ksort($routes['controllers']);
ksort($routes['api']);
return $routes;
}
/**
* Get Auth attributes from a controller's pre_dispatch method
*
* @param string $class_name Simple class name or FQCN
* @return array Array of Auth attribute instances
*/
public static function get_pre_dispatch_requires(string $class_name): array
{
static::init();
try {
// Try to get metadata by class name or FQCN
if (strpos($class_name, '\\') !== false) {
$metadata = static::php_get_metadata_by_fqcn($class_name);
} else {
$metadata = static::php_get_metadata_by_class($class_name);
}
// Check if pre_dispatch method exists and has Auth attributes
if (isset($metadata['public_static_methods']['pre_dispatch']['attributes']['Auth'])) {
return $metadata['public_static_methods']['pre_dispatch']['attributes']['Auth'];
}
return [];
} catch (\RuntimeException $e) {
// Class not found in manifest
return [];
}
}
/**
* Get statistics about the manifest
*/
public static function get_stats(): array
{
static::init();
$files = static::get_all();
$stats = [
'total_files' => count($files),
'php' => 0,
'js' => 0,
'blade' => 0,
'scss' => 0,
'css' => 0,
'other' => 0,
'classes' => 0,
'routes' => 0,
];
foreach ($files as $file => $metadata) {
$ext = $metadata['extension'] ?? '';
switch ($ext) {
case 'php':
$stats['php']++;
break;
case 'blade.php':
$stats['blade']++;
break;
case 'js':
case 'jsx':
case 'ts':
case 'tsx':
$stats['js']++;
break;
case 'scss':
case 'less':
$stats['scss']++;
break;
case 'css':
$stats['css']++;
break;
default:
$stats['other']++;
}
if (isset($metadata['class'])) {
$stats['classes']++;
}
}
$routes = static::get_routes();
$stats['routes'] = count($routes['controllers']) + count($routes['api']);
return $stats;
}
/**
* Check if manifest is built
*/
public static function is_built(): bool
{
return file_exists(static::__get_cache_file_path());
}
/**
* Get files from manifest by directory path
*
* @param string $directory Directory path without wildcards (e.g., 'rsx/ui')
* @return array Array of manifest entries for files in the directory
*/
public static function get_files_by_dir(string $directory): array
{
static::init();
$files = [];
// Normalize directory to forward slashes
$directory = str_replace('\\', '/', $directory);
$directory = rtrim($directory, '/'); // Remove trailing slash if present
// Check if we have cached data
if (empty(static::$data['data']['files'])) {
return $files;
}
// Iterate through all files in the manifest
foreach (static::$data['data']['files'] as $file_path => $file_data) {
// Normalize the file path to use forward slashes
$normalized_path = str_replace('\\', '/', $file_path);
// Check if the file is in the specified directory
if (str_starts_with($normalized_path, $directory . '/')) {
$files[$file_path] = $file_data;
}
}
// Sort alphabetically by filename to ensure deterministic behavior and prevent race condition bugs
ksort($files);
return $files;
}
/**
* Get the full manifest structure including metadata
* Used for debugging and manifest:dump command
* Returns by reference to avoid copying large array
*/
public static function &get_full_manifest(): array
{
static::init();
return static::$data;
}
/**
* TODO: UPDATE DOCUMENTATION
* Initialize and ensure manifest is loaded
* Handles all loading/rebuilding logic internally:
* - If already initialized, returns immediately
* - If cache exists, loads it and updates only changed files
* - If no cache exists, processes all files (equivalent to rebuild)
*/
public static function init(): void
{
// Acquire application lock before any manifest operations
// This ensures proper concurrency control for all processes
// Already initialized, nothing to do
if (static::$_has_init) {
return;
}
static::$_has_init = true;
// Gets application concurrency lock, does other not manifest init steps
// Todo: review the process - should this happen here? Maybe this is all better suited in rspade provider?
\App\RSpade\Core\Bootstrap\RsxBootstrap::initialize();
// Scan handles both incremental updates and full rebuilds
// Load cached data if it exists
$cache_file_path = self::__get_cache_file_path();
console_debug('MANIFEST', 'Checking for manifest cache', $cache_file_path);
$loaded_cache = self::__load_cached_data();
if ($loaded_cache && env('APP_ENV') == 'production') {
// In prod mode, if cache loaded, assume the cache is good, we are done with it
console_debug('MANIFEST', 'Manifest cache loaded successfully (production mode)');
self::$_has_manifest_ready = true;
\App\RSpade\Core\Autoloader::register();
return;
}
if ($loaded_cache) {
console_debug('MANIFEST', 'Manifest cache loaded successfully (development mode), validating...');
if (self::__validate_cached_data()) {
console_debug('MANIFEST', 'Manifest is valid');
self::$_has_manifest_ready = true;
\App\RSpade\Core\Autoloader::register();
return;
}
console_debug('MANIFEST', 'Manifest is out of date');
} else {
console_debug('MANIFEST', 'Manifest could not be loaded');
}
console_debug('MANIFEST', 'Aquiring manifest build lock');
// Get a manifest build lock
self::$_manifest_compile_lock = RsxLocks::get_lock(
RsxLocks::SERVER_LOCK,
RsxLocks::LOCK_MANIFEST_BUILD,
RsxLocks::WRITE_LOCK,
config('rsx.locking.timeout', 30)
);
console_debug('MANIFEST', 'Manifest build lock acquired, checking to see if manifest cache was updated');
// Maybe the manifest was regenerated again? double check now that we are exclusive
$cache_loaded = self::__load_cached_data();
$cache_valid = self::__validate_cached_data();
if (!$cache_valid) {
// Log only for full rebuilds (cache doesn't exist)
// Incremental updates are normal and don't need logging
$cache_file = self::__get_cache_file_path();
if (!$cache_loaded) {
console_debug_force('MANIFEST', 'Manifest cache does not exist, performing full rebuild', $cache_file);
}
// zug zug
self::_refresh_manifest();
// jobs done
console_debug('MANIFEST', 'Refreshing manifest *completed*');
// Verify cache was written successfully
if (file_exists($cache_file)) {
$file_size = filesize($cache_file);
$file_perms = substr(sprintf('%o', fileperms($cache_file)), -4);
console_debug('MANIFEST', 'Cache file written successfully', [
'path' => $cache_file,
'size' => $file_size,
'permissions' => $file_perms
]);
} else {
console_debug_force('MANIFEST', 'WARNING: Cache file does not exist after rebuild!', $cache_file);
}
} else {
console_debug('MANIFEST', 'Manifest cache is valid, no rebuild needed');
}
RsxLocks::release_lock(self::$_manifest_compile_lock);
console_debug('MANIFEST', 'Released manifest build lock');
self::$_has_manifest_ready = true;
\App\RSpade\Core\Autoloader::register();
}
/**
* Clear the manifest cache
*/
public static function clear(): void
{
static::$data = [
'generated' => date('Y-m-d H:i:s'),
'hash' => '',
'data' => [
'files' => [],
'autoloader_class_map' => [],
],
];
static::$_has_init = false;
static::$_has_manifest_ready = false;
$cache_file = static::__get_cache_file_path();
if (file_exists($cache_file)) {
unlink($cache_file);
}
}
/**
* Unlink the manifest cache file only (fast rebuild trigger)
*
* This removes only the manifest cache file, preserving all parsed AST data
* and incremental caches. On next load, the manifest will do a full scan and
* reindex but will reuse existing parsed metadata where files haven't changed.
*
* This is much faster than rsx:clean which wipes all caches including parsed
* AST data, forcing expensive re-parsing of all PHP/JS files.
*
* Use this after database migrations or schema changes that affect model
* metadata without changing the actual source code.
*/
public static function _unlink_cache(): void
{
$cache_file = static::__get_cache_file_path();
if (file_exists($cache_file)) {
@unlink($cache_file);
}
}
/**
* Signal that manifest needs to restart due to file rename
* Called by code quality rules when auto-renaming files
*/
public static function flag_needs_restart(): void
{
static::$_needs_manifest_restart = true;
}
/**
* Normalize class name to simple name (strip namespace qualifiers)
*
* Since RSX enforces unique simple class names across the codebase,
* we normalize all class references to simple names for consistent
* comparison and storage. FQCNs are only needed at actual class loading time.
*
* Examples:
* \Rsx\Lib\DataGrid → DataGrid
* Rsx\Lib\DataGrid → DataGrid
* DataGrid → DataGrid
*
* @param string $class_name Class name in any format (with or without namespace)
* @return string Simple class name without namespace
*/
public static function _normalize_class_name(string $class_name): string
{
// Strip leading backslash
$class_name = ltrim($class_name, '\\');
// Extract just the class name (last part after final backslash)
$parts = explode('\\', $class_name);
return end($parts);
}
/**
* Get the build key for cache prefixing
* This is the manifest hash that uniquely identifies the current code state
*
* IMPORTANT: This will throw a fatal error if called before the manifest is loaded
* The manifest must have completed loading (either from cache or scan) before this is available
*
* @return string The manifest build key (hash)
* @throws RuntimeException if manifest is not yet loaded
*/
public static function get_build_key(): string
{
// Check if manifest has been loaded
if (!static::$_has_manifest_ready) {
shouldnt_happen('Manifest::get_build_key() called before manifest was loaded. The manifest must complete loading before the build key is available.');
}
// Also verify we actually have a hash
if (empty(static::$data['hash'])) {
shouldnt_happen('Manifest is loaded but has no build hash. This should not happen.');
}
return static::$data['hash'];
}
// ------------------------------------------------------------------------
// ---- RSpade Public Internal Methods:
// ------------------------------------------------------------------------
public static function _refresh_manifest()
{
manifest_start:
// Reset manifest restart flag at the beginning of each pass
static::$_needs_manifest_restart = false;
// Reset manifest structure, retaining only existing files data
$existing_files = static::$data['data']['files'] ?? [];
static::$data = [
'generated' => date('Y-m-d H:i:s'),
'hash' => '',
'data' => [
'files' => $existing_files,
'autoloader_class_map' => [],
],
];
// =======================================================
// Phase 1: Collect all files in manifest scan directories
// =======================================================
$files = static::__get_rsx_files();
$changes = false;
// Check if any files have changed
$files_to_process = [];
foreach ($files as $file) {
if (static::__has_changed($file)) {
$files_to_process[] = $file;
$changes = true;
}
}
console_debug('MANIFEST', 'Phase 1: File Discovery - ' . count($files) . ' files, ' . count($files_to_process) . ' changed');
// If any files have changed and we're not in production, run auto-reformat
if ($changes && env('APP_ENV') !== 'production') {
$formatter_path = base_path('bin/rsx-format');
// Lets rethink this before we enable iut again
// if (file_exists($formatter_path)) {
// // Run the formatter with the hidden --auto-reformat-periodic flag
// // This ensures formatting happens BEFORE any RSX file is loaded/parsed
// $command = escapeshellcmd($formatter_path) . ' --auto-reformat-periodic 2>&1';
// \exec_safe($command, $output, $return_code);
// // Only log errors, not normal operation
// if ($return_code !== 0) {
// error_log('RSX auto-reformat-periodic failed: ' . implode("\n", $output));
// }
// }
}
// =======================================================
// Phase 2: Parse Metadata - Extract basic metadata via token parsing
// =======================================================
console_debug('MANIFEST', 'Phase 2: Parse Metadata - Processing ' . count($files_to_process) . ' files');
// Filter out storage files from the manifest
static::$data['data']['files'] = array_filter(
static::$data['data']['files'],
function ($key) {
return !str_starts_with($key, 'storage/');
},
ARRAY_FILTER_USE_KEY
);
// Remove deleted files from manifest BEFORE processing
$existing_files = array_flip($files);
foreach (array_keys(static::$data['data']['files']) as $cached_file) {
if (!isset($existing_files[$cached_file])) {
unset(static::$data['data']['files'][$cached_file]);
$changes = true;
}
}
foreach ($files_to_process as $file) {
static::$data['data']['files'][$file] = static::__process_file($file);
}
// Skip validation message if no changes detected
if (!$changes && file_exists(static::__get_cache_file_path())) {
// This case shouldn't happen as it should have been caught earlier
// but we don't need to log it as it's not an error condition
}
// 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_modified_files = [];
if (!app()->environment('production')) {
$php_fixer_modified_files = static::__run_php_fixer($files_to_process);
// Re-parse files that Php_Fixer modified to update manifest with corrected metadata
// This ensures namespace/class/fqcn data matches what's actually in the file
// CRITICAL: Without this, we'd have stale FQCNs from before Php_Fixer ran
if (!empty($php_fixer_modified_files)) {
console_debug('MANIFEST', 'Re-parsing ' . count($php_fixer_modified_files) . ' files modified by Php_Fixer');
foreach ($php_fixer_modified_files as $file_path) {
// Re-extract metadata with corrected namespace
$absolute_path = base_path($file_path);
$php_metadata = \App\RSpade\Core\PHP\Php_Parser::parse($absolute_path, static::$data);
// Update manifest with corrected metadata
static::$data['data']['files'][$file_path] = array_merge(
static::$data['data']['files'][$file_path],
$php_metadata
);
// Recalculate file hash since file was modified
clearstatcache(true, $absolute_path);
$updated_stat = stat($absolute_path);
static::$data['data']['files'][$file_path]['hash'] = sha1_file($absolute_path);
static::$data['data']['files'][$file_path]['mtime'] = $updated_stat['mtime'];
static::$data['data']['files'][$file_path]['size'] = $updated_stat['size'];
}
}
}
// Phase 2 complete. At this point we have a list of all files, and for php and js, their class data
// =======================================================
// Phase 3: Load Dependencies - Load PHP files in dependency order
// =======================================================
console_debug('MANIFEST', 'Phase 3: Load Dependencies');
// Only load PHP files that have actually changed
static::__load_changed_php_files($files_to_process);
// Process code quality rule metadata extraction
static::__process_code_quality_metadata($files_to_process);
// =======================================================
// Phase 4: Extract Reflection - Extract reflection data from PHP classes
// =======================================================
console_debug('MANIFEST', 'Phase 4: Extract Reflection');
// Extract reflection data for changed PHP files only
static::__extract_reflection_for_changed_files($files_to_process);
// Collate files by classes - MUST be called after reflection extraction
// so that abstract property is available for subclass filtering
static::__collate_files_by_classes();
// =======================================================
// Phase 5: Process Modules - Run manifest support modules and build autoloader
// =======================================================
console_debug('MANIFEST', 'Phase 5: Process Modules');
// Build autoloader class map
// The major thing that happens here is this also scans app/RSpade for additional classes which arent on manifest,
// which is a somewhat expensive operation (50 ms). This is acceptable for a incremental manifest rebuild
static::$data['data']['autoloader_class_map'] = static::__build_autoloader_class_map();
// Process manifest support modules
$support_modules = config('rsx.manifest_support', []);
foreach ($support_modules as $support_module_class) {
if (!class_exists($support_module_class)) {
throw new \RuntimeException("Manifest support module class not found: {$support_module_class}");
}
if (!self::php_is_subclass_of($support_module_class, ManifestSupport_Abstract::class)) {
throw new \RuntimeException("Manifest support module must extend ManifestSupport_Abstract: {$support_module_class}");
}
if ($support_module_class::should_run()) {
$support_module_class::process(static::$data);
}
}
// Note: Validation checks have been moved to code quality rules that run at manifest-time
// =======================================================
// Phase 6: Generate Stubs - Generate JavaScript API and model stubs
// =======================================================
console_debug('MANIFEST', 'Phase 6: Generate Stubs');
// Call generate_manifest_stubs on all registered integrations
foreach (IntegrationRegistry::get_all() as $integration_class) {
if (method_exists($integration_class, 'generate_manifest_stubs')) {
$integration_class::generate_manifest_stubs(static::$data);
}
}
// =======================================================
// Phase 7: Save & Finalize - Save manifest, clear caches, run checks
// =======================================================
$php_class_count = count(static::$data['data']['php_classes'] ?? []);
$js_class_count = count(static::$data['data']['js_classes'] ?? []);
console_debug('MANIFEST', 'Phase 7: Saving manifest (' . count($files) . " files, {$php_class_count} PHP classes, {$js_class_count} JS classes)");
static::__generate_vscode_stubs();
static::__save();
// Clear view cache when manifest changes to prevent stale @rsx_extends references
// This ensures that renamed blade files with @rsx_extends are properly recompiled
\Illuminate\Support\Facades\Artisan::call('view:clear', [], new \Symfony\Component\Console\Output\NullOutput());
// Run manifest-time code quality checks (development only)
if (env('APP_ENV') !== 'production' && $changes) {
static::__run_manifest_time_code_quality_checks();
}
// Check if a file was auto-renamed and manifest needs to restart
if (static::$_needs_manifest_restart) {
console_debug('MANIFEST', 'File auto-renamed during code quality check, restarting manifest build');
goto manifest_start;
}
}
/**
* Load a class and all its parent classes from manifest data
*
* This utility method ensures a class and its entire parent hierarchy
* are loaded before doing reflection or other operations.
* Classes are loaded in dependency order (parents first).
* Used by stub generators and reflection extraction.
*
* @param string $fqcn Fully qualified class name to load
* @param array $manifest_data The manifest data array
* @return void
* @throws \RuntimeException if class or parent cannot be loaded
*/
public static function _load_class_hierarchy(string $fqcn, array $manifest_data): void
{
// Already loaded? Nothing to do
if (class_exists($fqcn, false) || interface_exists($fqcn, false) || trait_exists($fqcn, false)) {
return;
}
// Build list of classes to load in hierarchy order
$hierarchy = [];
$current_fqcn = $fqcn;
while ($current_fqcn) {
// Find this class in the manifest
$found = false;
foreach ($manifest_data['data']['files'] as $file_path => $metadata) {
if (isset($metadata['fqcn']) && $metadata['fqcn'] === $current_fqcn) {
// Add to front of hierarchy (parents first)
array_unshift($hierarchy, [
'fqcn' => $current_fqcn,
'file' => $file_path,
'extends' => $metadata['extends'] ?? null,
]);
$found = true;
// Move to parent class
// extends is always stored as simple class name (normalized by parser)
if (isset($metadata['extends'])) {
$parent_simple_name = static::_normalize_class_name($metadata['extends']);
// Look for this class by simple name in manifest
// Only check PHP files (those with fqcn key)
$parent_fqcn = null;
foreach ($manifest_data['data']['files'] as $parent_file => $parent_meta) {
if (isset($parent_meta['class']) && $parent_meta['class'] === $parent_simple_name && isset($parent_meta['fqcn'])) {
$parent_fqcn = $parent_meta['fqcn'];
break;
}
}
$current_fqcn = $parent_fqcn;
} else {
$current_fqcn = null;
}
break;
}
}
if (!$found && $current_fqcn) {
// Check if it's a built-in or framework class that can be autoloaded
// Try to autoload it
if (class_exists($current_fqcn, true) ||
interface_exists($current_fqcn, true) ||
trait_exists($current_fqcn, true)) {
// Framework or built-in class, stop here
break;
}
// If still not found, it's a fatal error
shouldnt_happen("Parent class {$current_fqcn} not found in manifest or autoloader for {$fqcn}");
}
}
// Load classes in order (parents first)
foreach ($hierarchy as $class_info) {
if (!class_exists($class_info['fqcn'], false) &&
!interface_exists($class_info['fqcn'], false) &&
!trait_exists($class_info['fqcn'], false)) {
$full_path = base_path($class_info['file']);
if (!file_exists($full_path)) {
shouldnt_happen("Class file not found: {$full_path} for {$class_info['fqcn']}");
}
// This includes the file.
// A side effect of this include is this line also lints the file. Past this point, we can assume all php
// files (well, class files) have valid syntax.
include_once $full_path;
// Verify the class loaded successfully
if (!class_exists($class_info['fqcn'], false) &&
!interface_exists($class_info['fqcn'], false) &&
!trait_exists($class_info['fqcn'], false)) {
shouldnt_happen("Failed to load class {$class_info['fqcn']} from {$full_path}");
}
}
}
}
/**
* Mark manifest as bad to force rebuild on next load
*/
public static function _set_manifest_is_bad(): void
{
static::$_manifest_is_bad = true;
static::__save();
}
// ------------------------------------------------------------------------
// ---- Private / Protected Methods:
// ------------------------------------------------------------------------
/**
* Generate JavaScript stub files for PHP controllers with Internal API methods
* These stubs enable IDE autocomplete for Ajax.call() invocations from JavaScript
*/
protected static function _generate_js_api_stubs(): void
{
$stub_dir = storage_path('rsx-build/js-stubs');
// Create directory if it doesn't exist
if (!is_dir($stub_dir)) {
mkdir($stub_dir, 0755, true);
}
// Track generated stub files for cleanup
$generated_stubs = [];
// Process each file
foreach (static::$data['data']['files'] as $file_path => &$metadata) {
// Skip non-PHP files
if (!isset($metadata['extension']) || $metadata['extension'] !== 'php') {
continue;
}
// Skip files without classes or public static methods
if (!isset($metadata['class']) || !isset($metadata['public_static_methods'])) {
continue;
}
// Check if this is a controller (extends Rsx_Controller_Abstract)
$class_name = $metadata['class'] ?? '';
if (!static::php_is_subclass_of($class_name, 'Rsx_Controller_Abstract')) {
continue;
}
// Check if this controller has any Ajax_Endpoint methods
$api_methods = [];
foreach ($metadata['public_static_methods'] as $method_name => $method_info) {
if (!isset($method_info['attributes'])) {
continue;
}
// Check for Ajax_Endpoint attribute
$has_api_internal = false;
foreach ($method_info['attributes'] as $attr_name => $attr_data) {
if ($attr_name === 'Ajax_Endpoint' ||
basename(str_replace('\\', '/', $attr_name)) === 'Ajax_Endpoint') {
$has_api_internal = true;
break;
}
}
if ($has_api_internal && isset($method_info['static']) && $method_info['static']) {
$api_methods[$method_name] = [
'name' => $method_name,
];
}
}
// If no API methods, remove js_stub property if it exists and skip
if (empty($api_methods)) {
if (isset($metadata['js_stub'])) {
unset($metadata['js_stub']);
}
continue;
}
// Generate stub filename and paths
$controller_name = $metadata['class'];
$stub_filename = static::_sanitize_stub_filename($controller_name) . '.js';
$stub_relative_path = 'storage/rsx-build/js-stubs/' . $stub_filename;
$stub_full_path = base_path($stub_relative_path);
// Check if stub needs regeneration
$needs_regeneration = true;
if (file_exists($stub_full_path)) {
// Get mtime of source PHP file
$source_mtime = $metadata['mtime'] ?? 0;
$stub_mtime = filemtime($stub_full_path);
// Only regenerate if source is newer than stub
if ($stub_mtime >= $source_mtime) {
// Also check if the API methods signature has changed
// by comparing a hash of the methods
$api_methods_hash = md5(json_encode($api_methods));
$old_api_hash = $metadata['api_methods_hash'] ?? '';
if ($api_methods_hash === $old_api_hash) {
$needs_regeneration = false;
}
}
}
// Store the API methods hash for future comparisons
$metadata['api_methods_hash'] = md5(json_encode($api_methods));
if ($needs_regeneration) {
// Generate stub content
$stub_content = static::_generate_stub_content($controller_name, $api_methods);
// Write stub file
file_put_contents($stub_full_path, $stub_content);
}
$generated_stubs[] = $stub_filename;
// Add js_stub property to manifest data (relative path)
$metadata['js_stub'] = $stub_relative_path;
// Add the stub file itself to the manifest
// This is critical because storage/rsx-build/js-stubs is not in scan directories
$stat = stat($stub_full_path);
static::$data['data']['files'][$stub_relative_path] = [
'file' => $stub_relative_path,
'hash' => sha1_file($stub_full_path),
'mtime' => $stat['mtime'],
'size' => $stat['size'],
'extension' => 'js',
'class' => $controller_name,
'is_stub' => true, // Mark this as a generated stub
'source_controller' => $file_path, // Reference to the source controller
];
}
// Clean up orphaned stub files (both from disk and manifest)
$existing_stubs = glob($stub_dir . '/*.js');
foreach ($existing_stubs as $existing_stub) {
$filename = basename($existing_stub);
if (!in_array($filename, $generated_stubs)) {
// Remove from disk
unlink($existing_stub);
// Remove from manifest
$stub_relative_path = 'storage/rsx-build/js-stubs/' . $filename;
if (isset(static::$data['data']['files'][$stub_relative_path])) {
unset(static::$data['data']['files'][$stub_relative_path]);
}
}
}
}
/**
* Generate JavaScript stub files for ORM models
* These stubs enable IDE autocomplete and provide relationship methods
*/
protected static function _generate_js_model_stubs(): void
{
$stub_dir = storage_path('rsx-build/js-model-stubs');
// Create directory if it doesn't exist
if (!is_dir($stub_dir)) {
mkdir($stub_dir, 0755, true);
}
// Track generated stub files for cleanup
$generated_stubs = [];
// Get all models from the manifest
$model_entries = static::get_extending('Rsx_Model_Abstract');
foreach ($model_entries as $model_entry) {
if (!isset($model_entry['fqcn'])) {
continue;
}
$fqcn = $model_entry['fqcn'];
$class_name = $model_entry['class'] ?? '';
// Skip if it extends Rsx_System_Model_Abstract
if (static::php_is_subclass_of($fqcn, 'Rsx_System_Model_Abstract')) {
continue;
}
// Model class MUST exist if it's in the manifest as a model
// If it doesn't exist, this indicates a serious problem with the manifest
if (!class_exists($fqcn)) {
shouldnt_happen("Model class {$fqcn} from manifest cannot be loaded. This should not happen - models extending Rsx_Model_Abstract must be loadable. File: " . ($model_entry['file'] ?? 'unknown'));
}
// Check if this is an abstract class
$reflection = new ReflectionClass($fqcn);
if ($reflection->isAbstract()) {
continue;
}
// Get model metadata from manifest
$file_path = $model_entry['file'] ?? '';
$metadata = isset(static::$data['data']['files'][$file_path]) ? static::$data['data']['files'][$file_path] : [];
// Generate stub filename and paths
$stub_filename = static::_sanitize_model_stub_filename($class_name) . '.js';
// Check if user has created their own JS class
$user_class_exists = static::_check_user_model_class_exists($class_name);
// Use Base_ prefix if user class exists
$stub_class_name = $user_class_exists ? 'Base_' . $class_name : $class_name;
$stub_filename = static::_sanitize_model_stub_filename($stub_class_name) . '.js';
$stub_relative_path = 'storage/rsx-build/js-model-stubs/' . $stub_filename;
$stub_full_path = base_path($stub_relative_path);
// Check if stub needs regeneration
$needs_regeneration = true;
if (file_exists($stub_full_path)) {
// Get mtime of source PHP file
$source_mtime = $metadata['mtime'] ?? 0;
$stub_mtime = filemtime($stub_full_path);
// Only regenerate if source is newer than stub
if ($stub_mtime >= $source_mtime) {
// Also check if the model metadata has changed
// by comparing a hash of enums, relationships, and columns
$model_metadata = [];
// Get relationships
$model_metadata['rel'] = $fqcn::get_relationships();
// Get enums
if (property_exists($fqcn, 'enums')) {
$model_metadata['enums'] = $fqcn::$enums ?? [];
}
// Get columns from models metadata if available
if (isset(static::$data['data']['models'][$class_name]['columns'])) {
$model_metadata['columns'] = static::$data['data']['models'][$class_name]['columns'];
}
$model_metadata_hash = md5(json_encode($model_metadata));
$old_metadata_hash = $metadata['model_metadata_hash'] ?? '';
if ($model_metadata_hash === $old_metadata_hash) {
$needs_regeneration = false;
}
// Store the hash for future comparisons
static::$data['data']['files'][$file_path]['model_metadata_hash'] = $model_metadata_hash;
}
}
if ($needs_regeneration) {
// Generate stub content
$stub_content = static::_generate_model_stub_content($fqcn, $class_name, $stub_class_name);
// Write stub file
file_put_contents($stub_full_path, $stub_content);
// Store the metadata hash for future comparisons if not already done
if (!isset(static::$data['data']['files'][$file_path]['model_metadata_hash'])) {
$model_metadata = [];
// Get relationships
$model_metadata['rel'] = $fqcn::get_relationships();
// Get enums
if (property_exists($fqcn, 'enums')) {
$model_metadata['enums'] = $fqcn::$enums ?? [];
}
// Get columns from models metadata if available
if (isset(static::$data['data']['models'][$class_name]['columns'])) {
$model_metadata['columns'] = static::$data['data']['models'][$class_name]['columns'];
}
static::$data['data']['files'][$file_path]['model_metadata_hash'] = md5(json_encode($model_metadata));
}
}
$generated_stubs[] = $stub_filename;
// Add js_stub property to manifest data
$metadata['js_stub'] = $stub_relative_path;
// Write the updated metadata back to the manifest
static::$data['data']['files'][$file_path]['js_stub'] = $stub_relative_path;
// Add the stub file itself to the manifest
$stat = stat($stub_full_path);
static::$data['data']['files'][$stub_relative_path] = [
'file' => $stub_relative_path,
'hash' => sha1_file($stub_full_path),
'mtime' => $stat['mtime'],
'size' => $stat['size'],
'extension' => 'js',
'class' => $stub_class_name,
'is_model_stub' => true, // Mark this as a generated model stub
'source_model' => $file_path, // Reference to the source model
];
}
// Clean up orphaned stub files
$existing_stubs = glob($stub_dir . '/*.js');
foreach ($existing_stubs as $existing_stub) {
$filename = basename($existing_stub);
if (!in_array($filename, $generated_stubs)) {
// Remove from disk
unlink($existing_stub);
// Remove from manifest
$stub_relative_path = 'storage/rsx-build/js-model-stubs/' . $filename;
if (isset(static::$data['data']['files'][$stub_relative_path])) {
unset(static::$data['data']['files'][$stub_relative_path]);
}
}
}
}
/**
* Check if user has created a JavaScript model class
*/
private static function _check_user_model_class_exists(string $model_name): bool
{
// Check if there's a JS file with this class name in the manifest
foreach (static::$data['data']['files'] as $file_path => $metadata) {
if (isset($metadata['extension']) && $metadata['extension'] === 'js') {
if (isset($metadata['class']) && $metadata['class'] === $model_name) {
// Don't consider our own stubs
if (!isset($metadata['is_stub']) && !isset($metadata['is_model_stub'])) {
return true;
}
}
}
}
return false;
}
/**
* Sanitize model name for use as a filename
*/
private static function _sanitize_model_stub_filename(string $model_name): string
{
// Replace underscores with hyphens and lowercase
// e.g., User_Model becomes user-model
return strtolower(str_replace('_', '-', $model_name));
}
/**
* Generate JavaScript stub content for a model
*/
private static function _generate_model_stub_content(string $fqcn, string $class_name, string $stub_class_name): string
{
// Get model instance to introspect
$model = new $fqcn();
// Get relationships
$relationships = $fqcn::get_relationships();
// Get enums
$enums = $fqcn::$enums ?? [];
// Get table name
$table = $model->getTable();
// Get columns from models metadata if available
$columns = [];
if (isset(static::$data['data']['models'][$class_name]['columns'])) {
$columns = static::$data['data']['models'][$class_name]['columns'];
}
// Start building the stub content
$content = "/**\n";
$content .= " * Auto-generated JavaScript stub for {$class_name}\n";
$content .= ' * Generated by RSX Manifest at ' . date('Y-m-d H:i:s') . "\n";
$content .= " * DO NOT EDIT - This file is automatically regenerated\n";
$content .= " */\n\n";
$content .= "class {$stub_class_name} extends Rsx_Js_Model {\n";
// Add static table property
$content .= " static table = '{$table}';\n\n";
// Add static model name for API calls
$content .= " static get name() {\n";
$content .= " return '{$class_name}';\n";
$content .= " }\n\n";
// Generate enum constants and methods
foreach ($enums as $column => $enum_values) {
// Generate constants
foreach ($enum_values as $value => $props) {
if (!empty($props['constant'])) {
$content .= " static {$props['constant']} = {$value};\n";
}
}
if (!empty($enum_values)) {
$content .= "\n";
}
// Generate enum value getter
$content .= " static {$column}_enum_val() {\n";
$content .= ' return ' . json_encode($enum_values, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
// Fix indentation in JSON output
$content = preg_replace('/\n/', "\n ", $content);
$content = rtrim($content) . ";\n";
$content .= " }\n\n";
// Generate enum label list
$content .= " static {$column}_label_list() {\n";
$content .= " const values = {};\n";
foreach ($enum_values as $value => $props) {
if (isset($props['label'])) {
$label = addslashes($props['label']);
$content .= " values[{$value}] = '{$label}';\n";
}
}
$content .= " return values;\n";
$content .= " }\n\n";
// Generate enum select method (for dropdowns)
$content .= " static {$column}_enum_select() {\n";
$content .= " const values = this.{$column}_enum_val();\n";
$content .= " const result = {};\n";
$content .= " // Sort by order property if present\n";
$content .= " const sorted = Object.entries(values)\n";
$content .= " .sort(([,a], [,b]) => (a.order || 0) - (b.order || 0));\n";
$content .= " for (const [key, val] of sorted) {\n";
$content .= " // Skip if selectable is false\n";
$content .= " if (val.selectable !== false) {\n";
$content .= " result[key] = val.label || key;\n";
$content .= " }\n";
$content .= " }\n";
$content .= " return result;\n";
$content .= " }\n\n";
}
// Generate relationship methods
foreach ($relationships as $relationship) {
$content .= " /**\n";
$content .= " * Fetch {$relationship} relationship\n";
$content .= " * @returns {Promise} Related model instance(s) or false\n";
$content .= " */\n";
$content .= " async {$relationship}() {\n";
$content .= " if (!this.id) {\n";
$content .= " shouldnt_happen('Cannot fetch relationship without id property');\n";
$content .= " }\n\n";
$content .= " const response = await $.ajax({\n";
$content .= " url: `/_fetch_rel/{$class_name}/\${this.id}/{$relationship}`,\n";
$content .= " method: 'POST',\n";
$content .= " dataType: 'json'\n";
$content .= " });\n\n";
$content .= " // Handle response based on type\n";
$content .= " if (response === false || response === null) {\n";
$content .= " return response;\n";
$content .= " }\n\n";
$content .= " // __MODEL SYSTEM: Enables automatic ORM instantiation when fetching from PHP models.\n";
$content .= " // PHP models add \"__MODEL\": \"ClassName\" to JSON, JavaScript uses it to create proper instances.\n";
$content .= " // This provides typed model objects instead of plain JSON, with methods and type checking.\n\n";
$content .= " // Use the recursive processor to scan response and instantiate any models found.\n";
$content .= " // This generated stub method delegates to the base class processor.\n";
$content .= " return Rsx_Js_Model._instantiate_models_recursive(response);\n";
$content .= " }\n\n";
}
$content .= "}\n\n";
// Export the class globally
$content .= "// Make the class globally available\n";
$content .= "window.{$stub_class_name} = {$stub_class_name};\n";
// If using Base_ prefix, also create alias without prefix for convenience
if ($stub_class_name !== $class_name) {
$content .= "\n// Create alias without Base_ prefix if user class doesn't exist\n";
$content .= "if (typeof window.{$class_name} === 'undefined') {\n";
$content .= " window.{$class_name} = {$stub_class_name};\n";
$content .= "}\n";
}
return $content;
}
/**
* Sanitize controller name for use as a filename
*/
private static function _sanitize_stub_filename(string $controller_name): string
{
// Replace underscores with hyphens and lowercase
// e.g., User_Controller becomes user-controller
return strtolower(str_replace('_', '-', $controller_name));
}
/**
* Generate JavaScript stub content for a controller
*/
private static function _generate_stub_content(string $controller_name, array $api_methods): string
{
$content = "/**\n";
$content .= " * Auto-generated JavaScript stub for {$controller_name}\n";
$content .= ' * Generated by RSX Manifest at ' . date('Y-m-d H:i:s') . "\n";
$content .= " * DO NOT EDIT - This file is automatically regenerated\n";
$content .= " */\n\n";
$content .= "class {$controller_name} {\n";
foreach ($api_methods as $method_name => $method_info) {
$content .= " /**\n";
$content .= " * Call {$controller_name}::{$method_name} via Ajax.call()\n";
$content .= " * @param {...*} args - Arguments to pass to the method\n";
$content .= " * @returns {Promise<*>}\n";
$content .= " */\n";
$content .= " static async {$method_name}(...args) {\n";
$content .= " return Ajax.call('{$controller_name}', '{$method_name}', args);\n";
$content .= " }\n\n";
}
$content .= "}\n";
return $content;
}
/**
* Get or create the kernel instance
*/
protected static function __get_kernel(): ManifestKernel
{
if (static::$kernel === null) {
static::$kernel = app(ManifestKernel::class);
}
return static::$kernel;
}
/**
* Get the full cache file path
*/
protected static function __get_cache_file_path(): string
{
return base_path() . static::CACHE_FILE;
}
// move to lower soon
private static function __validate_cached_data()
{
// If cache exists, check if anything changed
$files = static::__get_rsx_files();
$any_changes = false;
// Check for changed files
foreach ($files as $file) {
if (static::__has_changed($file)) {
return false;
}
}
// Check for deleted files
if (!$any_changes) {
$existing_files = array_flip($files);
foreach (array_keys(static::$data['data']['files']) as $cached_file) {
// Skip storage files - they're not part of the manifest
if (str_starts_with($cached_file, 'storage/')) {
continue;
}
if (!isset($existing_files[$cached_file])) {
// Only show the message once per page load
if (!self::$__shown_rescan_message) {
console_debug('MANIFEST', '* Deleted file ' . $cached_file . ' is triggering manifest rescan *');
self::$__shown_rescan_message = true;
}
return false;
}
}
}
return true;
}
/**
* Check for duplicate base class names within the same file type
* Throws a fatal error if any base class name appears in multiple files of the same type
*/
protected static function __check_unique_base_class_names(): void
{
// Group classes by extension first, then by class name
$classes_by_extension = [];
foreach (static::$data['data']['files'] as $file => $metadata) {
if (isset($metadata['class']) && !empty($metadata['class'])) {
$base_class_name = $metadata['class'];
$extension = $metadata['extension'] ?? '';
// Group JavaScript-like files together
if (in_array($extension, ['js', 'jsx', 'ts', 'tsx'])) {
$extension = 'js';
}
if (!isset($classes_by_extension[$extension])) {
$classes_by_extension[$extension] = [];
}
if (!isset($classes_by_extension[$extension][$base_class_name])) {
$classes_by_extension[$extension][$base_class_name] = [];
}
$classes_by_extension[$extension][$base_class_name][] = $file;
}
}
// Check for duplicates within each extension type
foreach ($classes_by_extension as $extension => $base_class_files) {
foreach ($base_class_files as $class_name => $files) {
if (count($files) > 1) {
$file_type = $extension === 'php' ? 'PHP' : ($extension === 'js' ? 'JavaScript' : $extension);
throw new \RuntimeException(
"Fatal: Duplicate {$file_type} class name '{$class_name}' found in multiple files:\n" .
' - ' . implode("\n - ", $files) . "\n" .
'Each class name must be unique within the same file type.'
);
}
}
}
}
/**
* Collate files by class names and build inheritance indices
*
* This method creates two types of indices for both JavaScript and PHP classes:
*
* 1. Class indices (js_classes, php_classes):
* Maps class names to their file metadata for O(1) lookups by class name
*
* 2. Subclass indices (js_subclass_index, php_subclass_index):
* Maps each parent class name to an array of its direct subclasses
* Example: Rsx_Controller_Abstract => ['Demo_Index_Controller', 'Backend_Controller', ...]
*
* The subclass indices enable efficient inheritance checking without iterating
* through the entire manifest. Instead of O(n) complexity for finding subclasses,
* we get O(1) direct subclass lookups.
*
* @return void
*/
protected static function __collate_files_by_classes()
{
foreach (['js', 'php'] as $ext) {
static::$data['data'][$ext . '_classes'] = [];
// Step 1: Index files by class name for quick lookups
// This creates a map of className => filename (not full metadata to save space)
foreach (static::$data['data']['files'] as $file => $filedata) {
if ($filedata['extension'] == $ext && !empty($filedata['class'])) {
static::$data['data'][$ext . '_classes'][$filedata['class']] = $file;
}
}
// Step 2: Build parent chain index for each class
// This traverses up the inheritance tree and collects all parent classes
static::$data['data'][$ext . '_subclass_index'] = [];
foreach (static::$data['data'][$ext . '_classes'] as $class => $file_path) {
// Get metadata from files array
$classdata = static::$data['data']['files'][$file_path];
// Walk up the parent chain until we reach the root
do {
$extends = !empty($classdata['extends']) ? $classdata['extends'] : null;
if (!empty($extends)) {
// Initialize the parent chain array if needed
if (empty(static::$data['data'][$ext . '_subclass_index'][$extends])) {
static::$data['data'][$ext . '_subclass_index'][$extends] = [];
}
// Add this parent to the chain
static::$data['data'][$ext . '_subclass_index'][$extends][] = $class;
// Move up to the parent's metadata (if it exists in manifest)
if (!empty(static::$data['data'][$ext . '_classes'][$extends])) {
$parent_file = static::$data['data'][$ext . '_classes'][$extends];
$classdata = static::$data['data']['files'][$parent_file];
} else {
// Parent not in manifest (e.g., Laravel framework class), stop here
$classdata = null;
}
} else {
// No parent, we've reached the root
$classdata = null;
}
} while (!empty($classdata));
}
}
}
/**
* Load changed PHP files and their dependencies
*
* This method loads only the PHP files that have changed and ensures
* their parent class hierarchies are loaded first.
*
* @param array $changed_files Array of changed file paths
* @return void
*/
protected static function __load_changed_php_files(array $changed_files): void
{
// Filter to only PHP files
$trait_files = [];
$class_files = [];
foreach ($changed_files as $file) {
if (isset(static::$data['data']['files'][$file]['extension']) &&
static::$data['data']['files'][$file]['extension'] === 'php' &&
isset(static::$data['data']['files'][$file]['fqcn'])) {
// Separate traits from classes - traits must be loaded first
if (isset(static::$data['data']['files'][$file]['is_trait']) &&
static::$data['data']['files'][$file]['is_trait']) {
$trait_files[] = $file;
} else {
$class_files[] = $file;
}
}
}
// Load traits first (they have no dependencies and are used by classes)
foreach ($trait_files as $file) {
$fqcn = static::$data['data']['files'][$file]['fqcn'];
static::_load_class_hierarchy($fqcn, static::$data);
}
// Then load classes with their hierarchies
foreach ($class_files as $file) {
$fqcn = static::$data['data']['files'][$file]['fqcn'];
static::_load_class_hierarchy($fqcn, static::$data);
}
}
/**
* Extract reflection data only for changed files
* Uses caching to avoid re-extracting unchanged files
*/
protected static function __extract_reflection_for_changed_files(array $changed_files): void
{
$cache_dir = storage_path('rsx-tmp/persistent/php_reflection');
// Ensure cache directory exists
if (!is_dir($cache_dir)) {
mkdir($cache_dir, 0755, true);
}
// Build a set of changed files for quick lookup
$changed_files_set = array_flip($changed_files);
// Process ALL PHP files to restore cached data or extract new reflection
foreach (static::$data['data']['files'] as $file => &$metadata) {
// Skip non-PHP files
if (!isset($metadata['extension']) || $metadata['extension'] !== 'php') {
continue;
}
// Skip files without classes
if (!isset($metadata['fqcn'])) {
continue;
}
$fqcn = $metadata['fqcn'];
$cache_key = $metadata['hash'];
$cache_file = $cache_dir . '/' . $cache_key . '.json';
// Get absolute path - rsx/ files are in project root, not system/
if (str_starts_with($file, 'rsx/')) {
$absolute_path = rsxrealpath(base_path('../' . $file));
} else {
$absolute_path = rsxrealpath(base_path($file));
}
// Check if this file changed
$file_changed = isset($changed_files_set[$file]);
// Try to use cached data if file hasn't changed
if (!$file_changed && file_exists($cache_file)) {
$cache_mtime = filemtime($cache_file);
$source_mtime = filemtime($absolute_path);
// If cache is newer than source, use cached data
if ($cache_mtime >= $source_mtime) {
$cached_data = json_decode(file_get_contents($cache_file), true);
if ($cached_data !== null) {
// Merge cached reflection data into manifest without breaking the reference
foreach ($cached_data as $key => $value) {
$metadata[$key] = $value;
}
continue;
}
}
}
// Need fresh reflection data - ensure class and its hierarchy are loaded
static::_load_class_hierarchy($fqcn, static::$data);
// Extract reflection data (path already normalized with realpath above)
static::__extract_reflection_data($absolute_path, $fqcn, $metadata);
// Cache the reflection data
$reflection_data = [];
// Add abstract property for correct subclass filtering
if (isset($metadata['abstract'])) {
$reflection_data['abstract'] = $metadata['abstract'];
}
if (isset($metadata['attributes'])) {
$reflection_data['attributes'] = $metadata['attributes'];
}
if (isset($metadata['public_static_methods'])) {
$reflection_data['public_static_methods'] = $metadata['public_static_methods'];
}
if (isset($metadata['public_instance_methods'])) {
$reflection_data['public_instance_methods'] = $metadata['public_instance_methods'];
}
if (isset($metadata['properties'])) {
$reflection_data['properties'] = $metadata['properties'];
}
if (isset($metadata['implements'])) {
$reflection_data['implements'] = $metadata['implements'];
}
if (isset($metadata['traits'])) {
$reflection_data['traits'] = $metadata['traits'];
}
if (!empty($reflection_data)) {
file_put_contents($cache_file, json_encode($reflection_data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
}
}
}
/**
* Load all PHP files in dependency order
* This ensures base classes are loaded before their subclasses
* Must be called after Phase 2 (basic metadata extraction) completes
* @deprecated Use __load_changed_php_files() for incremental builds
*/
/**
* Run Php_Fixer on all PHP files in rsx/ and app/RSpade/
* Called before Phase 2 parsing to ensure all files are fixed
*
* @param array $changed_files List of changed files from Phase 1
* @return array List of files that were modified by Php_Fixer
*/
protected static function __run_php_fixer(array $changed_files): array
{
$modified_files = [];
// Build hash array of all PHP classes to detect structural changes
$class_structure_hash_data = [];
foreach (static::$data['data']['files'] as $file_path => $metadata) {
// Only process PHP files with classes
if (!isset($metadata['extension']) || $metadata['extension'] !== 'php') {
continue;
}
if (!isset($metadata['class'])) {
continue;
}
// Build hash entry: filename => ClassName:ParentClass
$class_name = $metadata['class'];
$extends = $metadata['extends'] ?? '';
$class_structure_hash_data[$file_path] = $class_name . ':' . $extends;
}
// Calculate hash of class structure
$new_class_structure_hash = sha1(json_encode($class_structure_hash_data));
// Check if class structure has changed
$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/
$php_files_to_fix = [];
foreach (static::$data['data']['files'] as $file_path => $metadata) {
// Only process PHP files
if (!isset($metadata['extension']) || $metadata['extension'] !== 'php') {
continue;
}
// Only process files in rsx/ or app/RSpade/
if (!str_starts_with($file_path, 'rsx/') && !str_starts_with($file_path, 'app/RSpade/')) {
continue;
}
$php_files_to_fix[] = $file_path;
}
// Run Php_Fixer on all files and collect modified ones
foreach ($php_files_to_fix as $file_path) {
if (\App\RSpade\Core\PHP\Php_Fixer::fix($file_path, static::$data)) {
$modified_files[] = $file_path;
}
}
// Store updated hash
static::$data['data']['php_fixer_hash'] = $new_class_structure_hash;
} else {
// Class structure unchanged - only fix changed PHP files with classes
$php_files_to_fix = [];
foreach ($changed_files as $file_path) {
// Check if this is a PHP file with a class in the manifest
if (!isset(static::$data['data']['files'][$file_path])) {
continue;
}
$metadata = static::$data['data']['files'][$file_path];
// Only process PHP files with classes
if (!isset($metadata['extension']) || $metadata['extension'] !== 'php') {
continue;
}
if (!isset($metadata['class'])) {
continue;
}
$php_files_to_fix[] = $file_path;
}
// Run Php_Fixer on changed files only and collect modified ones
foreach ($php_files_to_fix as $file_path) {
if (\App\RSpade\Core\PHP\Php_Fixer::fix($file_path, static::$data)) {
$modified_files[] = $file_path;
}
}
}
return $modified_files;
}
protected static function __load_php_files_in_dependency_order(): void
{
if (!isset(static::$data['data']['files'])) {
throw new \RuntimeException(
'Fatal: Manifest::load_php_files_in_dependency_order() called but manifest data structure is not initialized. ' .
"This shouldn't happen - Phase 2 should have populated the files array."
);
}
$files = static::$data['data']['files'];
$loaded = [];
$to_load = [];
// Build list of files with class information
foreach ($files as $file_path => $metadata) {
// Check for PHP files by extension
if (!isset($metadata['extension']) || $metadata['extension'] !== 'php') {
continue;
}
if (!isset($metadata['class'])) {
// No class defined, include it immediately
$full_path = base_path($file_path);
if (file_exists($full_path)) {
include_once $full_path;
}
continue;
}
$to_load[$file_path] = [
'class' => $metadata['class'] ?? null,
'extends' => $metadata['extends'] ?? null,
'namespace' => $metadata['namespace'] ?? null,
'fqcn' => $metadata['fqcn'] ?? null,
];
}
// Load files in dependency order
$max_iterations = count($to_load) + 10; // Prevent infinite loop
$iteration = 0;
while (!empty($to_load) && $iteration < $max_iterations) {
$iteration++;
$loaded_this_round = false;
foreach ($to_load as $file_path => $info) {
// Check if dependencies are met
$can_load = true;
if (!empty($info['extends'])) {
// Check if parent class is already loaded
$parent_loaded = false;
// Check if it's a Laravel/PHP built-in class
if (class_exists($info['extends'], false) || interface_exists($info['extends'], false)) {
$parent_loaded = true;
} else {
// Check if parent is in our loaded list
foreach ($loaded as $loaded_info) {
if ($loaded_info['class'] === $info['extends']) {
$parent_loaded = true;
break;
}
}
}
if (!$parent_loaded) {
$can_load = false;
}
}
if ($can_load) {
// Load the file
$full_path = base_path($file_path);
if (file_exists($full_path)) {
include_once $full_path;
}
// Mark as loaded
$loaded[$file_path] = $info;
unset($to_load[$file_path]);
$loaded_this_round = true;
}
}
// If nothing was loaded this round, we might have circular dependencies
// Load remaining files anyway
if (!$loaded_this_round && !empty($to_load)) {
foreach ($to_load as $file_path => $info) {
$full_path = base_path($file_path);
if (file_exists($full_path)) {
include_once $full_path;
}
}
break;
}
}
}
// /**
// * Extract reflection data for all PHP files
// * Must be called after Phase 3 (dependency loading) completes
// */
// protected static function __extract_all_reflection_data(): void
// {
// if (!isset(static::$data['data']['files'])) {
// throw new \RuntimeException(
// 'Fatal: Manifest::extract_all_reflection_data() called but manifest data structure is not initialized. ' .
// "This shouldn't happen - Phase 2 should have populated the files array."
// );
// }
// foreach (static::$data['data']['files'] as $file_path => &$metadata) {
// // Only process PHP files with classes
// if (!isset($metadata['extension']) || $metadata['extension'] !== 'php') {
// continue;
// }
// if (!isset($metadata['fqcn'])) {
// continue;
// }
// var_dump($file_path);
// // Extract reflection data (class should already be loaded)
// static::__extract_reflection_data(base_path($file_path), $metadata['fqcn'], $metadata);
// }
// }
/**
* Build the autoloader class map for simplified class name resolution
* Maps simple class names to their fully qualified class names
* @return array Map of simple names to arrays of FQCNs
*/
protected static function __build_autoloader_class_map(): array
{
$class_map = [];
// First, collect classes from the manifest files
foreach (static::$data['data']['files'] as $file_path => $file_data) {
if (isset($file_data['class']) && isset($file_data['namespace'])) {
$simple_name = $file_data['class'];
$fqcn = $file_data['namespace'] . '\\' . $simple_name;
if (!isset($class_map[$simple_name])) {
$class_map[$simple_name] = [];
}
$class_map[$simple_name][] = $fqcn;
}
}
// Second, scan app/RSpade directory for additional classes
$rspade_path = base_path('app/RSpade');
if (is_dir($rspade_path)) {
$rspade_classes = static::__scan_directory_for_classes($rspade_path);
foreach ($rspade_classes as $simple_name => $fqcns) {
foreach ($fqcns as $fqcn) {
if (!isset($class_map[$simple_name])) {
$class_map[$simple_name] = [];
}
// Only add if not already present
if (!in_array($fqcn, $class_map[$simple_name])) {
$class_map[$simple_name][] = $fqcn;
}
}
}
}
return $class_map;
}
/**
* Scan a directory for PHP classes, excluding vendor directories
* @param string $directory The directory to scan
* @return array Map of simple class names to FQCNs
*/
protected static function __scan_directory_for_classes(string $directory): array
{
$classes = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
// Skip non-PHP files
if ($file->getExtension() !== 'php') {
continue;
}
// Skip vendor directories
$path = $file->getPathname();
if (strpos($path, '/vendor/') !== false) {
continue;
}
// Extract class information using token parsing
$content = file_get_contents($path);
$tokens = token_get_all($content);
$namespace = '';
$class_name = '';
$getting_namespace = false;
$getting_class = false;
foreach ($tokens as $i => $token) {
if (is_array($token)) {
if ($token[0] === T_NAMESPACE) {
$getting_namespace = true;
$namespace = '';
} elseif ($token[0] === T_CLASS || $token[0] === T_INTERFACE || $token[0] === T_TRAIT) {
// Make sure this isn't an anonymous class
$next_token_idx = $i + 1;
while ($next_token_idx < count($tokens) && is_array($tokens[$next_token_idx]) && $tokens[$next_token_idx][0] === T_WHITESPACE) {
$next_token_idx++;
}
if ($next_token_idx < count($tokens) && is_array($tokens[$next_token_idx]) && $tokens[$next_token_idx][0] === T_STRING) {
$getting_class = true;
}
} elseif ($getting_namespace && ($token[0] === T_NAME_QUALIFIED || $token[0] === T_STRING || $token[0] === T_NS_SEPARATOR)) {
$namespace .= $token[1];
} elseif ($getting_class && $token[0] === T_STRING) {
$class_name = $token[1];
$getting_class = false;
// We have both namespace and class name, add to map
if ($class_name) {
$fqcn = $namespace ? $namespace . '\\' . $class_name : $class_name;
if (!isset($classes[$class_name])) {
$classes[$class_name] = [];
}
$classes[$class_name][] = $fqcn;
}
}
} else {
// Non-array token (like ; or {)
if ($token === ';' || $token === '{') {
$getting_namespace = false;
}
}
}
}
return $classes;
}
/**
* Process a single file and extract comprehensive metadata
* @param string $file_path Relative path to file
*/
protected static function __process_file(string $file_path): array
{
$absolute_path = base_path($file_path);
$stat = stat($absolute_path);
$extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
// Handle .blade.php as special case
if (str_ends_with($file_path, '.blade.php')) {
$extension = 'blade.php';
}
$data = [
'file' => $file_path, // Store relative path
'hash' => sha1_file($absolute_path),
'mtime' => $stat['mtime'],
'size' => $stat['size'],
'extension' => $extension,
];
// Use kernel for basic processing (kernel expects absolute path)
$kernel = static::__get_kernel();
$kernel_data = $kernel->process($absolute_path, $data);
$data = array_merge($data, $kernel_data);
// Add advanced extraction based on file type (pass absolute path)
switch ($extension) {
case 'php':
$php_metadata = \App\RSpade\Core\PHP\Php_Parser::parse($absolute_path, static::$data);
$data = array_merge($data, $php_metadata);
// Php_Parser::parse() may have modified the file via Php_Fixer in development mode
// Recalculate file stats to reflect any changes made during parsing
if (!app()->environment('production')) {
clearstatcache(true, $absolute_path);
$updated_stat = stat($absolute_path);
$data['hash'] = sha1_file($absolute_path);
$data['mtime'] = $updated_stat['mtime'];
$data['size'] = $updated_stat['size'];
}
break;
case 'js':
$js_metadata = \App\RSpade\Core\JavaScript\Js_Parser::extract_metadata($absolute_path);
$data = array_merge($data, $js_metadata);
break;
case 'blade.php':
case 'phtml':
static::__extract_view_info($absolute_path, $data);
break;
default:
// Check if this extension has a registered handler
if (ExtensionRegistry::process_file($extension, $absolute_path, $data)) {
// Handler processed the file
break;
}
}
return $data;
}
/**
* Extract public static methods and their attributes using PHP reflection
* Note: This is called in Phase 4 after all PHP files have been loaded
*/
protected static function __extract_reflection_data(string $file_path, string $full_class_name, array &$data): void
{
// Skip method indexing for traits - methods will be indexed when we process classes that use them
if (isset($data['is_trait']) && $data['is_trait']) {
return;
}
// Class MUST exist if it's in the manifest
// Fail loud if it doesn't - this indicates a serious problem
if (!class_exists($full_class_name)) {
shouldnt_happen("Class {$full_class_name} from manifest cannot be loaded. File: {$file_path}");
}
$reflection = new ReflectionClass($full_class_name);
// Extract whether the class is abstract
if ($reflection->isAbstract()) {
$data['abstract'] = true;
} else {
$data['abstract'] = false;
}
// Extract parent class FQCN if it exists
$parent_class = $reflection->getParentClass();
if ($parent_class !== false) {
$data['extends_fqcn'] = $parent_class->getName();
}
// Extract class attributes (using simple names, not FQCNs)
$class_attributes = [];
foreach ($reflection->getAttributes() as $attribute) {
$attr_full_name = $attribute->getName();
// Extract just the simple class name (e.g., "Route" from "App\RSpade\Core\Attributes\Route")
$attr_simple_name = substr(strrchr($attr_full_name, '\\'), 1) ?: $attr_full_name;
$attr_args = $attribute->getArguments();
if (!isset($class_attributes[$attr_simple_name])) {
$class_attributes[$attr_simple_name] = [];
}
$class_attributes[$attr_simple_name][] = $attr_args;
}
if (!empty($class_attributes)) {
$data['attributes'] = $class_attributes;
}
// Get list of trait files used by this class
$trait_files = [];
foreach ($reflection->getTraits() as $trait) {
$trait_files[] = $trait->getFileName();
}
// Extract ONLY public static methods - the only methods we care about in RSX
$public_static_methods = [];
// Normalize file_path for comparison (without resolving symlinks)
$normalized_file_path = rsxrealpath($file_path);
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC) as $method) {
// Include methods from:
// 1. This file (the class itself)
// 2. Traits used by this class (checked via file path)
$method_file = $method->getFileName();
$is_from_trait = in_array($method_file, $trait_files);
// Skip inherited methods from parent classes (but include trait methods)
// Use rsxrealpath for comparison to avoid symlink resolution
if (rsxrealpath($method_file) !== $normalized_file_path && !$is_from_trait) {
continue;
}
$method_data = [
'name' => $method->getName(),
'static' => true, // Always true since we filtered for static
'visibility' => 'public', // Always public since we filtered for public
'line' => $method->getStartLine(),
];
// For trait methods, store the file path (only if different from class file)
// This enables IDE helpers to locate trait methods correctly
if ($is_from_trait) {
$method_data['file'] = $method_file;
}
// Check if method calls parent::methodname()
// Use the correct file for trait methods
$source_file_to_read = $is_from_trait ? $method_file : $file_path;
try {
$method_source = file($source_file_to_read);
$start_line = $method->getStartLine() - 1;
$end_line = $method->getEndLine() - 1;
if ($method_source && $start_line >= 0 && $end_line < count($method_source)) {
$method_body = implode('', array_slice($method_source, $start_line, $end_line - $start_line + 1));
// Look for parent::methodName() pattern
$method_name = $method->getName();
if (preg_match('/parent\s*::\s*' . preg_quote($method_name, '/') . '\s*\(/i', $method_body)) {
$method_data['__calls_parent'] = true;
}
}
} catch (Exception $e) {
// Silently skip if we can't read the source
}
// Extract method attributes (using simple names, not FQCNs)
$method_attributes = [];
foreach ($method->getAttributes() as $attribute) {
$attr_full_name = $attribute->getName();
// Extract just the simple class name (e.g., "Route" from "App\RSpade\Core\Attributes\Route")
$attr_simple_name = substr(strrchr($attr_full_name, '\\'), 1) ?: $attr_full_name;
$attr_args = $attribute->getArguments();
if (!isset($method_attributes[$attr_simple_name])) {
$method_attributes[$attr_simple_name] = [];
}
$method_attributes[$attr_simple_name][] = $attr_args;
}
if (!empty($method_attributes)) {
$method_data['attributes'] = $method_attributes;
}
// Extract parameters with types
$parameters = [];
foreach ($method->getParameters() as $param) {
$param_data = [
'name' => $param->getName(),
'optional' => $param->isOptional(),
];
// Get type if available
$type = $param->getType();
if ($type !== null) {
$param_data['type'] = $type instanceof ReflectionNamedType ? $type->getName() : (string) $type;
$param_data['nullable'] = $type->allowsNull();
}
// Get default value if available
if ($param->isDefaultValueAvailable()) {
$param_data['default'] = $param->getDefaultValue();
}
$parameters[] = $param_data;
}
if (!empty($parameters)) {
$method_data['parameters'] = $parameters;
}
$public_static_methods[$method->getName()] = $method_data;
}
if (!empty($public_static_methods)) {
$data['public_static_methods'] = $public_static_methods;
}
// Extract public instance methods for RequireParentCall and other rules
$public_instance_methods = [];
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
// Skip inherited methods unless from same file
if ($method->getFileName() !== $file_path) {
continue;
}
// Skip static methods - we only want instance methods
if ($method->isStatic()) {
continue;
}
$method_data = [
'name' => $method->getName(),
'static' => false, // Always false for instance methods
'visibility' => 'public', // Always public since we filtered for public
'line' => $method->getStartLine(),
];
// Check if method calls parent::methodname()
try {
$method_source = file($file_path);
$start_line = $method->getStartLine() - 1;
$end_line = $method->getEndLine() - 1;
if ($method_source && $start_line >= 0 && $end_line < count($method_source)) {
$method_body = implode('', array_slice($method_source, $start_line, $end_line - $start_line + 1));
// Look for parent::methodName() pattern
$method_name = $method->getName();
if (preg_match('/parent\s*::\s*' . preg_quote($method_name, '/') . '\s*\(/i', $method_body)) {
$method_data['__calls_parent'] = true;
}
}
} catch (Exception $e) {
// Silently skip if we can't read the source
}
// Extract method attributes (using simple names, not FQCNs)
$method_attributes = [];
foreach ($method->getAttributes() as $attribute) {
$attr_full_name = $attribute->getName();
// Extract just the simple class name (e.g., "Route" from "App\RSpade\Core\Attributes\Route")
$attr_simple_name = substr(strrchr($attr_full_name, '\\'), 1) ?: $attr_full_name;
$attr_args = $attribute->getArguments();
if (!isset($method_attributes[$attr_simple_name])) {
$method_attributes[$attr_simple_name] = [];
}
$method_attributes[$attr_simple_name][] = $attr_args;
}
if (!empty($method_attributes)) {
$method_data['attributes'] = $method_attributes;
}
// Extract parameters with types
$parameters = [];
foreach ($method->getParameters() as $param) {
$param_data = [
'name' => $param->getName(),
'optional' => $param->isOptional(),
];
// Get type if available
$type = $param->getType();
if ($type !== null) {
$param_data['type'] = $type instanceof ReflectionNamedType ? $type->getName() : (string) $type;
$param_data['nullable'] = $type->allowsNull();
}
// Get default value if available
if ($param->isDefaultValueAvailable()) {
$param_data['default'] = $param->getDefaultValue();
}
$parameters[] = $param_data;
}
if (!empty($parameters)) {
$method_data['parameters'] = $parameters;
}
$public_instance_methods[$method->getName()] = $method_data;
}
// Always set public_instance_methods, even if empty, for consistency
$data['public_instance_methods'] = $public_instance_methods;
// Extract interfaces
$interfaces = $reflection->getInterfaceNames();
if (!empty($interfaces)) {
$data['implements'] = $interfaces;
}
// Extract traits
$traits = $reflection->getTraitNames();
if (!empty($traits)) {
$data['traits'] = $traits;
}
// Extract properties
$properties = [];
foreach ($reflection->getProperties() as $property) {
if ($property->getDeclaringClass()->getName() === $full_class_name) {
$properties[] = [
'name' => $property->getName(),
'visibility' => $property->isPublic() ? 'public' : ($property->isProtected() ? 'protected' : 'private'),
'static' => $property->isStatic(),
];
}
}
if (!empty($properties)) {
$data['properties'] = $properties;
}
}
/**
/**
* Extract view information from template files
*/
protected static function __extract_view_info(string $file_path, array &$data): void
{
$content = file_get_contents($file_path);
// Look for @id comment or directive
if (preg_match('/(?:<!--\s*@id\s+(\w+)\s*-->|@id\([\'"](\w+)[\'"]\))/', $content, $matches)) {
$data['id'] = $matches[1] ?: $matches[2];
}
// Extract blade sections
if (str_ends_with($file_path, '.blade.php')) {
$sections = [];
if (preg_match_all('/@section\([\'"](\w+)[\'"]\)/', $content, $matches)) {
$sections = $matches[1];
}
if (!empty($sections)) {
$data['sections'] = $sections;
}
// Extract extends
if (preg_match('/@extends\([\'"]([^\'"\)]+)[\'"]\)/', $content, $matches)) {
$data['extends'] = $matches[1];
}
}
}
/**
* Check if a file has changed (expects relative path)
*/
protected static function __has_changed(string $file): bool
{
if (isset(self::$__has_changed_cache[$file])) {
return self::$__has_changed_cache[$file];
}
if (!isset(static::$data['data']['files'][$file])) {
// Only show the message once per page load
if (!self::$__shown_rescan_message) {
console_debug('MANIFEST', '* New file ' . $file . ' is triggering manifest rescan *');
self::$__shown_rescan_message = true;
}
$__has_changed_cache[$file] = true;
return true;
}
$old = static::$data['data']['files'][$file];
$absolute_path = base_path($file);
// Make sure file exists
if (!file_exists($absolute_path)) {
// Only show the message once per page load
if (!self::$__shown_rescan_message) {
console_debug('MANIFEST', '* File ' . $file . ' appears to be deleted in ' . $absolute_path . ', triggering manifest rescan *');
self::$__shown_rescan_message = true;
}
$__has_changed_cache[$file] = true;
return true;
}
$current_size = filesize($absolute_path);
// Stage 1: Size check
if ($old['size'] != $current_size) {
// Only show the message once per page load
if (!self::$__shown_rescan_message) {
console_debug('MANIFEST', '* File ' . $file . ' has changed size, triggering manifest rescan *');
self::$__shown_rescan_message = true;
}
$__has_changed_cache[$file] = true;
return true;
}
// Stage 2: mtime check
$current_mtime = filemtime($absolute_path);
if ($old['mtime'] != $current_mtime) {
// Only show the message once per page load
if (!self::$__shown_rescan_message) {
console_debug('MANIFEST', '* File ' . $file . ' has changed mtime, triggering manifest rescan *');
self::$__shown_rescan_message = true;
}
$__has_changed_cache[$file] = true;
return true;
}
$__has_changed_cache[$file] = false;
return false;
}
/**
* Get all files in configured scan directories (returns relative paths)
*/
protected static function __get_rsx_files(): array
{
if (!empty(self::$__get_rsx_files_cache)) {
return self::$__get_rsx_files_cache;
}
$base_path = base_path();
$scan_paths = config('rsx.manifest.scan_directories', ['rsx']);
$files = [];
foreach ($scan_paths as $scan_path) {
$full_path = base_path($scan_path);
// Check if path exists - throw fatal error if not (except for app/RSpade/temp)
if (!file_exists($full_path)) {
// Special case: silently skip app/RSpade/temp if it doesn't exist
if ($scan_path === 'app/RSpade/temp') {
continue;
}
throw new \RuntimeException(
"Manifest scan path does not exist: '{$scan_path}'\n" .
"Please ensure all paths in config('rsx.manifest.scan_directories') exist.\n" .
'Current configuration includes: ' . implode(', ', $scan_paths)
);
}
// If it's a file, add it directly
if (is_file($full_path)) {
// Convert to relative path and normalize to forward slashes
$relative_path = str_replace($base_path . '/', '', $full_path);
$relative_path = str_replace('\\', '/', $relative_path);
// Remove leading slash if present
$relative_path = ltrim($relative_path, '/');
$files[] = $relative_path;
continue;
}
// If it's a directory, recursively scan it
if (is_dir($full_path)) {
$iterator = new RecursiveIteratorIterator(
new RecursiveCallbackFilterIterator(
new RecursiveDirectoryIterator($full_path, RecursiveDirectoryIterator::SKIP_DOTS),
function ($file, $key, $iterator) {
$basename = $file->getBasename();
// Skip hidden files and directories (those starting with .)
if (strpos($basename, '.') === 0) {
return false;
}
// If it's a directory, check if it should be excluded
if ($iterator->hasChildren()) {
// Don't recurse into excluded directories
$excluded_dirs = config('rsx.manifest.excluded_dirs', static::EXCLUDED_DIRS);
return !in_array($basename, $excluded_dirs);
}
// It's a file, check if filename should be excluded
$excluded_files = config('rsx.manifest.excluded_files', []);
if (in_array($basename, $excluded_files)) {
return false;
}
return true;
},
),
RecursiveIteratorIterator::SELF_FIRST,
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$absolute_path = $file->getPathname();
// Convert to relative path and normalize to forward slashes
$absolute_path = str_replace('\\', '/', $absolute_path);
$base_path_normalized = str_replace('\\', '/', $base_path);
$relative_path = str_replace($base_path_normalized . '/', '', $absolute_path);
// Remove leading slash if present
$relative_path = ltrim($relative_path, '/');
// Check for disallowed .old. naming pattern
if (preg_match('/\\.old\\.\\w+$/', $relative_path)) {
ManifestErrors::old_file_pattern($relative_path);
}
$files[] = $relative_path;
}
}
}
}
// Hardcoded ./json includes
$hardcoded_files = ['package-lock.json', 'composer.lock'];
foreach ($hardcoded_files as $file) {
if (file_exists($file) && !in_array($file, $files)) {
$files[] = $file;
}
}
// Remove duplicates and sort
$files = array_unique($files);
sort($files);
self::$__get_rsx_files_cache = $files;
return $files;
}
/**
* Load cached manifest data
*/
private static function __load_cached_data()
{
$cache_file = static::__get_cache_file_path();
if (file_exists($cache_file)) {
static::$data = include $cache_file;
// Validate structure
if (is_array(static::$data) && isset(static::$data['data']['files']) && count(static::$data['data']['files']) > 0) {
// Check if manifest is marked as bad due to code quality violations
if (isset(static::$data['data']['manifest_is_bad']) && static::$data['data']['manifest_is_bad']) {
// Clear the data to force a rebuild
static::$data = [
'generated' => date('Y-m-d H:i:s'),
'hash' => '',
'data' => ['files' => []],
];
return false;
}
return true;
}
}
// Cache doesn't exist or is invalid - return false without logging
// Logging happens in init() after we determine we actually need to rebuild
static::$data = [
'generated' => date('Y-m-d H:i:s'),
'hash' => '',
'data' => ['files' => []],
];
return false;
}
/**
* Validate manifest data for consistency
*/
protected static function __validate_manifest_data(): void
{
if (!isset(static::$data['data']['files'])) {
throw new \RuntimeException(
'Fatal: Manifest::validate_manifest_data() called but manifest data structure is not initialized. ' .
"This shouldn't happen - data should be populated before validation."
);
}
// Track unique names by file type
$php_classes = [];
$js_classes = [];
$blade_ids = [];
$jqhtml_ids = [];
foreach (static::$data['data']['files'] as $file_path => $metadata) {
$extension = $metadata['extension'] ?? '';
// Check PHP class uniqueness
if ($extension === 'php' && isset($metadata['class'])) {
$class = $metadata['class'];
if (isset($php_classes[$class])) {
throw new \RuntimeException(
"Duplicate PHP class name detected: {$class}\n" .
"Found in:\n" .
" - {$php_classes[$class]}\n" .
" - {$file_path}\n\n" .
"PHP class names must be unique across all files.\n\n" .
"To resolve: Add specificity to the class names by prefixing with directory segments.\n" .
"For example, if a class named 'Index_Controller' resides in the 'demo' module directory,\n" .
"rename it to 'Demo_Index_Controller'. Similarly, classes in nested directories like\n" .
"'demo/admin/' could be named 'Demo_Admin_Index_Controller'. After renaming, refactor\n" .
'all usages of the old class name throughout the codebase to use the new, more specific name.'
);
}
$php_classes[$class] = $file_path;
}
// Check JavaScript class uniqueness
if (in_array($extension, ['js', 'jsx', 'ts', 'tsx']) && isset($metadata['class'])) {
$class = $metadata['class'];
if (isset($js_classes[$class])) {
throw new \RuntimeException(
"Duplicate JavaScript class name detected: {$class}\n" .
"Found in:\n" .
" - {$js_classes[$class]}\n" .
" - {$file_path}\n\n" .
"JavaScript class names must be unique across all files.\n\n" .
"To resolve: Add specificity to the class names by prefixing with directory segments.\n" .
"For example, if a class named 'Base' resides in the 'demo' module directory,\n" .
"rename it to 'Demo_Base'. Similarly, classes in nested directories like\n" .
"'demo/admin/' could be named 'Demo_Admin_Base'. After renaming, refactor\n" .
'all usages of the old class name throughout the codebase to use the new, more specific name.'
);
}
$js_classes[$class] = $file_path;
}
// Check Blade ID uniqueness
if ($extension === 'blade.php' && isset($metadata['id'])) {
$id = $metadata['id'];
if (isset($blade_ids[$id])) {
throw new \RuntimeException(
"Duplicate Blade @rsx_id detected: {$id}\n" .
"Found in:\n" .
" - {$blade_ids[$id]}\n" .
" - {$file_path}\n\n" .
"Blade @rsx_id values must be unique across all files.\n\n" .
"To resolve: Add specificity to the @rsx_id by prefixing with directory segments.\n" .
"For example, if a view with @rsx_id('Layout') resides in the 'demo' module directory,\n" .
"change it to @rsx_id('Demo_Layout'). Similarly, views in nested directories like\n" .
"'demo/sections/' could use @rsx_id('Demo_Sections_Layout'). After renaming, refactor\n" .
'all references to the old @rsx_id (in @rsx_extends, @rsx_include, etc.) to use the new name.'
);
}
$blade_ids[$id] = $file_path;
}
// Check Jqhtml component ID uniqueness
if ($extension === 'jqhtml' && isset($metadata['id'])) {
$id = $metadata['id'];
if (isset($jqhtml_ids[$id])) {
throw new \RuntimeException(
"Duplicate jqhtml component name detected: {$id}\n" .
"Found in:\n" .
" - {$jqhtml_ids[$id]}\n" .
" - {$file_path}\n\n" .
"Jqhtml component names (<Define:ComponentName>) must be unique across all files.\n\n" .
"To resolve: Add specificity to the component name by prefixing with directory segments.\n" .
"For example, if a component named 'Card' resides in the 'demo' module directory,\n" .
"rename it to 'Demo_Card' in the <Define:> tag. Similarly, components in nested directories\n" .
"like 'demo/widgets/' could be named 'Demo_Widgets_Card'. After renaming, refactor all\n" .
'usages of the component (in Blade templates and JavaScript) to use the new, more specific name.'
);
}
$jqhtml_ids[$id] = $file_path;
}
// Check that controller actions don't have both Route and Ajax_Endpoint
if ($extension === 'php' && isset($metadata['extends'])) {
// Check if this is a controller (extends Rsx_Controller_Abstract)
$is_controller = static::__is_controller_class($metadata);
if ($is_controller && isset($metadata['public_static_methods'])) {
foreach ($metadata['public_static_methods'] as $method_name => $method_info) {
if (!isset($method_info['attributes'])) {
continue;
}
$has_route = false;
$has_ajax_endpoint = false;
foreach ($method_info['attributes'] as $attr_name => $attr_instances) {
if ($attr_name === 'Route' || str_ends_with($attr_name, '\\Route')) {
$has_route = true;
}
if ($attr_name === 'Ajax_Endpoint' || str_ends_with($attr_name, '\\Ajax_Endpoint')) {
$has_ajax_endpoint = true;
}
}
if ($has_route && $has_ajax_endpoint) {
$class_name = $metadata['class'] ?? 'Unknown';
throw new \RuntimeException(
"Controller action cannot have both Route and Ajax_Endpoint attributes.\n" .
"Class: {$class_name}\n" .
"Method: {$method_name}\n" .
"File: {$file_path}\n" .
'A controller action must be either a web route OR an AJAX endpoint, not both.'
);
}
}
}
}
}
}
/**
* Check if metadata represents a controller class
* @param array $metadata File metadata
* @return bool True if class extends Rsx_Controller_Abstract
*/
protected static function __is_controller_class(array $metadata): bool
{
$extends = $metadata['extends'] ?? '';
if ($extends === 'Rsx_Controller_Abstract') {
return true;
}
// Check parent hierarchy
$current_class = $extends;
$max_depth = 10;
while ($current_class && $max_depth-- > 0) {
try {
$parent_metadata = static::php_get_metadata_by_class($current_class);
if (($parent_metadata['extends'] ?? '') === 'Rsx_Controller_Abstract') {
return true;
}
$current_class = $parent_metadata['extends'] ?? '';
} catch (\RuntimeException $e) {
// Check FQCN match
if ($current_class === 'Rsx_Controller_Abstract' ||
$current_class === 'App\\RSpade\\Core\\Controller\\Rsx_Controller_Abstract') {
return true;
}
break;
}
}
return false;
}
/**
* Generate VS Code IDE helper stubs for attributes and class aliases
*/
private static function __generate_vscode_stubs(): void
{
// Generate to project root (parent of system/)
$project_root = dirname(base_path());
$stub_file = $project_root . '/._rsx_helper.php';
$output = "<?php\n";
$output .= "/* @noinspection ALL */\n";
$output .= "// @formatter:off\n";
$output .= "// phpcs:ignoreFile\n\n";
$output .= "/**\n";
$output .= " * RSX Framework Attribute Stubs for IDE Support\n";
$output .= " *\n";
$output .= " * AUTO-GENERATED FILE - DO NOT EDIT MANUALLY\n";
$output .= " *\n";
$output .= " * This file is automatically regenerated during manifest builds when:\n";
$output .= " * - Running `php artisan rsx:manifest:build`\n";
$output .= " * - File changes detected in development mode\n";
$output .= " * - Any PHP file with attributes is modified\n";
$output .= " *\n";
$output .= " * These are stub definitions to provide IDE autocomplete and eliminate warnings\n";
$output .= " * for RSX framework attributes. These classes are never actually loaded or used\n";
$output .= " * at runtime - they exist purely for IDE IntelliSense.\n";
$output .= " *\n";
$output .= " * This file should not be included in your code, only analyzed by your IDE!\n";
$output .= " */\n\n";
// Generate attribute stubs
$attributes = [];
foreach (static::$data['data']['files'] as $file_path => $metadata) {
if (!isset($metadata['extension']) || $metadata['extension'] !== 'php') {
continue;
}
if (!isset($metadata['class'])) {
continue;
}
// Collect attributes from the class itself
if (isset($metadata['attributes'])) {
foreach ($metadata['attributes'] as $attr_name => $attr_data) {
// Extract simple attribute name (last part after backslash)
$simple_attr_name = basename(str_replace('\\', '/', $attr_name));
$attributes[$simple_attr_name] = true;
}
}
// Collect attributes from public static methods
if (isset($metadata['public_static_methods'])) {
foreach ($metadata['public_static_methods'] as $method_name => $method_info) {
if (isset($method_info['attributes'])) {
foreach ($method_info['attributes'] as $attr_name => $attr_data) {
// Extract simple attribute name (last part after backslash)
$simple_attr_name = basename(str_replace('\\', '/', $attr_name));
$attributes[$simple_attr_name] = true;
}
}
}
}
}
if (!empty($attributes)) {
$output .= "namespace {\n";
foreach (array_keys($attributes) as $attr_name) {
$output .= " #[\\Attribute(\\Attribute::TARGET_ALL | \\Attribute::IS_REPEATABLE)]\n";
$output .= " class {$attr_name} {\n";
$output .= " public function __construct(...\$args) {}\n";
$output .= " }\n\n";
}
$output .= "}\n";
}
file_put_contents($stub_file, $output);
}
/**
* Save manifest data to cache
*/
protected static function __save(): void
{
// If manifest is marked as bad, handle specially
if (static::$_manifest_is_bad) {
// Check if manifest has been initialized
if (!static::$_has_init) {
return; // Don't save if not initialized
}
$cache_file = static::__get_cache_file_path();
// Check if cache file exists
if (!file_exists($cache_file)) {
return; // Don't save if cache doesn't exist yet
}
// Check if we've already marked it as bad
if (isset(static::$data['data']['manifest_is_bad'])) {
return; // Already marked, don't save again
}
// Mark as bad and save
static::$data['data']['manifest_is_bad'] = true;
// Fall through to normal save logic below
}
// Validate manifest data before saving
static::__validate_manifest_data();
$cache_file = static::__get_cache_file_path();
// Ensure directory exists
$dir = dirname($cache_file);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
// Sort files array by key for predictable output
if (isset(static::$data['data']['files'])) {
ksort(static::$data['data']['files']);
}
// Update metadata
static::$data['generated'] = date('Y-m-d H:i:s');
// In development mode, use random hash for cache-busting on every request
// if (env('APP_ENV') !== 'production') {
// static::$data['hash'] = bin2hex(random_bytes(16)); // 32 character random hash
// } else {
// In production, use SHA-256 truncated to 32 chars for cryptographic security with consistent length
static::$data['hash'] = substr(hash('sha256', json_encode(static::$data['data'])), 0, 32);
// }
$php_content = "<?php\n\n";
$php_content .= "// Generated manifest cache - DO NOT EDIT\n";
$php_content .= '// Generated: ' . static::$data['generated'] . "\n";
$php_content .= '// Files: ' . count(static::$data['data']['files']) . "\n";
$php_content .= '// Hash: ' . static::$data['hash'] . "\n\n";
$php_content .= 'return ' . var_export(static::$data, true) . ";\n";
file_put_contents($cache_file, $php_content);
}
/**
* Process code quality metadata extraction for changed files
* This runs during Phase 3.5, after PHP classes are loaded but before reflection
*/
protected static function __process_code_quality_metadata(array $changed_files): void
{
// Only run in development mode
if (env('APP_ENV') === 'production') {
return;
}
// Discover rules with on_manifest_file_update method
$rules_with_update = [];
$rule_files = glob(base_path('app/RSpade/CodeQuality/Rules/**/*Rule.php'));
foreach ($rule_files as $rule_file) {
$rule_class = null;
$content = file_get_contents($rule_file);
// Extract class name from file
if (preg_match('/class\s+(\w+)\s+extends/', $content, $matches)) {
$class_name = $matches[1];
// Extract namespace
if (preg_match('/namespace\s+([^;]+);/', $content, $ns_matches)) {
$namespace = $ns_matches[1];
$rule_class = $namespace . '\\' . $class_name;
}
}
if ($rule_class && class_exists($rule_class)) {
// Check if it has the on_manifest_file_update method
if (method_exists($rule_class, 'on_manifest_file_update')) {
$rule_instance = new $rule_class(new \App\RSpade\CodeQuality\Support\ViolationCollector());
// Check if it's a manifest-time rule
if ($rule_instance->is_called_during_manifest_scan()) {
$rules_with_update[] = $rule_instance;
}
}
}
}
// Process each changed file with each rule
foreach ($changed_files as $file) {
$absolute_path = base_path($file);
// Skip if file doesn't exist
if (!file_exists($absolute_path)) {
continue;
}
// Initialize code_quality_metadata if not present
if (!isset(static::$data['data']['files'][$file]['code_quality_metadata'])) {
static::$data['data']['files'][$file]['code_quality_metadata'] = [];
}
// Process with each rule that has on_manifest_file_update
foreach ($rules_with_update as $rule) {
try {
$metadata = $rule->on_manifest_file_update(
$absolute_path,
file_get_contents($absolute_path),
static::$data['data']['files'][$file] ?? []
);
// Store metadata if returned
if ($metadata !== null) {
$rule_id = $rule->get_id();
static::$data['data']['files'][$file]['code_quality_metadata'][$rule_id] = $metadata;
}
} catch (Throwable $e) {
// If the rule throws during metadata extraction, it's a violation
// Mark manifest as bad and throw the error
static::_set_manifest_is_bad();
throw $e;
}
}
}
}
/**
* Run code quality checks during manifest scan
* Only runs in development mode after manifest changes
* Throws fatal exception on first violation found
*/
protected static function __run_manifest_time_code_quality_checks(): void
{
// Create a minimal violation collector
$collector = new \App\RSpade\CodeQuality\Support\ViolationCollector();
// Discover rules that should run during manifest scan
// This uses direct file scanning, not the manifest, to avoid pollution
$rules = \App\RSpade\CodeQuality\Support\RuleDiscovery::discover_rules(
$collector,
[],
true // Only get rules with is_called_during_manifest_scan() = true
);
foreach ($rules as $rule) {
// Get file patterns this rule applies to
$patterns = $rule->get_file_patterns();
// Check each file in the manifest
foreach (static::$data['data']['files'] as $file_path => $metadata) {
// Check if file matches any pattern
$matches = false;
foreach ($patterns as $pattern) {
$pattern = str_replace('*', '.*', $pattern);
if (preg_match('/^' . $pattern . '$/i', basename($file_path))) {
$matches = true;
break;
}
}
if (!$matches) {
continue;
}
// Get absolute path
$absolute_path = base_path($file_path);
// Skip if file doesn't exist
if (!file_exists($absolute_path)) {
continue;
}
// Read file contents
$contents = file_get_contents($absolute_path);
// Run the rule check - include code_quality_metadata if available
$enhanced_metadata = $metadata;
if (isset(static::$data['data']['files'][$file_path]['code_quality_metadata'][$rule->get_id()])) {
$enhanced_metadata['rule_metadata'] = static::$data['data']['files'][$file_path]['code_quality_metadata'][$rule->get_id()];
}
$rule->check($absolute_path, $contents, $enhanced_metadata);
// Check if any violations were found - throw on first violation
$violations = $collector->get_all();
if (!empty($violations)) {
// Get the first violation
$first_violation = $violations[0];
$data = $first_violation->to_array();
// Mark manifest as bad to force re-checking on next attempt
static::_set_manifest_is_bad();
// Format file path relative to project root
$relative_file = str_replace(base_path() . '/', '', $data['file']);
// Build error message
$error_message = "Code Quality Violation ({$data['type']}) - {$data['message']}\n\n";
$error_message .= "File: {$relative_file}:{$data['line']}\n\n";
if (!empty($data['code'])) {
$error_message .= "Code:\n " . $data['code'] . "\n\n";
}
if (!empty($data['resolution'])) {
$error_message .= "Resolution:\n" . $data['resolution'];
}
// Throw fatal exception with the first violation, passing file and line for accurate error display
throw new \App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException(
$error_message,
0,
null,
$data['file'],
$data['line']
);
}
}
}
}
/**
* Get list of all database tables from manifest model metadata
*
* Returns only tables that have been indexed via Model_ManifestSupport
* (i.e., tables with corresponding model classes)
*
* @return array Array of table names
*/
public static function db_get_tables(): array
{
static::init();
if (!isset(static::$data['data']['models'])) {
return [];
}
$tables = [];
foreach (static::$data['data']['models'] as $model_data) {
if (isset($model_data['table'])) {
$tables[] = $model_data['table'];
}
}
return array_values(array_unique($tables));
}
/**
* Get columns for a specific table with their types from manifest
*
* Returns simplified column information extracted during manifest build.
* Types are from Model_ManifestSupport::__parse_column_type() which simplifies
* MySQL types (e.g., 'bigint', 'varchar(255)' -> 'integer', 'string')
*
* @param string $table Table name
* @return array Associative array of column_name => type, or empty if table not found
*/
public static function db_get_table_columns(string $table): array
{
static::init();
if (!isset(static::$data['data']['models'])) {
return [];
}
// Find the model for this table
foreach (static::$data['data']['models'] as $model_data) {
if (isset($model_data['table']) && $model_data['table'] === $table) {
if (!isset($model_data['columns'])) {
return [];
}
$result = [];
foreach ($model_data['columns'] as $column_name => $column_data) {
$result[$column_name] = $column_data['type'] ?? 'unknown';
}
return $result;
}
}
return [];
}
/**
* Get columns of a specific type for a table from manifest
*
* Useful for finding all boolean columns (tinyint), integer columns, etc.
*
* @param string $table Table name
* @param string $type Type to filter by (from Model_ManifestSupport simplified types)
* @return array Array of column names matching the type
*/
public static function db_get_columns_by_type(string $table, string $type): array
{
$columns = static::db_get_table_columns($table);
return array_keys(array_filter($columns, function ($col_type) use ($type) {
return $col_type === $type;
}));
}
}