Files
rspade_system/app/RSpade/Core/Manifest/Manifest.php
root 45838aafd2 Working state snapshot with debug settings and pending changes
Fix manifest helper delegator missing return statements

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-14 22:01:13 +00:00

1596 lines
60 KiB
PHP

<?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;
use App\RSpade\Core\Manifest\_Manifest_Builder_Helper;
use App\RSpade\Core\Manifest\_Manifest_Cache_Helper;
use App\RSpade\Core\Manifest\_Manifest_Database_Helper;
use App\RSpade\Core\Manifest\_Manifest_JS_Reflection_Helper;
use App\RSpade\Core\Manifest\_Manifest_PHP_Reflection_Helper;
use App\RSpade\Core\Manifest\_Manifest_Quality_Helper;
use App\RSpade\Core\Manifest\_Manifest_Reflection_Helper;
use App\RSpade\Core\Manifest\_Manifest_Scanner_Helper;
use App\RSpade\Core\Mode\Rsx_Mode;
/**
* 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
*/
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 = null;
public static array $_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()
{
return _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()
{
return _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()
{
return _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()
{
return _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);
}
}