[ // 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 */ public const EXCLUDED_DIRS = ['resource', 'public', 'vendor', 'node_modules', '.git', 'storage']; /** * File extensions to process * @deprecated Use ExtensionRegistry::get_all_extensions() instead */ public const PROCESSABLE_EXTENSIONS = ['php', 'js', 'jsx', 'ts', 'tsx', 'phtml', 'scss', 'less', 'css', 'blade.php']; /** * Path to the cache file */ public 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...]] * ] */ public static ?array $data = null; /** * Whether data has been loaded */ public static bool $_has_init = false; /** * Flag to signal manifest needs to restart due to file rename */ public static bool $_needs_manifest_restart = false; // The manifest kernel instance (cached) (???) public static ?ManifestKernel $kernel = null; public static $_manifest_compile_lock; public static $_get_rsx_files_cache; public static bool $_has_changed_cache; public static bool $_has_manifest_ready = false; public static bool $_manifest_is_bad = false; // Track if we've already shown the manifest rescan message this page load public static bool $__shown_rescan_message = false; // Files that changed in the most recent manifest scan (for incremental code quality checks) public static array $_changed_files = []; // Flag to allow forced rebuilding in production-like modes (used by rsx:prod:build) public static bool $_force_build = 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 the list of files that changed in the most recent manifest scan * * Used by code quality rules that need to know which files changed for * incremental processing. Returns empty array if manifest was fully rebuilt. * * @return array Array of relative file paths that changed */ public static function get_changed_files(): array { return static::$_changed_files; } /** * 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 { return _Manifest_PHP_Reflection_Helper::php_find_class($class_name); } /** * Find a PHP class by fully qualified name */ public static function find_php_fqcn(string $fqcn): string { return _Manifest_PHP_Reflection_Helper::find_php_fqcn($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 { return _Manifest_PHP_Reflection_Helper::php_get_metadata_by_class($class_name); } /** * 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 { return _Manifest_PHP_Reflection_Helper::php_get_metadata_by_fqcn($fqcn); } /** * Find a JavaScript class */ public static function js_find_class(string $class_name): string { return _Manifest_JS_Reflection_Helper::js_find_class($class_name); } /** * Find a view by ID */ public static function find_view(string $id): string { return _Manifest_Reflection_Helper::find_view($id); } /** * Find a view by RSX ID (path-agnostic identifier) */ public static function find_view_by_rsx_id(string $id): string { return _Manifest_Reflection_Helper::find_view_by_rsx_id($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 { return _Manifest_Reflection_Helper::get_path_by_filename($filename); } /** * 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 { return _Manifest_PHP_Reflection_Helper::php_get_extending($parentclass); } /** * 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 { return _Manifest_JS_Reflection_Helper::js_get_extending($parentclass); } /** * 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 { return _Manifest_PHP_Reflection_Helper::php_is_subclass_of($subclass, $superclass); } /** * 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 { return _Manifest_PHP_Reflection_Helper::php_is_abstract($class_name); } /** * 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 { return _Manifest_PHP_Reflection_Helper::php_get_lineage($class_name); } /** * Check if a class name corresponds to a PHP model class (exists in models index) * * This is used by the JS model system to recognize PHP model class names that may * appear in JS inheritance chains but don't exist as JS classes in the manifest. * PHP models like "Project_Model" generate JS stubs during bundle compilation. * * @param string $class_name The class name to check * @return bool True if this is a PHP model class name */ public static function is_php_model_class(string $class_name): bool { return _Manifest_PHP_Reflection_Helper::is_php_model_class($class_name); } /** * 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 { return _Manifest_JS_Reflection_Helper::js_is_subclass_of($subclass, $superclass); } /** * 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 { return _Manifest_JS_Reflection_Helper::js_get_lineage($class_name); } /** * 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 { return _Manifest_PHP_Reflection_Helper::php_get_subclasses_of($class_name, $concrete_only); } /** * 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 _Manifest_JS_Reflection_Helper::js_get_subclasses_of($class_name); } /** * Get all classes with a specific attribute */ public static function get_with_attribute(string $attribute_class): array { return _Manifest_Reflection_Helper::get_with_attribute($attribute_class); } /** * Get all routes from the manifest * * Returns unified route structure: $routes[$pattern] => route_data * where route_data contains: * - methods: ['GET', 'POST'] * - type: 'spa' | 'standard' * - class: Full class name * - method: Method name * - file: File path * - require: Auth requirements * - js_action_class: (SPA routes only) JavaScript action class */ public static function get_routes(): array { return _Manifest_Reflection_Helper::get_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 { return _Manifest_Reflection_Helper::get_pre_dispatch_requires($class_name); } /** * 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); 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; } /** * Check if current CLI command is "safe" - doesn't require manifest to be built * * These commands can run in production mode without a pre-built manifest because * they don't actually use manifest data (e.g., rsx:clean just deletes directories). */ protected static function _is_safe_command(): bool { if (php_sapi_name() !== 'cli') { return false; } $argv = $_SERVER['argv'] ?? []; if (count($argv) < 2) { return false; } // Commands that can safely run without manifest $safe_commands = [ 'rsx:clean', 'rsx:prod:build', // This builds the manifest itself 'rsx:mode:set', // This calls prod:build internally ]; $command = $argv[1] ?? ''; return in_array($command, $safe_commands, true); } /** * 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(); // In production-like modes (debug/production), require pre-built manifest // Unless force_build flag is set (used by rsx:prod:build) // Check both the static flag and the environment variable (set before process starts) // Also auto-allow for safe commands that don't need manifest (e.g., rsx:clean) $force_build = self::$_force_build || env('RSX_FORCE_BUILD', false) || self::_is_safe_command(); if (Rsx_Mode::is_production_like() && !$force_build) { if (!$loaded_cache) { throw new \RuntimeException( 'Manifest not built for production mode. Run: php artisan rsx:prod:build' ); } console_debug('MANIFEST', 'Manifest cache loaded (production mode)'); self::post_init(); return; } // Development mode: validate cache and rebuild if needed if ($loaded_cache) { console_debug('MANIFEST', 'Manifest cache loaded (development mode), validating...'); if (self::_validate_cached_data()) { console_debug('MANIFEST', 'Manifest is valid'); self::post_init(); 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('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('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::post_init(); } /** * Post-initialization hook called after manifest is fully loaded * * Called at the end of init() after manifest data is loaded (either from cache * or after scanning/rebuilding). At this point, the manifest is complete and * ready for queries. * * Current responsibilities: * - Sets $_has_manifest_ready flag to indicate manifest is available * - Registers the autoloader (which depends on manifest data) * - Loads classless PHP files (helpers, constants, procedural code) * * This is the appropriate place to perform operations that require the complete * manifest to be available, such as loading non-class PHP files that were * indexed during the scan. */ public static function post_init() { self::$_has_manifest_ready = true; \App\RSpade\Core\Autoloader::register(); // Load classless PHP files (helper functions, constants, etc.) $classless_files = self::$data['data']['classless_php_files'] ?? []; foreach ($classless_files as $file_path) { $full_path = base_path($file_path); if (file_exists($full_path)) { include_once $full_path; } } } /** * 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 { _Manifest_Cache_Helper::_unlink_cache(); } /** * 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 { return _Manifest_PHP_Reflection_Helper::_normalize_class_name($class_name); } /** * 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 caches at the beginning of each pass (important for restarts) static::$_needs_manifest_restart = false; self::$_get_rsx_files_cache = null; // 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' => [], 'routes' => [], ], ]; // ======================================================= // 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; } } // Store changed files for incremental code quality checks static::$_changed_files = $files_to_process; 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 } // ================================================================================== // PHP FIXER INTEGRATION POINT // ================================================================================== // CRITICAL: Php_Fixer MUST run BEFORE _check_unique_base_class_names() so that // when a class override is detected and framework files are renamed to .upstream, // all use statements have already been updated to point to the rsx/ location. // This prevents autoloader failures when the manifest restarts. // // WHAT PHP_FIXER DOES: // 1. Fixes namespaces to match file paths // 2. Redirects use statements to correct FQCN based on manifest (rsx/ takes priority) // 3. Replaces FQCNs like \Rsx\Models\User_Model with simple names User_Model // 4. Adds #[Relationship] attributes to model ORM methods // 5. Removes leading backslashes from attributes: #[\Route] → #[Route] // // SMART REBUILDING: // - Tracks SHA1 hash of all class structures (ClassName:ParentClass) // - If structure changed: Fixes ALL files (cascading updates needed) // - If structure unchanged: Fixes ONLY $files_to_process (incremental) // // RE-PARSING LOOP BELOW: // - If Php_Fixer modified files, we MUST re-parse them // - This updates manifest with corrected namespace/class/FQCN data // - Without this, manifest would reference old class locations // ================================================================================== $php_fixer_modified_files = []; if (!app()->environment('production')) { $php_fixer_modified_files = static::_run_php_fixer($files_to_process); // 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']; } } } // ================================================================================== // CLASS OVERRIDE DETECTION // ================================================================================== // Check for duplicate class names. When rsx/ contains a class that also exists in // app/RSpade/, rename the framework file to .upstream and restart the manifest. // At this point, Php_Fixer has already updated use statements to point to rsx/. // ================================================================================== static::_check_unique_base_class_names(); // If a class override was detected (rsx/ overriding app/RSpade/), restart manifest build if (static::$_needs_manifest_restart) { console_debug('MANIFEST', 'Class override detected, restarting manifest build'); goto manifest_start; } // 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(); // Check if a class override was detected and framework file renamed if (static::$_needs_manifest_restart) { console_debug('MANIFEST', 'Class override detected, restarting manifest build'); goto manifest_start; } // Build event handler index from attributes static::_build_event_handler_index(); // Build classless PHP files index static::_build_classless_php_files_index(); // ======================================================= // 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) // Skip during migrations - database may not be provisioned yet if (env('APP_ENV') !== 'production' && $changes && !static::_is_migration_context()) { static::_verify_database_provisioned(); static::_run_manifest_time_code_quality_checks($files_to_process); } // 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 { _Manifest_PHP_Reflection_Helper::_load_class_hierarchy($fqcn, $manifest_data); } /** * 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: // ------------------------------------------------------------------------ // DEAD CODE REMOVED: _generate_js_api_stubs() // Controller stub generation now handled by Controller_BundleIntegration // ------------------------------------------------------------------------ // ------------------------------------------------------------------------ // DEAD CODE REMOVED: _generate_js_model_stubs() // Model stub generation now handled by Database_BundleIntegration // ------------------------------------------------------------------------ /** * Get or create the kernel instance */ public static function _get_kernel(): ManifestKernel { return _Manifest_Cache_Helper::_get_kernel(); } /** * Get the full cache file path */ public static function _get_cache_file_path(): string { return _Manifest_Cache_Helper::_get_cache_file_path(); } // move to lower soon public static function _validate_cached_data() { _Manifest_Cache_Helper::_validate_cached_data(); } /** * Check for duplicate base class names within the same file type * * This method handles two scenarios: * 1. RESTORE: If a .upstream file exists but no active override exists, restore it * 2. OVERRIDE: When a class exists in both rsx/ and app/RSpade/, rename framework to .upstream * * Throws a fatal error if duplicates exist within the same area (both rsx/ or both app/RSpade/) */ public static function _check_unique_base_class_names(): void { _Manifest_Quality_Helper::_check_unique_base_class_names(); } /** * Restore orphaned .upstream files when their override no longer exists * * Scans all php.upstream files in the manifest. For each one, checks if a .php file * with the same class name exists. If not, the override was removed and we should * restore the framework file. */ public static function _restore_orphaned_upstream_files(): void { _Manifest_Scanner_Helper::_restore_orphaned_upstream_files(); } /** * 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 */ public static function _collate_files_by_classes() { _Manifest_Builder_Helper::_collate_files_by_classes(); } /** * Build event handler index from OnEvent attributes * * Scans all PHP files for methods with #[OnEvent] attributes and builds * an index of event_name => [handlers] for fast event dispatching. * * Index structure: * ['event_handlers'] => [ * 'event.name' => [ * ['class' => 'Class_Name', 'method' => 'method_name', 'priority' => 100], * ['class' => 'Other_Class', 'method' => 'other_method', 'priority' => 200], * ] * ] * * Handlers are sorted by priority (lower numbers execute first). * * @return void */ public static function _build_event_handler_index() { _Manifest_Builder_Helper::_build_event_handler_index(); } /** * Build index of classless PHP files * * Creates a simple array of file paths for all PHP files in the manifest * that do not contain a class. These files typically contain helper functions, * constants, or other procedural code that needs to be loaded during post_init(). * * Stored in manifest data at: $data['data']['classless_php_files'] */ public static function _build_classless_php_files_index() { _Manifest_Builder_Helper::_build_classless_php_files_index(); } /** * 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 */ public static function _load_changed_php_files(array $changed_files): void { _Manifest_Scanner_Helper::_load_changed_php_files($changed_files); } /** * Extract reflection data only for changed files * Uses caching to avoid re-extracting unchanged files */ public static function _extract_reflection_for_changed_files(array $changed_files): void { _Manifest_Scanner_Helper::_extract_reflection_for_changed_files($changed_files); } /** * 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 * * SMART REBUILD STRATEGY: * This method implements an intelligent rebuild strategy to avoid unnecessary file writes: * * 1. STRUCTURE HASH: Creates SHA1 hash of "ClassName:ParentClass" for ALL classes * - Detects when classes are added, removed, renamed, or inheritance changes * * 2. FULL REBUILD TRIGGERS: * - New class added (may need new use statements elsewhere) * - Class renamed (all references need updating) * - Inheritance changed (may affect use statement resolution) * → When triggered: Fix ALL PHP files in rsx/ and app/RSpade/ * * 3. INCREMENTAL REBUILD: * - Structure hash unchanged (no new/renamed classes) * - Only fixes files that actually changed on disk * → More efficient, avoids touching unchanged files * * WHY THIS MATTERS: * - use statement management depends on knowing all available classes * - FQCN replacement needs to check class name uniqueness * - When class structure changes, files referencing those classes need updating * - When structure stable, only changed files need processing * * @param array $changed_files List of changed files from Phase 1 * @return array List of files that were modified by Php_Fixer */ public static function _run_php_fixer(array $changed_files): array { return _Manifest_Scanner_Helper::_run_php_fixer($changed_files); } public static function _load_php_files_in_dependency_order(): void { _Manifest_Scanner_Helper::_load_php_files_in_dependency_order(); } // /** // * Extract reflection data for all PHP files // * Must be called after Phase 3 (dependency loading) completes // */ // public 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 */ public static function _build_autoloader_class_map(): array { return _Manifest_Builder_Helper::_build_autoloader_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 */ public static function _scan_directory_for_classes(string $directory): array { return _Manifest_Scanner_Helper::_scan_directory_for_classes($directory); } /** * Process a single file and extract comprehensive metadata * @param string $file_path Relative path to file */ public static function _process_file(string $file_path): array { return _Manifest_Scanner_Helper::_process_file($file_path); } /** * Extract public static methods and their attributes using PHP reflection * Note: This is called in Phase 4 after all PHP files have been loaded */ public static function _extract_reflection_data(string $file_path, string $full_class_name, array &$data): void { _Manifest_Scanner_Helper::_extract_reflection_data($file_path, $full_class_name, $data); } /** /** * Extract view information from template files */ public static function _extract_view_info(string $file_path, array &$data): void { _Manifest_Scanner_Helper::_extract_view_info($file_path, $data); } /** * Check if a file has changed (expects relative path) */ public static function _has_changed(string $file): bool { return _Manifest_Scanner_Helper::_has_changed($file); } /** * Get all files in configured scan directories (returns relative paths) */ public static function _get_rsx_files(): array { return _Manifest_Scanner_Helper::_get_rsx_files(); } /** * Load cached manifest data */ public static function _load_cached_data() { return _Manifest_Cache_Helper::_load_cached_data(); } /** * Validate manifest data for consistency */ public static function _validate_manifest_data(): void { _Manifest_Cache_Helper::_validate_manifest_data(); } /** * Check if metadata represents a controller class * @param array $metadata File metadata * @return bool True if class extends Rsx_Controller_Abstract */ public static function _is_controller_class(array $metadata): bool { return _Manifest_Reflection_Helper::_is_controller_class($metadata); } /** * Generate VS Code IDE helper stubs for attributes and class aliases */ public static function _generate_vscode_stubs(): void { _Manifest_Quality_Helper::_generate_vscode_stubs(); } /** * Save manifest data to cache */ public static function _save(): void { _Manifest_Cache_Helper::_save(); } /** * Process code quality metadata extraction for changed files * This runs during Phase 3.5, after PHP classes are loaded but before reflection */ public static function _process_code_quality_metadata(array $changed_files): void { _Manifest_Quality_Helper::_process_code_quality_metadata($changed_files); } /** * Run code quality checks during manifest scan * Only runs in development mode after manifest changes * Throws fatal exception on first violation found * * Supports two types of rules: * - Incremental rules (is_incremental() = true): Only check changed files * - Cross-file rules (is_incremental() = false): Run once with full manifest context * * @param array $changed_files Files that changed in this manifest scan */ public static function _run_manifest_time_code_quality_checks(array $changed_files = []): void { _Manifest_Quality_Helper::_run_manifest_time_code_quality_checks($changed_files); } /** * Check if we're running in a migration context * * Returns true if running migrate, make:migration, or other DB setup commands. * Used to skip code quality checks that depend on database state. */ public static function _is_migration_context(): bool { return _Manifest_Database_Helper::_is_migration_context(); } /** * Verify database has been provisioned before running code quality checks * * Checks that: * 1. The _migrations table exists (created by Laravel migrate) * 2. At least one migration has been applied * * This prevents confusing code quality errors when the real issue is * that migrations haven't been run yet. */ public static function _verify_database_provisioned(): void { _Manifest_Database_Helper::_verify_database_provisioned(); } /** * 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 { return _Manifest_Database_Helper::db_get_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 { return _Manifest_Database_Helper::db_get_table_columns($table); } /** * 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 { return _Manifest_Database_Helper::db_get_columns_by_type($table, $type); } }