class-name.php (no class- prefix, just kebab-case). * * @package Accessibility_Audio_TTS */ namespace Accessibility_Audio_TTS\Core\Bootstrap; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Enhanced PSR-4 kebab-case autoloader for the plugin. */ class Enhanced_Autoloader { /** * Namespace-to-directory mappings. * * @var array */ private array $namespace_mappings = array(); /** * Resolved file path cache. * * @var array */ private array $path_cache = array(); /** * Base directory for class loading. * * @var string */ private string $base_dir; /** * External namespace root directories. * * @var array */ private array $external_namespace_roots = array(); /** * Namespace prefix for this autoloader. * * @var string */ private string $namespace_prefix = 'Accessibility_Audio_TTS\\'; /** * Constructor. * * @param string $base_dir Base directory path. */ public function __construct( string $base_dir ) { $this->base_dir = rtrim( $base_dir, '/' ) . '/'; $this->initialize_mappings(); } /** * Register the autoloader with SPL. * * @return void */ public function register(): void { spl_autoload_register( array( $this, 'load_class' ), true, true ); accessibility_audio_tts_debug_log( 'Enhanced autoloader registered successfully.' ); } /** * Unregister the autoloader from SPL. * * @return void */ public function unregister(): void { spl_autoload_unregister( array( $this, 'load_class' ) ); } /** * Load a class by its fully-qualified name. * * @param string $class_name Fully-qualified class name. * @return bool Whether the class was loaded. */ public function load_class( string $class_name ): bool { if ( strncmp( $this->namespace_prefix, $class_name, strlen( $this->namespace_prefix ) ) !== 0 ) { return false; } if ( class_exists( $class_name, false ) || interface_exists( $class_name, false ) || trait_exists( $class_name, false ) ) { return true; } $bootstrap_classes = array( 'Accessibility_Audio_TTS\\Core\\Bootstrap\\Enhanced_Autoloader', 'Accessibility_Audio_TTS\\Core\\Bootstrap\\Core_Initializer', 'Accessibility_Audio_TTS\\Core\\Bootstrap\\Admin_Initializer', 'Accessibility_Audio_TTS\\Core\\Bootstrap\\Frontend_Initializer', 'Accessibility_Audio_TTS\\Core\\Utilities\\Lazy_Loader', ); if ( in_array( $class_name, $bootstrap_classes, true ) ) { return true; } if ( isset( $this->path_cache[ $class_name ] ) ) { return $this->require_file( $this->path_cache[ $class_name ] ); } $file_path = $this->resolve_class_path( $class_name ); if ( $file_path && file_exists( $file_path ) ) { $this->path_cache[ $class_name ] = $file_path; return $this->require_file( $file_path ); } $external_path = $this->resolve_external_path( $class_name ); if ( $external_path && file_exists( $external_path ) ) { $this->path_cache[ $class_name ] = $external_path; return $this->require_file( $external_path ); } accessibility_audio_tts_debug_log( "Failed to load class: {$class_name}" ); return false; } /** * Resolve the file path for a given class name. * * @param string $class_name Fully-qualified class name. * @return string|null File path or null if not found. */ private function resolve_class_path( string $class_name ): ?string { $relative_class = substr( $class_name, strlen( $this->namespace_prefix ) ); $recording_classes = array( 'Core\\Recording_Metadata' => 'shared/recording-metadata.php', 'Core\\Recording_Rebuild' => 'shared/recording-rebuild.php', 'Admin\\Recording_Rebuild_Controller' => 'admin/posts/recording-rebuild-controller.php', ); if ( isset( $recording_classes[ $relative_class ] ) ) { return $this->base_dir . $recording_classes[ $relative_class ]; } $path_parts = explode( '\\', $relative_class ); $class_name_only = array_pop( $path_parts ); $file_name = $this->convert_class_to_filename( $class_name_only ); $namespace_path = $this->resolve_namespace_path( $path_parts ); if ( null !== $namespace_path ) { $full_path = $this->base_dir . $namespace_path . $file_name; if ( file_exists( $full_path ) ) { return $full_path; } } return $this->scan_directories_for_file( $file_name ); } /** * Scan known directories for a file. * * @param string $file_name File name to search for. * @return string|null Full file path or null if not found. */ private function scan_directories_for_file( string $file_name ): ?string { $search_dirs = $this->get_search_directories(); foreach ( $search_dirs as $dir ) { $full_path = $this->base_dir . $dir . $file_name; if ( file_exists( $full_path ) ) { return $full_path; } } return null; } /** * Get list of directories to search for class files. * * @return array List of directory paths. */ private function get_search_directories(): array { static $dirs = null; if ( null !== $dirs ) { return $dirs; } $dirs = array( 'shared/', 'bootstrap/', 'admin/', 'admin/settings/', 'admin/posts/', 'admin/statistics/', 'admin/debug/', 'admin/data-management/', 'infrastructure/ajax/', 'infrastructure/cron/', 'infrastructure/rest/', 'infrastructure/cli/', 'infrastructure/database/', 'infrastructure/cache/', 'infrastructure/', ); $feature_base = $this->base_dir . 'features/'; if ( is_dir( $feature_base ) ) { $features = array_filter( (array) scandir( $feature_base ), function ( $item ) use ( $feature_base ) { return '.' !== $item && '..' !== $item && is_dir( $feature_base . $item ); } ); foreach ( $features as $feature ) { $dirs[] = 'features/' . $feature . '/'; } } return $dirs; } /** * Resolve a class path using external namespace roots. * * @param string $class_name Fully-qualified class name. * @return string|null File path or null if not found. */ private function resolve_external_path( string $class_name ): ?string { $relative = substr( $class_name, strlen( $this->namespace_prefix ) ); $parts = explode( '\\', $relative ); if ( empty( $parts ) ) { return null; } $top_level = array_shift( $parts ); if ( ! isset( $this->external_namespace_roots[ $top_level ] ) ) { return null; } $external_root = rtrim( $this->external_namespace_roots[ $top_level ], '/' ) . '/'; if ( empty( $parts ) ) { return null; } $class_name_only = array_pop( $parts ); $file_name = $this->convert_class_to_filename( $class_name_only ); $namespace_path = $this->resolve_namespace_path( $parts ); $full = $external_root . ( $namespace_path ?? '' ) . $file_name; if ( file_exists( $full ) ) { return $full; } return null; } /** * Convert a class name to a kebab-case file name. * * @param string $class_name Class name without namespace. * @return string Kebab-case file name with .php extension. */ private function convert_class_to_filename( string $class_name ): string { return strtolower( str_replace( '_', '-', $class_name ) ) . '.php'; } /** * Resolve a namespace to a directory path. * * @param array $path_parts Namespace parts. * @return string|null Directory path or null. */ private function resolve_namespace_path( array $path_parts ): ?string { if ( empty( $path_parts ) ) { return ''; } $namespace_key = implode( '\\', $path_parts ); if ( isset( $this->namespace_mappings[ $namespace_key ] ) ) { return $this->namespace_mappings[ $namespace_key ]; } for ( $i = count( $path_parts ); $i > 0; $i-- ) { $partial_namespace = implode( '\\', array_slice( $path_parts, 0, $i ) ); if ( isset( $this->namespace_mappings[ $partial_namespace ] ) ) { $remaining_parts = array_slice( $path_parts, $i ); $remaining = empty( $remaining_parts ) ? '' : strtolower( implode( '/', $remaining_parts ) ) . '/'; return $this->namespace_mappings[ $partial_namespace ] . $remaining; } } return strtolower( implode( '/', $path_parts ) ) . '/'; } /** * Require a file and return success status. * * @param string $file_path Absolute file path. * @return bool Whether the file was loaded successfully. */ private function require_file( string $file_path ): bool { try { require_once $file_path; return true; } catch ( \Throwable $e ) { accessibility_audio_tts_debug_log( "Error loading from {$file_path}: " . $e->getMessage() ); return false; } } /** * Initialize namespace-to-directory mappings. * * @return void */ private function initialize_mappings(): void { $this->namespace_mappings = array( // Bootstrap. 'Core' => 'shared/', 'Core\\Bootstrap' => 'bootstrap/', 'Core\\Utilities' => 'bootstrap/', 'Core\\Security' => 'shared/', 'Shared' => 'shared/', // Admin. 'Admin' => 'admin/', 'Admin\\Core' => 'admin/', 'Admin\\Controllers' => 'admin/', 'Admin\\Views' => 'admin/', 'Admin\\Assets' => 'admin/', 'Admin\\Ajax' => 'admin/debug/', 'Admin\\Database' => 'infrastructure/database/', 'Admin\\Renderers' => 'admin/', 'Admin\\Utils' => 'admin/', // CLI. 'CLI' => 'infrastructure/cli/', // Frontend (audio-playback). 'Frontend' => 'features/audio-playback/', 'Frontend\\Views' => 'features/audio-playback/', 'Frontend\\Renderers' => 'features/audio-playback/', // API / ElevenLabs. 'API' => 'features/audio-generation/', 'API\\ElevenLabs' => 'features/audio-generation/', 'API\\Interfaces' => 'features/audio-generation/', 'API\\Handlers' => 'features/audio-generation/', 'API\\Audio' => 'features/storage/', 'API\\Batch' => 'features/audio-generation/', 'API\\Text' => 'features/text-preparation/', 'API\\Core' => 'features/audio-generation/', // Audio. 'Audio' => 'features/audio-generation/', 'Audio\\Handlers' => 'features/audio-generation/', 'Audio\\Managers' => 'features/audio-generation/', 'Audio\\Processors' => 'features/audio-generation/', 'Audio\\Storage' => 'features/storage/', 'Audio\\Validators' => 'features/audio-generation/', // Text. 'Text' => 'features/text-preparation/', 'Text\\Processors' => 'features/text-preparation/', 'Abstracts' => 'shared/', // Interfaces. 'Interfaces' => 'shared/', 'Interfaces\\Core' => 'shared/', 'Interfaces\\Text' => 'features/text-preparation/', 'Interfaces\\Audio' => 'features/audio-generation/', 'Interfaces\\API' => 'features/audio-generation/', // Handlers. 'Handlers' => 'features/audio-generation/', 'Handlers\\Cron' => 'infrastructure/cron/', // Cron. 'Cron' => 'infrastructure/cron/', 'Cron\\Jobs' => 'infrastructure/cron/', 'Cron\\Schedulers' => 'infrastructure/cron/', 'Cron\\Monitors' => 'infrastructure/cron/', // Exceptions. 'Exceptions' => 'shared/', 'Exceptions\\API' => 'shared/', 'Exceptions\\Audio' => 'shared/', 'Exceptions\\Text' => 'shared/', 'Exceptions\\Validation' => 'shared/', // Features. 'Features\\AudioGeneration' => 'features/audio-generation/', 'Features\\AudioGeneration\\Actions' => 'features/audio-generation/', 'Features\\AudioGeneration\\Services' => 'features/audio-generation/', 'Features\\AudioGeneration\\Validators' => 'features/audio-generation/', 'Features\\AudioGeneration\\ElevenLabs' => 'features/audio-generation/', 'Features\\AudioGeneration\\Interfaces' => 'features/audio-generation/', 'Features\\AudioStorage' => 'features/storage/', 'Features\\AudioStorage\\Services' => 'features/storage/', 'Features\\AudioStorage\\Abstracts' => 'features/storage/', 'Features\\AudioPlayer' => 'features/audio-playback/', 'Features\\AudioPlayer\\Renderers' => 'features/audio-playback/', 'Features\\ContentParsing' => 'features/text-preparation/', 'Features\\ContentParsing\\Services' => 'features/text-preparation/', 'Features\\ContentParsing\\Cleaners' => 'features/text-preparation/', 'Features\\ContentParsing\\Validators' => 'features/text-preparation/', 'Features\\ContentParsing\\Interfaces' => 'features/text-preparation/', 'Features\\Tracking' => 'features/tracking/', 'Features\\Tracking\\Actions' => 'features/tracking/', 'Features\\Tracking\\Services' => 'features/tracking/', 'Features\\CronProcessing' => 'infrastructure/cron/', 'Features\\CronProcessing\\Actions' => 'infrastructure/cron/', 'Features\\CronProcessing\\Jobs' => 'infrastructure/cron/', 'Features\\CronProcessing\\Services' => 'infrastructure/cron/', 'Features\\CronProcessing\\Renderers' => 'infrastructure/cron/', 'Features\\AdminSettings' => 'admin/settings/', 'Features\\AdminSettings\\Actions' => 'admin/settings/', 'Features\\AdminSettings\\Services' => 'admin/settings/', 'Features\\AdminSettings\\Renderers' => 'admin/settings/', 'Features\\AdminSettings\\Controllers' => 'admin/settings/', 'Features\\AdminPosts' => 'admin/posts/', 'Features\\AdminPosts\\Actions' => 'admin/posts/', 'Features\\AdminPosts\\Services' => 'admin/posts/', 'Features\\AdminPosts\\Controllers' => 'admin/posts/', 'Features\\AdminPosts\\Renderers' => 'admin/posts/', 'Features\\AdminStatistics' => 'admin/statistics/', 'Features\\AdminStatistics\\Services' => 'admin/statistics/', 'Features\\AdminStatistics\\Controllers' => 'admin/statistics/', 'Features\\AdminDebug' => 'admin/debug/', 'Features\\AdminDebug\\Actions' => 'admin/debug/', 'Features\\AdminDebug\\Services' => 'admin/debug/', // Legacy mappings. 'Admin\\Settings' => 'admin/settings/', 'Admin\\Statistics' => 'admin/statistics/', 'Admin\\Cost' => 'admin/statistics/', 'Admin\\Conversion' => 'features/audio-generation/', 'Admin\\Cron' => 'infrastructure/cron/', ); accessibility_audio_tts_debug_log( 'Enhanced autoloader mappings initialized.' ); } /** * Add a namespace-to-directory mapping. * * @param string $ns_prefix Namespace prefix. * @param string $path Directory path. * @return void */ public function add_namespace_mapping( string $ns_prefix, string $path ): void { $this->namespace_mappings[ $ns_prefix ] = rtrim( $path, '/' ) . '/'; } /** * Add a direct class-to-file mapping. * * @param string $class_name Fully-qualified class name. * @param string $file_path Absolute file path. * @return void */ public function add_class_mapping( string $class_name, string $file_path ): void { $this->path_cache[ $class_name ] = $file_path; } /** * Clear the path cache. * * @return void */ public function clear_cache(): void { $this->path_cache = array(); } /** * Get autoloader statistics. * * @return array Autoloader stats. */ public function get_stats(): array { return array( 'cached_classes' => count( $this->path_cache ), 'namespace_mappings' => count( $this->namespace_mappings ), ); } /** * Add an external namespace root directory. * * @param string $top_level_namespace Top-level namespace name. * @param string $absolute_includes_path Absolute path to the includes directory. * @return void */ public function add_external_namespace_root( string $top_level_namespace, string $absolute_includes_path ): void { if ( '' === $top_level_namespace || '' === $absolute_includes_path ) { return; } $this->external_namespace_roots[ $top_level_namespace ] = rtrim( $absolute_includes_path, '/' ) . '/'; } }