process_all_files(); $already_run = true; } } private function process_all_files(): void { // Get all PHP classes from manifest $files = Manifest::get_all(); foreach ($files as $file_path => $file_metadata) { // Skip non-PHP files if (($file_metadata['extension'] ?? '') !== 'php') { continue; } // Skip files without classes if (empty($file_metadata['class'])) { continue; } $class_name = $file_metadata['class']; // Check both static and instance methods for RequireParentCall attribute $methods_to_check = []; if (!empty($file_metadata['public_static_methods'])) { foreach ($file_metadata['public_static_methods'] as $method_name => $method_data) { $methods_to_check[$method_name] = $method_data; } } if (!empty($file_metadata['public_instance_methods'])) { foreach ($file_metadata['public_instance_methods'] as $method_name => $method_data) { $methods_to_check[$method_name] = $method_data; } } // Check each method for RequireParentCall attribute foreach ($methods_to_check as $method_name => $method_data) { // Check if this method has RequireParentCall attribute if (empty($method_data['attributes']) || !isset($method_data['attributes']['RequireParentCall'])) { continue; } // This method requires parent call - check all subclasses $this->check_subclasses_for_parent_call($class_name, $method_name, $file_path); } } } private function check_subclasses_for_parent_call(string $parent_class, string $method_name, string $parent_file): void { // Get all subclasses (including abstract ones) $subclasses = Manifest::php_get_subclasses_of($parent_class, false); foreach ($subclasses as $subclass_name) { // Get the subclass metadata $subclass_data = Manifest::php_get_metadata_by_class($subclass_name); if (!$subclass_data) { continue; } // Check if subclass overrides this method (in either static or instance methods) $subclass_method = null; if (!empty($subclass_data['public_static_methods'][$method_name])) { $subclass_method = $subclass_data['public_static_methods'][$method_name]; } elseif (!empty($subclass_data['public_instance_methods'][$method_name])) { $subclass_method = $subclass_data['public_instance_methods'][$method_name]; } if (!$subclass_method) { continue; } // Check if the overridden method calls parent if (empty($subclass_method['__calls_parent'])) { // Get line number if available $line = 0; // Get the actual parent FQCN for clearer error message $parent_fqcn = Manifest::php_get_metadata_by_class($parent_class)['fqcn'] ?? $parent_class; $subclass_fqcn = $subclass_data['fqcn'] ?? $subclass_name; $message = "Method {$subclass_name}::{$method_name}() must call parent::{$method_name}() because {$parent_class}::{$method_name}() is marked with #[AuthParentCall]. "; $message .= "The parent method implements critical functionality that must not be skipped in overriding implementations."; $suggestion = "Add a call to parent::{$method_name}() in the {$subclass_name}::{$method_name}() implementation. "; $suggestion .= "This ensures the essential parent logic is executed."; // Get code snippet if possible $code_snippet = ''; if (file_exists($subclass_data['file'])) { $file_contents = file_get_contents($subclass_data['file']); if (preg_match('/public\s+static\s+function\s+' . preg_quote($method_name, '/') . '\s*\([^{]*\{/m', $file_contents, $matches, PREG_OFFSET_CAPTURE)) { $offset = $matches[0][1]; $line = substr_count(substr($file_contents, 0, $offset), "\n") + 1; $lines = explode("\n", $file_contents); if ($line > 0 && $line <= count($lines)) { $code_snippet = trim($lines[$line - 1]); } } } $this->add_violation( $subclass_data['file'], $line, $message, $code_snippet, $suggestion, 'critical' ); } } } }