From 9064de4fdb63ca1cca63282996c66eb70f968050 Mon Sep 17 00:00:00 2001 From: Armin Date: Thu, 4 Sep 2025 19:04:54 +0330 Subject: [PATCH 1/2] feat!: better cache layers and more optimized and simpler discovery flows --- README.md | 1024 ++++------------- composer.json | 3 +- config/modulite.php | 50 +- src/Attributes/ComponentDiscovery.php | 233 ---- src/Attributes/FilamentPage.php | 103 -- src/Attributes/FilamentPanel.php | 146 --- src/Attributes/FilamentResource.php | 103 -- src/Attributes/FilamentWidget.php | 103 -- src/Console/Commands/BaseModuliteCommand.php | 255 ---- src/Console/Commands/ClearCacheCommand.php | 89 -- .../Commands/DiscoverComponentsCommand.php | 194 ---- .../Commands/DiscoverPanelsCommand.php | 211 ---- .../Commands/ModuliteBenchmarkCommand.php | 254 ---- .../Commands/ModuliteClearCacheCommand.php | 49 +- .../Commands/ModuliteOptimizeCommand.php | 139 +++ .../Commands/ModuliteStatusCommand.php | 45 +- src/Contracts/ComponentScannerInterface.php | 4 +- src/Contracts/ModuleResolverInterface.php | 55 + src/Contracts/PanelScannerInterface.php | 12 +- src/Exceptions/CacheException.php | 74 -- src/Exceptions/ScanException.php | 99 -- src/Plugins/ModulitePlugin.php | 486 ++++---- src/Providers/ModuliteServiceProvider.php | 193 +++- src/Services/CacheManager.php | 503 -------- src/Services/ComponentDiscoveryService.php | 264 +++-- src/Services/ComponentScannerService.php | 774 ------------- .../ModuleResolvers/NwidartModuleResolver.php | 157 +++ .../PanicDevsModuleResolver.php | 147 +++ src/Services/PanelScannerService.php | 245 ++-- src/Services/UnifiedCacheManager.php | 150 ++- 30 files changed, 1643 insertions(+), 4521 deletions(-) delete mode 100644 src/Attributes/ComponentDiscovery.php delete mode 100644 src/Attributes/FilamentPage.php delete mode 100644 src/Attributes/FilamentPanel.php delete mode 100644 src/Attributes/FilamentResource.php delete mode 100644 src/Attributes/FilamentWidget.php delete mode 100644 src/Console/Commands/BaseModuliteCommand.php delete mode 100644 src/Console/Commands/ClearCacheCommand.php delete mode 100644 src/Console/Commands/DiscoverComponentsCommand.php delete mode 100644 src/Console/Commands/DiscoverPanelsCommand.php delete mode 100644 src/Console/Commands/ModuliteBenchmarkCommand.php create mode 100644 src/Console/Commands/ModuliteOptimizeCommand.php create mode 100644 src/Contracts/ModuleResolverInterface.php delete mode 100644 src/Exceptions/CacheException.php delete mode 100644 src/Exceptions/ScanException.php delete mode 100644 src/Services/CacheManager.php delete mode 100644 src/Services/ComponentScannerService.php create mode 100644 src/Services/ModuleResolvers/NwidartModuleResolver.php create mode 100644 src/Services/ModuleResolvers/PanicDevsModuleResolver.php diff --git a/README.md b/README.md index 24824a7..66a53ee 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,19 @@ -# ๐Ÿš€ Modulite +# Modulite -[![Latest Version on Packagist](https://img.shields.io/packagist/v/panicdevs/modulite.svg?style=flat-square)](https://packagist.org/packages/panicdevs/modulite) -[![Total Downloads](https://img.shields.io/packagist/dt/panicdevs/modulite.svg?style=flat-square)](https://packagist.org/packages/panicdevs/modulite) -[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE) -[![GitHub Sponsors](https://img.shields.io/github/sponsors/panicdevs?style=flat-square&logo=github)](https://github.com/sponsors/panicdevs) +**Automatic Filament Panel Provider discovery for modular Laravel applications with intelligent caching and performance optimization.** -**Automatic Filament Panel Provider discovery for modular Laravel applications with multi-layer caching and performance optimization.** +Modulite automatically discovers and registers Filament panels and components across your modular application structure, eliminating the need for manual registration while providing production-ready performance optimizations. -Modulite revolutionizes how you organize and manage Filament panels in modular Laravel applications. It automatically discovers and registers your Filament Panel Providers across modules, eliminating boilerplate configuration while maintaining peak performance through intelligent caching. +## Features -## โœจ Features +- ๐Ÿ” **Automatic Discovery**: Finds Filament panels, resources, pages, and widgets across modules +- โšก **Production Optimized**: File-based caching system similar to Laravel's bootstrap cache +- ๐Ÿ—๏ธ **Modular Support**: Works with both `nwidart/laravel-modules` and `panicdevs/modules` +- ๐ŸŽฏ **Flexible Patterns**: Configurable naming patterns and discovery locations +- ๐Ÿ“Š **Performance Insights**: Built-in commands for monitoring and optimization +- ๐Ÿ›ก๏ธ **Robust Error Handling**: Graceful failure modes for production environments -- ๐Ÿ” **Auto-Discovery**: Automatically find and register Filament Panel Providers across modules -- โšก **Performance Optimized**: Multi-layer caching with production-grade optimizations -- ๐Ÿ—๏ธ **Modular Architecture**: Perfect for nwidart/laravel-modules and modular applications -- ๐ŸŽฏ **Attribute-Based**: Use `#[FilamentPanel]` attributes for clean, explicit registration -- ๐Ÿ”ง **Flexible Configuration**: Comprehensive configuration options for every use case -- ๐Ÿ“Š **Component Discovery**: Auto-discover Resources, Pages, and Widgets for existing panels -- ๐Ÿ›ก๏ธ **Production Ready**: Built for enterprise applications with robust error handling -- ๐Ÿ”„ **Smart Caching**: File-based cache system similar to Laravel's bootstrap cache -- ๐Ÿ“ˆ **Performance Monitoring**: Built-in benchmarking and performance analysis tools -- ๐ŸŒ **Environment Aware**: Different configurations for development and production - -## ๐ŸŽฏ Quick Start - -### Installation +## Installation Install via Composer: @@ -38,915 +27,376 @@ Publish the configuration file: php artisan vendor:publish --tag=modulite-config ``` -### Basic Usage - -1. **Mark your Panel Provider with the attribute:** - -```php -default() - ->id('admin') - ->path('/admin') - ->colors([ - 'primary' => '#1f2937', - ]); - } -} -``` +### Panel Registration -2. **That's it!** Modulite will automatically discover and register your panel. - -### Advanced Usage - -For more control over panel registration: +Add the `ModulitePlugin` to each panel where you want component discovery: ```php -#[FilamentPanel( - priority: 10, - environment: 'production', - autoRegister: true -)] -class ManagerPanelProvider extends PanelProvider +// In your Panel Provider (e.g., AdminPanelProvider.php) +use PanicDevs\Modulite\Plugins\ModulitePlugin; + +public function panel(Panel $panel): Panel { - // Panel configuration... + return $panel + ->default() + ->id('admin') + ->path('/admin') + ->plugins([ + ModulitePlugin::make(), // Add this to discover components + // ... other plugins + ]) + // ... other panel configuration } ``` -## ๐Ÿ“š Table of Contents +That's it! Modulite will automatically discover and register components for this panel. -- [Installation & Setup](#-installation--setup) -- [Configuration Guide](#-configuration-guide) -- [Usage Examples](#-usage-examples) -- [Component Discovery](#-component-discovery) -- [Performance Optimization](#-performance-optimization) -- [Commands & Tools](#-commands--tools) -- [Troubleshooting](#-troubleshooting) -- [API Reference](#-api-reference) -- [Contributing](#-contributing) +## How It Works -## ๐Ÿ› ๏ธ Installation & Setup +### Discovery Process -### Requirements +Modulite works on two levels: -- PHP 8.2 or higher -- Laravel 10.0 or higher -- Filament 4.0 or higher -- nwidart/laravel-modules (recommended) +1. **Panel Providers**: Automatically discovered and registered by the service provider +2. **Components**: Discovered by the plugin for specific panels + - **Resources**: Filament resource classes for CRUD operations + - **Pages**: Custom Filament pages + - **Widgets**: Dashboard widgets and components -### Step-by-Step Installation +### Module Structure -1. **Install the package:** +For a module named `User`, Modulite expects this structure: -```bash -composer require panicdevs/modulite ``` - -2. **Publish configuration (optional):** - -```bash -php artisan vendor:publish --tag=modulite-config +modules/ +โ”œโ”€โ”€ User/ +โ”‚ โ”œโ”€โ”€ Providers/ +โ”‚ โ”‚ โ””โ”€โ”€ Filament/ +โ”‚ โ”‚ โ””โ”€โ”€ Panels/ +โ”‚ โ”‚ โ””โ”€โ”€ UserPanelProvider.php # Panel definition +โ”‚ โ””โ”€โ”€ Filament/ +โ”‚ โ”œโ”€โ”€ Admin/ # For 'admin' panel +โ”‚ โ”‚ โ”œโ”€โ”€ Resources/ +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ UserResource.php +โ”‚ โ”‚ โ”œโ”€โ”€ Pages/ +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ UserDashboard.php +โ”‚ โ”‚ โ””โ”€โ”€ Widgets/ +โ”‚ โ”‚ โ””โ”€โ”€ UserStatsWidget.php +โ”‚ โ””โ”€โ”€ Manager/ # For 'manager' panel +โ”‚ โ””โ”€โ”€ Resources/ +โ”‚ โ””โ”€โ”€ ProfileResource.php ``` -3. **Create your first panel:** - -```bash -php artisan make:filament-panel admin -``` +### Registration Flow -4. **Add the FilamentPanel attribute:** +1. **Bootstrap**: Service provider registers core services during Laravel boot +2. **Panel Discovery**: Panel providers are automatically discovered and registered +3. **Plugin Registration**: `ModulitePlugin` is registered with specific panels for component discovery +4. **Component Discovery**: When panel loads, plugin discovers components (resources, pages, widgets) +5. **Cache Check**: Fast path checks cache file first (per panel) +6. **Scan & Cache**: On cache miss, scans filesystem and caches results +7. **Component Registration**: Discovered components auto-register to the specific panel -```php -use PanicDevs\Modulite\Attributes\FilamentPanel; +## Configuration -#[FilamentPanel] -class AdminPanelProvider extends PanelProvider -{ - // Your panel configuration -} -``` +### Cache Settings -5. **Verify everything works:** +Configure caching behavior for optimal performance: -```bash -php artisan modulite:status +```php +'cache' => [ + 'enabled' => env('MODULITE_CACHE_ENABLED', !app()->hasDebugModeEnabled()), + 'file' => base_path('bootstrap/cache/modulite.php'), + 'ttl' => env('MODULITE_CACHE_TTL', app()->hasDebugModeEnabled() ? 300 : 0), + 'auto_invalidate' => app()->hasDebugModeEnabled(), +], ``` -### Laravel Sail & Docker - -Modulite works seamlessly with Laravel Sail and Docker environments. No special configuration required. - -## โš™๏ธ Configuration Guide +**Key Settings:** +- `enabled`: Master cache toggle (auto: off in development, on in production) +- `ttl`: Cache lifetime in seconds (0 = never expires, recommended for production) +- `auto_invalidate`: Automatically clear cache when files change (development only) -### Configuration File Overview +### Discovery Locations -The configuration file `config/modulite.php` contains comprehensive options: +Define where to scan for components: ```php -return [ - 'panels' => [ - 'locations' => [ - 'modules/*/Providers/Filament/Panels', - 'foundation/*/Providers/Filament/Panels', - ], - 'patterns' => [ - 'files' => ['*PanelProvider.php', '*Panel.php'], - 'classes' => ['*PanelProvider', '*Panel'], - ], - // ... more options +'panels' => [ + 'locations' => [ + 'modules/*/Providers/Filament/Panels', + 'foundation/*/Providers/Filament/Panels', ], - 'cache' => [ - 'enabled' => env('MODULITE_CACHE_ENABLED', !app()->hasDebugModeEnabled()), - 'file' => base_path('bootstrap/cache/modulite.php'), - 'ttl' => env('MODULITE_CACHE_TTL', app()->hasDebugModeEnabled() ? 300 : 0), +], + +'components' => [ + 'locations' => [ + 'modules/*/Filament/{panel}/Resources', + 'modules/*/Filament/{panel}/Pages', + 'modules/*/Filament/{panel}/Widgets', ], - // ... more configurations -]; +], ``` -### Key Configuration Sections +**Placeholders:** +- `*`: Module wildcard (e.g., `User`, `Blog`) +- `{panel}`: Panel ID placeholder (e.g., `Admin`, `Manager`) -#### Panel Discovery +### Validation Rules -Configure where and how panels are discovered: +Control how strict discovery validation should be: ```php 'panels' => [ - 'locations' => [ - 'modules/*/Providers/Filament/Panels', // Your module structure - 'app/Filament/Panels', // App-level panels - ], - 'patterns' => [ - 'files' => ['*PanelProvider.php'], // File naming patterns - 'classes' => ['*PanelProvider'], // Class naming patterns - ], 'validation' => [ - 'strict_inheritance' => false, // Require exact Filament classes + 'strict_inheritance' => env('MODULITE_STRICT_INHERITANCE', false), 'must_extend' => 'Filament\PanelProvider', + 'must_be_instantiable' => true, + 'allow_custom_base_classes' => env('MODULITE_ALLOW_CUSTOM_BASE_CLASSES', true), ], -] +], ``` -#### Cache Configuration +**When to Use:** +- `strict_inheritance => true`: Enforces exact class inheritance +- `allow_custom_base_classes => false`: Only allows direct Filament class inheritance +- Use strict settings for large teams to enforce conventions + +### Module Integration -Optimize performance with intelligent caching: +Choose your module management approach: ```php -'cache' => [ - 'enabled' => env('MODULITE_CACHE_ENABLED', !app()->hasDebugModeEnabled()), - 'file' => base_path('bootstrap/cache/modulite.php'), - 'ttl' => env('MODULITE_CACHE_TTL', 0), // 0 = never expires (production) - 'auto_invalidate' => app()->hasDebugModeEnabled(), -] +'modules' => [ + 'approach' => env('MODULITE_APPROACH', 'panicdevs'), // or 'nwidart' + 'scan_only_enabled' => true, + 'respect_module_priority' => true, +], ``` -#### Performance Settings +### Performance Optimization -Fine-tune performance for your use case: +Configure for production performance: ```php 'performance' => [ - 'lazy_discovery' => true, // Defer scanning until needed + 'lazy_discovery' => env('MODULITE_LAZY_DISCOVERY', true), 'memory_optimization' => [ 'batch_size' => 100, 'clear_stat_cache' => true, 'gc_after_scan' => true, ], -] +], ``` -### Environment Variables +## Environment Variables -Control Modulite behavior via environment variables: +Set these in your `.env` for easy configuration: -```env -# Cache Configuration +```bash +# Cache Control MODULITE_CACHE_ENABLED=true MODULITE_CACHE_TTL=0 # Performance MODULITE_LAZY_DISCOVERY=true +MODULITE_STATIC_CACHING=true -# Error Handling -MODULITE_FAIL_SILENTLY=false +# Validation +MODULITE_STRICT_INHERITANCE=false +MODULITE_ALLOW_CUSTOM_BASE_CLASSES=true -# Logging +# Debugging MODULITE_LOGGING_ENABLED=false -MODULITE_LOG_CHANNEL=stack -MODULITE_LOG_LEVEL=info ``` -### Production Configuration - -For optimal production performance: - -```env -# .env.production -APP_DEBUG=false -MODULITE_CACHE_ENABLED=true -MODULITE_CACHE_TTL=0 -MODULITE_LAZY_DISCOVERY=true -MODULITE_FAIL_SILENTLY=true -MODULITE_LOGGING_ENABLED=false -``` +## Production Setup -Then run: +### Optimization Commands ```bash -php artisan optimize -php artisan config:cache -``` - -## ๐ŸŽจ Usage Examples - -### Basic Panel Creation - -Create a simple admin panel: - -```php -default() - ->id('admin') - ->path('/admin') - ->login() - ->colors([ - 'primary' => '#1f2937', - ]) - ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') - ->pages([ - \App\Filament\Pages\Dashboard::class, - ]); - } -} -``` - -### Multi-Environment Panels - -Create panels that only register in specific environments: - -```php -#[FilamentPanel(environment: 'local')] -class DevelopmentPanelProvider extends PanelProvider -{ - public function panel(Panel $panel): Panel - { - return $panel - ->id('dev') - ->path('/dev') - ->colors(['primary' => '#10b981']); - } -} - -#[FilamentPanel(environment: 'production', priority: 100)] -class ProductionPanelProvider extends PanelProvider -{ - // Production-optimized panel configuration -} -``` - -### Conditional Panel Registration - -Use custom conditions for panel registration: - -```php -#[FilamentPanel( - conditions: ['feature.admin_panel_enabled'], - priority: 50 -)] -class ConditionalPanelProvider extends PanelProvider -{ - public function panel(Panel $panel): Panel - { - // Only registers if condition is met - return $panel->id('conditional'); - } -} -``` - -### Priority-Based Loading - -Control panel registration order: - -```php -#[FilamentPanel(priority: 100)] // Loads first -class CorePanelProvider extends PanelProvider { } - -#[FilamentPanel(priority: 50)] // Loads second -class ManagerPanelProvider extends PanelProvider { } - -#[FilamentPanel(priority: 10)] // Loads third -class UserPanelProvider extends PanelProvider { } -``` - -## ๐Ÿงฉ Component Discovery - -Modulite can automatically discover and register Filament components (Resources, Pages, Widgets) for existing panels. - -### Configuration - -Enable component discovery in your configuration: - -```php -'components' => [ - 'locations' => [ - 'modules/*/Filament/{panel}/Resources', - 'modules/*/Filament/{panel}/Pages', - 'modules/*/Filament/{panel}/Widgets', - ], - 'types' => [ - 'resources' => [ - 'enabled' => true, - 'naming_pattern' => '*Resource.php', - ], - 'pages' => [ - 'enabled' => true, - 'naming_pattern' => '*Page.php', - ], - 'widgets' => [ - 'enabled' => true, - 'naming_pattern' => '*Widget.php', - ], - ], -] -``` - -### Directory Structure - -Organize your components following this structure: - -``` -modules/ -โ”œโ”€โ”€ User/ -โ”‚ โ””โ”€โ”€ Filament/ -โ”‚ โ”œโ”€โ”€ Admin/ -โ”‚ โ”‚ โ”œโ”€โ”€ Resources/ -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ UserResource.php -โ”‚ โ”‚ โ”œโ”€โ”€ Pages/ -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ DashboardPage.php -โ”‚ โ”‚ โ””โ”€โ”€ Widgets/ -โ”‚ โ”‚ โ””โ”€โ”€ UserStatsWidget.php -โ”‚ โ””โ”€โ”€ Manager/ -โ”‚ โ”œโ”€โ”€ Resources/ -โ”‚ โ””โ”€โ”€ Pages/ -โ””โ”€โ”€ Blog/ - โ””โ”€โ”€ Filament/ - โ””โ”€โ”€ Admin/ - โ”œโ”€โ”€ Resources/ - โ”‚ โ””โ”€โ”€ PostResource.php - โ””โ”€โ”€ Pages/ -``` - -### Component Example - -```php -discoverComponents('admin'); +# Clear caches when needed +php artisan modulite:cache --force -// Discover specific component types -$resources = $componentDiscovery->discoverResources('admin'); -$pages = $componentDiscovery->discoverPages('admin'); -$widgets = $componentDiscovery->discoverWidgets('admin'); -``` - -## โšก Performance Optimization - -### Production Setup - -For maximum performance in production: - -1. **Configure caching:** - -```env -MODULITE_CACHE_ENABLED=true -MODULITE_CACHE_TTL=0 # Never expires -``` - -2. **Optimize Laravel:** +# Check status and performance +php artisan modulite:status -```bash -php artisan config:cache -php artisan route:cache -php artisan view:cache -php artisan optimize +# Detailed diagnostics +php artisan modulite:status --vvv ``` -3. **Enable OPcache** in your PHP configuration: +### Deployment Workflow -```ini -opcache.enable=1 -opcache.memory_consumption=128 -opcache.interned_strings_buffer=8 -opcache.max_accelerated_files=4000 -opcache.revalidate_freq=60 -``` +1. **Build Assets**: Run your normal build process +2. **Cache Application**: `php artisan optimize` +3. **Cache Modulite**: `php artisan modulite:cache` +4. **Deploy**: Your cached discoveries are ready -### Performance Monitoring +### Cache Management -Use the built-in benchmark command: +The cache system works like Laravel's bootstrap cache: ```bash -# Basic benchmark -php artisan modulite:benchmark - -# Detailed benchmark with warm cache -php artisan modulite:benchmark --warm-cache --show-details --iterations=1000 -``` +# Cache file location +bootstrap/cache/modulite.php -Sample output: +# Clear with Laravel caches +php artisan optimize:clear +# Or clear specifically +php artisan modulite:cache --force ``` -๐Ÿš€ Modulite Performance Benchmark -===================================== - -Environment Information: -+----------------+--------------------------------------------+ -| Setting | Value | -+----------------+--------------------------------------------+ -| Environment | production | -| Debug Mode | โœ— Disabled | -| Cache Enabled | โœ“ Enabled | -| Cache File | /var/www/html/bootstrap/cache/modulite.php | -| Cache TTL | 0 | -| Lazy Discovery | โœ“ Enabled | -| PHP Version | 8.4.11 | -+----------------+--------------------------------------------+ - -๐Ÿ“ˆ Benchmark Results: -+----------------------+------------+ -| Metric | Value | -+----------------------+------------+ -| Operation | Cache Read | -| Average (ms) | 0 | -| Median (ms) | 0 | -| Max (ms) | 0.02 | -| 95th Percentile (ms) | 0 | -+----------------------+------------+ - -+----------------------+-----------------+ -| Operation | Panel Discovery | -| Average (ms) | 0 | -| Median (ms) | 0 | -| Max (ms) | 0.011 | -+----------------------+-----------------+ - -๐Ÿ’ก Performance Recommendations: -โœ… Cache performance is excellent (0ms average) -โœ… Panel discovery performance is good (0ms average) -``` - -### Memory Optimization - -For large applications with many modules: -```php -'performance' => [ - 'memory_optimization' => [ - 'batch_size' => 50, // Process files in smaller batches - 'clear_stat_cache' => true, // Clear file stat cache regularly - 'gc_after_scan' => true, // Force garbage collection - ], - 'concurrent' => [ - 'enabled' => false, // Enable for very large codebases - 'max_workers' => 4, - ], -] -``` +## Troubleshooting -### Cache Strategies - -**Development (auto-invalidation):** -```php -'cache' => [ - 'ttl' => 300, // 5 minutes - 'auto_invalidate' => true, // Clear on file changes -] -``` - -**Production (persistent cache):** -```php -'cache' => [ - 'ttl' => 0, // Never expires - 'auto_invalidate' => false, // Manual cache clearing only -] -``` - -## ๐Ÿ”ง Commands & Tools - -### Status Command - -Check Modulite's current state: +### Check Discovery Status ```bash php artisan modulite:status ``` -Options: -- `--clear-cache`: Clear all Modulite cache -- `--scan`: Force rescan of panels and components -- `--vvv`: Show detailed information - -### Cache Management - -Clear Modulite cache: - -```bash -php artisan modulite:clear-cache -``` - -### Benchmark Command +This shows: +- Configuration summary +- Module status +- Discovered panels and components +- Cache statistics -Performance testing and optimization: - -```bash -# Basic benchmark -php artisan modulite:benchmark - -# Advanced benchmarking -php artisan modulite:benchmark \ - --iterations=1000 \ - --warm-cache \ - --show-details -``` +### Common Issues -### Panel Discovery +**No panels discovered:** +- Check module structure matches expected patterns +- Verify panel classes extend `PanelProvider` +- Check if modules are enabled -Manually discover panels (useful for debugging): +**Performance issues:** +- Enable caching: `MODULITE_CACHE_ENABLED=true` +- Set TTL to 0 for production: `MODULITE_CACHE_TTL=0` +- Run `php artisan modulite:cache` -```bash -php artisan modulite:discover-panels -``` +**Components not showing:** +- Verify directory structure matches panel patterns +- Check component naming (must end with `Resource`, `Page`, `Widget`) +- Ensure components extend proper Filament base classes -### Component Discovery +### Debug Mode -Discover components for a specific panel: +Enable detailed logging in development: ```bash -php artisan modulite:discover-components admin -``` - -## ๐Ÿ› Troubleshooting - -### Common Issues - -#### Cache Not Working - -**Problem:** Panels not being cached or cache misses. - -**Solutions:** -1. Check cache directory permissions: - ```bash - chmod 755 bootstrap/cache - ``` - -2. Verify cache configuration: - ```bash - php artisan modulite:status - ``` - -3. Clear and rebuild cache: - ```bash - php artisan modulite:clear-cache - php artisan optimize - ``` - -#### Panels Not Discovered - -**Problem:** Panel providers not being found. - -**Diagnostic steps:** -1. Check file location matches configuration: - ```php - // config/modulite.php - 'panels' => [ - 'locations' => [ - 'modules/*/Providers/Filament/Panels', // Check this path - ], - ] - ``` - -2. Verify attribute is present: - ```php - #[FilamentPanel] // Must be present - class YourPanelProvider extends PanelProvider - ``` - -3. Check class naming pattern: - ```php - 'patterns' => [ - 'files' => ['*PanelProvider.php'], // Must match filename - ] - ``` - -4. Run discovery command: - ```bash - php artisan modulite:discover-panels --verbose - ``` - -#### Performance Issues - -**Problem:** Slow panel loading or discovery. - -**Solutions:** -1. Enable caching in production: - ```env - MODULITE_CACHE_ENABLED=true - MODULITE_CACHE_TTL=0 - ``` - -2. Use lazy discovery: - ```env - MODULITE_LAZY_DISCOVERY=true - ``` - -3. Optimize scan locations (avoid deep directory structures): - ```php - 'scanning' => [ - 'max_depth' => 3, - 'excluded_directories' => ['tests', 'vendor', 'node_modules'], - ] - ``` - -4. Run benchmark to identify bottlenecks: - ```bash - php artisan modulite:benchmark --show-details - ``` - -#### Memory Issues - -**Problem:** High memory usage during discovery. - -**Solutions:** -1. Enable memory optimization: - ```php - 'performance' => [ - 'memory_optimization' => [ - 'batch_size' => 50, - 'gc_after_scan' => true, - ], - ] - ``` - -2. Increase PHP memory limit temporarily: - ```bash - php -d memory_limit=512M artisan modulite:status - ``` - -### Debugging Tools - -#### Enable Debug Logging - -```env MODULITE_LOGGING_ENABLED=true MODULITE_LOG_LEVEL=debug ``` -#### Verbose Status Check +### Cache Issues + +Clear all caches if you encounter stale data: ```bash -php artisan modulite:status --vvv +php artisan modulite:cache --force +php artisan optimize:clear ``` -#### Cache Analysis +## Performance -```bash -php artisan modulite:benchmark --show-details -``` +### Benchmarks -### Environment-Specific Issues +- **Cold start** (no cache): ~1-20ms depending on module count +- **Warm cache**: ~1-2ms (file include time) +- **Laravel response overhead**: <1ms when optimized -#### Docker/Sail Issues +### Production Optimizations -- Ensure proper file permissions in containers -- Check volume mounts for cache directories -- Verify PHP extensions are installed +Modulite automatically optimizes for production: -#### Production Issues +- Static caching eliminates repeated file reads +- Lazy discovery defers scanning until needed +- Single cache file minimizes I/O operations +- TTL of 0 prevents unnecessary expiration checks -- Always use `APP_DEBUG=false` -- Enable OPcache for better performance -- Use `php artisan optimize` after deployment +## Advanced Usage -## ๐Ÿ“– API Reference +### Custom Base Classes -### FilamentPanel Attribute +You can use custom base classes for components: ```php -#[FilamentPanel( - priority: int = 0, // Registration priority - environment: ?string = null, // Target environment - conditions: array = [], // Custom conditions - autoRegister: bool = true // Auto-registration flag -)] +'components' => [ + 'types' => [ + 'resources' => [ + 'allow_custom_base_classes' => true, + 'strict_inheritance' => false, + ], + ], +], ``` -**Methods:** -- `shouldRegister(string $environment): bool` - Check if panel should register -- `toArray(): array` - Get attribute configuration +### Manual Component Registration -### Cache Manager Interface +For edge cases, disable auto-discovery and register manually: ```php -interface CacheManagerInterface -{ - public function get(string $key, mixed $default = null): mixed; - public function put(string $key, mixed $value, ?int $ttl = null): bool; - public function remember(string $key, callable $callback): array; - public function forget(string $key): void; - public function flush(): void; - public function has(string $key): bool; - public function getStats(): array; -} +'components' => [ + 'registration' => [ + 'auto_register' => false, + ], +], ``` -### Panel Scanner Interface - -```php -interface PanelScannerInterface -{ - public function discoverPanels(): array; - public function getScanStats(): array; -} -``` +### Multiple Panel Support -### Component Scanner Interface +Modulite automatically handles multiple panels per module. Each panel needs the plugin registered for component discovery: ```php -interface ComponentScannerInterface +// AdminPanelProvider.php +public function panel(Panel $panel): Panel { - public function discoverComponents(string $panelName): array; - public function discoverComponentType(string $panelName, string $componentType): array; - public function getScanStats(): array; - public function isComponentType(string $className, string $componentType): bool; + return $panel + ->id('admin') + ->plugins([ + ModulitePlugin::make(), + ]); } -``` - -### Service Examples -#### Using the Panel Scanner - -```php -use PanicDevs\Modulite\Contracts\PanelScannerInterface; - -$scanner = app(PanelScannerInterface::class); -$panels = $scanner->discoverPanels(); - -foreach ($panels as $panelClass) { - // Register panel - app()->register($panelClass); +// ManagerPanelProvider.php +public function panel(Panel $panel): Panel +{ + return $panel + ->id('manager') + ->plugins([ + ModulitePlugin::make(), + ]); } ``` -#### Using Component Discovery - -```php -use PanicDevs\Modulite\Services\ComponentDiscoveryService; - -$discovery = new ComponentDiscoveryService(); - -// Discover all components for admin panel -$components = $discovery->discoverComponents('admin'); - -// Get specific component types -$resources = $discovery->discoverResources('admin'); -$pages = $discovery->discoverPages('admin'); -$widgets = $discovery->discoverWidgets('admin'); -``` - -#### Cache Management - -```php -use PanicDevs\Modulite\Contracts\CacheManagerInterface; - -$cache = app(CacheManagerInterface::class); - -// Store data -$cache->put('panels:admin', $panelData, 3600); - -// Retrieve data -$data = $cache->get('panels:admin', []); +Components are discovered based on directory structure: -// Remember pattern -$panels = $cache->remember('discovered_panels', function() { - return $this->scanForPanels(); -}); - -// Clear cache -$cache->flush(); ``` - -## ๐Ÿค Contributing - -We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. - -### Development Setup - -1. Clone the repository -2. Install dependencies: `composer install` -3. Run tests: `composer test` -4. Check code style: `composer cs-check` - -### Testing - -```bash -# Run all tests -composer test - -# Run with coverage -composer test-coverage - -# Run specific test suite -./vendor/bin/phpunit tests/Unit/PanelDiscoveryTest.php -``` - -### Code Style - -We use Laravel Pint for code formatting: - -```bash -composer cs-fix +modules/User/Filament/ +โ”œโ”€โ”€ Admin/Resources/UserResource.php # Registers to 'admin' panel +โ”œโ”€โ”€ Manager/Resources/ProfileResource.php # Registers to 'manager' panel +โ””โ”€โ”€ Public/Pages/LoginPage.php # Registers to 'public' panel ``` -## ๐Ÿ“ Changelog - -Please see [CHANGELOG.md](CHANGELOG.md) for recent changes. +## Requirements -## ๐Ÿ”’ Security +- PHP 8.2+ +- Filament 4.0+ -If you discover any security-related issues, please email security@panicdevs.agency instead of using the issue tracker. +## Support -## ๐Ÿ“„ License - -This package is open-sourced software licensed under the [MIT license](LICENSE). - -## ๐Ÿ’– Support - -- โญ Star this repository -- ๐Ÿ› [Report bugs](https://github.com/panicdevs/modulite/issues) -- ๐Ÿ’ก [Request features](https://github.com/panicdevs/modulite/issues) -- ๐Ÿ’ฐ [Sponsor development](https://github.com/sponsors/panicdevs) - -## ๐Ÿ™ Credits - -- **[Armin Hooshmand](https://github.com/NotifyHex)** - Creator & Maintainer -- **[PanicDevs](https://panicdevs.agency)** - Organization -- All [contributors](https://github.com/panicdevs/modulite/contributors) - -Built with โค๏ธ by [PanicDevs](https://panicdevs.agency) +- ๐Ÿ“– [Documentation](https://github.com/panicdevs/modulite#readme) +- ๐Ÿ› [Issues](https://github.com/panicdevs/modulite/issues) --- -
- -**[Documentation](https://github.com/panicdevs/modulite#readme) โ€ข [Installation](https://github.com/panicdevs/modulite#installation--setup) โ€ข [Configuration](https://github.com/panicdevs/modulite#configuration-guide) โ€ข [Examples](https://github.com/panicdevs/modulite#usage-examples)** - -
+**Made with โค๏ธ by [PanicDevs](https://github.com/panicdevs)** diff --git a/composer.json b/composer.json index addc9bb..5a79497 100644 --- a/composer.json +++ b/composer.json @@ -45,8 +45,7 @@ ], "require": { "php": ">=8.2", - "filament/filament": "^4.0", - "nwidart/laravel-modules": "^11.0|^12.0" + "filament/filament": "^4.0" }, "autoload": { "psr-4": { diff --git a/config/modulite.php b/config/modulite.php index c9c24f7..817694e 100644 --- a/config/modulite.php +++ b/config/modulite.php @@ -188,11 +188,25 @@ */ 'registration' => [ 'auto_register' => env('MODULITE_AUTO_REGISTER_COMPONENTS', true), - 'sort_by' => 'name', // 'name', 'priority', 'none' - 'validate_before_register' => app()->hasDebugModeEnabled(), + 'sort_by' => env('MODULITE_SORT_COMPONENTS', 'none'), // 'name', 'priority', 'none' + 'validate_before_register' => env('MODULITE_VALIDATE_COMPONENTS', app()->hasDebugModeEnabled()), 'group_by_module' => true, ], + /* + |-------------------------------------------------------------------------- + | Component Discovery Performance + |-------------------------------------------------------------------------- + | + | Performance settings for component discovery process. + | + */ + 'discovery' => [ + 'cache_discovery_stats' => env('MODULITE_CACHE_DISCOVERY_STATS', false), + 'skip_disabled_modules' => env('MODULITE_SKIP_DISABLED_MODULES', true), + 'batch_component_scans' => env('MODULITE_BATCH_COMPONENT_SCANS', true), + ], + /* |-------------------------------------------------------------------------- | Scanning Options @@ -286,6 +300,22 @@ 'enabled' => true, 'max_items' => 1000, ], + + /* + |-------------------------------------------------------------------------- + | Performance Optimizations + |-------------------------------------------------------------------------- + | + | Additional performance optimizations for production environments. + | These settings are automatically optimized for production vs development. + | + */ + 'optimizations' => [ + 'static_caching' => env('MODULITE_STATIC_CACHING', true), + 'defer_validation' => env('MODULITE_DEFER_VALIDATION', !app()->hasDebugModeEnabled()), + 'skip_duplicate_panels' => env('MODULITE_SKIP_DUPLICATE_PANELS', true), + 'disable_reflection' => env('MODULITE_DISABLE_REFLECTION', app()->isProduction()), + ], ], /* @@ -348,10 +378,24 @@ | Module Integration |-------------------------------------------------------------------------- | - | Configure integration with nwidart/laravel-modules package. + | Configure integration with different module management packages. | */ 'modules' => [ + /* + |-------------------------------------------------------------------------- + | Module System Approach + |-------------------------------------------------------------------------- + | + | Specify which module management system you're using: + | - 'nwidart': Use nwidart/laravel-modules package + | - 'panicdevs': Use panicdevs/modules package + | + | This determines how Modulite discovers and interacts with your modules. + | + */ + 'approach' => env('MODULITE_APPROACH', 'panicdevs'), + /* |-------------------------------------------------------------------------- | Module System Integration diff --git a/src/Attributes/ComponentDiscovery.php b/src/Attributes/ComponentDiscovery.php deleted file mode 100644 index c3aad5c..0000000 --- a/src/Attributes/ComponentDiscovery.php +++ /dev/null @@ -1,233 +0,0 @@ - true, 'pages' => false, 'widgets' => true], - * cacheEnabled: false, - * sortBy: 'priority' - * )] - * class AdminPanelProvider extends PanelProvider - * { - * // Panel implementation - * } - * - * // Minimal configuration - * #[ComponentDiscovery( - * enabledTypes: ['resources' => true, 'pages' => false, 'widgets' => false] - * )] - * class ApiPanelProvider extends PanelProvider - * { - * // Only resources, no pages or widgets - * } - * ``` - * - * @package PanicDevs\Modulite\Attributes - */ -#[Attribute(Attribute::TARGET_CLASS)] -class ComponentDiscovery -{ - /** - * Create a new ComponentDiscovery attribute. - * - * @param array|null $locations Custom scan locations for this panel. Null = use default - * @param array|null $enabledTypes Component types to enable/disable. Null = use default - * @param array|null $excludedDirectories Directories to exclude from scanning. Null = use default - * @param bool|null $cacheEnabled Enable/disable caching for this panel. Null = use default - * @param string|null $sortBy How to sort components ('name', 'priority', 'none'). Null = use default - * @param bool|null $validateComponents Enable/disable component validation. Null = use default - * @param int|null $maxDepth Maximum scan depth. Null = use default - * @param bool|null $followSymlinks Whether to follow symlinks. Null = use default - * @param array $options Additional custom options - */ - public function __construct( - public ?array $locations = null, - public ?array $enabledTypes = null, - public ?array $excludedDirectories = null, - public ?bool $cacheEnabled = null, - public ?string $sortBy = null, - public ?bool $validateComponents = null, - public ?int $maxDepth = null, - public ?bool $followSymlinks = null, - public array $options = [] - ) { - } - - /** - * Get scan locations for this panel. - * - * @param string $panelId Panel identifier for placeholder replacement - * @param array $defaultLocations Default locations from config - * @return array Resolved scan locations - */ - public function getLocations(string $panelId, array $defaultLocations = []): array - { - $locations = $this->locations ?? $defaultLocations; - - // Replace {PanelName} placeholder with actual panel ID - return array_map( - fn(string $location) => str_replace('{PanelName}', $panelId, $location), - $locations - ); - } - - /** - * Get enabled component types. - * - * @param array $defaultTypes Default types from config - * @return array Resolved enabled types - */ - public function getEnabledTypes(array $defaultTypes = []): array - { - if (null === $this->enabledTypes) { - return $defaultTypes; - } - - // Merge with defaults, allowing overrides - return array_merge($defaultTypes, $this->enabledTypes); - } - - /** - * Get excluded directories. - * - * @param array $defaultExcluded Default excluded directories from config - * @return array Resolved excluded directories - */ - public function getExcludedDirectories(array $defaultExcluded = []): array - { - return $this->excludedDirectories ?? $defaultExcluded; - } - - /** - * Check if caching is enabled. - * - * @param bool $defaultEnabled Default cache setting from config - * @return bool Whether caching is enabled - */ - public function isCacheEnabled(bool $defaultEnabled = true): bool - { - return $this->cacheEnabled ?? $defaultEnabled; - } - - /** - * Get sort method. - * - * @param string $defaultSort Default sort method from config - * @return string Sort method ('name', 'priority', 'none') - */ - public function getSortBy(string $defaultSort = 'name'): string - { - return $this->sortBy ?? $defaultSort; - } - - /** - * Check if component validation is enabled. - * - * @param bool $defaultValidation Default validation setting from config - * @return bool Whether validation is enabled - */ - public function isValidationEnabled(bool $defaultValidation = false): bool - { - return $this->validateComponents ?? $defaultValidation; - } - - /** - * Get maximum scan depth. - * - * @param int $defaultDepth Default max depth from config - * @return int Maximum scan depth - */ - public function getMaxDepth(int $defaultDepth = 10): int - { - return $this->maxDepth ?? $defaultDepth; - } - - /** - * Check if symlinks should be followed. - * - * @param bool $defaultFollow Default symlink following setting from config - * @return bool Whether to follow symlinks - */ - public function shouldFollowSymlinks(bool $defaultFollow = false): bool - { - return $this->followSymlinks ?? $defaultFollow; - } - - /** - * Get additional options. - * - * @return array Additional options - */ - public function getOptions(): array - { - return $this->options; - } - - /** - * Convert attribute to configuration array. - * - * @param string $panelId Panel identifier - * @param array $defaultConfig Default configuration - * @return array Resolved configuration - */ - public function toConfig(string $panelId, array $defaultConfig = []): array - { - $config = $defaultConfig; - - // Override scan locations - if (null !== $this->locations) { - $config['component_scan']['locations'] = $this->getLocations($panelId, []); - } - - // Override enabled types - if (null !== $this->enabledTypes) { - foreach ($this->enabledTypes as $type => $enabled) { - $config['component_scan']['types'][$type]['enabled'] = $enabled; - } - } - - // Override other settings - if (null !== $this->excludedDirectories) { - $config['component_scan']['excluded_directories'] = $this->excludedDirectories; - } - - if (null !== $this->cacheEnabled) { - $config['cache']['enabled'] = $this->cacheEnabled; - } - - if (null !== $this->sortBy) { - $config['component_scan']['registration']['sort_by'] = $this->sortBy; - } - - if (null !== $this->validateComponents) { - $config['component_scan']['registration']['validate_components'] = $this->validateComponents; - } - - if (null !== $this->maxDepth) { - $config['component_scan']['max_depth'] = $this->maxDepth; - } - - if (null !== $this->followSymlinks) { - $config['component_scan']['follow_symlinks'] = $this->followSymlinks; - } - - // Merge additional options - $config = array_merge_recursive($config, $this->options); - - return $config; - } -} diff --git a/src/Attributes/FilamentPage.php b/src/Attributes/FilamentPage.php deleted file mode 100644 index 6a9d92d..0000000 --- a/src/Attributes/FilamentPage.php +++ /dev/null @@ -1,103 +0,0 @@ - $panels Specific panels this page should be registered with. Empty = all panels - * @param array $options Additional options for the page - */ - public function __construct( - public int $priority = 0, - public bool $enabled = true, - public array $panels = [], - public array $options = [] - ) { - } - - /** - * Check if this page is enabled for auto-discovery. - */ - public function isEnabled(): bool - { - return $this->enabled; - } - - /** - * Check if this page should be registered with a specific panel. - * - * @param string $panelId Panel identifier - * @return bool True if page should be registered with panel - */ - public function isEnabledForPanel(string $panelId): bool - { - if (!$this->enabled) { - return false; - } - - // If no specific panels defined, enable for all panels - if (empty($this->panels)) { - return true; - } - - return in_array($panelId, $this->panels, true); - } - - /** - * Get the priority for sorting. - */ - public function getPriority(): int - { - return $this->priority; - } - - /** - * Get additional options. - */ - public function getOptions(): array - { - return $this->options; - } -} diff --git a/src/Attributes/FilamentPanel.php b/src/Attributes/FilamentPanel.php deleted file mode 100644 index df2f367..0000000 --- a/src/Attributes/FilamentPanel.php +++ /dev/null @@ -1,146 +0,0 @@ -default() - * ->id('admin') - * ->path('/admin'); - * } - * } - * ``` - * - * Advanced Usage with Configuration: - * - * ```php - * #[FilamentPanel(priority: 10, environment: 'production')] - * class ProductionPanelProvider extends PanelProvider - * { - * // Panel configuration... - * } - * ``` - * - * Requirements: - * - Class must be instantiable (not abstract or interface) - * - Class should extend Filament's PanelProvider or provide panel configuration - * - Class must be autoloadable through PSR-4 standards - * - * Discovery Process: - * 1. Modulite scans configured directories for PHP files - * 2. Extracts class names using token parsing - * 3. Uses reflection to check for this attribute - * 4. Registers matching classes as panel providers - * - * Performance Considerations: - * - Attribute detection uses reflection, which is cached for performance - * - Classes are only loaded when the attribute is confirmed to exist - * - File scanning is optimized with configurable depth limits and exclusions - * - * @package PanicDevs\Modulite\Attributes - * @since 1.0.0 - */ -#[Attribute(Attribute::TARGET_CLASS)] -readonly class FilamentPanel -{ - /** - * Create a new FilamentPanel attribute instance. - * - * @param int $priority Priority for panel registration (higher = earlier registration) - * @param string|null $environment Limit panel to specific environment(s) - * @param array $conditions Additional conditions for panel loading - * @param bool $autoRegister Whether to automatically register this panel (default: true) - */ - public function __construct( - public int $priority = 0, - public ?string $environment = null, - public array $conditions = [], - public bool $autoRegister = true, - ) { - } - - /** - * Check if this panel should be registered in the current environment. - * - * @param string $currentEnvironment Current application environment - * @return bool True if panel should be registered - */ - public function shouldRegister(string $currentEnvironment): bool - { - if (!$this->autoRegister) { - return false; - } - - if (null !== $this->environment && $this->environment !== $currentEnvironment) { - return false; - } - - // Additional condition checks could be implemented here - foreach ($this->conditions as $condition) { - if (!$this->evaluateCondition($condition)) { - return false; - } - } - - return true; - } - - /** - * Evaluate a registration condition. - * - * This method can be extended to support complex registration logic - * based on application state, configuration, or other factors. - * - * @param string $condition Condition to evaluate - * @return bool True if condition is met - */ - protected function evaluateCondition(string $condition): bool - { - // For now, this is a placeholder for future condition evaluation - // Could support things like: - // - config('app.feature_flags.admin_panel') - // - class_exists('SomeRequiredClass') - // - function_exists('some_required_function') - - return true; - } - - /** - * Get attribute configuration as array. - * - * @return array Attribute configuration - */ - public function toArray(): array - { - return [ - 'priority' => $this->priority, - 'environment' => $this->environment, - 'conditions' => $this->conditions, - 'auto_register' => $this->autoRegister, - ]; - } -} diff --git a/src/Attributes/FilamentResource.php b/src/Attributes/FilamentResource.php deleted file mode 100644 index 12a844e..0000000 --- a/src/Attributes/FilamentResource.php +++ /dev/null @@ -1,103 +0,0 @@ - $panels Specific panels this resource should be registered with. Empty = all panels - * @param array $options Additional options for the resource - */ - public function __construct( - public int $priority = 0, - public bool $enabled = true, - public array $panels = [], - public array $options = [] - ) { - } - - /** - * Check if this resource is enabled for auto-discovery. - */ - public function isEnabled(): bool - { - return $this->enabled; - } - - /** - * Check if this resource should be registered with a specific panel. - * - * @param string $panelId Panel identifier - * @return bool True if resource should be registered with panel - */ - public function isEnabledForPanel(string $panelId): bool - { - if (!$this->enabled) { - return false; - } - - // If no specific panels defined, enable for all panels - if (empty($this->panels)) { - return true; - } - - return in_array($panelId, $this->panels, true); - } - - /** - * Get the priority for sorting. - */ - public function getPriority(): int - { - return $this->priority; - } - - /** - * Get additional options. - */ - public function getOptions(): array - { - return $this->options; - } -} diff --git a/src/Attributes/FilamentWidget.php b/src/Attributes/FilamentWidget.php deleted file mode 100644 index 573eb6c..0000000 --- a/src/Attributes/FilamentWidget.php +++ /dev/null @@ -1,103 +0,0 @@ - $panels Specific panels this widget should be registered with. Empty = all panels - * @param array $options Additional options for the widget - */ - public function __construct( - public int $priority = 0, - public bool $enabled = true, - public array $panels = [], - public array $options = [] - ) { - } - - /** - * Check if this widget is enabled for auto-discovery. - */ - public function isEnabled(): bool - { - return $this->enabled; - } - - /** - * Check if this widget should be registered with a specific panel. - * - * @param string $panelId Panel identifier - * @return bool True if widget should be registered with panel - */ - public function isEnabledForPanel(string $panelId): bool - { - if (!$this->enabled) { - return false; - } - - // If no specific panels defined, enable for all panels - if (empty($this->panels)) { - return true; - } - - return in_array($panelId, $this->panels, true); - } - - /** - * Get the priority for sorting. - */ - public function getPriority(): int - { - return $this->priority; - } - - /** - * Get additional options. - */ - public function getOptions(): array - { - return $this->options; - } -} diff --git a/src/Console/Commands/BaseModuliteCommand.php b/src/Console/Commands/BaseModuliteCommand.php deleted file mode 100644 index f13d960..0000000 --- a/src/Console/Commands/BaseModuliteCommand.php +++ /dev/null @@ -1,255 +0,0 @@ -map(fn($module) => $module->getName()) - ->sort() - ->values() - ->toArray(); - } - - /** - * Get the available panels for selection. - */ - protected function getAvailablePanels(): array - { - $panels = ['admin', 'manager', 'dashboard']; // Common panel names - - // Try to discover existing panels from config - $configPanels = config('modulite.panels.locations', []); - foreach ($configPanels as $location) { - if (preg_match('/\{panel\}/', $location)) { - // This location uses panel placeholders, we need to discover actual panels - $discovered = $this->discoverExistingPanels(); - $panels = array_merge($panels, $discovered); - } - } - - return array_unique($panels); - } - - /** - * Discover existing panels from the filesystem. - */ - protected function discoverExistingPanels(): array - { - $panels = []; - $modules = $this->getAvailableModules(); - - foreach ($modules as $module) { - $filamentPath = base_path("modules/{$module}/Filament"); - - if (File::exists($filamentPath)) { - $directories = File::directories($filamentPath); - foreach ($directories as $dir) { - $panelName = mb_strtolower(basename($dir)); - if (!in_array($panelName, $panels)) { - $panels[] = $panelName; - } - } - } - } - - return $panels; - } - - /** - * Interactively select a module. - */ - protected function selectModule(): string - { - $modules = $this->getAvailableModules(); - - if (empty($modules)) { - $this->error('No modules found! Please create a module first using: php artisan module:make '); - exit(1); - } - - return $this->choice('Which module should contain this component?', $modules); - } - - /** - * Interactively select a panel. - */ - protected function selectPanel(): string - { - $panels = $this->getAvailablePanels(); - - $selected = $this->choice('Which panel should this component belong to?', $panels); - - // Ask if they want to create a new panel if they don't see what they want - if ($this->confirm('Don\'t see the panel you want? Would you like to create a new one?', false)) { - $selected = $this->ask('Enter the new panel name'); - } - - return mb_strtolower($selected); - } - - /** - * Get the base path for a module. - */ - protected function getModulePath(string $module): string - { - return base_path("modules/{$module}"); - } - - /** - * Get the Filament component path for a module and panel. - */ - protected function getFilamentPath(string $module, string $panel, string $type): string - { - return $this->getModulePath($module)."/Filament/".Str::studly($panel)."/".Str::studly($type); - } - - /** - * Get the namespace for a Filament component. - */ - protected function getFilamentNamespace(string $module, string $panel, string $type): string - { - return "Modules\\{$module}\\Filament\\".Str::studly($panel)."\\".Str::studly($type); - } - - /** - * Ensure the target directory exists. - */ - protected function ensureDirectoryExists(string $path): void - { - if (!File::exists($path)) { - File::makeDirectory($path, 0755, true); - $this->info("Created directory: {$path}"); - } - } - - /** - * Get the stub file content and replace placeholders. - */ - protected function buildClass($name): string - { - $stub = $this->files->get($this->getStub()); - - return $this->replaceNamespace($stub, $name) - ->replaceClass($stub, $name); - } - - /** - * Replace additional placeholders in the stub. - */ - protected function replaceAdditionalPlaceholders(string $stub, array $replacements): string - { - foreach ($replacements as $search => $replace) { - $stub = str_replace($search, $replace, $stub); - } - - return $stub; - } - - /** - * Display success message with next steps. - */ - protected function displaySuccessMessage(string $type, string $name, string $path): void - { - $this->info("โœ… {$type} created successfully!"); - $this->line("๐Ÿ“ Location: {$path}"); - $this->line("๐ŸŽฏ Class: {$name}"); - - $this->newLine(); - $this->comment('Next steps:'); - $this->line('โ€ข Customize the component to fit your needs'); - $this->line('โ€ข The component will be automatically discovered by Modulite'); - $this->line('โ€ข Run `php artisan modulite:status` to verify registration'); - } - - /** - * Check if a file already exists and ask for confirmation to overwrite. - */ - protected function confirmOverwrite(string $path): bool - { - if (File::exists($path)) { - return $this->confirm("The file {$path} already exists. Do you want to overwrite it?", false); - } - - return true; - } - - /** - * Get user input with validation. - */ - protected function askWithValidation(string $question, ?callable $validator = null): string - { - do { - $answer = $this->ask($question); - - if ($validator && !$validator($answer)) { - $this->error('Invalid input. Please try again.'); - continue; - } - - return $answer; - } while (true); - } - - /** - * Validate class name. - */ - protected function validateClassName(string $name): bool - { - return (bool)preg_match('/^[A-Z][a-zA-Z0-9]*$/', $name); - } - - /** - * Create related view file if needed. - */ - protected function createViewFile(string $module, string $panel, string $componentName): void - { - $viewName = Str::kebab($componentName); - $viewPath = $this->getModulePath($module)."/Resources/views/filament/".mb_strtolower($panel)."/pages/{$viewName}.blade.php"; - - if ($this->confirm("Would you like to create a corresponding view file?", true)) { - $this->ensureDirectoryExists(dirname($viewPath)); - - $viewContent = "
\n

{{ \$this->getTitle() }}

\n {{-- Your page content here --}}\n
\n"; - - File::put($viewPath, $viewContent); - $this->line("๐Ÿ“„ View created: {$viewPath}"); - } - } - - /** - * Get component type specific configurations. - */ - abstract protected function getComponentType(): string; - - /** - * Get additional interactive inputs specific to component type. - */ - protected function getAdditionalInputs(): array - { - return []; - } -} diff --git a/src/Console/Commands/ClearCacheCommand.php b/src/Console/Commands/ClearCacheCommand.php deleted file mode 100644 index bf798e6..0000000 --- a/src/Console/Commands/ClearCacheCommand.php +++ /dev/null @@ -1,89 +0,0 @@ -option('force') && !$this->confirm('Are you sure you want to clear all Modulite caches?')) { - $this->info('Cache clearing cancelled.'); - return self::SUCCESS; - } - - try { - $this->info('Clearing Modulite caches...'); - - $cacheManager->flush(); - - $this->components->info('Modulite caches cleared successfully!'); - - // Show cache status - $this->showCacheStatus($cacheManager); - - return self::SUCCESS; - - } catch (Throwable $e) { - $this->components->error('Failed to clear Modulite caches: '.$e->getMessage()); - - if ($this->getOutput()->isVerbose()) { - $this->line($e->getTraceAsString()); - } - - return self::FAILURE; - } - } - - /** - * Show cache status information. - */ - protected function showCacheStatus(CacheManagerInterface $cacheManager): void - { - $this->newLine(); - $this->components->twoColumnDetail('Cache Status', $cacheManager->isCacheEnabled() ? 'Enabled' : 'Disabled'); - - $config = config('modulite.cache', []); - $this->components->twoColumnDetail('Cache Driver', $config['driver'] ?? 'file'); - $this->components->twoColumnDetail('Cache TTL', ($config['ttl'] ?? 3600).' seconds'); - $this->components->twoColumnDetail('File Cache', ($config['file_cache']['enabled'] ?? true) ? 'Enabled' : 'Disabled'); - - if ($config['file_cache']['enabled'] ?? true) { - $filePath = $config['file_cache']['path'] ?? base_path('bootstrap/cache/modulite_panels.php'); - $exists = file_exists($filePath); - $this->components->twoColumnDetail('File Cache Path', $filePath); - $this->components->twoColumnDetail('File Cache Status', $exists ? 'Exists' : 'Not Found'); - } - } -} diff --git a/src/Console/Commands/DiscoverComponentsCommand.php b/src/Console/Commands/DiscoverComponentsCommand.php deleted file mode 100644 index 687eaa5..0000000 --- a/src/Console/Commands/DiscoverComponentsCommand.php +++ /dev/null @@ -1,194 +0,0 @@ -argument('panel'); - $componentType = $this->option('type'); - $showStats = $this->option('show-stats'); - $format = $this->option('format'); - - $this->info("Discovering components for panel: {$panelName}"); - - try { - if ($componentType) { - $components = [$componentType => $componentScanner->discoverComponentType($panelName, $componentType)]; - $this->info("Scanning for component type: {$componentType}"); - } else { - $components = $componentScanner->discoverComponents($panelName); - $this->info("Scanning for all component types"); - } - - $this->displayComponents($components, $format); - - if ($showStats) { - $this->displayStatistics($componentScanner->getScanStats()); - } - - $totalComponents = array_sum(array_map('count', $components)); - $this->info("\nโœ“ Discovery completed successfully. Found {$totalComponents} components."); - - return self::SUCCESS; - - } catch (Throwable $e) { - $this->error("Component discovery failed: {$e->getMessage()}"); - - if ($this->option('verbose')) { - $this->error("File: {$e->getFile()}"); - $this->error("Line: {$e->getLine()}"); - $this->error("Trace: {$e->getTraceAsString()}"); - } - - return self::FAILURE; - } - } - - /** - * Display discovered components in the specified format. - */ - protected function displayComponents(array $components, string $format): void - { - if (empty($components)) { - $this->warn('No components discovered.'); - return; - } - - switch ($format) { - case 'json': - $this->displayComponentsAsJson($components); - break; - case 'list': - $this->displayComponentsAsList($components); - break; - case 'table': - default: - $this->displayComponentsAsTable($components); - break; - } - } - - /** - * Display components as a table. - */ - protected function displayComponentsAsTable(array $components): void - { - $tableData = []; - - foreach ($components as $type => $classList) { - foreach ($classList as $className) { - $tableData[] = [ - 'Type' => ucfirst($type), - 'Class Name' => $className, - 'Module' => $this->extractModuleName($className), - ]; - } - } - - if (!empty($tableData)) { - $this->table(['Type', 'Class Name', 'Module'], $tableData); - } - } - - /** - * Display components as a simple list. - */ - protected function displayComponentsAsList(array $components): void - { - foreach ($components as $type => $classList) { - if (empty($classList)) { - continue; - } - - $this->line("\n".ucfirst($type).":"); - foreach ($classList as $className) { - $this->line(" โ€ข {$className}"); - } - } - } - - /** - * Display components as JSON. - */ - protected function displayComponentsAsJson(array $components): void - { - $this->line(json_encode($components, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - } - - /** - * Display scan statistics. - */ - protected function displayStatistics(array $stats): void - { - $this->line("\nDiscovery Statistics:"); - - $statsTable = [ - ['Files Scanned', $stats['files_scanned'] ?? 0], - ['Classes Found', $stats['classes_found'] ?? 0], - ['Components Discovered', $stats['components_discovered'] ?? 0], - ['Scan Time', round(($stats['scan_time'] ?? 0) * 1000, 2).' ms'], - ['Errors', $stats['errors'] ?? 0], - ]; - - $this->table(['Metric', 'Value'], $statsTable); - } - - /** - * Extract module name from a class name. - */ - protected function extractModuleName(string $className): string - { - // Try to extract module name from namespace - // E.g., "Modules\User\Filament\Admin\Resources\UserResource" -> "User" - if (preg_match('/(?:Modules|modules)\\\\([^\\\\]+)/', $className, $matches)) { - return $matches[1]; - } - - // Try foundation modules - if (preg_match('/(?:Foundation|foundation)\\\\([^\\\\]+)/', $className, $matches)) { - return $matches[1]; - } - - return 'Unknown'; - } -} diff --git a/src/Console/Commands/DiscoverPanelsCommand.php b/src/Console/Commands/DiscoverPanelsCommand.php deleted file mode 100644 index 857f139..0000000 --- a/src/Console/Commands/DiscoverPanelsCommand.php +++ /dev/null @@ -1,211 +0,0 @@ -info('Discovering Filament panel providers...'); - $this->newLine(); - - if ($this->option('no-cache')) { - $this->warn('Skipping cache - performing fresh scan...'); - } - - $startTime = microtime(true); - - // Discover panels - $panels = $scanner->discoverPanels(); - - $endTime = microtime(true); - $scanTime = round($endTime - $startTime, 3); - - // Display results - $this->displayResults($panels, $scanTime); - - // Display statistics if requested - if ($this->option('stats') || $this->getOutput()->isVerbose()) { - $this->displayStatistics($scanner->getScanStats()); - } - - return self::SUCCESS; - - } catch (Throwable $e) { - $this->components->error('Panel discovery failed: '.$e->getMessage()); - - if ($this->getOutput()->isVerbose()) { - $this->line($e->getTraceAsString()); - } - - return self::FAILURE; - } - } - - /** - * Display discovery results. - * - * @param array $panels - */ - protected function displayResults(array $panels, float $scanTime): void - { - $count = count($panels); - - if (0 === $count) { - $this->components->warn('No panel providers found!'); - $this->newLine(); - $this->line('Tips:'); - $this->line('โ€ข Make sure your panel providers are marked with #[FilamentPanel]'); - $this->line('โ€ข Check your scan locations in config/modulite.php'); - $this->line('โ€ข Verify that modules are enabled'); - return; - } - - $this->components->info("Found {$count} panel provider(s) in {$scanTime}s"); - $this->newLine(); - - // Group panels by module/namespace - $grouped = $this->groupPanelsByModule($panels); - - foreach ($grouped as $module => $modulePanels) { - $this->components->twoColumnDetail($module, count($modulePanels).' panel(s)'); - - foreach ($modulePanels as $panel) { - $className = class_basename($panel); - $this->line(" โ€ข {$className}"); - } - - $this->newLine(); - } - } - - /** - * Display scan statistics. - * - * @param array $stats - */ - protected function displayStatistics(array $stats): void - { - $this->components->info('Scan Statistics'); - $this->newLine(); - - $this->components->twoColumnDetail('Files Scanned', (string) ($stats['files_scanned'] ?? 0)); - $this->components->twoColumnDetail('Classes Found', (string) ($stats['classes_found'] ?? 0)); - $this->components->twoColumnDetail('Panels Discovered', (string) ($stats['panels_discovered'] ?? 0)); - $this->components->twoColumnDetail('Scan Time', round($stats['scan_time'] ?? 0, 3).'s'); - $this->components->twoColumnDetail('Errors', (string) ($stats['errors'] ?? 0)); - - if (($stats['errors'] ?? 0) > 0) { - $this->newLine(); - $this->components->warn('Some errors occurred during scanning. Enable verbose logging for details.'); - } - - $this->newLine(); - $this->displayConfiguration(); - } - - /** - * Display current configuration. - */ - protected function displayConfiguration(): void - { - $this->components->info('Configuration'); - $this->newLine(); - - $config = config('modulite', []); - - // Scan locations - $locations = $config['scan']['locations'] ?? []; - $this->components->twoColumnDetail('Scan Locations', count($locations).' configured'); - foreach ($locations as $location) { - $this->line(" โ€ข {$location}"); - } - - $this->newLine(); - - // Cache settings - $cacheEnabled = $config['cache']['enabled'] ?? false; - $this->components->twoColumnDetail('Cache Enabled', $cacheEnabled ? 'Yes' : 'No'); - - if ($cacheEnabled) { - $this->components->twoColumnDetail('Cache Driver', $config['cache']['driver'] ?? 'file'); - $this->components->twoColumnDetail('Cache TTL', ($config['cache']['ttl'] ?? 3600).'s'); - } - - // Performance settings - $this->components->twoColumnDetail('Max Depth', (string) ($config['scan']['max_depth'] ?? 10)); - $this->components->twoColumnDetail('Lazy Discovery', ($config['performance']['lazy_discovery'] ?? true) ? 'Yes' : 'No'); - } - - /** - * Group panels by their module/namespace. - * - * @param array $panels - * @return array> - */ - protected function groupPanelsByModule(array $panels): array - { - $grouped = []; - - foreach ($panels as $panel) { - $parts = explode('\\', $panel); - - // Try to determine module name - $module = 'Unknown'; - - if (count($parts) >= 2) { - if ('Modules' === $parts[0] && isset($parts[1])) { - $module = "Module: {$parts[1]}"; - } elseif ('App' === $parts[0]) { - $module = 'Application'; - } elseif ('Foundation' === $parts[0] && isset($parts[1])) { - $module = "Foundation: {$parts[1]}"; - } else { - $module = $parts[0]; - } - } - - $grouped[$module][] = $panel; - } - - // Sort by module name - ksort($grouped); - - return $grouped; - } -} diff --git a/src/Console/Commands/ModuliteBenchmarkCommand.php b/src/Console/Commands/ModuliteBenchmarkCommand.php deleted file mode 100644 index 80634e0..0000000 --- a/src/Console/Commands/ModuliteBenchmarkCommand.php +++ /dev/null @@ -1,254 +0,0 @@ -option('iterations'); - - $this->info("๐Ÿš€ Modulite Performance Benchmark"); - $this->line("====================================="); - $this->newLine(); - - // Show current environment and config - $this->displayEnvironmentInfo($cache); - - if ($this->option('warm-cache')) { - $this->warmUpCache($cache, $panelScanner, $componentScanner); - } - - // Benchmark different scenarios - $results = [ - 'cache_read' => $this->benchmarkCacheRead($cache, $iterations), - 'panel_discovery' => $this->benchmarkPanelDiscovery($panelScanner, $iterations, $cache), - 'component_discovery' => $this->benchmarkComponentDiscovery($componentScanner, $iterations), - ]; - - $this->displayResults($results); - $this->displayRecommendations($results); - - return self::SUCCESS; - } - - protected function displayEnvironmentInfo(CacheManagerInterface $cache): void - { - $this->info("Environment Information:"); - $this->table(['Setting', 'Value'], [ - ['Environment', app()->environment()], - ['Debug Mode', app()->hasDebugModeEnabled() ? 'โœ“ Enabled' : 'โœ— Disabled'], - ['Cache Enabled', $cache->isCacheEnabled() ? 'โœ“ Enabled' : 'โœ— Disabled'], - ['Cache File', $cache->getCacheFile()], - ['Cache TTL', config('modulite.cache.ttl', 'default')], - ['Lazy Discovery', config('modulite.performance.lazy_discovery', false) ? 'โœ“ Enabled' : 'โœ— Disabled'], - ['PHP Version', PHP_VERSION], - ['Memory Limit', ini_get('memory_limit')], - ]); - $this->newLine(); - } - - protected function warmUpCache( - CacheManagerInterface $cache, - PanelScannerInterface $panelScanner, - ComponentScannerInterface $componentScanner - ): void { - $this->info("๐Ÿ”ฅ Warming up cache..."); - - // Simulate the actual discovery process - $cacheKey = 'panels:'.md5('benchmark'); - $cache->put($cacheKey, $panelScanner->discoverPanels()); - - $componentScanner->discoverComponents('manager'); - - $this->info("โœ“ Cache warmed up"); - $this->newLine(); - } - - protected function benchmarkCacheRead(CacheManagerInterface $cache, int $iterations): array - { - $this->info("๐Ÿ“Š Benchmarking cache read performance..."); - - // Ensure we have some cache data - $testKey = 'benchmark_test_key'; - $testData = ['test' => 'data', 'panels' => ['TestPanel1', 'TestPanel2']]; - $cache->put($testKey, $testData); - - $times = []; - $progress = $this->output->createProgressBar($iterations); - - for ($i = 0; $i < $iterations; $i++) { - $start = hrtime(true); - $result = $cache->get($testKey); - $end = hrtime(true); - - $times[] = ($end - $start) / 1_000_000; // Convert to milliseconds - $progress->advance(); - } - - $progress->finish(); - $this->newLine(); - - // Clean up - $cache->forget($testKey); - - return $this->calculateStats($times, 'Cache Read'); - } - - protected function benchmarkPanelDiscovery( - PanelScannerInterface $panelScanner, - int $iterations, - CacheManagerInterface $cache - ): array { - $this->info("๐Ÿ” Benchmarking panel discovery with cache..."); - - $times = []; - $progress = $this->output->createProgressBar($iterations); - - for ($i = 0; $i < $iterations; $i++) { - $start = hrtime(true); - - // Simulate the production service provider flow - $cacheKey = 'panels:benchmark_'.$i; - $panels = $cache->get($cacheKey); - - if (null === $panels) { - $panels = $panelScanner->discoverPanels(); - $cache->put($cacheKey, $panels); - } - - $end = hrtime(true); - - $times[] = ($end - $start) / 1_000_000; // Convert to milliseconds - $progress->advance(); - } - - $progress->finish(); - $this->newLine(); - - return $this->calculateStats($times, 'Panel Discovery'); - } - - protected function benchmarkComponentDiscovery( - ComponentScannerInterface $componentScanner, - int $iterations - ): array { - $this->info("๐Ÿงฉ Benchmarking component discovery..."); - - $times = []; - $progress = $this->output->createProgressBar($iterations); - - for ($i = 0; $i < $iterations; $i++) { - $start = hrtime(true); - $components = $componentScanner->discoverComponents('manager'); - $end = hrtime(true); - - $times[] = ($end - $start) / 1_000_000; // Convert to milliseconds - $progress->advance(); - } - - $progress->finish(); - $this->newLine(); - - return $this->calculateStats($times, 'Component Discovery'); - } - - protected function calculateStats(array $times, string $operation): array - { - $count = count($times); - $total = array_sum($times); - $average = $total / $count; - $min = min($times); - $max = max($times); - - sort($times); - $median = 0 === $count % 2 - ? ($times[$count / 2 - 1] + $times[$count / 2]) / 2 - : $times[intval($count / 2)]; - - $p95Index = intval($count * 0.95); - $p95 = $times[$p95Index]; - - return [ - 'operation' => $operation, - 'iterations' => $count, - 'total_ms' => round($total, 3), - 'average_ms' => round($average, 3), - 'median_ms' => round($median, 3), - 'min_ms' => round($min, 3), - 'max_ms' => round($max, 3), - 'p95_ms' => round($p95, 3), - ]; - } - - protected function displayResults(array $results): void - { - $this->info("๐Ÿ“ˆ Benchmark Results:"); - - foreach ($results as $result) { - $this->table(['Metric', 'Value'], [ - ['Operation', $result['operation']], - ['Iterations', number_format($result['iterations'])], - ['Average (ms)', $result['average_ms']], - ['Median (ms)', $result['median_ms']], - ['Min (ms)', $result['min_ms']], - ['Max (ms)', $result['max_ms']], - ['95th Percentile (ms)', $result['p95_ms']], - ['Total Time (ms)', $result['total_ms']], - ]); - $this->newLine(); - } - } - - protected function displayRecommendations(array $results): void - { - $this->info("๐Ÿ’ก Performance Recommendations:"); - - $cacheRead = $results['cache_read']; - $panelDiscovery = $results['panel_discovery']; - - if ($cacheRead['average_ms'] > 0.5) { - $this->warn("โš ๏ธ Cache reading is slower than expected ({$cacheRead['average_ms']}ms)"); - $this->line(" โ†’ Consider setting MODULITE_CACHE_TTL=0 for production"); - $this->line(" โ†’ Ensure bootstrap/cache directory has proper permissions"); - } else { - $this->info("โœ… Cache performance is excellent ({$cacheRead['average_ms']}ms average)"); - } - - if ($panelDiscovery['average_ms'] > 2.0) { - $this->warn("โš ๏ธ Panel discovery is slow ({$panelDiscovery['average_ms']}ms)"); - $this->line(" โ†’ Cache might not be working properly"); - $this->line(" โ†’ Check if MODULITE_CACHE_ENABLED=true"); - } else { - $this->info("โœ… Panel discovery performance is good ({$panelDiscovery['average_ms']}ms average)"); - } - - $this->newLine(); - $this->info("๐ŸŽฏ Production Optimization Tips:"); - $this->line(" โ€ข Set MODULITE_CACHE_TTL=0 (never expire)"); - $this->line(" โ€ข Set APP_DEBUG=false"); - $this->line(" โ€ข Use 'php artisan optimize' for Laravel optimizations"); - $this->line(" โ€ข Enable OPcache in PHP for better performance"); - $this->line(" โ€ข Use 'php artisan modulite:benchmark --warm-cache' to test"); - } -} diff --git a/src/Console/Commands/ModuliteClearCacheCommand.php b/src/Console/Commands/ModuliteClearCacheCommand.php index 14ed92b..c6bacc7 100644 --- a/src/Console/Commands/ModuliteClearCacheCommand.php +++ b/src/Console/Commands/ModuliteClearCacheCommand.php @@ -5,9 +5,7 @@ namespace PanicDevs\Modulite\Console\Commands; use Illuminate\Console\Command; -use PanicDevs\Modulite\Services\SimpleFileStorage; -use PanicDevs\Modulite\Contracts\PanelScannerInterface; -use PanicDevs\Modulite\Contracts\ComponentDiscoveryInterface; +use PanicDevs\Modulite\Contracts\CacheManagerInterface; use Throwable; /** @@ -15,38 +13,37 @@ */ class ModuliteClearCacheCommand extends Command { - protected $signature = 'modulite:clear-cache'; + protected $signature = 'modulite:clear + {--force : Force clear without confirmation}'; - protected $description = 'Clear all Modulite storage (panels, components, and discovery data)'; + protected $description = 'Clear all Modulite caches'; - public function handle( - SimpleFileStorage $storage, - PanelScannerInterface $panelScanner, - ComponentDiscoveryInterface $componentDiscovery - ): int { - $this->info('Clearing Modulite storage...'); - - try { - // Clear main storage - $storage->clear(); - $this->line('โœ“ Main storage cleared'); + public function handle(CacheManagerInterface $cacheManager): int + { + if (!$this->option('force') && !$this->confirm('Are you sure you want to clear all Modulite caches?')) + { + $this->info('Cache clear cancelled.'); + return self::SUCCESS; + } - // Clear panel scanner cache - $panelScanner->refreshCache(); - $this->line('โœ“ Panel discovery cache cleared'); + $this->info('Clearing Modulite caches...'); - // Clear component discovery cache - $componentDiscovery->refreshCache(); - $this->line('โœ“ Component discovery cache cleared'); + try + { + // Clear all caches + $cacheManager->flush(); + $this->line('โœ“ All caches cleared'); - $this->info('All Modulite storage cleared successfully!'); + $this->info('Modulite cache cleared successfully!'); return self::SUCCESS; - } catch (Throwable $e) { - $this->error("Failed to clear storage: {$e->getMessage()}"); + } catch (Throwable $e) + { + $this->error("Failed to clear cache: {$e->getMessage()}"); - if ($this->getOutput()->isVerbose()) { + if ($this->getOutput()->isVerbose()) + { $this->error($e->getTraceAsString()); } diff --git a/src/Console/Commands/ModuliteOptimizeCommand.php b/src/Console/Commands/ModuliteOptimizeCommand.php new file mode 100644 index 0000000..e6e0331 --- /dev/null +++ b/src/Console/Commands/ModuliteOptimizeCommand.php @@ -0,0 +1,139 @@ +info('Optimizing Modulite caches...'); + + try + { + // Clear existing caches if force flag is used + if ($this->option('force')) + { + $this->info('Clearing existing caches...'); + $cacheManager->flush(); + } + + // Warm panel discovery cache + $this->line('โ€ข Warming panel discovery cache...'); + $panels = $panelScanner->discoverPanels(); + $panelCount = count($panels); + $this->line(" โœ“ Found {$panelCount} panel providers"); + + // Warm component discovery cache for all discovered panels + $this->line('โ€ข Warming component discovery cache...'); + $totalComponents = 0; + + foreach ($panels as $panelClass) + { + // Extract panel ID from class name (e.g., UserPanelProvider -> user) + $panelId = $this->extractPanelIdFromClass($panelClass); + $components = $componentScanner->discoverComponents($panelId); + $totalComponents += array_sum(array_map('count', $components)); + } + + $this->line(" โœ“ Found {$totalComponents} components"); + + // Display cache file locations + $this->displayCacheInfo(); + + $this->info('Modulite caches optimized successfully!'); + $this->line("- {$panelCount} panel providers cached"); + $this->line("- {$totalComponents} components cached"); + + return self::SUCCESS; + + } catch (Throwable $e) + { + $this->error("Failed to optimize Modulite caches: {$e->getMessage()}"); + + if ($this->getOutput()->isVerbose()) + { + $this->error($e->getTraceAsString()); + } + + return self::FAILURE; + } + } + + /** + * Extract panel ID from panel provider class name. + * + * Examples: + * - AdminPanelProvider -> admin + * - UserPanelProvider -> user + * - ManagerPanelProvider -> manager + */ + protected function extractPanelIdFromClass(string $className): string + { + // Get just the class name without namespace + $parts = explode('\\', $className); + $shortName = end($parts); + + // Remove 'Provider' suffix and 'Panel' + $panelId = str_replace(['PanelProvider', 'Panel', 'Provider'], '', $shortName); + + // Convert to lowercase + return mb_strtolower($panelId); + } + + /** + * Display cache file information. + */ + protected function displayCacheInfo(): void + { + $cacheFile = config('modulite.cache.file', base_path('bootstrap/cache/modulite.php')); + + if (file_exists($cacheFile)) + { + $this->line("- Cache file: {$cacheFile}"); + $size = filesize($cacheFile); + $this->line("- Cache size: ".number_format($size)." bytes"); + } else + { + $this->line("- Cache file: Not created yet"); + } + } +} diff --git a/src/Console/Commands/ModuliteStatusCommand.php b/src/Console/Commands/ModuliteStatusCommand.php index 7dd38a6..d18176b 100644 --- a/src/Console/Commands/ModuliteStatusCommand.php +++ b/src/Console/Commands/ModuliteStatusCommand.php @@ -28,11 +28,13 @@ public function handle( $this->line('==================='); // Handle options - if ($this->option('clear-cache')) { + if ($this->option('clear-cache')) + { $this->clearCache($cache); } - if ($this->option('scan')) { + if ($this->option('scan')) + { $this->forceScan($panelScanner, $componentScanner); } @@ -43,7 +45,8 @@ public function handle( $this->displayComponentStatus($componentScanner); $this->displayCacheStatus($cache); - if ($this->option('vvv')) { + if ($this->option('vvv')) + { $this->displayDetailedStats($panelScanner, $componentScanner, $cache); } @@ -69,10 +72,10 @@ protected function forceScan( $startTime = microtime(true); - $panels = $panelScanner->discoverPanels(); + $panels = $panelScanner->discoverPanels(); $resources = $componentScanner->discoverComponentType('manager', 'resources'); - $pages = $componentScanner->discoverComponentType('manager', 'pages'); - $widgets = $componentScanner->discoverComponentType('manager', 'widgets'); + $pages = $componentScanner->discoverComponentType('manager', 'pages'); + $widgets = $componentScanner->discoverComponentType('manager', 'widgets'); $duration = round((microtime(true) - $startTime) * 1000, 2); @@ -105,18 +108,23 @@ protected function displayModuleStatus(PanelScannerInterface $panelScanner): voi // Get enabled modules using nwidart modules if available $enabledModules = collect(); - if (class_exists(\Nwidart\Modules\Facades\Module::class)) { + if (class_exists(\Nwidart\Modules\Facades\Module::class)) + { $enabledModulesArray = \Nwidart\Modules\Facades\Module::allEnabled(); - foreach ($enabledModulesArray as $module) { + foreach ($enabledModulesArray as $module) + { $enabledModules->push($module->getName()); } } - if ($enabledModules->isEmpty()) { + if ($enabledModules->isEmpty()) + { $this->warn('No enabled modules found'); - } else { + } else + { $this->info("Found {$enabledModules->count()} enabled modules:"); - foreach ($enabledModules as $module) { + foreach ($enabledModules as $module) + { $this->line(" โ€ข {$module}"); } } @@ -130,11 +138,14 @@ protected function displayPanelStatus(PanelScannerInterface $panelScanner): void $panels = $panelScanner->discoverPanels(); - if (empty($panels)) { + if (empty($panels)) + { $this->warn('No panels discovered'); - } else { + } else + { $this->info("Discovered ".count($panels)." panels:"); - foreach ($panels as $panel) { + foreach ($panels as $panel) + { $this->line(" โ€ข {$panel}"); } } @@ -147,8 +158,8 @@ protected function displayComponentStatus(\PanicDevs\Modulite\Contracts\Componen $this->info('Component Discovery:'); $resources = $componentScanner->discoverComponentType('manager', 'resources'); - $pages = $componentScanner->discoverComponentType('manager', 'pages'); - $widgets = $componentScanner->discoverComponentType('manager', 'widgets'); + $pages = $componentScanner->discoverComponentType('manager', 'pages'); + $widgets = $componentScanner->discoverComponentType('manager', 'widgets'); $this->table(['Type', 'Count'], [ ['Resources', count($resources)], @@ -186,7 +197,7 @@ protected function displayDetailedStats( ): void { $this->info('Detailed Statistics:'); - $panelStats = $panelScanner->getScanStats(); + $panelStats = $panelScanner->getScanStats(); $componentStats = $componentScanner->getScanStats(); $this->info('Panel Scanner:'); diff --git a/src/Contracts/ComponentScannerInterface.php b/src/Contracts/ComponentScannerInterface.php index 27635b7..7036691 100644 --- a/src/Contracts/ComponentScannerInterface.php +++ b/src/Contracts/ComponentScannerInterface.php @@ -32,7 +32,7 @@ interface ComponentScannerInterface * 'widgets' => ['App\Modules\User\Filament\Admin\Widgets\StatsWidget'] * ] * - * @throws \PanicDevs\Modulite\Exceptions\ScanException When scanning fails critically + * */ public function discoverComponents(string $panelName): array; @@ -43,7 +43,7 @@ public function discoverComponents(string $panelName): array; * @param string $componentType Component type ('resources', 'pages', 'widgets') * @return array Array of fully qualified class names * - * @throws \PanicDevs\Modulite\Exceptions\ScanException When scanning fails critically + * */ public function discoverComponentType(string $panelName, string $componentType): array; diff --git a/src/Contracts/ModuleResolverInterface.php b/src/Contracts/ModuleResolverInterface.php new file mode 100644 index 0000000..8c3af43 --- /dev/null +++ b/src/Contracts/ModuleResolverInterface.php @@ -0,0 +1,55 @@ + Collection of module names + */ + public function getEnabledModules(): Collection; + + /** + * Check if a specific module is enabled. + * + * @param string $moduleName Module name to check + * @return bool True if module is enabled, false otherwise + */ + public function isModuleEnabled(string $moduleName): bool; + + /** + * Get all available modules (enabled and disabled). + * + * @return Collection Collection of all module names + */ + public function getAllModules(): Collection; + + /** + * Get the module system name/type. + * + * @return string Module system identifier (e.g., 'nwidart', 'panicdevs') + */ + public function getSystemType(): string; + + /** + * Check if the module system is available and properly configured. + * + * @return bool True if system is available, false otherwise + */ + public function isAvailable(): bool; +} diff --git a/src/Contracts/PanelScannerInterface.php b/src/Contracts/PanelScannerInterface.php index a3be280..ff81e2c 100644 --- a/src/Contracts/PanelScannerInterface.php +++ b/src/Contracts/PanelScannerInterface.php @@ -8,25 +8,23 @@ * Interface for Modulite panel discovery services. * * This interface defines the contract for scanning and discovering - * classes marked with the #[FilamentPanel] attribute across the application. + * Filament Panel Provider classes through directory structure and naming conventions. * * @package PanicDevs\Modulite\Contracts */ interface PanelScannerInterface { /** - * Discover all Filament Panel classes in configured scan locations. + * Discover all Filament Panel Provider classes in configured scan locations. * * This method should: * - Scan all configured locations for PHP files * - Parse files to extract class names - * - Use reflection to check for #[FilamentPanel] attribute + * - Use inheritance checking to identify panel providers * - Return fully qualified class names of discovered panels * - Handle errors gracefully based on configuration * - * @return array Array of fully qualified class names with #[FilamentPanel] attribute - * - * @throws \PanicDevs\Modulite\Exceptions\ScanException When scanning fails critically + * @return array Array of fully qualified panel provider class names */ public function discoverPanels(): array; @@ -36,7 +34,7 @@ public function discoverPanels(): array; * Returns metrics useful for debugging and performance monitoring: * - files_scanned: Number of PHP files examined * - classes_found: Total classes discovered in scanned files - * - panels_discovered: Classes with #[FilamentPanel] attribute + * - panels_discovered: Classes identified as panel providers * - scan_time: Time taken for the scan in seconds * - errors: Number of errors encountered during scan * diff --git a/src/Exceptions/CacheException.php b/src/Exceptions/CacheException.php deleted file mode 100644 index c7eb5fa..0000000 --- a/src/Exceptions/CacheException.php +++ /dev/null @@ -1,74 +0,0 @@ -plugin(ModulitePlugin::make()); - * - * // Or with custom configuration - * $panel->plugin( - * ModulitePlugin::make() - * ->enableCaching(false) - * ->sortComponentsBy('priority') - * ); * ``` * + * Directory Structure: + * - modules/{Module}/Filament/{Panel}/Resources/ + * - modules/{Module}/Filament/{Panel}/Pages/ + * - modules/{Module}/Filament/{Panel}/Widgets/ + * * @package PanicDevs\Modulite\Plugins */ class ModulitePlugin implements Plugin @@ -61,6 +58,27 @@ class ModulitePlugin implements Plugin */ protected ?CacheManagerInterface $cacheManager = null; + /** + * Static cache for repeated requests to avoid service container lookups. + * @var array + */ + protected static array $staticCache = []; + + /** + * Flag to track if discovery has been performed for this panel. + * @var array + */ + protected static array $discoveredPanels = []; + + /** + * Clear all static caches (useful for testing or cache invalidation). + */ + public static function clearStaticCaches(): void + { + static::$staticCache = []; + static::$discoveredPanels = []; + } + /** * Get the plugin identifier. */ @@ -81,25 +99,46 @@ public static function make(): static * Register the plugin with a panel. * * This is where the magic happens - components are discovered and registered. + * Optimized for performance with early returns and static caching. */ public function register(Panel $panel): void { - if (!$this->shouldPerformDiscovery()) { + // Fast path: check if discovery should be performed + if (!$this->shouldPerformDiscovery()) + { return; } - try { + try + { $panelId = $this->getPanelId($panel); - // Apply panel-specific configuration from attributes - $this->applyPanelConfiguration($panel); + // Fast path: avoid duplicate discovery for the same panel + if (isset(static::$discoveredPanels[$panelId])) + { + return; + } + + // Mark panel as discovered to prevent duplicate work + static::$discoveredPanels[$panelId] = true; - $components = $this->discoverComponents($panelId); + // Skip attribute-based configuration - use pure discovery + // $this->applyPanelConfigurationOptimized($panel); - $this->registerComponents($panel, $components); - $this->logRegistrationSuccess($panelId, $components); + // Discover components with enhanced caching + $components = $this->discoverComponentsOptimized($panelId); - } catch (Throwable $e) { + // Register components with minimal overhead + $this->registerComponentsOptimized($panel, $components); + + // Only log in development mode + if (app()->hasDebugModeEnabled()) + { + $this->logRegistrationSuccess($panelId, $components); + } + + } catch (Throwable $e) + { $this->handleRegistrationError($panel, $e); } } @@ -113,121 +152,132 @@ public function boot(Panel $panel): void $this->logPluginBooted($panel); } - /** - * Enable or disable caching for component discovery. - * - * @param bool $enabled Whether to enable caching - * @return static - */ - public function enableCaching(bool $enabled = true): static - { - $this->options['cache_enabled'] = $enabled; - return $this; - } + // Configuration methods removed - use config file instead for better performance /** - * Set how components should be sorted before registration. - * - * @param string $sortBy Sorting method: 'name', 'priority', or 'none' - * @return static + * Optimized component discovery with multi-layer caching. */ - public function sortComponentsBy(string $sortBy): static + protected function discoverComponentsOptimized(string $panelId): array { - $this->options['sort_by'] = $sortBy; - return $this; - } + // Layer 1: Static cache for current request + $staticKey = "components_{$panelId}"; + if (isset(static::$staticCache[$staticKey])) + { + return static::$staticCache[$staticKey]; + } - /** - * Enable or disable component validation. - * - * @param bool $enabled Whether to validate components - * @return static - */ - public function validateComponents(bool $enabled = true): static - { - $this->options['validate_components'] = $enabled; - return $this; - } + // Layer 2: Persistent cache + $cacheKey = "components.{$panelId}"; - /** - * Set custom scan locations for this plugin instance. - * - * @param array $locations Custom scan locations - * @return static - */ - public function scanLocations(array $locations): static - { - $this->options['scan_locations'] = $locations; - return $this; + if ($this->isCachingEnabled()) + { + $components = $this->getCacheManager()->remember($cacheKey, fn () => $this->performComponentDiscovery($panelId)); + } else + { + $components = $this->performComponentDiscovery($panelId); + } + + // Store in static cache for this request + static::$staticCache[$staticKey] = $components; + + return $components; } /** - * Enable or disable specific component types. + * Legacy method for backward compatibility. * - * @param array $types Component types to enable/disable - * @return static + * @deprecated Use discoverComponentsOptimized instead */ - public function componentTypes(array $types): static + protected function discoverComponents(string $panelId): array { - $this->options['component_types'] = $types; - return $this; + return $this->discoverComponentsOptimized($panelId); } /** - * Set excluded directories for scanning. - * - * @param array $excludedDirs Directories to exclude - * @return static + * Perform the actual component discovery. */ - public function excludeDirectories(array $excludedDirs): static + protected function performComponentDiscovery(string $panelId): array { - $this->options['excluded_directories'] = $excludedDirs; - return $this; + $scanner = $this->getComponentScanner(); + return $scanner->discoverComponentsForPanel($panelId); } /** - * Discover components for a panel. - * - * Uses caching when enabled for performance optimization. - * Also checks for panel-specific configuration from attributes. + * Optimized component registration with minimal overhead. */ - protected function discoverComponents(string $panelId): array + protected function registerComponentsOptimized(Panel $panel, array $components): void { - $cacheKey = "components.{$panelId}"; + // Fast path: early return if no components + if (empty($components)) + { + return; + } - if ($this->isCachingEnabled()) { - return $this->getCacheManager()->remember($cacheKey, fn () => $this->performComponentDiscovery($panelId)); + // Cache enabled types to avoid repeated config lookups + static $enabledTypesCache = null; + if (null === $enabledTypesCache) + { + $enabledTypesCache = $this->getEnabledComponentTypes(); } - return $this->performComponentDiscovery($panelId); + // Process each component type efficiently + foreach ($components as $type => $classList) + { + // Fast path: skip disabled types + if (empty($classList) || !($enabledTypesCache[$type] ?? true)) + { + continue; + } + + // Skip expensive operations if not needed + $finalComponents = $this->processComponentsForRegistration($classList, $type); + + if (!empty($finalComponents)) + { + $this->registerComponentType($panel, $type, $finalComponents); + } + } } /** - * Perform the actual component discovery. + * Legacy method for backward compatibility. + * + * @deprecated Use registerComponentsOptimized instead */ - protected function performComponentDiscovery(string $panelId): array + protected function registerComponents(Panel $panel, array $components): void { - $scanner = $this->getComponentScanner(); - return $scanner->discoverComponentsForPanel($panelId); + $this->registerComponentsOptimized($panel, $components); } /** - * Register discovered components with the panel. + * Process components for registration with optional sorting and validation. */ - protected function registerComponents(Panel $panel, array $components): void + protected function processComponentsForRegistration(array $classList, string $type): array { - $enabledTypes = $this->getEnabledComponentTypes(); + // Skip processing if validation and sorting are disabled + $needsValidation = $this->isValidationEnabled(); + $needsSorting = ($this->options['sort_by'] ?? 'none') !== 'none'; - foreach ($components as $type => $classList) { - if (!isset($enabledTypes[$type]) || !$enabledTypes[$type]) { - continue; - } + if (!$needsValidation && !$needsSorting) + { + return $classList; + } - $sortedComponents = $this->sortComponents($classList, $type); - $validatedComponents = $this->validateComponentsIfEnabled($sortedComponents, $type); + $components = $classList; + + // Apply sorting only if needed + if ($needsSorting) + { + $components = $this->sortComponents($components, $type); + } - $this->registerComponentType($panel, $type, $validatedComponents); + // Apply validation only if enabled + if ($needsValidation) + { + $components = $this->validateComponentsIfEnabled($components, $type); } + + return $components; } /** @@ -235,11 +285,13 @@ protected function registerComponents(Panel $panel, array $components): void */ protected function registerComponentType(Panel $panel, string $type, array $components): void { - if (empty($components)) { + if (empty($components)) + { return; } - switch ($type) { + switch ($type) + { case 'resources': $panel->resources($components); break; @@ -261,7 +313,8 @@ protected function sortComponents(array $components, string $type): array { $sortBy = $this->options['sort_by'] ?? config('modulite.components.registration.sort_by', 'name'); - switch ($sortBy) { + switch ($sortBy) + { case 'name': sort($components); break; @@ -294,11 +347,14 @@ protected function compareComponentPriority(string $a, string $b): int */ protected function getComponentPriority(string $className): int { - try { - if (method_exists($className, 'getPriority')) { + try + { + if (method_exists($className, 'getPriority')) + { return $className::getPriority(); } - } catch (Throwable) { + } catch (Throwable) + { // Ignore errors getting priority } @@ -310,17 +366,21 @@ protected function getComponentPriority(string $className): int */ protected function validateComponentsIfEnabled(array $components, string $type): array { - if (!$this->isValidationEnabled()) { + if (!$this->isValidationEnabled()) + { return $components; } - $scanner = $this->getComponentScanner(); + $scanner = $this->getComponentScanner(); $validComponents = []; - foreach ($components as $component) { - if ($scanner->isComponentType($component, $type)) { + foreach ($components as $component) + { + if ($scanner->isComponentType($component, $type)) + { $validComponents[] = $component; - } else { + } else + { $this->logInvalidComponent($component, $type); } } @@ -337,29 +397,50 @@ protected function getPanelId(Panel $panel): string } /** - * Check if discovery should be performed. + * Check if discovery should be performed with caching. */ protected function shouldPerformDiscovery(): bool { - return config('modulite.components.registration.auto_register', true); + static $shouldPerform = null; + + if (null === $shouldPerform) + { + $shouldPerform = config('modulite.components.registration.auto_register', true); + } + + return $shouldPerform; } /** - * Check if caching is enabled. + * Check if caching is enabled with static caching. */ protected function isCachingEnabled(): bool { - return $this->options['cache_enabled'] - ?? config('modulite.cache.enabled', true); + static $cacheEnabled = null; + + if (null === $cacheEnabled) + { + $cacheEnabled = $this->options['cache_enabled'] + ?? config('modulite.cache.enabled', true); + } + + return $cacheEnabled; } /** - * Check if validation is enabled. + * Check if validation is enabled with static caching. */ protected function isValidationEnabled(): bool { - return $this->options['validate_components'] - ?? config('modulite.components.registration.validate_before_register', false); + static $validationEnabled = null; + + if (null === $validationEnabled) + { + $validationEnabled = $this->options['validate_components'] + ?? config('modulite.components.registration.validate_before_register', false); + } + + return $validationEnabled; } /** @@ -368,10 +449,11 @@ protected function isValidationEnabled(): bool protected function getEnabledComponentTypes(): array { $configured = config('modulite.components.types', []); - $override = $this->options['component_types'] ?? []; + $override = $this->options['component_types'] ?? []; $enabled = []; - foreach ($configured as $type => $settings) { + foreach ($configured as $type => $settings) + { $enabled[$type] = $override[$type] ?? ($settings['enabled'] ?? true); } @@ -379,128 +461,43 @@ protected function getEnabledComponentTypes(): array } /** - * Get component scanner service. + * Get component scanner service with static caching. */ protected function getComponentScanner(): ComponentScannerInterface { - if (!$this->componentScanner) { - $this->componentScanner = app(ComponentScannerInterface::class); - } + // Use static cache to avoid repeated service container lookups + static $instance = null; - return $this->componentScanner; - } - - /** - * Get cache manager service. - */ - protected function getCacheManager(): CacheManagerInterface - { - if (!$this->cacheManager) { - $this->cacheManager = app(CacheManagerInterface::class); + if (null === $instance) + { + $instance = app(ComponentScannerInterface::class); } - return $this->cacheManager; + return $instance; } /** - * Apply panel-specific configuration from ComponentDiscovery attribute. - * - * Looks for the ComponentDiscovery attribute on panel provider classes - * and applies the configuration to override defaults. + * Get cache manager service with static caching. */ - protected function applyPanelConfiguration(Panel $panel): void + protected function getCacheManager(): CacheManagerInterface { - try { - // Try to find the panel provider class - $panelProvider = $this->findPanelProvider($panel); - - if (!$panelProvider) { - return; // No provider found, use defaults - } + // Use static cache to avoid repeated service container lookups + static $instance = null; - $reflection = new ReflectionClass($panelProvider); - $attributes = $reflection->getAttributes(ComponentDiscovery::class); - - if (empty($attributes)) { - return; // No ComponentDiscovery attribute, use defaults - } - - $discoveryAttribute = $attributes[0]->newInstance(); - $panelId = $this->getPanelId($panel); - - // Apply attribute configuration to plugin options - $this->applyAttributeConfiguration($discoveryAttribute, $panelId); - - } catch (Throwable $e) { - // Log error but don't fail - continue with defaults - $this->logConfigurationError($panel, $e); + if (null === $instance) + { + $instance = app(CacheManagerInterface::class); } - } - /** - * Find the panel provider class for a panel. - * - * This is a best-effort attempt to find the provider class. - */ - protected function findPanelProvider(Panel $panel): ?string - { - // Try common panel provider naming patterns - $panelId = $this->getPanelId($panel); - $patterns = [ - ucfirst($panelId).'PanelProvider', - ucfirst($panelId).'Panel', - 'App\\Providers\\Filament\\'.ucfirst($panelId).'PanelProvider', - 'App\\Providers\\'.ucfirst($panelId).'PanelProvider', - ]; - - foreach ($patterns as $pattern) { - if (class_exists($pattern)) { - return $pattern; - } - } - - return null; + return $instance; } - /** - * Apply configuration from ComponentDiscovery attribute. - */ - protected function applyAttributeConfiguration(ComponentDiscovery $attribute, string $panelId): void - { - // Apply scan locations - if (null !== $attribute->locations) { - $this->options['scan_locations'] = $attribute->getLocations($panelId); - } - // Apply enabled types - if (null !== $attribute->enabledTypes) { - $this->options['component_types'] = $attribute->getEnabledTypes(); - } - // Apply excluded directories - if (null !== $attribute->excludedDirectories) { - $this->options['excluded_directories'] = $attribute->getExcludedDirectories(); - } + // Note: Attribute-based configuration has been removed for performance. + // All configuration is now handled through the config file and directory structure. - // Apply cache setting - if (null !== $attribute->cacheEnabled) { - $this->options['cache_enabled'] = $attribute->isCacheEnabled(); - } - - // Apply sort setting - if (null !== $attribute->sortBy) { - $this->options['sort_by'] = $attribute->getSortBy(); - } - - // Apply validation setting - if (null !== $attribute->validateComponents) { - $this->options['validate_components'] = $attribute->isValidationEnabled(); - } - // Apply additional options - $additionalOptions = $attribute->getOptions(); - $this->options = array_merge($this->options, $additionalOptions); - } /** * Handle registration errors. @@ -509,7 +506,8 @@ protected function handleRegistrationError(Panel $panel, Throwable $e): void { $panelId = $this->getPanelId($panel); - if (config('modulite.error_handling.fail_silently', false)) { + if (config('modulite.error_handling.fail_silently', false)) + { $this->logRegistrationError($panelId, $e); return; } @@ -517,10 +515,13 @@ protected function handleRegistrationError(Panel $panel, Throwable $e): void throw $e; } - // Logging methods... + /** + * Log successful component registration. + */ protected function logRegistrationSuccess(string $panelId, array $components): void { - if (!config('modulite.logging.enabled', false)) { + if (!config('modulite.logging.enabled', false)) + { return; } @@ -534,9 +535,13 @@ protected function logRegistrationSuccess(string $panelId, array $components): v ]); } + /** + * Log component registration errors. + */ protected function logRegistrationError(string $panelId, Throwable $e): void { - if (!config('modulite.logging.enabled', false)) { + if (!config('modulite.logging.enabled', false)) + { return; } @@ -549,9 +554,13 @@ protected function logRegistrationError(string $panelId, Throwable $e): void ]); } + /** + * Log plugin boot completion. + */ protected function logPluginBooted(Panel $panel): void { - if (!config('modulite.logging.enabled', false) || !config('modulite.development.verbose_logging', false)) { + if (!config('modulite.logging.enabled', false) || !app()->hasDebugModeEnabled()) + { return; } @@ -559,9 +568,13 @@ protected function logPluginBooted(Panel $panel): void ->debug("ModulitePlugin: Plugin booted for panel: {$panel->getId()}"); } + /** + * Log unknown component type warnings. + */ protected function logUnknownComponentType(string $type): void { - if (!config('modulite.logging.enabled', false)) { + if (!config('modulite.logging.enabled', false)) + { return; } @@ -569,9 +582,13 @@ protected function logUnknownComponentType(string $type): void ->warning("ModulitePlugin: Unknown component type: {$type}"); } + /** + * Log invalid component discoveries. + */ protected function logInvalidComponent(string $component, string $type): void { - if (!config('modulite.logging.enabled', false) || !config('modulite.development.verbose_logging', false)) { + if (!config('modulite.logging.enabled', false) || !app()->hasDebugModeEnabled()) + { return; } @@ -579,18 +596,5 @@ protected function logInvalidComponent(string $component, string $type): void ->debug("ModulitePlugin: Invalid component for type {$type}: {$component}"); } - protected function logConfigurationError(Panel $panel, Throwable $e): void - { - if (!config('modulite.logging.enabled', false)) { - return; - } - Log::channel(config('modulite.logging.channel', 'default')) - ->warning("ModulitePlugin: Failed to apply panel configuration for panel: {$panel->getId()}", [ - 'panel_id' => $panel->getId(), - 'error' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine() - ]); - } } diff --git a/src/Providers/ModuliteServiceProvider.php b/src/Providers/ModuliteServiceProvider.php index 295bbc8..7616826 100644 --- a/src/Providers/ModuliteServiceProvider.php +++ b/src/Providers/ModuliteServiceProvider.php @@ -8,13 +8,16 @@ use Illuminate\Contracts\Foundation\Application; use Illuminate\Support\Facades\Log; use Illuminate\Support\ServiceProvider; -use PanicDevs\Modulite\Console\Commands\ModuliteBenchmarkCommand; use PanicDevs\Modulite\Console\Commands\ModuliteClearCacheCommand; use PanicDevs\Modulite\Console\Commands\ModuliteStatusCommand; +use PanicDevs\Modulite\Console\Commands\ModuliteOptimizeCommand; use PanicDevs\Modulite\Contracts\CacheManagerInterface; use PanicDevs\Modulite\Contracts\ComponentScannerInterface; +use PanicDevs\Modulite\Contracts\ModuleResolverInterface; use PanicDevs\Modulite\Contracts\PanelScannerInterface; use PanicDevs\Modulite\Services\ComponentDiscoveryService; +use PanicDevs\Modulite\Services\ModuleResolvers\NwidartModuleResolver; +use PanicDevs\Modulite\Services\ModuleResolvers\PanicDevsModuleResolver; use PanicDevs\Modulite\Services\PanelScannerService; use PanicDevs\Modulite\Services\UnifiedCacheManager; use Throwable; @@ -76,7 +79,7 @@ public function register(): void { $this->registerConfiguration(); $this->registerCoreServices(); - $this->registerPanelDiscovery(); + $this->app->beforeResolving(self::FILAMENT_NAMESPACE, fn() => $this->registerPanelDiscovery()); $this->registerCacheInvalidationListeners(); } @@ -109,43 +112,48 @@ protected function registerConfiguration(): void protected function registerCoreServices(): void { // Register UnifiedCacheManager with configuration - $this->app->singleton(CacheManagerInterface::class, function (Application $app) { + $this->app->singleton(CacheManagerInterface::class, function (Application $app) + { $config = $app['config']->get('modulite.cache'); return new UnifiedCacheManager($config); }); + // Register ModuleResolver based on configuration + $this->app->singleton(ModuleResolverInterface::class, fn (Application $app) => $this->createModuleResolver($app)); + // Register PanelScannerService with dependencies - $this->app->singleton(PanelScannerInterface::class, function (Application $app) { - $config = $app['config']->get('modulite'); - $basePath = $app->basePath(); + $this->app->singleton(PanelScannerInterface::class, function (Application $app) + { + $config = $app['config']->get('modulite'); + $basePath = $app->basePath(); $moduleManager = $this->getModuleManager($app); return new PanelScannerService($config, $basePath, $moduleManager); }); // Register ComponentDiscoveryService with dependencies - $this->app->singleton(ComponentScannerInterface::class, function (Application $app) { - $cacheManager = $app->make(CacheManagerInterface::class); - return new ComponentDiscoveryService($cacheManager); + $this->app->singleton(ComponentScannerInterface::class, function (Application $app) + { + $cacheManager = $app->make(CacheManagerInterface::class); + $moduleResolver = $app->make(ModuleResolverInterface::class); + return new ComponentDiscoveryService($cacheManager, $moduleResolver); }); } /** - * Register lazy panel discovery mechanism. + * Register panel discovery mechanism. * - * Panel discovery is deferred until Filament is actually being resolved - * to avoid unnecessary scanning on non-admin requests. + * In production, panels are discovered immediately to ensure routes are cached. + * In development, discovery is deferred until Filament is resolved for performance. */ protected function registerPanelDiscovery(): void { - if (!$this->shouldPerformDiscovery()) { + if (!$this->shouldPerformDiscovery()) + { return; } - $this->app->beforeResolving( - self::FILAMENT_NAMESPACE, - fn () => $this->discoverAndRegisterPanels() - ); + $this->discoverAndRegisterPanels(); } /** @@ -159,7 +167,8 @@ protected function registerPanelDiscovery(): void */ protected function discoverAndRegisterPanels(): void { - try { + try + { /** @var CacheManagerInterface $cacheManager */ $cacheManager = $this->app->make(CacheManagerInterface::class); @@ -168,11 +177,11 @@ protected function discoverAndRegisterPanels(): void // Fast path: Check cache first without loading scanner $panelClasses = $cacheManager->get($cacheKey); - - if (null === $panelClasses) { + if (null === $panelClasses) + { // Cache miss: Load scanner and perform discovery /** @var PanelScannerInterface $scanner */ - $scanner = $this->app->make(PanelScannerInterface::class); + $scanner = $this->app->make(PanelScannerInterface::class); $panelClasses = $scanner->discoverPanels(); $cacheManager->put($cacheKey, $panelClasses); @@ -183,7 +192,8 @@ protected function discoverAndRegisterPanels(): void // Register discovered panel providers $this->registerPanelProviders($panelClasses); - } catch (Throwable $e) { + } catch (Throwable $e) + { $this->handleDiscoveryError($e); } } @@ -195,10 +205,14 @@ protected function discoverAndRegisterPanels(): void */ protected function registerPanelProviders(array $panelClasses): void { - foreach ($panelClasses as $providerClass) { - try { + + foreach ($panelClasses as $providerClass) + { + try + { $this->app->register($providerClass); - } catch (Throwable $e) { + } catch (Throwable $e) + { $this->handleProviderRegistrationError($providerClass, $e); } } @@ -208,25 +222,28 @@ protected function registerPanelProviders(array $panelClasses): void * Generate cache key based on current module state. * * The cache key includes: - * - Enabled module list and their timestamps + * - Enabled module list and their configuration + * - Module resolver approach * - Configuration hash * - Application environment */ protected function generateCacheKey(): string { - $moduleManager = $this->getModuleManager($this->app); - $modules = $moduleManager ? $moduleManager->allEnabled() : []; + // Use the module resolver to get enabled modules + $moduleResolver = $this->app->make(ModuleResolverInterface::class); + $enabledModules = $moduleResolver->getEnabledModules(); - // Include module names and modification times - $moduleData = []; - foreach ($modules as $name => $module) { - $moduleData[$name] = $module->get('priority', 0); - } + // Include module names - use simple array for consistent hashing + $moduleData = $enabledModules->sort()->values()->toArray(); + + // Include the module resolver approach in cache key + $approach = config('modulite.modules.approach', 'nwidart'); // Include configuration in cache key $configHash = md5(serialize([ 'panels' => config('modulite.panels', []), - 'components' => config('modulite.components', []) + 'components' => config('modulite.components', []), + 'modules' => config('modulite.modules', []) ])); // Include environment @@ -234,6 +251,7 @@ protected function generateCacheKey(): string $keyData = [ 'modules' => $moduleData, + 'approach' => $approach, 'config' => $configHash, 'environment' => $environment, ]; @@ -246,15 +264,32 @@ protected function generateCacheKey(): string */ protected function getModuleManager(Application $app): mixed { - try { + try + { return $app->bound(self::NWIDART_MODULES_NAMESPACE) ? $app[self::NWIDART_MODULES_NAMESPACE] : null; - } catch (Throwable) { + } catch (Throwable) + { return null; } } + /** + * Create the appropriate module resolver based on configuration. + */ + protected function createModuleResolver(Application $app): ModuleResolverInterface + { + $approach = $app['config']->get('modulite.modules.approach', 'nwidart'); + + return match ($approach) + { + 'panicdevs' => new PanicDevsModuleResolver($app), + 'nwidart' => new NwidartModuleResolver(), + default => new NwidartModuleResolver(), // Default fallback + }; + } + /** * Register cache invalidation event listeners. * @@ -267,14 +302,17 @@ protected function registerCacheInvalidationListeners(): void { $triggers = config('modulite.cache.invalidation.triggers', []); - foreach ($triggers as $event) { - $this->app['events']->listen($event, function (): void { + foreach ($triggers as $event) + { + $this->app['events']->listen($event, function (): void + { $this->invalidateCache(); }); } // Also listen to config cache clear - $this->app['events']->listen('config:cache:cleared', function (): void { + $this->app['events']->listen('config:cache:cleared', function (): void + { $this->invalidateCache(); }); } @@ -284,13 +322,16 @@ protected function registerCacheInvalidationListeners(): void */ protected function invalidateCache(): void { - try { + try + { /** @var CacheManagerInterface $cacheManager */ $cacheManager = $this->app->make(CacheManagerInterface::class); $cacheManager->flush(); - } catch (Throwable $e) { + } catch (Throwable $e) + { // Log but don't throw - cache invalidation shouldn't break the app - if (config('modulite.logging.enabled', false)) { + if (config('modulite.logging.enabled', false)) + { Log::channel(config('modulite.logging.channel', 'default')) ->warning('Failed to invalidate Modulite cache', ['error' => $e->getMessage()]); } @@ -302,7 +343,8 @@ protected function invalidateCache(): void */ protected function publishConfiguration(): void { - if ($this->app->runningInConsole()) { + if ($this->app->runningInConsole()) + { $this->publishes([ self::CONFIG_PATH => config_path('modulite.php'), ], ['modulite', 'modulite-config']); @@ -314,7 +356,8 @@ protected function publishConfiguration(): void */ protected function validateConfiguration(): void { - if (!$this->app->hasDebugModeEnabled()) { + if (!$this->app->hasDebugModeEnabled()) + { return; } @@ -322,10 +365,13 @@ protected function validateConfiguration(): void // Validate panel scan locations exist $panelLocations = $config['panels']['locations'] ?? []; - foreach ($panelLocations as $location) { - if (!str_contains($location, '*')) { + foreach ($panelLocations as $location) + { + if (!str_contains($location, '*')) + { $fullPath = $this->app->basePath($location); - if (!is_dir($fullPath)) { + if (!is_dir($fullPath)) + { Log::channel(config('modulite.logging.channel', 'default')) ->warning("Modulite panel scan location does not exist: {$fullPath}"); } @@ -334,10 +380,13 @@ protected function validateConfiguration(): void // Validate component scan locations exist $componentLocations = $config['components']['locations'] ?? []; - foreach ($componentLocations as $location) { - if (!str_contains($location, '*')) { + foreach ($componentLocations as $location) + { + if (!str_contains($location, '*')) + { $fullPath = $this->app->basePath($location); - if (!is_dir($fullPath)) { + if (!is_dir($fullPath)) + { Log::channel(config('modulite.logging.channel', 'default')) ->warning("Modulite component scan location does not exist: {$fullPath}"); } @@ -345,9 +394,11 @@ protected function validateConfiguration(): void } // Validate cache configuration - if ($config['cache']['enabled'] ?? false) { + if ($config['cache']['enabled'] ?? false) + { $driver = $config['cache']['driver'] ?? 'file'; - if (!in_array($driver, ['file', 'redis', 'memcached', 'array', 'database'], true)) { + if (!in_array($driver, ['file', 'redis', 'memcached', 'array', 'database'], true)) + { Log::channel(config('modulite.logging.channel', 'default')) ->warning("Modulite cache driver '{$driver}' may not be supported"); } @@ -360,7 +411,8 @@ protected function validateConfiguration(): void protected function setupDevelopmentHelpers(): void { // Register artisan commands if available - if ($this->app->runningInConsole()) { + if ($this->app->runningInConsole()) + { $this->registerConsoleCommands(); } } @@ -373,8 +425,11 @@ protected function registerConsoleCommands(): void $this->commands([ ModuliteClearCacheCommand::class, ModuliteStatusCommand::class, - ModuliteBenchmarkCommand::class, + ModuliteOptimizeCommand::class, ]); + + // Note: Laravel optimize integration removed to prevent circular dependencies + // Users should manually run: php artisan modulite:cache } /** @@ -382,14 +437,22 @@ protected function registerConsoleCommands(): void */ protected function shouldPerformDiscovery(): bool { - // Don't discover in console unless explicitly needed - if ($this->app->runningInConsole()) { + // Check if Filament is available + if (!class_exists(FilamentServiceProvider::class)) + { return false; } - // Check if Filament is available - if (!class_exists(FilamentServiceProvider::class)) { - return false; + // In production, always perform discovery to ensure routes are cached properly + if ($this->app->isProduction()) + { + return true; + } + + // Don't discover in console unless explicitly needed (development only) + if ($this->app->runningInConsole()) + { + return true; } // Check configuration @@ -404,7 +467,8 @@ protected function shouldPerformDiscovery(): bool */ protected function logDiscoverySuccess(array $panels, array $stats): void { - if (!config('modulite.logging.log_discovery_time', false)) { + if (!config('modulite.logging.log_discovery_time', false)) + { return; } @@ -423,7 +487,8 @@ protected function handleDiscoveryError(Throwable $e): void { $failSilently = config('modulite.error_handling.fail_silently', false); - if (config('modulite.logging.enabled', false)) { + if (config('modulite.logging.enabled', false)) + { Log::channel(config('modulite.logging.channel', 'default')) ->error('Modulite panel discovery failed', [ 'error' => $e->getMessage(), @@ -431,7 +496,8 @@ protected function handleDiscoveryError(Throwable $e): void ]); } - if (!$failSilently) { + if (!$failSilently) + { throw $e; } } @@ -441,7 +507,8 @@ protected function handleDiscoveryError(Throwable $e): void */ protected function handleProviderRegistrationError(string $providerClass, Throwable $e): void { - if (config('modulite.logging.enabled', false)) { + if (config('modulite.logging.enabled', false)) + { Log::channel(config('modulite.logging.channel', 'default')) ->warning('Failed to register panel provider', [ 'provider' => $providerClass, diff --git a/src/Services/CacheManager.php b/src/Services/CacheManager.php deleted file mode 100644 index 39bd8eb..0000000 --- a/src/Services/CacheManager.php +++ /dev/null @@ -1,503 +0,0 @@ - - */ - protected array $memoryCache = []; - - /** - * Cache configuration. - * - * @var array - */ - protected array $config; - - /** - * Laravel cache repository instance. - */ - protected CacheRepository $cache; - - /** - * Create a new CacheManager instance. - * - * @param array $config Modulite cache configuration - */ - public function __construct(array $config) - { - $this->config = $config; - $this->cache = Cache::store($this->config['driver'] ?? 'file'); - } - - /** - * Retrieve cached panel classes using multi-layer cache strategy. - * - * @param string $key Cache key identifier - * @param callable(): array $callback Callback to generate data if cache miss - * @return array Array of panel class names - * - * @throws CacheException When cache operations fail - */ - public function remember(string $key, callable $callback): array - { - if (!$this->isCacheEnabled()) { - return $this->executeCallback($callback); - } - - // Layer 1: Memory cache (fastest) - if ($this->hasMemoryCache($key)) { - $this->logCacheHit('memory', $key); - return $this->getMemoryCache($key); - } - - // Layer 2: File cache (fast, persistent) - if ($this->hasFileCache($key)) { - $data = $this->getFileCache($key); - $this->setMemoryCache($key, $data); - $this->logCacheHit('file', $key); - return $data; - } - - // Layer 3: Laravel cache (Redis, Memcached, etc.) - $cacheKey = $this->buildCacheKey($key); - $cachedData = $this->cache->get($cacheKey); - - if (null !== $cachedData) { - $this->setMemoryCache($key, $cachedData); - $this->setFileCache($key, $cachedData); - $this->logCacheHit('store', $key); - return $cachedData; - } - - // Cache miss - generate data - $data = $this->executeCallback($callback); - $this->storeInAllLayers($key, $data); - - return $data; - } - - /** - * Store data in all available cache layers. - * - * @param string $key Cache key - * @param array $data Data to cache - */ - protected function storeInAllLayers(string $key, array $data): void - { - $this->setMemoryCache($key, $data); - - if ($this->isFileCacheEnabled()) { - $this->setFileCache($key, $data); - } - - $cacheKey = $this->buildCacheKey($key); - $ttl = $this->getCacheTtl(); - - $this->cache->put($cacheKey, $data, $ttl); - - $this->logCacheWrite($key); - } - - /** - * Invalidate all cache layers for a specific key. - * - * @param string $key Cache key to invalidate - */ - public function forget(string $key): void - { - // Clear memory cache - unset($this->memoryCache[$key]); - - // Clear file cache - if ($this->isFileCacheEnabled()) { - $this->clearFileCache($key); - } - - // Clear Laravel cache - $cacheKey = $this->buildCacheKey($key); - $this->cache->forget($cacheKey); - - $this->logCacheInvalidation($key); - } - - /** - * Clear all Modulite caches. - */ - public function flush(): void - { - // Clear memory cache - $this->memoryCache = []; - - // Clear file cache - if ($this->isFileCacheEnabled()) { - $this->clearAllFileCache(); - } - - // Clear Laravel cache with prefix - $prefix = $this->config['prefix'] ?? 'modulite'; - $this->cache->flush(); // Note: This flushes the entire cache store - - $this->logCacheFlush(); - } - - /** - * Check if cache is enabled based on configuration. - */ - public function isCacheEnabled(): bool - { - return $this->config['enabled'] ?? true; - } - - /** - * Check if file cache is enabled. - */ - protected function isFileCacheEnabled(): bool - { - return ($this->config['file_cache']['enabled'] ?? true) && $this->isCacheEnabled(); - } - - /** - * Check if memory cache contains the key. - */ - protected function hasMemoryCache(string $key): bool - { - return isset($this->memoryCache[$key]); - } - - /** - * Get data from memory cache. - * - * @return array - */ - protected function getMemoryCache(string $key): array - { - return $this->memoryCache[$key] ?? []; - } - - /** - * Set data in memory cache with size limit. - * - * @param array $data - */ - protected function setMemoryCache(string $key, array $data): void - { - $maxItems = $this->config['memory_cache']['max_items'] ?? 1000; - - if (count($this->memoryCache) >= $maxItems) { - // Remove oldest entry (simple FIFO) - array_shift($this->memoryCache); - } - - $this->memoryCache[$key] = $data; - } - - /** - * Check if file cache exists and is valid. - */ - protected function hasFileCache(string $key): bool - { - if (!$this->isFileCacheEnabled()) { - return false; - } - - $filePath = $this->getFileCachePath($key); - - if (!File::exists($filePath)) { - return false; - } - - // Check TTL if auto-invalidation is enabled - if ($this->config['invalidation']['auto_invalidate_on_file_change'] ?? false) { - $fileTime = File::lastModified($filePath); - $ttl = $this->getCacheTtl(); - - if ($ttl > 0 && (time() - $fileTime) > $ttl) { - File::delete($filePath); - return false; - } - } - - return true; - } - - /** - * Get data from file cache. - * - * @return array - * - * @throws CacheException When file cache is corrupted - */ - protected function getFileCache(string $key): array - { - $filePath = $this->getFileCachePath($key); - - try { - $content = File::get($filePath); - $data = include $filePath; - - if (!is_array($data)) { - throw new CacheException("File cache corrupted: {$filePath}"); - } - - return $data; - } catch (Throwable $e) { - // Remove corrupted cache file - File::delete($filePath); - throw new CacheException("Failed to read file cache: {$e->getMessage()}", 0, $e); - } - } - - /** - * Set data in file cache. - * - * @param array $data - * - * @throws CacheException When file cache write fails - */ - protected function setFileCache(string $key, array $data): void - { - if (!$this->isFileCacheEnabled()) { - return; - } - - $filePath = $this->getFileCachePath($key); - $backupPath = $this->getFileCacheBackupPath($key); - - try { - // Create backup of existing cache - if (File::exists($filePath) && File::exists(dirname($backupPath))) { - File::copy($filePath, $backupPath); - } - - // Ensure directory exists - File::ensureDirectoryExists(dirname($filePath)); - - // Generate PHP cache file content - $content = $this->generateFileCacheContent($data); - - // Atomic write using temporary file - $tempPath = $filePath.'.tmp'; - File::put($tempPath, $content); - File::move($tempPath, $filePath); - - // Set appropriate permissions - chmod($filePath, 0644); - - } catch (Throwable $e) { - // Restore from backup if available - if (File::exists($backupPath)) { - File::copy($backupPath, $filePath); - } - - throw new CacheException("Failed to write file cache: {$e->getMessage()}", 0, $e); - } - } - - /** - * Generate PHP file content for cache. - * - * @param array $data - */ - protected function generateFileCacheContent(array $data): string - { - $timestamp = date('Y-m-d H:i:s'); - $count = count($data); - - $export = var_export($data, true); - - return <<getFileCachePath($key); - $backupPath = $this->getFileCacheBackupPath($key); - - File::delete([$filePath, $backupPath]); - } - - /** - * Clear all file caches. - */ - protected function clearAllFileCache(): void - { - $pattern = dirname($this->getFileCachePath('*')).'/modulite_*.php'; - $files = File::glob($pattern); - - File::delete($files); - } - - /** - * Get file cache path for a key. - */ - protected function getFileCachePath(string $key): string - { - $basePath = $this->config['file_cache']['path'] ?? base_path('bootstrap/cache/modulite_panels.php'); - - if ('panels' === $key) { - return $basePath; - } - - $dir = dirname($basePath); - $filename = 'modulite_'.md5($key).'.php'; - - return $dir.'/'.$filename; - } - - /** - * Get file cache backup path for a key. - */ - protected function getFileCacheBackupPath(string $key): string - { - $basePath = $this->config['file_cache']['backup_path'] ?? base_path('bootstrap/cache/modulite_panels_backup.php'); - - if ('panels' === $key) { - return $basePath; - } - - $dir = dirname($basePath); - $filename = 'modulite_'.md5($key).'_backup.php'; - - return $dir.'/'.$filename; - } - - /** - * Build cache key with prefix. - */ - protected function buildCacheKey(string $key): string - { - $prefix = $this->config['prefix'] ?? 'modulite'; - return $prefix.':'.$key; - } - - /** - * Get cache TTL in seconds. - */ - protected function getCacheTtl(): int - { - return $this->config['ttl'] ?? 3600; - } - - /** - * Execute callback and handle any exceptions. - * - * @return array - * - * @throws CacheException When callback execution fails - */ - protected function executeCallback(callable $callback): array - { - try { - $result = $callback(); - - if (!is_array($result)) { - throw new CacheException('Callback must return an array'); - } - - return $result; - } catch (Throwable $e) { - throw new CacheException("Callback execution failed: {$e->getMessage()}", 0, $e); - } - } - - /** - * Log cache hit for debugging. - */ - protected function logCacheHit(string $layer, string $key): void - { - if ($this->shouldLog('cache_hits')) { - Log::channel($this->getLogChannel())->debug("Modulite cache hit [{$layer}]: {$key}"); - } - } - - /** - * Log cache write for debugging. - */ - protected function logCacheWrite(string $key): void - { - if ($this->shouldLog('cache_writes')) { - Log::channel($this->getLogChannel())->debug("Modulite cache write: {$key}"); - } - } - - /** - * Log cache invalidation. - */ - protected function logCacheInvalidation(string $key): void - { - if ($this->shouldLog('cache_invalidation')) { - Log::channel($this->getLogChannel())->info("Modulite cache invalidated: {$key}"); - } - } - - /** - * Log cache flush. - */ - protected function logCacheFlush(): void - { - if ($this->shouldLog('cache_flush')) { - Log::channel($this->getLogChannel())->info('Modulite cache flushed'); - } - } - - /** - * Check if logging is enabled for specific event. - */ - protected function shouldLog(string $event): bool - { - $loggingConfig = config('modulite.logging', []); - - return ($loggingConfig['enabled'] ?? false) && - ($loggingConfig['log_'.$event] ?? false); - } - - /** - * Get logging channel. - */ - protected function getLogChannel(): string - { - return config('modulite.logging.channel', 'default'); - } -} diff --git a/src/Services/ComponentDiscoveryService.php b/src/Services/ComponentDiscoveryService.php index 79f05fc..2617ce1 100644 --- a/src/Services/ComponentDiscoveryService.php +++ b/src/Services/ComponentDiscoveryService.php @@ -14,6 +14,7 @@ use Filament\Widgets\Widget; use PanicDevs\Modulite\Contracts\ComponentScannerInterface; use PanicDevs\Modulite\Contracts\CacheManagerInterface; +use PanicDevs\Modulite\Contracts\ModuleResolverInterface; use RecursiveCallbackFilterIterator; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -28,6 +29,7 @@ class ComponentDiscoveryService implements ComponentScannerInterface { protected CacheManagerInterface $cache; + protected ModuleResolverInterface $moduleResolver; protected array $stats = [ 'scanned_modules' => 0, 'scanned_files' => 0, @@ -37,9 +39,12 @@ class ComponentDiscoveryService implements ComponentScannerInterface 'scan_time' => 0, ]; - public function __construct(CacheManagerInterface $cache = null) - { - $this->cache = $cache ?: app(CacheManagerInterface::class); + public function __construct( + CacheManagerInterface $cache = null, + ModuleResolverInterface $moduleResolver = null + ) { + $this->cache = $cache ?: app(CacheManagerInterface::class); + $this->moduleResolver = $moduleResolver ?: app(ModuleResolverInterface::class); } /** @@ -51,40 +56,97 @@ public function discoverComponentsForPanel(string $panelId): array } /** - * Discover all Filament components in configured scan locations. + * Discover all Filament components in configured scan locations with optimizations. * Implementation of ComponentScannerInterface method. */ public function discoverComponents(string $panelName): array { - $startTime = microtime(true); $cacheKey = "panel_components:{$panelName}"; - // Try to get from cache first + // Fast path: try cache first with minimal overhead $cached = $this->cache->get($cacheKey); - if (null !== $cached) { + if (null !== $cached) + { return $cached; } + // Only measure time in development + $startTime = app()->hasDebugModeEnabled() ? microtime(true) : 0; + + // Discover components efficiently + $components = $this->performOptimizedDiscovery($panelName); + + // Update stats only in development + if (app()->hasDebugModeEnabled()) + { + $this->stats['scan_time'] = microtime(true) - $startTime; + } + + // Store the results with appropriate TTL + $this->cache->put($cacheKey, $components); + + return $components; + } + + /** + * Perform optimized component discovery. + */ + protected function performOptimizedDiscovery(string $panelName): array + { + // Check if any component types are enabled before scanning + $enabledTypes = $this->getEnabledComponentTypes(); + $components = [ - 'resources' => $this->discoverResources($panelName)->toArray(), - 'pages' => $this->discoverPages($panelName)->toArray(), - 'widgets' => $this->discoverWidgets($panelName)->toArray(), + 'resources' => [], + 'pages' => [], + 'widgets' => [], ]; - $this->stats['scan_time'] = microtime(true) - $startTime; + // Only discover enabled types to avoid unnecessary work + if ($enabledTypes['resources'] ?? true) + { + $components['resources'] = $this->discoverResources($panelName)->toArray(); + } - // Store the results - $this->cache->put($cacheKey, $components); + if ($enabledTypes['pages'] ?? true) + { + $components['pages'] = $this->discoverPages($panelName)->toArray(); + } + + if ($enabledTypes['widgets'] ?? true) + { + $components['widgets'] = $this->discoverWidgets($panelName)->toArray(); + } return $components; } + /** + * Get enabled component types with caching. + */ + protected function getEnabledComponentTypes(): array + { + static $enabledTypes = null; + + if (null === $enabledTypes) + { + $enabledTypes = [ + 'resources' => $this->isComponentTypeEnabled('resources'), + 'pages' => $this->isComponentTypeEnabled('pages'), + 'widgets' => $this->isComponentTypeEnabled('widgets'), + ]; + } + + return $enabledTypes; + } + /** * Discover components of a specific type. */ public function discoverComponentType(string $panelName, string $componentType): array { - return match ($componentType) { + return match ($componentType) + { 'resources' => $this->discoverResources($panelName)->toArray(), 'pages' => $this->discoverPages($panelName)->toArray(), 'widgets' => $this->discoverWidgets($panelName)->toArray(), @@ -145,16 +207,20 @@ public function discoverComponentsFromModule(string $moduleName, ?string $panelI $scanLocations = $this->getScanLocations($panelId); - foreach ($scanLocations as $location) { - foreach (['resources', 'pages', 'widgets'] as $type) { - if (!$this->isComponentTypeEnabled($type)) { + foreach ($scanLocations as $location) + { + foreach (['resources', 'pages', 'widgets'] as $type) + { + if (!$this->isComponentTypeEnabled($type)) + { continue; } $typePath = $this->resolveComponentPath($moduleName, $location, $type, $panelId); - if (File::isDirectory($typePath)) { - $found = $this->scanDirectoryForComponents($typePath, $moduleName, $type); + if (File::isDirectory($typePath)) + { + $found = $this->scanDirectoryForComponents($typePath, $moduleName, $type); $components[$type] = $components[$type]->merge($found); } } @@ -168,30 +234,35 @@ public function discoverComponentsFromModule(string $moduleName, ?string $panelI } /** - * Check if a class is a valid Filament component. + * Check if a class is a valid Filament component - simplified without attributes. */ public function isValidComponent(string $className, string $type): bool { - if (!class_exists($className)) { + if (!class_exists($className)) + { return false; } - try { + try + { $reflection = new ReflectionClass($className); - if ($reflection->isAbstract() || $reflection->isInterface() || $reflection->isTrait()) { + if ($reflection->isAbstract() || $reflection->isInterface() || $reflection->isTrait()) + { return false; } - // More flexible validation - check if the class is compatible with the component type - return match ($type) { + // Simple inheritance-based validation without attribute checking + return match ($type) + { 'resources' => $this->isValidResourceComponent($reflection), 'pages' => $this->isValidPageComponent($reflection), 'widgets' => $this->isValidWidgetComponent($reflection), default => false, }; - } catch (ReflectionException $e) { + } catch (ReflectionException $e) + { return false; } } @@ -203,24 +274,29 @@ public function isValidComponent(string $className, string $type): bool protected function isValidResourceComponent(ReflectionClass $reflection): bool { // First check if it's a subclass of Resource (the common case) - if ($reflection->isSubclassOf(Resource::class)) { + if ($reflection->isSubclassOf(Resource::class)) + { return true; } // Check if strict inheritance is required - if (config('modulite.components.types.resources.strict_inheritance', false)) { + if (config('modulite.components.types.resources.strict_inheritance', false)) + { return false; } // Check if custom base classes are allowed - if (!config('modulite.components.types.resources.allow_custom_base_classes', true)) { + if (!config('modulite.components.types.resources.allow_custom_base_classes', true)) + { return false; } // Check for required resource methods (duck typing approach) $requiredMethods = ['getModel', 'form', 'table']; - foreach ($requiredMethods as $method) { - if (!$reflection->hasMethod($method)) { + foreach ($requiredMethods as $method) + { + if (!$reflection->hasMethod($method)) + { return false; } } @@ -235,31 +311,37 @@ protected function isValidResourceComponent(ReflectionClass $reflection): bool protected function isValidPageComponent(ReflectionClass $reflection): bool { // First check if it's a subclass of Page (the common case) - if ($reflection->isSubclassOf(Page::class)) { + if ($reflection->isSubclassOf(Page::class)) + { return true; } // Check if strict inheritance is required - if (config('modulite.components.types.pages.strict_inheritance', false)) { + if (config('modulite.components.types.pages.strict_inheritance', false)) + { return false; } // Check if custom base classes are allowed - if (!config('modulite.components.types.pages.allow_custom_base_classes', true)) { + if (!config('modulite.components.types.pages.allow_custom_base_classes', true)) + { return false; } // Check for page characteristics (duck typing approach) // Pages typically implement Livewire Component interface - if ($reflection->implementsInterface(\Livewire\Component::class)) { + if ($reflection->implementsInterface(\Livewire\Component::class)) + { return true; } // Check for common page methods - $pageMethods = ['render', 'mount']; + $pageMethods = ['render', 'mount']; $hasPageMethod = false; - foreach ($pageMethods as $method) { - if ($reflection->hasMethod($method)) { + foreach ($pageMethods as $method) + { + if ($reflection->hasMethod($method)) + { $hasPageMethod = true; break; } @@ -275,31 +357,37 @@ protected function isValidPageComponent(ReflectionClass $reflection): bool protected function isValidWidgetComponent(ReflectionClass $reflection): bool { // First check if it's a subclass of Widget (the common case) - if ($reflection->isSubclassOf(Widget::class)) { + if ($reflection->isSubclassOf(Widget::class)) + { return true; } // Check if strict inheritance is required - if (config('modulite.components.types.widgets.strict_inheritance', false)) { + if (config('modulite.components.types.widgets.strict_inheritance', false)) + { return false; } // Check if custom base classes are allowed - if (!config('modulite.components.types.widgets.allow_custom_base_classes', true)) { + if (!config('modulite.components.types.widgets.allow_custom_base_classes', true)) + { return false; } // Check for widget characteristics (duck typing approach) // Widgets typically implement Livewire Component interface - if ($reflection->implementsInterface(\Livewire\Component::class)) { + if ($reflection->implementsInterface(\Livewire\Component::class)) + { return true; } // Check for common widget methods - $widgetMethods = ['render', 'getData']; + $widgetMethods = ['render', 'getData']; $hasWidgetMethod = false; - foreach ($widgetMethods as $method) { - if ($reflection->hasMethod($method)) { + foreach ($widgetMethods as $method) + { + if ($reflection->hasMethod($method)) + { $hasWidgetMethod = true; break; } @@ -328,23 +416,26 @@ protected function discoverComponentsByType(string $type, ?string $panelId = nul // Check cache first $cached = $this->cache->get($cacheKey); - if (null !== $cached) { + if (null !== $cached) + { return collect($cached); } // Compute the value - if (!$this->isComponentTypeEnabled($type)) { + if (!$this->isComponentTypeEnabled($type)) + { return collect(); } - $components = collect(); + $components = collect(); $enabledModules = $this->getEnabledModules(); $this->stats['scanned_modules'] = $enabledModules->count(); - foreach ($enabledModules as $moduleName) { + foreach ($enabledModules as $moduleName) + { $moduleComponents = $this->discoverComponentsFromModule($moduleName, $panelId); - $components = $components->merge($moduleComponents[$type]); + $components = $components->merge($moduleComponents[$type]); } $this->stats["found_{$type}"] = $components->count(); @@ -363,10 +454,12 @@ protected function discoverComponentsByType(string $type, ?string $panelId = nul protected function scanDirectoryForComponents(string $path, string $moduleName, string $type): Collection { $components = collect(); - $iterator = $this->createDirectoryIterator($path); + $iterator = $this->createDirectoryIterator($path); - foreach ($iterator as $file) { - if (!$file->isFile() || 'php' !== $file->getExtension()) { + foreach ($iterator as $file) + { + if (!$file->isFile() || 'php' !== $file->getExtension()) + { continue; } @@ -374,7 +467,8 @@ protected function scanDirectoryForComponents(string $path, string $moduleName, $className = $this->extractClassNameFromFile($file->getPathname(), $moduleName); - if ($className && $this->isValidComponent($className, $type)) { + if ($className && $this->isValidComponent($className, $type)) + { $components->push($className); } } @@ -394,13 +488,15 @@ protected function createDirectoryIterator(string $path): RecursiveIteratorItera $filterIterator = new RecursiveCallbackFilterIterator( $directoryIterator, - function ($current, $key, $iterator) { + function ($current, $key, $iterator) + { // Skip excluded directories $excludedDirs = config('modulite.components.scanning.excluded_directories', [ 'tests', 'migrations', 'seeders', 'factories', '.git', 'node_modules', 'vendor' ]); - if ($current->isDir()) { + if ($current->isDir()) + { $dirName = $current->getFilename(); return !in_array($dirName, $excludedDirs); } @@ -423,12 +519,14 @@ function ($current, $key, $iterator) { */ protected function extractClassNameFromFile(string $filePath, string $moduleName): ?string { - try { + try + { $content = File::get($filePath); // Extract namespace $namespacePattern = '/namespace\s+([^;]+);/'; - if (!preg_match($namespacePattern, $content, $namespaceMatches)) { + if (!preg_match($namespacePattern, $content, $namespaceMatches)) + { return null; } @@ -436,7 +534,8 @@ protected function extractClassNameFromFile(string $filePath, string $moduleName // Extract class name $classPattern = '/class\s+(\w+)(?:\s+extends\s+[\w\\\\]+)?(?:\s+implements\s+[\w,\s\\\\]+)?\s*{/'; - if (!preg_match($classPattern, $content, $classMatches)) { + if (!preg_match($classPattern, $content, $classMatches)) + { return null; } @@ -444,7 +543,8 @@ protected function extractClassNameFromFile(string $filePath, string $moduleName return $namespace.'\\'.$className; - } catch (Throwable $e) { + } catch (Throwable $e) + { return null; } } @@ -464,9 +564,11 @@ protected function getScanLocations(?string $panelId = null): array ]); // Replace {panel} placeholder with actual panel ID - if ($panelId) { + if ($panelId) + { $locations = array_map(fn ($location) => str_replace('{panel}', Str::studly($panelId), $location), $locations); - } else { + } else + { // For global discovery, we might want to scan all panels or use a default $locations = array_map(fn ($location) => str_replace('{panel}', '*', $location), $locations); } @@ -481,12 +583,14 @@ protected function resolveComponentPath(string $moduleName, string $locationPatt { $resolved = str_replace('*', $moduleName, $locationPattern); - if ($panelId && str_contains($resolved, '{panel}')) { + if ($panelId && str_contains($resolved, '{panel}')) + { $resolved = str_replace('{panel}', Str::studly($panelId), $resolved); } // If location pattern doesn't include the component type, append it - if (!str_ends_with(mb_strtolower($resolved), mb_strtolower($type))) { + if (!str_ends_with(mb_strtolower($resolved), mb_strtolower($type))) + { $resolved = mb_rtrim($resolved, '/').'/'.Str::studly($type); } @@ -502,43 +606,21 @@ protected function isComponentTypeEnabled(string $type): bool } /** - * Get enabled modules. + * Get enabled modules using the configured module resolver. */ protected function getEnabledModules(): Collection { - // Reuse logic from PanelScannerService or inject it as dependency $cacheKey = 'enabled_modules'; // Check cache first $cached = $this->cache->get($cacheKey); - if (null !== $cached) { + if (null !== $cached) + { return collect($cached); } - // Compute the value - $modules = collect(); - - // Check if nwidart/laravel-modules is available - if (class_exists(\Nwidart\Modules\Facades\Module::class)) { - $enabledModules = \Nwidart\Modules\Facades\Module::allEnabled(); - - foreach ($enabledModules as $module) { - $modules->push($module->getName()); - } - } else { - // Fallback: scan modules_statuses.json - $statusFile = base_path('modules_statuses.json'); - - if (File::exists($statusFile)) { - $statuses = json_decode(File::get($statusFile), true); - - foreach ($statuses as $moduleName => $enabled) { - if ($enabled) { - $modules->push($moduleName); - } - } - } - } + // Get modules from the resolver + $modules = $this->moduleResolver->getEnabledModules(); // Store the result $this->cache->put($cacheKey, $modules->toArray()); diff --git a/src/Services/ComponentScannerService.php b/src/Services/ComponentScannerService.php deleted file mode 100644 index b2fec5e..0000000 --- a/src/Services/ComponentScannerService.php +++ /dev/null @@ -1,774 +0,0 @@ - - */ - protected array $config; - - /** - * Base application path. - */ - protected string $basePath; - - /** - * Module manager instance. - */ - protected mixed $moduleManager; - - /** - * Discovered components cache for current scan. - * - * @var array>> - */ - protected array $discoveredComponents = []; - - /** - * Scan statistics for performance monitoring. - * - * @var array - */ - protected array $scanStats = [ - 'files_scanned' => 0, - 'classes_found' => 0, - 'components_discovered' => 0, - 'scan_time' => 0, - 'errors' => 0, - ]; - - /** - * Component type mapping to attribute classes. - * - * @var array - */ - protected array $componentTypeMap = [ - 'resources' => FilamentResource::class, - 'pages' => FilamentPage::class, - 'widgets' => FilamentWidget::class, - ]; - - /** - * Component type mapping to base classes (fallback detection). - * - * @var array - */ - protected array $baseClassMap = [ - 'resources' => Resource::class, - 'pages' => Page::class, - 'widgets' => Widget::class, - ]; - - /** - * Create a new ComponentScannerService instance. - * - * @param array $config Scanner configuration - * @param string $basePath Application base path - * @param mixed $moduleManager nwidart module manager - */ - public function __construct(array $config, string $basePath, mixed $moduleManager = null) - { - $this->config = $config; - $this->basePath = mb_rtrim($basePath, '/'); - $this->moduleManager = $moduleManager; - } - - /** - * {@inheritDoc} - */ - public function discoverComponents(string $panelName): array - { - $startTime = microtime(true); - $this->resetScanStats(); - - try { - $this->logScanStart($panelName); - - $allComponents = []; - $scanLocations = $this->resolveScanLocations($panelName); - - foreach ($scanLocations as $location) { - $discovered = $this->scanLocationForComponents($location); - $allComponents = $this->mergeComponents($allComponents, $discovered); - } - - // Remove duplicates and ensure stable ordering - $allComponents = $this->cleanupComponents($allComponents); - - $this->scanStats['components_discovered'] = $this->countTotalComponents($allComponents); - $this->scanStats['scan_time'] = microtime(true) - $startTime; - - $this->logScanComplete($panelName, $allComponents); - - return $allComponents; - - } catch (Throwable $e) { - $this->scanStats['scan_time'] = microtime(true) - $startTime; - $this->logScanError($panelName, $e); - - if ($this->shouldFailSilently()) { - return []; - } - - throw new ScanException("Component discovery failed for panel {$panelName}: {$e->getMessage()}", 0, $e); - } - } - - /** - * {@inheritDoc} - */ - public function discoverComponentType(string $panelName, string $componentType): array - { - if (!isset($this->componentTypeMap[$componentType])) { - throw new ScanException("Invalid component type: {$componentType}"); - } - - $allComponents = $this->discoverComponents($panelName); - return $allComponents[$componentType] ?? []; - } - - /** - * {@inheritDoc} - */ - public function getScanStats(): array - { - return $this->scanStats; - } - - /** - * {@inheritDoc} - */ - public function isComponentType(string $className, string $componentType): bool - { - if (!isset($this->componentTypeMap[$componentType])) { - return false; - } - - try { - if (!class_exists($className)) { - return false; - } - - $reflection = new ReflectionClass($className); - - // Check if class is instantiable first - if (!$reflection->isInstantiable()) { - return false; - } - - // Primary check: Look for component attribute - $attributeClass = $this->componentTypeMap[$componentType]; - $attributes = $reflection->getAttributes($attributeClass); - - if (!empty($attributes)) { - // Found the attribute, check if it's enabled for the panel - $attribute = $attributes[0]->newInstance(); - return $attribute->isEnabled(); - } - - // Fallback check: Check if it extends the base class - $baseClass = $this->baseClassMap[$componentType] ?? null; - return (bool) ($baseClass && $reflection->isSubclassOf($baseClass)) - - - - ; - - } catch (Throwable $e) { - $this->logComponentCheckError($className, $componentType, $e); - return false; - } - } - - /** - * Check if a component is enabled for a specific panel. - * - * @param string $className Component class name - * @param string $componentType Component type - * @param string $panelId Panel identifier - * @return bool True if component should be registered with panel - */ - public function isComponentEnabledForPanel(string $className, string $componentType, string $panelId): bool - { - try { - if (!class_exists($className)) { - return false; - } - - $reflection = new ReflectionClass($className); - $attributeClass = $this->componentTypeMap[$componentType] ?? null; - - if (!$attributeClass) { - return true; // No attribute type defined, assume enabled - } - - $attributes = $reflection->getAttributes($attributeClass); - - if (!empty($attributes)) { - $attribute = $attributes[0]->newInstance(); - return $attribute->isEnabledForPanel($panelId); - } - - // No attribute found, check fallback - return $this->isComponentType($className, $componentType); - - } catch (Throwable $e) { - $this->logComponentCheckError($className, $componentType, $e); - return false; - } - } - - /** - * Get component priority from attribute. - * - * @param string $className Component class name - * @param string $componentType Component type - * @return int Component priority (higher = first) - */ - public function getComponentPriority(string $className, string $componentType): int - { - try { - if (!class_exists($className)) { - return 0; - } - - $reflection = new ReflectionClass($className); - $attributeClass = $this->componentTypeMap[$componentType] ?? null; - - if (!$attributeClass) { - return 0; - } - - $attributes = $reflection->getAttributes($attributeClass); - - if (!empty($attributes)) { - $attribute = $attributes[0]->newInstance(); - return $attribute->getPriority(); - } - - // Fallback: Check for static getPriority method - if (method_exists($className, 'getPriority')) { - return $className::getPriority(); - } - - return 0; - - } catch (Throwable $e) { - $this->logComponentCheckError($className, $componentType, $e); - return 0; - } - } - - /** - * Filter components by panel and sort by priority. - * - * @param array> $components Components by type - * @param string $panelId Panel identifier - * @return array> Filtered and sorted components - */ - public function filterAndSortComponentsForPanel(array $components, string $panelId): array - { - $filtered = []; - - foreach ($components as $type => $classList) { - $enabledComponents = []; - - foreach ($classList as $className) { - if ($this->isComponentEnabledForPanel($className, $type, $panelId)) { - $enabledComponents[] = [ - 'class' => $className, - 'priority' => $this->getComponentPriority($className, $type) - ]; - } - } - - // Sort by priority (higher first), then by name - usort($enabledComponents, function ($a, $b) { - if ($a['priority'] === $b['priority']) { - return strcmp($a['class'], $b['class']); - } - return $b['priority'] <=> $a['priority']; - }); - - $filtered[$type] = array_column($enabledComponents, 'class'); - } - - return $filtered; - } - - /** - * Resolve scan locations for a specific panel. - * - * @param string $panelName Panel name to resolve locations for - * @return array Array of resolved absolute paths - */ - protected function resolveScanLocations(string $panelName): array - { - $locations = $this->config['component_scan']['locations'] ?? []; - $resolvedLocations = []; - - foreach ($locations as $pattern) { - $resolved = $this->resolveLocationPattern($pattern, $panelName); - $resolvedLocations = array_merge($resolvedLocations, $resolved); - } - - // Remove duplicates and ensure directories exist - $resolvedLocations = array_unique($resolvedLocations); - $resolvedLocations = array_filter($resolvedLocations, 'is_dir'); - - return array_values($resolvedLocations); - } - - /** - * Resolve a location pattern to absolute paths for a panel. - * - * @param string $pattern Location pattern with placeholders - * @param string $panelName Panel name to substitute - * @return array Array of resolved absolute paths - */ - protected function resolveLocationPattern(string $pattern, string $panelName): array - { - // Replace panel name placeholder - $pattern = str_replace('{PanelName}', $panelName, $pattern); - - // Convert relative pattern to absolute - if (!str_starts_with($pattern, '/')) { - $pattern = $this->basePath.'/'.mb_ltrim($pattern, '/'); - } - - // Handle wildcard patterns - if (str_contains($pattern, '*')) { - return $this->expandWildcardPattern($pattern); - } - - // Single directory - return [$pattern]; - } - - /** - * Expand wildcard patterns to concrete paths. - * - * @param string $pattern Pattern with wildcards - * @return array Array of expanded paths - */ - protected function expandWildcardPattern(string $pattern): array - { - $paths = []; - - try { - $globPaths = glob($pattern, GLOB_ONLYDIR); - - if (false !== $globPaths) { - $paths = $globPaths; - } - - } catch (Throwable $e) { - $this->logPatternExpansionError($pattern, $e); - } - - return $paths; - } - - /** - * Scan a specific location for component classes. - * - * @param string $location Directory path to scan - * @return array> Discovered components by type - */ - protected function scanLocationForComponents(string $location): array - { - if (!is_dir($location)) { - $this->logLocationSkipped($location, 'Directory does not exist'); - return []; - } - - $components = [ - 'resources' => [], - 'pages' => [], - 'widgets' => [] - ]; - - try { - $files = $this->getPhpFilesInLocation($location); - - foreach ($files as $file) { - $discovered = $this->scanFileForComponents($file); - $components = $this->mergeComponents($components, $discovered); - } - - } catch (Throwable $e) { - $this->logLocationError($location, $e); - $this->scanStats['errors']++; - - if (!$this->shouldFailSilently()) { - throw new ScanException("Failed to scan location {$location}: {$e->getMessage()}", 0, $e); - } - } - - return $components; - } - - /** - * Scan a single PHP file for component classes. - * - * @param string $filePath Path to PHP file - * @return array> Component classes found in file by type - */ - protected function scanFileForComponents(string $filePath): array - { - $this->scanStats['files_scanned']++; - - try { - $classes = $this->extractClassesFromFile($filePath); - $this->scanStats['classes_found'] += count($classes); - - $components = [ - 'resources' => [], - 'pages' => [], - 'widgets' => [] - ]; - - foreach ($classes as $className) { - foreach ($this->componentTypeMap as $type => $attributeClass) { - if ($this->isComponentType($className, $type)) { - $components[$type][] = $className; - break; // A class can only be one component type - } - } - } - - return $components; - - } catch (Throwable $e) { - $this->logFileError($filePath, $e); - $this->scanStats['errors']++; - - if (!$this->shouldFailSilently()) { - throw new ScanException("Failed to scan file {$filePath}: {$e->getMessage()}", 0, $e); - } - - return [ - 'resources' => [], - 'pages' => [], - 'widgets' => [] - ]; - } - } - - /** - * Extract class names from a PHP file using token parsing. - * - * @param string $filePath Path to PHP file - * @return array Array of fully qualified class names - */ - protected function extractClassesFromFile(string $filePath): array - { - $content = File::get($filePath); - $tokens = token_get_all($content); - $classes = []; - - $namespace = ''; - $className = ''; - $braceLevel = 0; - $inClass = false; - - for ($i = 0, $count = count($tokens); $i < $count; $i++) { - $token = $tokens[$i]; - - if (is_array($token)) { - switch ($token[0]) { - case T_NAMESPACE: - $namespace = $this->parseNamespace($tokens, $i); - break; - - case T_CLASS: - $className = $this->parseClassName($tokens, $i); - if ($className) { - $fullyQualified = $namespace ? $namespace.'\\'.$className : $className; - $classes[] = $fullyQualified; - $inClass = true; - } - break; - } - } elseif ('{' === $token) { - if ($inClass) { - $braceLevel++; - } - } elseif ('}' === $token) { - if ($inClass) { - $braceLevel--; - if (0 === $braceLevel) { - $inClass = false; - $className = ''; - } - } - } - } - - return $classes; - } - - /** - * Parse namespace from tokens. - */ - protected function parseNamespace(array $tokens, int &$index): string - { - $namespace = ''; - - for ($i = $index + 1; $i < count($tokens); $i++) { - $token = $tokens[$i]; - - if (is_array($token) && in_array($token[0], [T_STRING, T_NS_SEPARATOR])) { - $namespace .= $token[1]; - } elseif (';' === $token) { - break; - } - } - - return mb_trim($namespace); - } - - /** - * Parse class name from tokens. - */ - protected function parseClassName(array $tokens, int &$index): string - { - for ($i = $index + 1; $i < count($tokens); $i++) { - $token = $tokens[$i]; - - if (is_array($token) && T_STRING === $token[0]) { - return $token[1]; - } - } - - return ''; - } - - /** - * Get PHP files in a location with filtering. - * - * @param string $location Directory to scan - * @return Generator Generator yielding file paths - */ - protected function getPhpFilesInLocation(string $location): Generator - { - $maxDepth = $this->config['component_scan']['max_depth'] ?? 10; - $excludedDirs = $this->config['component_scan']['excluded_directories'] ?? []; - $followSymlinks = $this->config['component_scan']['follow_symlinks'] ?? false; - - $flags = RecursiveDirectoryIterator::SKIP_DOTS; - if ($followSymlinks) { - $flags |= RecursiveDirectoryIterator::FOLLOW_SYMLINKS; - } - - $iterator = new RecursiveIteratorIterator( - new RecursiveCallbackFilterIterator( - new RecursiveDirectoryIterator($location, $flags), - function (SplFileInfo $file) use ($excludedDirs) { - if ($file->isDir()) { - $name = $file->getFilename(); - - // Skip excluded directories - if (in_array($name, $excludedDirs, true) || str_starts_with($name, '.')) { - return false; - } - } - - return $file->isDir() || 'php' === $file->getExtension(); - } - ), - RecursiveIteratorIterator::LEAVES_ONLY - ); - - if ($maxDepth > 0) { - $iterator->setMaxDepth($maxDepth); - } - - foreach ($iterator as $file) { - /** @var SplFileInfo $file */ - if ($file->isFile() && 'php' === $file->getExtension()) { - yield $file->getPathname(); - } - } - } - - /** - * Merge two component arrays. - */ - protected function mergeComponents(array $target, array $source): array - { - foreach ($source as $type => $components) { - if (!isset($target[$type])) { - $target[$type] = []; - } - $target[$type] = array_merge($target[$type], $components); - } - - return $target; - } - - /** - * Clean up components array by removing duplicates. - */ - protected function cleanupComponents(array $components): array - { - foreach ($components as $type => $classList) { - $components[$type] = array_values(array_unique($classList)); - } - - return $components; - } - - /** - * Count total components across all types. - */ - protected function countTotalComponents(array $components): int - { - $total = 0; - foreach ($components as $type => $classList) { - $total += count($classList); - } - return $total; - } - - /** - * Reset scan statistics. - */ - protected function resetScanStats(): void - { - $this->scanStats = [ - 'files_scanned' => 0, - 'classes_found' => 0, - 'components_discovered' => 0, - 'scan_time' => 0, - 'errors' => 0, - ]; - } - - /** - * Check if scanner should fail silently. - */ - protected function shouldFailSilently(): bool - { - return $this->config['error_handling']['fail_silently'] ?? false; - } - - // Logging methods... - protected function logScanStart(string $panelName): void - { - if ($this->config['logging']['enabled'] ?? false) { - Log::channel($this->config['logging']['channel'] ?? 'default') - ->info("Starting component discovery for panel: {$panelName}"); - } - } - - protected function logScanComplete(string $panelName, array $components): void - { - if ($this->config['logging']['enabled'] ?? false) { - $total = $this->countTotalComponents($components); - Log::channel($this->config['logging']['channel'] ?? 'default') - ->info("Component discovery completed for panel: {$panelName}", [ - 'components_found' => $total, - 'scan_time' => $this->scanStats['scan_time'], - 'files_scanned' => $this->scanStats['files_scanned'] - ]); - } - } - - protected function logScanError(string $panelName, Throwable $e): void - { - if ($this->config['logging']['enabled'] ?? false) { - Log::channel($this->config['logging']['channel'] ?? 'default') - ->error("Component discovery failed for panel: {$panelName}", [ - 'error' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine() - ]); - } - } - - protected function logLocationSkipped(string $location, string $reason): void - { - if (($this->config['logging']['enabled'] ?? false) && ($this->config['development']['verbose_logging'] ?? false)) { - Log::channel($this->config['logging']['channel'] ?? 'default') - ->debug("Skipped component scan location: {$location}", ['reason' => $reason]); - } - } - - protected function logLocationError(string $location, Throwable $e): void - { - if ($this->config['logging']['enabled'] ?? false) { - Log::channel($this->config['logging']['channel'] ?? 'default') - ->warning("Component scan location error: {$location}", [ - 'error' => $e->getMessage() - ]); - } - } - - protected function logFileError(string $filePath, Throwable $e): void - { - if ($this->config['logging']['enabled'] ?? false) { - Log::channel($this->config['logging']['channel'] ?? 'default') - ->warning("Component scan file error: {$filePath}", [ - 'error' => $e->getMessage() - ]); - } - } - - protected function logComponentCheckError(string $className, string $componentType, Throwable $e): void - { - if (($this->config['logging']['enabled'] ?? false) && ($this->config['development']['verbose_logging'] ?? false)) { - Log::channel($this->config['logging']['channel'] ?? 'default') - ->debug("Component type check error: {$className}", [ - 'component_type' => $componentType, - 'error' => $e->getMessage() - ]); - } - } - - protected function logPatternExpansionError(string $pattern, Throwable $e): void - { - if ($this->config['logging']['enabled'] ?? false) { - Log::channel($this->config['logging']['channel'] ?? 'default') - ->warning("Pattern expansion error: {$pattern}", [ - 'error' => $e->getMessage() - ]); - } - } -} diff --git a/src/Services/ModuleResolvers/NwidartModuleResolver.php b/src/Services/ModuleResolvers/NwidartModuleResolver.php new file mode 100644 index 0000000..07183c4 --- /dev/null +++ b/src/Services/ModuleResolvers/NwidartModuleResolver.php @@ -0,0 +1,157 @@ + Collection of module names + */ + public function getEnabledModules(): Collection + { + if (!$this->isAvailable()) + { + return $this->getFallbackModules(); + } + + try + { + $enabledModules = \Nwidart\Modules\Facades\Module::allEnabled(); + $modules = collect(); + + foreach ($enabledModules as $module) + { + $modules->push($module->getName()); + } + + return $modules; + } catch (Throwable $e) + { + // Log error if needed + return $this->getFallbackModules(); + } + } + + /** + * Check if a specific module is enabled. + * + * @param string $moduleName Module name to check + * @return bool True if module is enabled, false otherwise + */ + public function isModuleEnabled(string $moduleName): bool + { + if (!$this->isAvailable()) + { + return $this->getFallbackModules()->contains($moduleName); + } + + try + { + return \Nwidart\Modules\Facades\Module::find($moduleName)?->isEnabled() ?? false; + } catch (Throwable $e) + { + return $this->getFallbackModules()->contains($moduleName); + } + } + + /** + * Get all available modules (enabled and disabled). + * + * @return Collection Collection of all module names + */ + public function getAllModules(): Collection + { + if (!$this->isAvailable()) + { + return $this->getFallbackModules(); + } + + try + { + $allModules = \Nwidart\Modules\Facades\Module::all(); + $modules = collect(); + + foreach ($allModules as $module) + { + $modules->push($module->getName()); + } + + return $modules; + } catch (Throwable $e) + { + return $this->getFallbackModules(); + } + } + + /** + * Get the module system name/type. + * + * @return string Module system identifier + */ + public function getSystemType(): string + { + return 'nwidart'; + } + + /** + * Check if the module system is available and properly configured. + * + * @return bool True if system is available, false otherwise + */ + public function isAvailable(): bool + { + return class_exists(\Nwidart\Modules\Facades\Module::class); + } + + /** + * Get fallback modules from modules_statuses.json. + * + * @return Collection Collection of module names + */ + protected function getFallbackModules(): Collection + { + $modules = collect(); + $statusFile = base_path('modules_statuses.json'); + + if (File::exists($statusFile)) + { + try + { + $statuses = json_decode(File::get($statusFile), true); + + if (is_array($statuses)) + { + foreach ($statuses as $moduleName => $enabled) + { + if ($enabled) + { + $modules->push($moduleName); + } + } + } + } catch (Throwable $e) + { + // Silently fail - return empty collection + } + } + + return $modules; + } +} diff --git a/src/Services/ModuleResolvers/PanicDevsModuleResolver.php b/src/Services/ModuleResolvers/PanicDevsModuleResolver.php new file mode 100644 index 0000000..56e0a19 --- /dev/null +++ b/src/Services/ModuleResolvers/PanicDevsModuleResolver.php @@ -0,0 +1,147 @@ +app = $app; + } + + /** + * Get collection of enabled module names. + * + * @return Collection Collection of module names + */ + public function getEnabledModules(): Collection + { + if (!$this->isAvailable()) + { + return collect(); + } + + try + { + $modules = $this->getModuleService()->getEnabledByPriority(); + + return collect($modules)->pluck('name'); + } catch (Throwable $e) + { + // Log error if needed + return collect(); + } + } + + /** + * Check if a specific module is enabled. + * + * @param string $moduleName Module name to check + * @return bool True if module is enabled, false otherwise + */ + public function isModuleEnabled(string $moduleName): bool + { + if (!$this->isAvailable()) + { + return false; + } + + try + { + return $this->getModuleService()->isEnabled($moduleName); + } catch (Throwable $e) + { + return false; + } + } + + /** + * Get all available modules (enabled and disabled). + * + * @return Collection Collection of all module names + */ + public function getAllModules(): Collection + { + if (!$this->isAvailable()) + { + return collect(); + } + + try + { + $modules = $this->getModuleService()->getAll(); + + return collect($modules)->pluck('name'); + } catch (Throwable $e) + { + return collect(); + } + } + + /** + * Get the module system name/type. + * + * @return string Module system identifier + */ + public function getSystemType(): string + { + return 'panicdevs'; + } + + /** + * Check if the module system is available and properly configured. + * + * @return bool True if system is available, false otherwise + */ + public function isAvailable(): bool + { + return class_exists(ModuleService::class) && + $this->app->bound(ModuleService::class); + } + + /** + * Get the module service instance. + * + * @return ModuleService + */ + protected function getModuleService(): ModuleService + { + if (null === $this->moduleService) + { + $this->moduleService = $this->app->make(ModuleService::class); + } + + return $this->moduleService; + } +} diff --git a/src/Services/PanelScannerService.php b/src/Services/PanelScannerService.php index ea67516..cbca912 100644 --- a/src/Services/PanelScannerService.php +++ b/src/Services/PanelScannerService.php @@ -6,9 +6,7 @@ use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Log; -use PanicDevs\Modulite\Attributes\FilamentPanel; use PanicDevs\Modulite\Contracts\PanelScannerInterface; -use PanicDevs\Modulite\Exceptions\ScanException; use ReflectionClass; use RecursiveCallbackFilterIterator; use RecursiveDirectoryIterator; @@ -23,7 +21,7 @@ * This service is responsible for: * - Scanning configured paths for PHP files * - Extracting class names using token parsing - * - Filtering classes with #[FilamentPanel] attribute + * - Filtering panel provider classes by naming conventions and inheritance * - Performance optimization with depth limits and exclusions * - Comprehensive error handling and logging * @@ -77,31 +75,31 @@ class PanelScannerService implements PanelScannerInterface */ public function __construct(array $config, string $basePath, mixed $moduleManager = null) { - $this->config = $config; - $this->basePath = mb_rtrim($basePath, '/'); + $this->config = $config; + $this->basePath = mb_rtrim($basePath, '/'); $this->moduleManager = $moduleManager; } /** * Discover all Filament Panel classes in configured locations. * - * @return array Array of fully qualified class names with #[FilamentPanel] attribute - * - * @throws ScanException When scanning fails critically + * @return array Array of fully qualified panel provider class names */ public function discoverPanels(): array { $startTime = microtime(true); $this->resetScanStats(); - try { + try + { $this->logScanStart(); - $panelClasses = []; + $panelClasses = []; $scanLocations = $this->resolveScanLocations(); - foreach ($scanLocations as $location) { - $discovered = $this->scanLocation($location); + foreach ($scanLocations as $location) + { + $discovered = $this->scanLocation($location); $panelClasses = array_merge($panelClasses, $discovered); } @@ -109,17 +107,19 @@ public function discoverPanels(): array $panelClasses = array_values(array_unique($panelClasses)); $this->scanStats['panels_discovered'] = count($panelClasses); - $this->scanStats['scan_time'] = microtime(true) - $startTime; + $this->scanStats['scan_time'] = microtime(true) - $startTime; $this->logScanComplete($panelClasses); return $panelClasses; - } catch (Throwable $e) { + } catch (Throwable $e) + { $this->scanStats['scan_time'] = microtime(true) - $startTime; $this->logScanError($e); - if ($this->shouldFailSilently()) { + if ($this->shouldFailSilently()) + { return []; } @@ -135,26 +135,31 @@ public function discoverPanels(): array */ protected function scanLocation(string $location): array { - if (!is_dir($location)) { + if (!is_dir($location)) + { $this->logLocationSkipped($location, 'Directory does not exist'); return []; } $panelClasses = []; - try { + try + { $files = $this->getPhpFilesInLocation($location); - foreach ($files as $file) { - $discovered = $this->scanFile($file); + foreach ($files as $file) + { + $discovered = $this->scanFile($file); $panelClasses = array_merge($panelClasses, $discovered); } - } catch (Throwable $e) { + } catch (Throwable $e) + { $this->logLocationError($location, $e); $this->scanStats['errors']++; - if (!$this->shouldFailSilently()) { + if (!$this->shouldFailSilently()) + { throw new ScanException("Failed to scan location {$location}: {$e->getMessage()}", 0, $e); } } @@ -172,29 +177,35 @@ protected function scanFile(string $filePath): array { $this->scanStats['files_scanned']++; - try { + try + { $classes = $this->extractClassesFromFile($filePath); $this->scanStats['classes_found'] += count($classes); $panelClasses = []; - foreach ($classes as $className) { - if ($this->isPanelClass($className)) { + foreach ($classes as $className) + { + if ($this->isPanelClass($className)) + { $panelClasses[] = $className; } } - if (!empty($panelClasses)) { + if (!empty($panelClasses)) + { $this->logPanelsFound($filePath, $panelClasses); } return $panelClasses; - } catch (Throwable $e) { + } catch (Throwable $e) + { $this->logFileError($filePath, $e); $this->scanStats['errors']++; - if (!$this->shouldFailSilently()) { + if (!$this->shouldFailSilently()) + { throw new ScanException("Failed to scan file {$filePath}: {$e->getMessage()}", 0, $e); } @@ -211,18 +222,20 @@ protected function scanFile(string $filePath): array protected function extractClassesFromFile(string $filePath): array { // Use cache if available for current scan - if (isset($this->discoveredClasses[$filePath])) { + if (isset($this->discoveredClasses[$filePath])) + { return $this->discoveredClasses[$filePath]; } - $code = File::get($filePath); + $code = File::get($filePath); $tokens = token_get_all($code); - if (!is_array($tokens)) { + if (!is_array($tokens)) + { return []; } - $classes = $this->parseClassesFromTokens($tokens); + $classes = $this->parseClassesFromTokens($tokens); $this->discoveredClasses[$filePath] = $classes; return $classes; @@ -242,28 +255,33 @@ protected function extractClassesFromFile(string $filePath): array */ protected function parseClassesFromTokens(array $tokens): array { - $namespace = ''; - $collectNs = false; - $currentNs = []; - $classes = []; + $namespace = ''; + $collectNs = false; + $currentNs = []; + $classes = []; $collectClass = false; - $classTypes = [T_CLASS, T_INTERFACE, T_TRAIT]; + $classTypes = [T_CLASS, T_INTERFACE, T_TRAIT]; - for ($i = 0, $c = count($tokens); $i < $c; $i++) { + for ($i = 0, $c = count($tokens); $i < $c; $i++) + { $token = $tokens[$i]; // Handle namespace declaration - if (is_array($token) && T_NAMESPACE === $token[0]) { + if (is_array($token) && T_NAMESPACE === $token[0]) + { $currentNs = []; $collectNs = true; continue; } // Collect namespace parts - if ($collectNs) { - if (is_array($token) && in_array($token[0], [T_STRING, T_NAME_QUALIFIED, T_NS_SEPARATOR], true)) { + if ($collectNs) + { + if (is_array($token) && in_array($token[0], [T_STRING, T_NAME_QUALIFIED, T_NS_SEPARATOR], true)) + { $currentNs[] = $token[1]; - } elseif (';' === $token || '{' === $token) { + } elseif (';' === $token || '{' === $token) + { $namespace = mb_trim(implode('', $currentNs)); $collectNs = false; } @@ -271,9 +289,11 @@ protected function parseClassesFromTokens(array $tokens): array } // Handle class/interface/trait declaration - if (is_array($token) && in_array($token[0], $classTypes, true)) { + if (is_array($token) && in_array($token[0], $classTypes, true)) + { // Check if this is an anonymous class - if ($this->isAnonymousClass($tokens, $i)) { + if ($this->isAnonymousClass($tokens, $i)) + { continue; } @@ -282,10 +302,11 @@ protected function parseClassesFromTokens(array $tokens): array } // Collect class name - if ($collectClass && is_array($token) && T_STRING === $token[0]) { - $className = $token[1]; - $fqcn = $this->buildFullyQualifiedClassName($namespace, $className); - $classes[] = $fqcn; + if ($collectClass && is_array($token) && T_STRING === $token[0]) + { + $className = $token[1]; + $fqcn = $this->buildFullyQualifiedClassName($namespace, $className); + $classes[] = $fqcn; $collectClass = false; } } @@ -302,14 +323,17 @@ protected function parseClassesFromTokens(array $tokens): array protected function isAnonymousClass(array $tokens, int $currentIndex): bool { // Look backwards for "new" keyword - for ($i = $currentIndex - 1; $i >= 0; $i--) { + for ($i = $currentIndex - 1; $i >= 0; $i--) + { $token = $tokens[$i]; - if (is_array($token) && in_array($token[0], [T_WHITESPACE, T_FINAL, T_ABSTRACT], true)) { + if (is_array($token) && in_array($token[0], [T_WHITESPACE, T_FINAL, T_ABSTRACT], true)) + { continue; } - if (is_array($token) && T_NEW === $token[0]) { + if (is_array($token) && T_NEW === $token[0]) + { return true; } @@ -324,7 +348,8 @@ protected function isAnonymousClass(array $tokens, int $currentIndex): bool */ protected function buildFullyQualifiedClassName(string $namespace, string $className): string { - if (empty($namespace)) { + if (empty($namespace)) + { return $className; } @@ -332,31 +357,57 @@ protected function buildFullyQualifiedClassName(string $namespace, string $class } /** - * Check if a class is a panel class (has #[FilamentPanel] attribute). + * Check if a class is a panel provider class using naming conventions and inheritance. */ protected function isPanelClass(string $className): bool { - try { + try + { // Check if class exists (avoid autoload failures) - if (!class_exists($className)) { + if (!class_exists($className)) + { return false; } $reflection = new ReflectionClass($className); // Only instantiable classes can be panel providers - if (!$reflection->isInstantiable()) { + if (!$reflection->isInstantiable()) + { + return false; + } + + // Check naming convention: must contain "Panel" and end with "Provider" + $classBaseName = $reflection->getShortName(); + if (!str_contains($classBaseName, 'Panel') || !str_ends_with($classBaseName, 'Provider')) + { return false; } - // Check for FilamentPanel attribute - $attributes = $reflection->getAttributes(FilamentPanel::class); + // Check if it extends PanelProvider (if available) + $parentClass = $reflection->getParentClass(); + if ($parentClass && str_contains($parentClass->getName(), 'PanelProvider')) + { + return true; + } + + // Check if it has panel-related methods (duck typing) + $panelMethods = ['panel', 'configure', 'boot']; + foreach ($panelMethods as $method) + { + if ($reflection->hasMethod($method)) + { + return true; + } + } - return !empty($attributes); + return false; - } catch (Throwable $e) { + } catch (Throwable $e) + { // Log reflection errors if configured - if ($this->config['error_handling']['log_reflection_errors'] ?? true) { + if ($this->config['error_handling']['log_reflection_errors'] ?? true) + { $this->logReflectionError($className, $e); } @@ -371,11 +422,12 @@ protected function isPanelClass(string $className): bool */ protected function resolveScanLocations(): array { - $locations = $this->config['panels']['locations'] ?? []; + $locations = $this->config['panels']['locations'] ?? []; $resolvedLocations = []; - foreach ($locations as $pattern) { - $resolved = $this->resolveLocationPattern($pattern); + foreach ($locations as $pattern) + { + $resolved = $this->resolveLocationPattern($pattern); $resolvedLocations = array_merge($resolvedLocations, $resolved); } @@ -394,12 +446,14 @@ protected function resolveScanLocations(): array protected function resolveLocationPattern(string $pattern): array { // Convert relative pattern to absolute - if (!str_starts_with($pattern, '/')) { + if (!str_starts_with($pattern, '/')) + { $pattern = $this->basePath.'/'.mb_ltrim($pattern, '/'); } // Handle wildcard patterns - if (str_contains($pattern, '*')) { + if (str_contains($pattern, '*')) + { return $this->expandWildcardPattern($pattern); } @@ -416,13 +470,16 @@ protected function expandWildcardPattern(string $pattern): array { $paths = []; - try { + try + { $globPaths = glob($pattern, GLOB_ONLYDIR); - if (false !== $globPaths) { + if (false !== $globPaths) + { $paths = $globPaths; } - } catch (Throwable $e) { + } catch (Throwable $e) + { $this->logPatternError($pattern, $e); } @@ -436,24 +493,28 @@ protected function expandWildcardPattern(string $pattern): array */ protected function getPhpFilesInLocation(string $location): Generator { - $maxDepth = $this->config['panels']['scanning']['max_depth'] ?? 5; - $excludedDirs = $this->config['panels']['scanning']['excluded_directories'] ?? []; + $maxDepth = $this->config['panels']['scanning']['max_depth'] ?? 5; + $excludedDirs = $this->config['panels']['scanning']['excluded_directories'] ?? []; $followSymlinks = $this->config['panels']['scanning']['follow_symlinks'] ?? false; $flags = RecursiveDirectoryIterator::SKIP_DOTS; - if ($followSymlinks) { + if ($followSymlinks) + { $flags |= RecursiveDirectoryIterator::FOLLOW_SYMLINKS; } $iterator = new RecursiveIteratorIterator( new RecursiveCallbackFilterIterator( new RecursiveDirectoryIterator($location, $flags), - function (SplFileInfo $file) use ($excludedDirs) { - if ($file->isDir()) { + function (SplFileInfo $file) use ($excludedDirs) + { + if ($file->isDir()) + { $name = $file->getFilename(); // Skip excluded directories - if (in_array($name, $excludedDirs, true) || str_starts_with($name, '.')) { + if (in_array($name, $excludedDirs, true) || str_starts_with($name, '.')) + { return false; } } @@ -464,13 +525,16 @@ function (SplFileInfo $file) use ($excludedDirs) { RecursiveIteratorIterator::LEAVES_ONLY ); - if ($maxDepth > 0) { + if ($maxDepth > 0) + { $iterator->setMaxDepth($maxDepth); } - foreach ($iterator as $file) { + foreach ($iterator as $file) + { /** @var SplFileInfo $file */ - if ($file->isFile() && 'php' === $file->getExtension()) { + if ($file->isFile() && 'php' === $file->getExtension()) + { yield $file->getPathname(); } } @@ -514,7 +578,8 @@ protected function shouldFailSilently(): bool protected function logScanStart(): void { - if ($this->shouldLog()) { + if ($this->shouldLog()) + { Log::channel($this->getLogChannel())->info('Modulite panel discovery started'); } } @@ -524,7 +589,8 @@ protected function logScanStart(): void */ protected function logScanComplete(array $panels): void { - if ($this->shouldLog()) { + if ($this->shouldLog()) + { $stats = $this->scanStats; Log::channel($this->getLogChannel())->info('Modulite panel discovery completed', [ 'panels_found' => count($panels), @@ -538,7 +604,8 @@ protected function logScanComplete(array $panels): void protected function logScanError(Throwable $e): void { - if ($this->shouldLog()) { + if ($this->shouldLog()) + { Log::channel($this->getLogChannel())->error('Modulite panel discovery failed', [ 'error' => $e->getMessage(), 'scan_stats' => $this->scanStats, @@ -548,14 +615,16 @@ protected function logScanError(Throwable $e): void protected function logLocationSkipped(string $location, string $reason): void { - if ($this->shouldLog()) { + if ($this->shouldLog()) + { Log::channel($this->getLogChannel())->debug("Skipping location: {$location}", ['reason' => $reason]); } } protected function logLocationError(string $location, Throwable $e): void { - if ($this->shouldLog()) { + if ($this->shouldLog()) + { Log::channel($this->getLogChannel())->warning("Error scanning location: {$location}", [ 'error' => $e->getMessage(), ]); @@ -564,7 +633,8 @@ protected function logLocationError(string $location, Throwable $e): void protected function logFileError(string $filePath, Throwable $e): void { - if ($this->shouldLog()) { + if ($this->shouldLog()) + { Log::channel($this->getLogChannel())->warning("Error scanning file: {$filePath}", [ 'error' => $e->getMessage(), ]); @@ -576,7 +646,8 @@ protected function logFileError(string $filePath, Throwable $e): void */ protected function logPanelsFound(string $filePath, array $panels): void { - if ($this->shouldLog()) { + if ($this->shouldLog()) + { Log::channel($this->getLogChannel())->debug("Found panels in {$filePath}", [ 'panels' => $panels, ]); @@ -585,7 +656,8 @@ protected function logPanelsFound(string $filePath, array $panels): void protected function logReflectionError(string $className, Throwable $e): void { - if ($this->shouldLog()) { + if ($this->shouldLog()) + { Log::channel($this->getLogChannel())->debug("Reflection error for class: {$className}", [ 'error' => $e->getMessage(), ]); @@ -594,7 +666,8 @@ protected function logReflectionError(string $className, Throwable $e): void protected function logPatternError(string $pattern, Throwable $e): void { - if ($this->shouldLog()) { + if ($this->shouldLog()) + { Log::channel($this->getLogChannel())->warning("Error expanding pattern: {$pattern}", [ 'error' => $e->getMessage(), ]); diff --git a/src/Services/UnifiedCacheManager.php b/src/Services/UnifiedCacheManager.php index 3b4209a..a1313c4 100644 --- a/src/Services/UnifiedCacheManager.php +++ b/src/Services/UnifiedCacheManager.php @@ -21,6 +21,7 @@ class UnifiedCacheManager implements CacheManagerInterface protected bool $loaded = false; protected bool $enabled; protected int $ttl; + protected bool $needsDelayedSave = false; /** * Static cache for production to avoid repeated file includes @@ -30,38 +31,51 @@ class UnifiedCacheManager implements CacheManagerInterface public function __construct(array $config = []) { $this->cacheFile = $config['file'] ?? base_path('bootstrap/cache/modulite.php'); - $this->enabled = $config['enabled'] ?? true; - $this->ttl = $config['ttl'] ?? 3600; + $this->enabled = $config['enabled'] ?? true; + $this->ttl = $config['ttl'] ?? 3600; } /** - * Get an item from the cache. + * Get an item from the cache with optimized performance. */ public function get(string $key, mixed $default = null): mixed { - if (!$this->enabled) { + // Fast path: disabled cache + if (!$this->enabled) + { return $default; } $this->loadCache(); - if (!isset($this->cache['data'][$key])) { + // Fast path: key doesn't exist + if (!isset($this->cache['data'][$key])) + { return $default; } $item = $this->cache['data'][$key]; - // Fast path: skip expiration check in production when TTL is 0 (never expires) - if ($this->ttl <= 0) { + // Ultra-fast path: never expires (production optimization) + if ($this->ttl <= 0) + { return $item['value'] ?? $default; } - // Check if item has expired - if (isset($item['expires']) && time() > $item['expires']) { + // Fast path: no expiration set + if (!isset($item['expires'])) + { + return $item['value'] ?? $default; + } + + // Check expiration only when necessary + if (time() > $item['expires']) + { unset($this->cache['data'][$key]); - // Only save if we're not in a critical path - if (!app()->isProduction()) { - $this->saveCache(); + // Defer cache save to avoid I/O during request + if (!app()->isProduction()) + { + $this->scheduleDelayedSave(); } return $default; } @@ -74,14 +88,15 @@ public function get(string $key, mixed $default = null): mixed */ public function put(string $key, mixed $value, ?int $ttl = null): bool { - if (!$this->enabled) { + if (!$this->enabled) + { return false; } $this->loadCache(); $effectiveTtl = $ttl ?? $this->ttl; - $expires = $effectiveTtl > 0 ? time() + $effectiveTtl : null; + $expires = $effectiveTtl > 0 ? time() + $effectiveTtl : null; $this->cache['data'][$key] = [ 'value' => $value, @@ -100,7 +115,8 @@ public function remember(string $key, callable $callback): array { $value = $this->get($key); - if (null !== $value) { + if (null !== $value) + { return is_array($value) ? $value : []; } @@ -110,36 +126,20 @@ public function remember(string $key, callable $callback): array return is_array($value) ? $value : []; } - /** - * Remember an item in the cache with flexible types and TTL. - * Internal method for more flexible caching. - */ - public function rememberFlexible(string $key, callable $callback, ?int $ttl = null): mixed - { - $value = $this->get($key); - - if (null !== $value) { - return $value; - } - - $value = $callback(); - $this->put($key, $value, $ttl); - - return $value; - } - /** * Remove an item from the cache. */ public function forget(string $key): void { - if (!$this->enabled) { + if (!$this->enabled) + { return; } $this->loadCache(); - if (isset($this->cache['data'][$key])) { + if (isset($this->cache['data'][$key])) + { unset($this->cache['data'][$key]); $this->saveCache(); } @@ -156,7 +156,8 @@ public function flush(): void 'data' => [], ]; - if (File::exists($this->cacheFile)) { + if (File::exists($this->cacheFile)) + { File::delete($this->cacheFile); } } @@ -184,19 +185,23 @@ public function getStats(): array 'cache_created' => $this->cache['created'] ?? null, ]; - if (isset($this->cache['data'])) { + if (isset($this->cache['data'])) + { $expired = 0; - $valid = 0; + $valid = 0; - foreach ($this->cache['data'] as $item) { - if (isset($item['expires']) && time() > $item['expires']) { + foreach ($this->cache['data'] as $item) + { + if (isset($item['expires']) && time() > $item['expires']) + { $expired++; - } else { + } else + { $valid++; } } - $stats['valid_items'] = $valid; + $stats['valid_items'] = $valid; $stats['expired_items'] = $expired; } @@ -208,37 +213,44 @@ public function getStats(): array */ protected function loadCache(): void { - if ($this->loaded || !$this->enabled) { + if ($this->loaded || !$this->enabled) + { return; } // Production optimization: use static cache to avoid repeated file includes $cacheKey = $this->cacheFile; - if (app()->isProduction() && isset(static::$staticCache[$cacheKey])) { - $this->cache = static::$staticCache[$cacheKey]; + if (app()->isProduction() && isset(static::$staticCache[$cacheKey])) + { + $this->cache = static::$staticCache[$cacheKey]; $this->loaded = true; return; } - if (!File::exists($this->cacheFile)) { + if (!File::exists($this->cacheFile)) + { $this->cache = [ 'version' => '1.0', 'created' => time(), 'data' => [], ]; - } else { - try { + } else + { + try + { $this->cache = include $this->cacheFile; // Validate cache structure - if (!is_array($this->cache) || !isset($this->cache['data'])) { + if (!isset($this->cache['data'])) + { $this->cache = [ 'version' => '1.0', 'created' => time(), 'data' => [], ]; } - } catch (Throwable $e) { + } catch (Throwable) + { // Corrupted cache file, reset $this->cache = [ 'version' => '1.0', @@ -249,7 +261,8 @@ protected function loadCache(): void } // Store in static cache for production - if (app()->isProduction()) { + if (app()->isProduction()) + { static::$staticCache[$cacheKey] = $this->cache; } @@ -261,14 +274,17 @@ protected function loadCache(): void */ protected function saveCache(): bool { - if (!$this->enabled) { + if (!$this->enabled) + { return false; } - try { + try + { // Ensure cache directory exists $cacheDir = dirname($this->cacheFile); - if (!File::isDirectory($cacheDir)) { + if (!File::isDirectory($cacheDir)) + { File::makeDirectory($cacheDir, 0755, true); } @@ -279,7 +295,8 @@ protected function saveCache(): bool return false !== File::put($this->cacheFile, $content); - } catch (Throwable $e) { + } catch (Throwable) + { return false; } } @@ -308,4 +325,27 @@ public function isCacheEnabled(): bool { return $this->enabled; } + + /** + * Schedule a delayed save to avoid I/O during critical paths. + */ + protected function scheduleDelayedSave(): void + { + if ($this->needsDelayedSave) + { + return; // Already scheduled + } + + $this->needsDelayedSave = true; + + // Register shutdown function to save cache after response + register_shutdown_function(function (): void + { + if ($this->needsDelayedSave) + { + $this->saveCache(); + $this->needsDelayedSave = false; + } + }); + } } From 6de3f88fba470d5c7ad15e0693e615d4d8a82dc2 Mon Sep 17 00:00:00 2001 From: Armin Date: Thu, 4 Sep 2025 19:11:18 +0330 Subject: [PATCH 2/2] feat!: enhanced module discovery and simpler flows --- CHANGELOG.md | 362 ------------- CONTRIBUTING.md | 749 -------------------------- docs/API_REFERENCE.md | 797 ---------------------------- docs/CONFIGURATION.md | 726 ------------------------- docs/EXAMPLES.md | 1117 --------------------------------------- docs/INSTALLATION.md | 771 --------------------------- docs/README.md | 306 ----------- docs/TROUBLESHOOTING.md | 804 ---------------------------- 8 files changed, 5632 deletions(-) delete mode 100644 CHANGELOG.md delete mode 100644 CONTRIBUTING.md delete mode 100644 docs/API_REFERENCE.md delete mode 100644 docs/CONFIGURATION.md delete mode 100644 docs/EXAMPLES.md delete mode 100644 docs/INSTALLATION.md delete mode 100644 docs/README.md delete mode 100644 docs/TROUBLESHOOTING.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 3f975b6..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,362 +0,0 @@ -# ๐Ÿ“ Changelog - -All notable changes to Modulite will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Added -- Nothing yet - -### Changed -- Nothing yet - -### Deprecated -- Nothing yet - -### Removed -- Nothing yet - -### Fixed -- Nothing yet - -### Security -- Nothing yet - -## [1.0.0] - 2024-01-15 - -### Added -- ๐ŸŽ‰ **Initial Release** - Complete Modulite package implementation -- ๐Ÿ” **Automatic Panel Discovery** - Auto-discover Filament Panel Providers using `#[FilamentPanel]` attribute -- โšก **Multi-layer Caching System** - File-based cache with in-memory optimization for production -- ๐Ÿ—๏ธ **Modular Architecture Support** - Full integration with nwidart/laravel-modules -- ๐Ÿงฉ **Component Auto-Discovery** - Automatic discovery of Resources, Pages, and Widgets -- ๐ŸŽฏ **Attribute-Based Registration** - Clean, explicit panel registration with attributes -- ๐Ÿ“Š **Performance Monitoring** - Built-in benchmarking and performance analysis tools -- ๐Ÿ›ก๏ธ **Production-Ready Features** - Robust error handling and production optimizations -- ๐Ÿ”ง **Flexible Configuration** - Comprehensive configuration options for every use case -- ๐ŸŒ **Environment-Aware** - Different behavior for development/staging/production -- ๐Ÿ“ˆ **Smart Caching Strategies** - Auto-invalidation and TTL-based caching -- ๐ŸŽ›๏ธ **Console Commands** - Full suite of management and debugging commands -- ๐Ÿ“š **Component Discovery Service** - Discover and register Filament components across modules -- ๐Ÿท๏ธ **Priority-Based Loading** - Control panel registration order with priority system -- ๐Ÿ”„ **Lazy Discovery** - Defer panel scanning until Filament is actually needed -- ๐Ÿ’พ **Memory Optimization** - Batch processing and memory management for large codebases -- ๐ŸŽจ **Custom Base Classes** - Support for custom panel provider base classes -- ๐Ÿ”’ **Environment Constraints** - Limit panels to specific environments -- ๐Ÿ“ **Comprehensive Logging** - Detailed logging for development and debugging -- ๐Ÿงช **Testing Support** - Built-in testing utilities and helpers - -### Features Breakdown - -#### Core Features -- **Panel Provider Discovery**: Automatic scanning and registration of Filament Panel Providers -- **Attribute-Based Registration**: Use `#[FilamentPanel]` to mark discoverable panels -- **File-Based Caching**: Simple, robust caching similar to Laravel's bootstrap cache -- **Module Integration**: Seamless integration with nwidart/laravel-modules package -- **Performance Optimization**: Multiple layers of optimization for production use - -#### Configuration System -- **Flexible Location Patterns**: Configure where to scan for panels and components -- **Validation Rules**: Control how discovered classes are validated -- **Scanning Options**: Customize file scanning behavior and performance -- **Error Handling**: Configure error behavior for different environments -- **Cache Management**: Fine-tune caching strategies and performance - -#### Component Discovery -- **Resource Discovery**: Auto-discover Filament Resources -- **Page Discovery**: Auto-discover Filament Pages -- **Widget Discovery**: Auto-discover Filament Widgets -- **Custom Component Types**: Support for custom component types -- **Panel-Specific Discovery**: Discover components for specific panels - -#### Developer Experience -- **Console Commands**: - - `modulite:status` - Show discovery status and statistics - - `modulite:clear-cache` - Clear all Modulite caches - - `modulite:benchmark` - Performance benchmarking tool - - `modulite:discover-panels` - Manual panel discovery - - `modulite:discover-components` - Manual component discovery -- **Comprehensive Documentation**: Complete guides and API reference -- **Error Messages**: Clear, actionable error messages -- **Debug Tools**: Built-in debugging and diagnostic tools - -#### Production Features -- **Zero-Downtime Deployments**: Cache management for production deployments -- **Memory Efficient**: Optimized for large codebases with many modules -- **Fast Boot Times**: Lazy loading to minimize application startup time -- **OPcache Compatible**: Works seamlessly with PHP OPcache -- **Load Balancer Ready**: Stateless design for horizontal scaling - -### Environment Support - -#### Development -- Auto-invalidation on file changes -- Detailed logging and debugging -- Flexible error handling -- Hot reloading support - -#### Staging -- Balanced performance and debugging -- Configurable cache TTL -- Error logging without breaking execution -- Testing environment support - -#### Production -- Maximum performance optimization -- Persistent caching (TTL=0) -- Silent error handling -- Minimal logging overhead - -### Architecture - -#### Service Providers -- **ModuliteServiceProvider**: Main service provider with lazy discovery -- **Automatic Registration**: Auto-registers discovered panel providers -- **Event Listeners**: Cache invalidation on module changes -- **Service Binding**: Proper dependency injection setup - -#### Services -- **UnifiedCacheManager**: File-based cache with memory optimization -- **PanelScannerService**: Panel discovery with token parsing -- **ComponentDiscoveryService**: Component discovery across modules -- **BaseModuliteCommand**: Base class for console commands - -#### Contracts -- **CacheManagerInterface**: Cache management contract -- **PanelScannerInterface**: Panel discovery contract -- **ComponentScannerInterface**: Component discovery contract - -#### Attributes -- **FilamentPanel**: Marks classes for automatic discovery -- **ComponentDiscovery**: Future: Mark component classes -- **FilamentResource**: Future: Resource-specific configuration -- **FilamentPage**: Future: Page-specific configuration -- **FilamentWidget**: Future: Widget-specific configuration - -### Configuration Options - -#### Panel Discovery -```php -'panels' => [ - 'locations' => ['modules/*/Providers/Filament/Panels'], - 'patterns' => ['*PanelProvider.php'], - 'validation' => ['strict_inheritance' => false], - 'registration' => ['auto_register' => true], - 'scanning' => ['max_depth' => 5], -] -``` - -#### Component Discovery -```php -'components' => [ - 'locations' => ['modules/*/Filament/{panel}/Resources'], - 'types' => ['resources', 'pages', 'widgets'], - 'registration' => ['auto_register' => true], -] -``` - -#### Cache Configuration -```php -'cache' => [ - 'enabled' => true, - 'file' => 'bootstrap/cache/modulite.php', - 'ttl' => 0, // Never expires in production - 'memory_cache' => ['enabled' => true], -] -``` - -#### Performance Settings -```php -'performance' => [ - 'lazy_discovery' => true, - 'memory_optimization' => ['batch_size' => 100], - 'concurrent' => ['enabled' => false], -] -``` - -### Compatibility - -#### Framework Versions -- **Laravel**: 10.x, 11.x -- **Filament**: 4.x -- **PHP**: 8.2, 8.3, 8.4 - -#### Package Compatibility -- **nwidart/laravel-modules**: 11.x, 12.x -- **Laravel Octane**: โœ… Supported -- **Laravel Horizon**: โœ… Supported -- **Laravel Telescope**: โœ… Supported -- **Laravel Sail**: โœ… Supported - -#### Server Environments -- **Apache**: โœ… Supported -- **Nginx**: โœ… Supported -- **Docker**: โœ… Supported -- **Serverless**: โš ๏ธ Limited (caching considerations) - -### Migration Path - -#### From Manual Registration -1. Add `#[FilamentPanel]` attribute to existing panel providers -2. Remove manual registration from service providers -3. Configure Modulite scan locations -4. Test with `php artisan modulite:status` - -#### From Other Auto-Discovery Solutions -1. Install Modulite alongside existing solution -2. Gradually migrate panels to use `#[FilamentPanel]` attribute -3. Disable old auto-discovery system -4. Remove old dependencies - -### Performance Benchmarks - -#### Production Environment (Actual Results) -**With Cache Warming:** -- **Cache Read**: 0ms average (500 iterations) -- **Panel Discovery**: 1.054ms average (with cache simulation) -- **Component Discovery**: 0ms average - -**Without Cache Warming (Cached Operations):** -- **Cache Read**: 0ms average -- **Panel Discovery**: 0ms average (cached) -- **Component Discovery**: 0ms average - -### Documentation - -#### Complete Documentation Suite -- **README.md**: Overview and quick start guide -- **INSTALLATION.md**: Detailed installation instructions -- **CONFIGURATION.md**: Complete configuration reference -- **EXAMPLES.md**: Real-world usage examples and patterns -- **TROUBLESHOOTING.md**: Common issues and solutions -- **API_REFERENCE.md**: Complete API documentation -- **CHANGELOG.md**: Version history and changes - -#### Code Examples -- Basic panel setup -- Multi-panel applications -- Enterprise patterns -- Testing strategies -- Deployment configurations -- Custom implementations - -### Testing - -#### Test Coverage -- Unit tests for core services -- Integration tests for discovery -- Feature tests for panel registration -- Performance benchmarks -- Error handling scenarios - -#### CI/CD Pipeline -- GitHub Actions workflow -- Multiple PHP version testing -- Multiple Laravel version testing -- Code quality checks -- Security scanning - -### Security - -#### Security Considerations -- Safe file scanning (no code execution) -- Path traversal protection -- Input validation -- Error message sanitization -- Permission-based access - -#### Audit Trail -- Discovery operation logging -- Cache access logging -- Error tracking -- Performance monitoring - ---- - -## Release Notes - -### v1.0.0 Release Highlights - -This initial release represents months of development and testing, bringing a production-ready solution for automatic Filament panel discovery in modular Laravel applications. - -**Key Benefits:** -- ๐Ÿš€ **Zero Configuration**: Works out of the box with sensible defaults -- โšก **High Performance**: Optimized for production with intelligent caching -- ๐Ÿ—๏ธ **Modular Ready**: Built specifically for modular Laravel applications -- ๐Ÿ“ˆ **Scalable**: Handles large codebases with many modules efficiently -- ๐Ÿ›ก๏ธ **Production Tested**: Robust error handling and edge case management - -**Perfect For:** -- Enterprise Laravel applications with multiple Filament panels -- Modular applications using nwidart/laravel-modules -- Teams wanting to reduce boilerplate configuration -- Applications requiring dynamic panel discovery -- Performance-critical applications needing fast boot times - -**Getting Started:** -```bash -composer require panicdevs/modulite -php artisan vendor:publish --tag=modulite-config -``` - -Add to your panel provider: -```php -#[FilamentPanel] -class AdminPanelProvider extends PanelProvider -{ - // Your panel configuration -} -``` - -That's it! Modulite will automatically discover and register your panels. - ---- - -## Future Roadmap - -### Planned Features (v1.1.0) -- Enhanced component discovery with automatic registration -- Plugin system for custom discovery strategies -- GraphQL API for panel metadata -- Real-time discovery updates -- Advanced caching strategies (Redis, Memcached) - -### Planned Features (v1.2.0) -- Visual panel management dashboard -- Discovery analytics and insights -- Custom attribute validation rules -- Multi-tenant panel discovery -- Integration with Laravel Nova - -### Long-term Vision (v2.0.0) -- Framework-agnostic design -- Support for other admin panel libraries -- Advanced AI-powered discovery -- Cloud-based panel registry -- Enterprise management features - ---- - -## Contributing - -We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details. - -## License - -Modulite is open-sourced software licensed under the [MIT license](LICENSE). - -## Credits - -- **[Armin Hooshmand](https://github.com/NotifyHex)** - Creator & Lead Maintainer -- **[PanicDevs](https://panicdevs.agency)** - Development Team -- All [contributors](https://github.com/panicdevs/modulite/contributors) who helped make this project possible - -## Support - -- ๐Ÿ“ง Email: [support@panicdevs.agency](mailto:support@panicdevs.agency) -- ๐Ÿ› Issues: [GitHub Issues](https://github.com/panicdevs/modulite/issues) -- ๐Ÿ’ฌ Discussions: [GitHub Discussions](https://github.com/panicdevs/modulite/discussions) -- ๐Ÿ’ฐ Sponsor: [GitHub Sponsors](https://github.com/sponsors/panicdevs) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index bec8430..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,749 +0,0 @@ -# ๐Ÿค Contributing to Modulite - -Thank you for considering contributing to Modulite! We welcome contributions from the community and are grateful for your support. - -## ๐Ÿ“‹ Table of Contents - -- [Code of Conduct](#code-of-conduct) -- [Getting Started](#getting-started) -- [Development Setup](#development-setup) -- [Contributing Guidelines](#contributing-guidelines) -- [Pull Request Process](#pull-request-process) -- [Issue Guidelines](#issue-guidelines) -- [Development Workflow](#development-workflow) -- [Testing](#testing) -- [Documentation](#documentation) -- [Release Process](#release-process) - -## Code of Conduct - -This project and everyone participating in it is governed by our Code of Conduct. By participating, you are expected to uphold this code. - -### Our Pledge - -We pledge to make participation in our project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. - -### Our Standards - -Examples of behavior that contributes to creating a positive environment include: - -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members - -### Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at conduct@panicdevs.agency. - -## Getting Started - -### Prerequisites - -Before you begin, ensure you have: - -- **PHP 8.2+** with required extensions -- **Composer** for dependency management -- **Git** for version control -- **Node.js & NPM** (for documentation development) -- A **Laravel application** for testing - -### Fork and Clone - -1. **Fork the repository** `panicdevs/modulite` on GitHub to your own account -2. **Clone your fork** locally: - ```bash - git clone https://github.com/YOUR_USERNAME/modulite.git - cd modulite - ``` - -**Example:** If John wants to contribute: -- John forks `panicdevs/modulite` โ†’ creates `john/modulite` -- John clones: `git clone https://github.com/john/modulite.git` -- John works on `john/modulite` and sends PR to `panicdevs/modulite` - -That's it! No additional remotes needed. - -## Development Setup - -### 1. Install Dependencies - -```bash -# Install PHP dependencies -composer install - -# Install development tools -composer install --dev -``` - -### 2. Set Up Test Environment - -```bash -# Copy environment file -cp .env.example .env - -# Set up test database -php artisan migrate --env=testing - -# Run tests to verify setup -composer test -``` - -### 3. Configure IDE - -**VS Code:** -```json -{ - "php.validate.executablePath": "/usr/bin/php", - "php.suggest.basic": false, - "phpcs.enable": true, - "phpcs.standard": "PSR12" -} -``` - -**PhpStorm:** -- Enable PHP CS Fixer -- Set code style to PSR-12 -- Configure PHPUnit test runner - -### 4. Development Commands - -```bash -# Run tests -composer test - -# Run tests with coverage -composer test-coverage - -# Check code style -composer cs-check - -# Fix code style -composer cs-fix - -# Run static analysis -composer analyze - -# Run all quality checks -composer quality -``` - -## Contributing Guidelines - -### Types of Contributions - -We welcome several types of contributions: - -1. **๐Ÿ› Bug Fixes** - Fix issues and improve reliability -2. **โœจ New Features** - Add new functionality -3. **๐Ÿ“š Documentation** - Improve or add documentation -4. **โšก Performance** - Optimize performance -5. **๐Ÿงช Tests** - Add or improve test coverage -6. **๐Ÿ”ง Tools** - Improve development tools and workflow - -### Contribution Process - -1. **Check existing issues** before starting work -2. **Create an issue** for new features or major changes -3. **Discuss the approach** with maintainers -4. **Fork the repository** to your GitHub account -5. **Create a branch** on your fork from `develop` -6. **Implement changes** following our guidelines -7. **Add tests** for new functionality -8. **Update documentation** as needed -9. **Push changes** to your fork -10. **Submit a pull request** from your fork to the original repository - -### Branch Naming - -Use descriptive branch names with prefixes: - -```bash -# Features -feature/panel-discovery-optimization -feature/component-auto-registration - -# Bug fixes -bugfix/cache-invalidation-issue -bugfix/memory-leak-scanner - -# Documentation -docs/api-reference-update -docs/troubleshooting-guide - -# Refactoring -refactor/service-architecture -refactor/command-structure -``` - -### Commit Messages - -Follow [Conventional Commits](https://www.conventionalcommits.org/): - -```bash -# Format -type(scope): description - -# Examples -feat(discovery): add component auto-registration -fix(cache): resolve memory leak in scanner -docs(api): update interface documentation -test(scanner): add edge case tests -refactor(services): improve service architecture -perf(cache): optimize file-based caching -``` - -**Types:** -- `feat` - New features -- `fix` - Bug fixes -- `docs` - Documentation changes -- `test` - Test additions/changes -- `refactor` - Code refactoring -- `perf` - Performance improvements -- `style` - Code style changes -- `chore` - Build/dependency updates - -## Pull Request Process - -### Before Submitting - -1. **Run quality checks:** - ```bash - composer quality - ``` - -2. **Update documentation** if needed - -3. **Test thoroughly** on different environments - -4. **Push to your fork:** - ```bash - git push origin your-feature-branch - ``` - -### PR Template - -When creating a pull request, use this template: - -```markdown -## Description -Brief description of the changes made. - -## Type of Change -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] Documentation update - -## Testing -- [ ] Tests pass locally -- [ ] Added tests for new functionality -- [ ] Manual testing completed - -## Checklist -- [ ] My code follows the project's style guidelines -- [ ] I have performed a self-review of my own code -- [ ] I have commented my code, particularly in hard-to-understand areas -- [ ] I have made corresponding changes to the documentation -- [ ] My changes generate no new warnings -- [ ] New and existing unit tests pass locally with my changes - -## Screenshots (if applicable) -Add screenshots to help explain your changes. - -## Additional Notes -Any additional information or context about the changes. -``` - -### Review Process - -1. **Automated checks** must pass -2. **Code review** by maintainers -3. **Manual testing** if needed -4. **Approval** from at least one maintainer -5. **Merge** by maintainers - -## Issue Guidelines - -### Before Creating an Issue - -1. **Search existing issues** to avoid duplicates -2. **Check documentation** for solutions -3. **Try the latest version** to see if it's already fixed - -### Issue Types - -#### ๐Ÿ› Bug Reports - -Use the bug report template: - -```markdown -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Environment:** -- OS: [e.g. Ubuntu 20.04] -- PHP Version: [e.g. 8.2.1] -- Laravel Version: [e.g. 10.32.1] -- Filament Version: [e.g. 4.0.0] -- Modulite Version: [e.g. 1.0.0] - -**Additional context** -Add any other context about the problem here. -``` - -#### โœจ Feature Requests - -Use the feature request template: - -```markdown -**Is your feature request related to a problem?** -A clear and concise description of what the problem is. - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. -``` - -#### ๐Ÿ“š Documentation Issues - -```markdown -**Documentation Issue** -- [ ] Missing documentation -- [ ] Incorrect documentation -- [ ] Unclear documentation -- [ ] Outdated documentation - -**Page/Section** -Link to the documentation page or section. - -**Description** -Clear description of the issue and suggested improvements. -``` - -## Development Workflow - -### Setting Up for Development - -1. **Create a test Laravel application:** - ```bash - composer create-project laravel/laravel modulite-test - cd modulite-test - ``` - -2. **Link your local Modulite:** - ```json - // composer.json - { - "repositories": [ - { - "type": "path", - "url": "../modulite" - } - ], - "require": { - "panicdevs/modulite": "*" - } - } - ``` - -3. **Install and test:** - ```bash - composer install - php artisan vendor:publish --tag=modulite-config - ``` - -### Development Standards - -#### Code Style - -We follow **PSR-12** coding standards: - -```php -riskyOperation(); -} catch (SpecificException $e) { - $this->logError($e); - - if ($this->shouldFailSilently()) { - return $defaultValue; - } - - throw $e; -} - -// Return types over exceptions when appropriate -public function findPanel(string $name): ?PanelProvider -{ - // Return null instead of throwing for "not found" - return $this->panels[$name] ?? null; -} -``` - -## Testing - -### Test Structure - -``` -tests/ -โ”œโ”€โ”€ Unit/ -โ”‚ โ”œโ”€โ”€ Services/ -โ”‚ โ”‚ โ”œโ”€โ”€ CacheManagerTest.php -โ”‚ โ”‚ โ”œโ”€โ”€ PanelScannerTest.php -โ”‚ โ”‚ โ””โ”€โ”€ ComponentDiscoveryTest.php -โ”‚ โ””โ”€โ”€ Attributes/ -โ”‚ โ””โ”€โ”€ FilamentPanelTest.php -โ”œโ”€โ”€ Feature/ -โ”‚ โ”œโ”€โ”€ PanelDiscoveryTest.php -โ”‚ โ”œโ”€โ”€ ComponentDiscoveryTest.php -โ”‚ โ””โ”€โ”€ CacheIntegrationTest.php -โ””โ”€โ”€ Integration/ - โ”œโ”€โ”€ ModuleIntegrationTest.php - โ””โ”€โ”€ FilamentIntegrationTest.php -``` - -### Writing Tests - -#### Unit Tests - -```php -cache = new UnifiedCacheManager([ - 'enabled' => true, - 'file' => '/tmp/test-cache.php', - ]); - } - - protected function tearDown(): void - { - $this->cache->flush(); - parent::tearDown(); - } - - public function test_can_store_and_retrieve_data(): void - { - $key = 'test-key'; - $value = ['test' => 'data']; - - $this->assertTrue($this->cache->put($key, $value)); - $this->assertEquals($value, $this->cache->get($key)); - } - - public function test_returns_default_for_missing_key(): void - { - $default = 'default-value'; - - $this->assertEquals($default, $this->cache->get('missing-key', $default)); - } -} -``` - -#### Feature Tests - -```php -createTestPanel(); - - $scanner = $this->app->make(PanelScannerInterface::class); - $panels = $scanner->discoverPanels(); - - $this->assertContains('Tests\\Fixtures\\TestPanelProvider', $panels); - } - - private function createTestPanel(): void - { - // Implementation to create test panel - } -} -``` - -### Test Commands - -```bash -# Run all tests -composer test - -# Run specific test suite -composer test -- --testsuite=Unit -composer test -- --testsuite=Feature - -# Run with coverage -composer test-coverage - -# Run specific test -composer test -- --filter=PanelDiscoveryTest - -# Run tests with debugging -composer test -- --debug -``` - -### Test Configuration - -```xml - - - - - - ./tests/Unit - - - ./tests/Feature - - - ./tests/Integration - - - - - - ./src - - - -``` - -## Documentation - -### Documentation Standards - -1. **Code Comments** - Document complex logic -2. **DocBlocks** - Complete PHPDoc for all public methods -3. **README Updates** - Update README for new features -4. **API Documentation** - Update API reference -5. **Examples** - Provide practical examples - -### Documentation Types - -#### Code Documentation - -```php -/** - * Discover all Filament Panel classes in configured locations. - * - * This method scans the configured directories for PHP files containing - * classes marked with the #[FilamentPanel] attribute. It uses token - * parsing for performance and caches results to avoid repeated scanning. - * - * @return array Array of fully qualified class names with #[FilamentPanel] attribute - * - * @throws ScanException When scanning fails critically - * - * @example - * ```php - * $scanner = app(PanelScannerInterface::class); - * $panels = $scanner->discoverPanels(); - * // ['Modules\Admin\Providers\Filament\Panels\AdminPanelProvider', ...] - * ``` - */ -public function discoverPanels(): array -{ - // Implementation -} -``` - -#### User Documentation - -- Keep documentation up-to-date with code changes -- Use clear, concise language -- Provide practical examples -- Include troubleshooting information -- Test documentation examples - -### Documentation Build - -```bash -# Build documentation locally -npm install -npm run docs:build - -# Serve documentation locally -npm run docs:serve - -# Lint documentation -npm run docs:lint -``` - -## Release Process - -### Version Management - -We follow [Semantic Versioning](https://semver.org/): - -- **MAJOR** (X.0.0) - Breaking changes -- **MINOR** (X.Y.0) - New features, backward compatible -- **PATCH** (X.Y.Z) - Bug fixes, backward compatible - -### Release Checklist - -#### Pre-Release - -- [ ] All tests pass -- [ ] Documentation updated -- [ ] CHANGELOG.md updated -- [ ] Version bumped in composer.json -- [ ] Tag created with proper version -- [ ] Release notes prepared - -#### Release Process - -1. **Create release branch:** - ```bash - git checkout develop - git pull upstream develop - git checkout -b release/v1.1.0 - ``` - -2. **Update version numbers:** - ```json - // composer.json - { - "version": "1.1.0" - } - ``` - -3. **Update CHANGELOG.md** - -4. **Create pull request** to `main` - -5. **After merge, create tag:** - ```bash - git tag -a v1.1.0 -m "Release version 1.1.0" - git push upstream v1.1.0 - ``` - -6. **Create GitHub release** with release notes - -#### Post-Release - -- [ ] Announce on social media -- [ ] Update documentation site -- [ ] Notify community -- [ ] Monitor for issues - -## Getting Help - -### Development Support - -- **Discord**: [Join our Discord](https://discord.gg/panicdevs) -- **GitHub Discussions**: [Community discussions](https://github.com/YOUR_USERNAME/modulite/discussions) -- **Email**: [dev@panicdevs.agency](mailto:dev@panicdevs.agency) - -### Resources - -- **Laravel Documentation**: [laravel.com/docs](https://laravel.com/docs) -- **Filament Documentation**: [filamentphp.com/docs](https://filamentphp.com/docs) -- **PSR-12 Standard**: [PHP-FIG PSR-12](https://www.php-fig.org/psr/psr-12/) -- **Conventional Commits**: [conventionalcommits.org](https://www.conventionalcommits.org/) - -## Recognition - -Contributors are recognized in: - -- **CHANGELOG.md** - Feature contributors -- **README.md** - All contributors section -- **GitHub Contributors** - Automatic recognition -- **Release Notes** - Major contributors highlighted - -## License - -By contributing to Modulite, you agree that your contributions will be licensed under the [MIT License](LICENSE). - ---- - -Thank you for contributing to Modulite! Your efforts help make this project better for everyone. ๐Ÿ™ - -If you have any questions about contributing, feel free to reach out to us at [contribute@panicdevs.agency](mailto:contribute@panicdevs.agency). diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md deleted file mode 100644 index ee41e07..0000000 --- a/docs/API_REFERENCE.md +++ /dev/null @@ -1,797 +0,0 @@ -# ๐Ÿ“š API Reference - -Complete API reference for Modulite's interfaces, services, and attributes. - -## ๐Ÿ“‹ Table of Contents - -- [Attributes](#attributes) -- [Interfaces](#interfaces) -- [Services](#services) -- [Console Commands](#console-commands) -- [Exceptions](#exceptions) -- [Configuration](#configuration) -- [Helper Functions](#helper-functions) - -## Attributes - -### FilamentPanel - -The `FilamentPanel` attribute marks classes for automatic discovery and registration as Filament Panel Providers. - -```php -#[Attribute(Attribute::TARGET_CLASS)] -readonly class FilamentPanel -``` - -#### Constructor Parameters - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `priority` | `int` | `0` | Registration priority (higher = earlier registration) | -| `environment` | `?string` | `null` | Limit panel to specific environment | -| `conditions` | `array` | `[]` | Additional conditions for panel loading | -| `autoRegister` | `bool` | `true` | Whether to automatically register this panel | - -#### Methods - -##### `shouldRegister(string $currentEnvironment): bool` - -Determines if the panel should be registered in the current environment. - -**Parameters:** -- `$currentEnvironment` - Current application environment - -**Returns:** `bool` - True if panel should be registered - -**Example:** -```php -$attribute = new FilamentPanel(environment: 'production'); -$shouldRegister = $attribute->shouldRegister(app()->environment()); // true in production -``` - -##### `toArray(): array` - -Returns attribute configuration as an array. - -**Returns:** `array` - Attribute configuration - -**Example:** -```php -$attribute = new FilamentPanel(priority: 10, environment: 'local'); -$config = $attribute->toArray(); -// ['priority' => 10, 'environment' => 'local', 'conditions' => [], 'auto_register' => true] -``` - -#### Usage Examples - -**Basic Usage:** -```php -#[FilamentPanel] -class AdminPanelProvider extends PanelProvider -{ - // Panel implementation -} -``` - -**Advanced Usage:** -```php -#[FilamentPanel( - priority: 100, - environment: 'production', - conditions: ['feature.admin_panel'], - autoRegister: true -)] -class ProductionAdminPanelProvider extends PanelProvider -{ - // Panel implementation -} -``` - -## Interfaces - -### CacheManagerInterface - -Interface for cache management operations. - -```php -interface CacheManagerInterface -``` - -#### Methods - -##### `get(string $key, mixed $default = null): mixed` - -Retrieve an item from the cache. - -**Parameters:** -- `$key` - Cache key -- `$default` - Default value if key not found - -**Returns:** `mixed` - Cached value or default - -##### `put(string $key, mixed $value, ?int $ttl = null): bool` - -Store an item in the cache. - -**Parameters:** -- `$key` - Cache key -- `$value` - Value to store -- `$ttl` - Time to live in seconds (null for default) - -**Returns:** `bool` - Success status - -##### `remember(string $key, callable $callback): array` - -Get an item from cache or store the result of callback. - -**Parameters:** -- `$key` - Cache key -- `$callback` - Callback to execute if cache miss - -**Returns:** `array` - Cached or computed value - -##### `forget(string $key): void` - -Remove an item from the cache. - -**Parameters:** -- `$key` - Cache key to remove - -##### `flush(): void` - -Clear all cached items. - -##### `has(string $key): bool` - -Check if cache has an item. - -**Parameters:** -- `$key` - Cache key to check - -**Returns:** `bool` - True if key exists - -##### `getStats(): array` - -Get cache statistics. - -**Returns:** `array` - Cache statistics - -### PanelScannerInterface - -Interface for panel discovery operations. - -```php -interface PanelScannerInterface -``` - -#### Methods - -##### `discoverPanels(): array` - -Discover all Filament Panel classes. - -**Returns:** `array` - Array of fully qualified class names - -**Throws:** `ScanException` - When scanning fails - -##### `getScanStats(): array` - -Get scanning statistics. - -**Returns:** `array` - Statistics from last scan - -### ComponentScannerInterface - -Interface for component discovery operations. - -```php -interface ComponentScannerInterface -``` - -#### Methods - -##### `discoverComponents(string $panelName): array` - -Discover all components for a specific panel. - -**Parameters:** -- `$panelName` - Panel identifier - -**Returns:** `array` - Components grouped by type - -##### `discoverComponentType(string $panelName, string $componentType): array` - -Discover components of a specific type. - -**Parameters:** -- `$panelName` - Panel identifier -- `$componentType` - Component type ('resources', 'pages', 'widgets') - -**Returns:** `array` - Array of component class names - -##### `getScanStats(): array` - -Get component discovery statistics. - -**Returns:** `array` - Statistics from last scan - -##### `isComponentType(string $className, string $componentType): bool` - -Check if a class is a valid component of specified type. - -**Parameters:** -- `$className` - Fully qualified class name -- `$componentType` - Component type to check - -**Returns:** `bool` - True if class is valid component - -## Services - -### UnifiedCacheManager - -Main cache implementation using file-based storage. - -```php -class UnifiedCacheManager implements CacheManagerInterface -``` - -#### Constructor - -```php -public function __construct(array $config = []) -``` - -**Parameters:** -- `$config` - Cache configuration array - -#### Methods - -##### `getCacheFile(): string` - -Get the cache file path. - -**Returns:** `string` - Absolute path to cache file - -##### `isEnabled(): bool` - -Check if caching is enabled. - -**Returns:** `bool` - Cache enabled status - -##### `isCacheEnabled(): bool` - -Alias for `isEnabled()`. Interface-compliant method. - -**Returns:** `bool` - Cache enabled status - -#### Configuration - -```php -$config = [ - 'file' => base_path('bootstrap/cache/modulite.php'), - 'enabled' => true, - 'ttl' => 3600, -]; - -$cache = new UnifiedCacheManager($config); -``` - -### PanelScannerService - -Service for discovering Filament Panel Providers. - -```php -class PanelScannerService implements PanelScannerInterface -``` - -#### Constructor - -```php -public function __construct(array $config, string $basePath, mixed $moduleManager = null) -``` - -**Parameters:** -- `$config` - Scanner configuration -- `$basePath` - Application base path -- `$moduleManager` - Optional module manager instance - -#### Methods - -##### `discoverPanels(): array` - -Discovers all panel providers with `#[FilamentPanel]` attribute. - -**Returns:** `array` - Panel provider class names - -**Example:** -```php -$scanner = app(PanelScannerInterface::class); -$panels = $scanner->discoverPanels(); -// ['Modules\Admin\Providers\Filament\Panels\AdminPanelProvider', ...] -``` - -##### `getScanStats(): array` - -Get detailed statistics from the last scan operation. - -**Returns:** `array` - Scan statistics - -**Example Response:** -```php -[ - 'files_scanned' => 25, - 'classes_found' => 8, - 'panels_discovered' => 3, - 'scan_time' => 0.156, - 'errors' => 0, -] -``` - -### ComponentDiscoveryService - -Service for discovering Filament components. - -```php -class ComponentDiscoveryService implements ComponentScannerInterface -``` - -#### Constructor - -```php -public function __construct(CacheManagerInterface $cache = null) -``` - -**Parameters:** -- `$cache` - Optional cache manager instance - -#### Methods - -##### `discoverComponentsForPanel(string $panelId): array` - -Discover all components for a specific panel. - -**Parameters:** -- `$panelId` - Panel identifier - -**Returns:** `array` - Components grouped by type - -##### `discoverResources(?string $panelId = null): Collection` - -Discover Resource components. - -**Parameters:** -- `$panelId` - Optional panel identifier - -**Returns:** `Collection` - Resource class names - -##### `discoverPages(?string $panelId = null): Collection` - -Discover Page components. - -**Parameters:** -- `$panelId` - Optional panel identifier - -**Returns:** `Collection` - Page class names - -##### `discoverWidgets(?string $panelId = null): Collection` - -Discover Widget components. - -**Parameters:** -- `$panelId` - Optional panel identifier - -**Returns:** `Collection` - Widget class names - -##### `isValidComponent(string $className, string $type): bool` - -Validate if a class is a proper component of specified type. - -**Parameters:** -- `$className` - Fully qualified class name -- `$type` - Component type ('resources', 'pages', 'widgets') - -**Returns:** `bool` - Validation result - -##### `refreshCache(): void` - -Clear component discovery cache. - -#### Usage Examples - -```php -use PanicDevs\Modulite\Services\ComponentDiscoveryService; - -$discovery = new ComponentDiscoveryService(); - -// Discover all components for admin panel -$components = $discovery->discoverComponents('admin'); -/* -[ - 'resources' => ['App\Filament\Resources\UserResource', ...], - 'pages' => ['App\Filament\Pages\Dashboard', ...], - 'widgets' => ['App\Filament\Widgets\StatsWidget', ...] -] -*/ - -// Discover only resources -$resources = $discovery->discoverResources('admin'); - -// Check if class is valid resource -$isValid = $discovery->isValidComponent(UserResource::class, 'resources'); -``` - -## Console Commands - -### modulite:status - -Display Modulite discovery status and statistics. - -```bash -php artisan modulite:status [options] -``` - -#### Options - -| Option | Description | -|--------|-------------| -| `--clear-cache` | Clear all Modulite cache before showing status | -| `--scan` | Force rescan of panels and components | -| `--vvv` | Show detailed information | - -#### Example Output - -``` -Modulite Status Report -=================== - -Configuration: -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Setting โ”‚ Value โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ Cache Enabled โ”‚ โœ“ โ”‚ -โ”‚ Lazy Discovery โ”‚ โœ“ โ”‚ -โ”‚ Logging Enabled โ”‚ โœ— โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - -Panel Discovery: -Discovered 3 panels: - โ€ข AdminPanelProvider - โ€ข UserPanelProvider - โ€ข SupportPanelProvider -``` - -### modulite:clear-cache - -Clear all Modulite cache files. - -```bash -php artisan modulite:clear-cache -``` - -### modulite:benchmark - -Benchmark Modulite performance. - -```bash -php artisan modulite:benchmark [options] -``` - -#### Options - -| Option | Default | Description | -|--------|---------|-------------| -| `--iterations` | `100` | Number of benchmark iterations | -| `--warm-cache` | `false` | Warm up cache before testing | -| `--show-details` | `false` | Show detailed performance breakdown | - -#### Example Output - -``` -๐Ÿš€ Modulite Performance Benchmark -===================================== - -๐Ÿ“ˆ Benchmark Results: -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Metric โ”‚ Value โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ Average (ms) โ”‚ 1.234 โ”‚ -โ”‚ 95th Percentile (ms)โ”‚ 2.456 โ”‚ -โ”‚ Cache Hit Rate โ”‚ 98.5% โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -### modulite:discover-panels - -Manually discover panels (debugging). - -```bash -php artisan modulite:discover-panels [options] -``` - -#### Options - -| Option | Description | -|--------|-------------| -| `--verbose` | Show detailed discovery information | - -### modulite:discover-components - -Discover components for a specific panel. - -```bash -php artisan modulite:discover-components {panel} [options] -``` - -#### Arguments - -| Argument | Description | -|----------|-------------| -| `panel` | Panel identifier to discover components for | - -#### Options - -| Option | Description | -|--------|-------------| -| `--type` | Filter by component type (resources, pages, widgets) | - -## Exceptions - -### ScanException - -Thrown when scanning operations fail. - -```php -class ScanException extends Exception -``` - -#### Usage - -```php -use PanicDevs\Modulite\Exceptions\ScanException; - -try { - $panels = $scanner->discoverPanels(); -} catch (ScanException $e) { - Log::error('Panel discovery failed: ' . $e->getMessage()); -} -``` - -### CacheException - -Thrown when cache operations fail. - -```php -class CacheException extends Exception -``` - -#### Usage - -```php -use PanicDevs\Modulite\Exceptions\CacheException; - -try { - $cache->put('key', $value); -} catch (CacheException $e) { - Log::error('Cache operation failed: ' . $e->getMessage()); -} -``` - -## Configuration - -### Configuration Structure - -The configuration array structure for Modulite: - -```php -return [ - 'panels' => [ - 'locations' => array, // Scan locations for panels - 'patterns' => [ - 'files' => array, // File naming patterns - 'classes' => array, // Class naming patterns - ], - 'validation' => [ - 'strict_inheritance' => bool, - 'must_extend' => string, - 'must_be_instantiable' => bool, - 'check_panel_method' => bool, - 'allow_custom_base_classes' => bool, - ], - 'registration' => [ - 'auto_register' => bool, - 'sort_by' => string, // 'priority', 'name', 'none' - 'respect_environment' => bool, - 'validate_before_register' => bool, - ], - 'scanning' => [ - 'max_depth' => int, - 'follow_symlinks' => bool, - 'extensions' => array, - 'excluded_directories' => array, - ], - ], - 'components' => [ - 'locations' => array, - 'types' => [ - 'resources' => [ - 'enabled' => bool, - 'strict_inheritance' => bool, - 'must_extend' => string, - 'naming_pattern' => string, - 'allow_custom_base_classes' => bool, - ], - // Similar for 'pages' and 'widgets' - ], - 'registration' => [ - 'auto_register' => bool, - 'sort_by' => string, - 'validate_before_register' => bool, - 'group_by_module' => bool, - ], - 'scanning' => [ - 'max_depth' => int, - 'follow_symlinks' => bool, - 'extensions' => array, - 'excluded_directories' => array, - ], - ], - 'cache' => [ - 'enabled' => bool, - 'file' => string, - 'ttl' => int, - 'auto_invalidate' => bool, - 'memory_cache' => [ - 'enabled' => bool, - 'max_items' => int, - ], - ], - 'performance' => [ - 'lazy_discovery' => bool, - 'memory_optimization' => [ - 'batch_size' => int, - 'clear_stat_cache' => bool, - 'gc_after_scan' => bool, - ], - 'concurrent' => [ - 'enabled' => bool, - 'max_workers' => int, - ], - ], - 'modules' => [ - 'namespace' => string, - 'scan_only_enabled' => bool, - 'respect_module_priority' => bool, - 'status_cache_ttl' => int, - ], - 'logging' => [ - 'enabled' => bool, - 'channel' => string, - 'level' => string, - 'log_discovery_time' => bool, - 'log_cache_hits' => bool, - 'log_scan_stats' => bool, - ], - 'error_handling' => [ - 'fail_silently' => bool, - 'log_errors' => bool, - 'max_errors_per_scan' => int, - 'throw_on_invalid_class' => bool, - 'throw_on_missing_requirements' => bool, - ], -]; -``` - -### Environment Variables - -Complete list of environment variables: - -| Variable | Type | Default | Description | -|----------|------|---------|-------------| -| `MODULITE_CACHE_ENABLED` | `bool` | `!debug` | Enable/disable caching | -| `MODULITE_CACHE_TTL` | `int` | `0` | Cache time-to-live in seconds | -| `MODULITE_LAZY_DISCOVERY` | `bool` | `true` | Enable lazy panel discovery | -| `MODULITE_AUTO_REGISTER_PANELS` | `bool` | `true` | Auto-register discovered panels | -| `MODULITE_AUTO_REGISTER_COMPONENTS` | `bool` | `true` | Auto-register discovered components | -| `MODULITE_STRICT_INHERITANCE` | `bool` | `false` | Require strict class inheritance | -| `MODULITE_ALLOW_CUSTOM_BASE_CLASSES` | `bool` | `true` | Allow custom base classes | -| `MODULITE_STRICT_COMPONENT_INHERITANCE` | `bool` | `false` | Strict component inheritance | -| `MODULITE_ALLOW_CUSTOM_RESOURCE_CLASSES` | `bool` | `true` | Allow custom resource classes | -| `MODULITE_ALLOW_CUSTOM_PAGE_CLASSES` | `bool` | `true` | Allow custom page classes | -| `MODULITE_ALLOW_CUSTOM_WIDGET_CLASSES` | `bool` | `true` | Allow custom widget classes | -| `MODULITE_FAIL_SILENTLY` | `bool` | `!debug` | Handle errors silently | -| `MODULITE_LOGGING_ENABLED` | `bool` | `debug` | Enable logging | -| `MODULITE_LOG_CHANNEL` | `string` | `stack` | Log channel to use | -| `MODULITE_LOG_LEVEL` | `string` | `info` | Log level | - -## Helper Functions - -### Service Resolution - -Access Modulite services through Laravel's service container: - -```php -// Get cache manager -$cache = app(\PanicDevs\Modulite\Contracts\CacheManagerInterface::class); - -// Get panel scanner -$scanner = app(\PanicDevs\Modulite\Contracts\PanelScannerInterface::class); - -// Get component scanner -$componentScanner = app(\PanicDevs\Modulite\Contracts\ComponentScannerInterface::class); -``` - -### Direct Service Instantiation - -```php -use PanicDevs\Modulite\Services\UnifiedCacheManager; -use PanicDevs\Modulite\Services\PanelScannerService; -use PanicDevs\Modulite\Services\ComponentDiscoveryService; - -// Create cache manager -$cache = new UnifiedCacheManager(config('modulite.cache')); - -// Create panel scanner -$scanner = new PanelScannerService( - config('modulite'), - base_path(), - app('modules') // nwidart module manager -); - -// Create component discovery service -$discovery = new ComponentDiscoveryService($cache); -``` - -### Configuration Helpers - -```php -// Get specific configuration -$cacheEnabled = config('modulite.cache.enabled'); -$scanLocations = config('modulite.panels.locations'); - -// Check if feature is enabled -$lazyDiscovery = config('modulite.performance.lazy_discovery', true); - -// Get environment-specific settings -$failSilently = config('modulite.error_handling.fail_silently', !app()->hasDebugModeEnabled()); -``` - -### Cache Key Patterns - -Modulite uses specific cache key patterns: - -```php -// Panel cache keys -"panels:{hash}" // Main panel discovery cache -"panels:benchmark_{iteration}" // Benchmark cache keys - -// Component cache keys -"panel_components:{panel}" // Panel-specific components -"discovered_resources:{panel}" // Panel resources -"discovered_pages:{panel}" // Panel pages -"discovered_widgets:{panel}" // Panel widgets -"enabled_modules" // Enabled modules list -``` - -### Performance Monitoring - -```php -// Measure discovery performance -$start = microtime(true); -$panels = $scanner->discoverPanels(); -$duration = microtime(true) - $start; - -echo "Discovery took: " . round($duration * 1000, 2) . "ms\n"; - -// Get cache statistics -$stats = $cache->getStats(); -echo "Cache hit ratio: " . ($stats['hits'] / $stats['requests'] * 100) . "%\n"; - -// Get scan statistics -$scanStats = $scanner->getScanStats(); -echo "Files scanned: " . $scanStats['files_scanned'] . "\n"; -echo "Panels found: " . $scanStats['panels_discovered'] . "\n"; -``` - ---- - -This API reference provides complete documentation for all public interfaces, classes, and methods in Modulite. For usage examples and best practices, see the [Examples Guide](EXAMPLES.md). diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md deleted file mode 100644 index b2f9215..0000000 --- a/docs/CONFIGURATION.md +++ /dev/null @@ -1,726 +0,0 @@ -# ๐Ÿ“‹ Configuration Guide - -This comprehensive guide covers all configuration options available in Modulite, with practical examples and use cases for each setting. - -## ๐Ÿ“‚ Table of Contents - -- [Configuration File Overview](#configuration-file-overview) -- [Panel Discovery Configuration](#panel-discovery-configuration) -- [Component Discovery Configuration](#component-discovery-configuration) -- [Cache Configuration](#cache-configuration) -- [Performance Configuration](#performance-configuration) -- [Module Integration](#module-integration) -- [Logging & Debugging](#logging--debugging) -- [Error Handling](#error-handling) -- [Environment Variables](#environment-variables) -- [Production Optimizations](#production-optimizations) - -## Configuration File Overview - -The main configuration file is located at `config/modulite.php`. You can publish it using: - -```bash -php artisan vendor:publish --tag=modulite-config -``` - -The configuration is organized into logical sections: - -```php -return [ - 'panels' => [/* Panel discovery settings */], - 'components' => [/* Component discovery settings */], - 'cache' => [/* Caching configuration */], - 'performance' => [/* Performance optimizations */], - 'modules' => [/* Module system integration */], - 'logging' => [/* Logging configuration */], - 'error_handling' => [/* Error handling behavior */], -]; -``` - -## Panel Discovery Configuration - -### Discovery Locations - -Configure where Modulite looks for Panel Provider classes: - -```php -'panels' => [ - 'locations' => [ - 'modules/*/Providers/Filament/Panels', // nwidart/laravel-modules structure - 'foundation/*/Providers/Filament/Panels', // Custom foundation modules - 'app/Filament/Panels', // App-level panels - 'packages/*/src/Filament/Panels', // Custom packages - ], -] -``` - -**Supported Patterns:** -- `*` - Single directory wildcard -- `**` - Recursive directory wildcard (use with caution) - -**Examples:** - -```php -// For different module structures -'locations' => [ - // Standard nwidart structure - 'modules/*/Providers/Filament/Panels', - - // Nested module structure - 'src/Modules/*/Filament/Panels', - - // Domain-driven design structure - 'app/Domains/*/Infrastructure/Filament/Panels', - - // Package-based structure - 'packages/*/src/Providers/Filament', -], -``` - -### Naming Patterns - -Define file and class naming conventions: - -```php -'panels' => [ - 'patterns' => [ - 'files' => [ - '*PanelProvider.php', // Standard naming - '*Panel.php', // Shorter naming - 'Panel*.php', // Prefix naming - ], - 'classes' => [ - '*PanelProvider', // Must match file patterns - '*Panel', - 'Panel*', - ], - ], -] -``` - -**Best Practices:** -- Keep file and class patterns consistent -- Use descriptive naming conventions -- Avoid overly broad patterns that might match unintended files - -### Validation Rules - -Control how discovered classes are validated: - -```php -'panels' => [ - 'validation' => [ - // Require exact inheritance from Filament classes - 'strict_inheritance' => env('MODULITE_STRICT_INHERITANCE', false), - - // Base class that panels must extend - 'must_extend' => 'Filament\PanelProvider', - - // Ensure classes can be instantiated - 'must_be_instantiable' => true, - - // Verify panel() method exists - 'check_panel_method' => true, - - // Allow custom base classes (duck typing) - 'allow_custom_base_classes' => env('MODULITE_ALLOW_CUSTOM_BASE_CLASSES', true), - ], -] -``` - -**Validation Levels:** - -**Strict Mode (`strict_inheritance: true`):** -```php -// Only classes that directly extend Filament\PanelProvider -class AdminPanelProvider extends \Filament\PanelProvider { } -``` - -**Flexible Mode (`strict_inheritance: false`):** -```php -// Custom base classes are allowed -class AdminPanelProvider extends MyBasePanelProvider { } -class MyBasePanelProvider extends \Filament\PanelProvider { } -``` - -### Registration Options - -Configure how discovered panels are registered: - -```php -'panels' => [ - 'registration' => [ - // Automatically register discovered panels - 'auto_register' => env('MODULITE_AUTO_REGISTER_PANELS', true), - - // Sort order: 'priority', 'name', 'none' - 'sort_by' => 'priority', - - // Check environment constraints from #[FilamentPanel] attributes - 'respect_environment' => true, - - // Validate classes before registration (debug mode only) - 'validate_before_register' => app()->hasDebugModeEnabled(), - ], -] -``` - -**Sort Options:** -- `priority` - Sort by `#[FilamentPanel(priority: X)]` attribute -- `name` - Sort alphabetically by class name -- `none` - Registration order is not guaranteed - -### Scanning Options - -Optimize the file scanning process: - -```php -'panels' => [ - 'scanning' => [ - // Maximum directory depth to scan - 'max_depth' => 5, - - // Follow symbolic links - 'follow_symlinks' => false, - - // File extensions to scan - 'extensions' => ['php'], - - // Directories to exclude from scanning - 'excluded_directories' => [ - 'tests', - 'migrations', - 'seeders', - 'factories', - '.git', - 'node_modules', - 'vendor', - 'storage', - 'bootstrap/cache', - ], - ], -] -``` - -**Performance Impact:** -- Lower `max_depth` = faster scanning, might miss nested files -- Higher `max_depth` = thorough scanning, slower performance -- More `excluded_directories` = faster scanning - -## Component Discovery Configuration - -### Discovery Locations - -Configure where to find Filament components: - -```php -'components' => [ - 'locations' => [ - // Panel-specific components - 'modules/*/Filament/{panel}/Resources', - 'modules/*/Filament/{panel}/Pages', - 'modules/*/Filament/{panel}/Widgets', - - // Foundation modules - 'foundation/*/Filament/{panel}/Resources', - 'foundation/*/Filament/{panel}/Pages', - 'foundation/*/Filament/{panel}/Widgets', - - // App-level components - 'app/Filament/{panel}/Resources', - 'app/Filament/{panel}/Pages', - 'app/Filament/{panel}/Widgets', - ], -] -``` - -**Placeholder Variables:** -- `{panel}` - Replaced with actual panel ID (e.g., 'admin', 'manager') -- `{module}` - Replaced with module name - -**Examples:** - -```php -// Directory structure for multi-panel setup -modules/ -โ”œโ”€โ”€ User/ -โ”‚ โ””โ”€โ”€ Filament/ -โ”‚ โ”œโ”€โ”€ Admin/ -โ”‚ โ”‚ โ”œโ”€โ”€ Resources/UserResource.php -โ”‚ โ”‚ โ”œโ”€โ”€ Pages/UsersPage.php -โ”‚ โ”‚ โ””โ”€โ”€ Widgets/UserStatsWidget.php -โ”‚ โ””โ”€โ”€ Manager/ -โ”‚ โ”œโ”€โ”€ Resources/UserResource.php -โ”‚ โ””โ”€โ”€ Pages/UserManagementPage.php -โ””โ”€โ”€ Blog/ - โ””โ”€โ”€ Filament/ - โ””โ”€โ”€ Admin/ - โ”œโ”€โ”€ Resources/PostResource.php - โ””โ”€โ”€ Widgets/PostsWidget.php -``` - -### Component Types - -Configure individual component type discovery: - -```php -'components' => [ - 'types' => [ - 'resources' => [ - 'enabled' => true, - 'strict_inheritance' => env('MODULITE_STRICT_COMPONENT_INHERITANCE', false), - 'must_extend' => 'Filament\Resources\Resource', - 'naming_pattern' => '*Resource.php', - 'allow_custom_base_classes' => env('MODULITE_ALLOW_CUSTOM_RESOURCE_CLASSES', true), - ], - 'pages' => [ - 'enabled' => true, - 'strict_inheritance' => env('MODULITE_STRICT_COMPONENT_INHERITANCE', false), - 'must_extend' => 'Filament\Pages\Page', - 'naming_pattern' => '*Page.php', - 'allow_custom_base_classes' => env('MODULITE_ALLOW_CUSTOM_PAGE_CLASSES', true), - ], - 'widgets' => [ - 'enabled' => true, - 'strict_inheritance' => env('MODULITE_STRICT_COMPONENT_INHERITANCE', false), - 'must_extend' => 'Filament\Widgets\Widget', - 'naming_pattern' => '*Widget.php', - 'allow_custom_base_classes' => env('MODULITE_ALLOW_CUSTOM_WIDGET_CLASSES', true), - ], - ], -] -``` - -**Custom Component Types:** - -You can add custom component types: - -```php -'types' => [ - 'actions' => [ - 'enabled' => true, - 'must_extend' => 'Filament\Actions\Action', - 'naming_pattern' => '*Action.php', - ], - 'livewire' => [ - 'enabled' => true, - 'must_extend' => 'Livewire\Component', - 'naming_pattern' => '*Component.php', - ], -] -``` - -### Component Registration - -Configure how components are registered to panels: - -```php -'components' => [ - 'registration' => [ - // Automatically register discovered components - 'auto_register' => env('MODULITE_AUTO_REGISTER_COMPONENTS', true), - - // Sort components: 'name', 'priority', 'none' - 'sort_by' => 'name', - - // Validate components before registration (debug mode) - 'validate_before_register' => app()->hasDebugModeEnabled(), - - // Group components by module in navigation - 'group_by_module' => true, - ], -] -``` - -## Cache Configuration - -### Cache Enable/Disable - -Master switch for caching functionality: - -```php -'cache' => [ - 'enabled' => env('MODULITE_CACHE_ENABLED', !app()->hasDebugModeEnabled()), -] -``` - -**Environment-Based Defaults:** -- Development: `false` (disabled for quick iteration) -- Production: `true` (enabled for performance) - -### Cache File Configuration - -```php -'cache' => [ - 'file' => base_path('bootstrap/cache/modulite.php'), -] -``` - -**Alternative Locations:** - -```php -// Different cache locations for different environments -'file' => match(app()->environment()) { - 'testing' => storage_path('framework/cache/modulite.php'), - 'local' => base_path('bootstrap/cache/modulite.php'), - default => base_path('bootstrap/cache/modulite.php'), -}, -``` - -### Cache TTL (Time To Live) - -```php -'cache' => [ - 'ttl' => env('MODULITE_CACHE_TTL', app()->hasDebugModeEnabled() ? 300 : 0), -] -``` - -**TTL Settings:** -- `0` - Never expires (production) -- `300` - 5 minutes (development) -- `3600` - 1 hour (staging) -- `86400` - 24 hours (long-term caching) - -### Auto-Invalidation - -```php -'cache' => [ - 'auto_invalidate' => app()->hasDebugModeEnabled(), -] -``` - -**When enabled:** -- File modification time changes trigger cache invalidation -- New files in scan directories clear cache -- Module enable/disable events clear cache - -### Memory Cache - -In-memory caching for current request: - -```php -'cache' => [ - 'memory_cache' => [ - 'enabled' => true, - 'max_items' => 1000, - ], -] -``` - -## Performance Configuration - -### Lazy Discovery - -Defer scanning until Filament is actually needed: - -```php -'performance' => [ - 'lazy_discovery' => env('MODULITE_LAZY_DISCOVERY', true), -] -``` - -**Benefits:** -- Faster application boot time -- Reduced memory usage on non-admin requests -- Better performance for API-only endpoints - -### Memory Optimization - -For large codebases with many modules: - -```php -'performance' => [ - 'memory_optimization' => [ - // Process files in batches to reduce memory usage - 'batch_size' => 100, - - // Clear file stat cache regularly - 'clear_stat_cache' => true, - - // Force garbage collection after scanning - 'gc_after_scan' => true, - ], -] -``` - -**Tuning Guidelines:** -- Large projects (1000+ files): `batch_size: 50` -- Medium projects (100-1000 files): `batch_size: 100` -- Small projects (<100 files): `batch_size: 200` - -### Concurrent Processing - -**โš ๏ธ Experimental Feature** - -Enable concurrent file processing: - -```php -'performance' => [ - 'concurrent' => [ - 'enabled' => false, - 'max_workers' => 4, - ], -] -``` - -**Requirements:** -- PHP with `pcntl` extension -- Unix-like operating system -- Sufficient system resources - -## Module Integration - -### nwidart/laravel-modules Integration - -```php -'modules' => [ - 'namespace' => 'modules', - 'scan_only_enabled' => true, - 'respect_module_priority' => true, - 'status_cache_ttl' => 300, -] -``` - -**Configuration Options:** - -```php -'modules' => [ - // Module directory namespace - 'namespace' => 'modules', - - // Only scan enabled modules - 'scan_only_enabled' => true, - - // Respect module loading priority - 'respect_module_priority' => true, - - // Cache module status for performance - 'status_cache_ttl' => 300, // 5 minutes -] -``` - -### Custom Module Systems - -For custom module systems: - -```php -'modules' => [ - 'custom_module_resolver' => function() { - // Return array of enabled module names - return ['User', 'Blog', 'Shop']; - }, -] -``` - -## Logging & Debugging - -### Logging Control - -```php -'logging' => [ - 'enabled' => env('MODULITE_LOGGING_ENABLED', app()->hasDebugModeEnabled()), - 'channel' => env('MODULITE_LOG_CHANNEL', 'stack'), - 'level' => env('MODULITE_LOG_LEVEL', 'info'), -] -``` - -**Log Levels:** -- `debug` - Verbose logging, file-by-file discovery details -- `info` - Standard logging, summary information -- `warning` - Important issues that don't break functionality -- `error` - Serious issues that may affect functionality - -### Performance Logging - -```php -'logging' => [ - 'log_discovery_time' => app()->hasDebugModeEnabled(), - 'log_cache_hits' => app()->hasDebugModeEnabled(), - 'log_scan_stats' => app()->hasDebugModeEnabled(), -] -``` - -**Sample Log Output:** - -``` -[2024-01-15 10:30:45] local.INFO: Modulite panel discovery started -[2024-01-15 10:30:45] local.DEBUG: Scanning location: /app/modules/User/Providers/Filament/Panels -[2024-01-15 10:30:45] local.DEBUG: Found panels in /app/modules/User/Providers/Filament/Panels/AdminPanelProvider.php ["AdminPanelProvider"] -[2024-01-15 10:30:45] local.INFO: Modulite panel discovery completed {"panels_found":3,"files_scanned":25,"scan_time":0.156} -``` - -## Error Handling - -### Error Behavior - -```php -'error_handling' => [ - 'fail_silently' => !app()->hasDebugModeEnabled(), - 'log_errors' => true, - 'max_errors_per_scan' => 10, -] -``` - -**Fail Silently Modes:** -- `true` (Production): Log errors but continue execution -- `false` (Development): Throw exceptions for debugging - -### Validation Errors - -```php -'error_handling' => [ - 'throw_on_invalid_class' => app()->hasDebugModeEnabled(), - 'throw_on_missing_requirements' => app()->hasDebugModeEnabled(), -] -``` - -**Error Types:** -- Invalid class structure -- Missing required methods -- File parsing errors -- Reflection exceptions - -## Environment Variables - -### Complete Environment Variable Reference - -```env -# Cache Configuration -MODULITE_CACHE_ENABLED=true -MODULITE_CACHE_TTL=0 - -# Performance -MODULITE_LAZY_DISCOVERY=true - -# Panel Discovery -MODULITE_AUTO_REGISTER_PANELS=true -MODULITE_STRICT_INHERITANCE=false -MODULITE_ALLOW_CUSTOM_BASE_CLASSES=true - -# Component Discovery -MODULITE_AUTO_REGISTER_COMPONENTS=true -MODULITE_STRICT_COMPONENT_INHERITANCE=false -MODULITE_ALLOW_CUSTOM_RESOURCE_CLASSES=true -MODULITE_ALLOW_CUSTOM_PAGE_CLASSES=true -MODULITE_ALLOW_CUSTOM_WIDGET_CLASSES=true - -# Error Handling -MODULITE_FAIL_SILENTLY=false - -# Logging -MODULITE_LOGGING_ENABLED=false -MODULITE_LOG_CHANNEL=stack -MODULITE_LOG_LEVEL=info -``` - -### Environment-Specific Configurations - -**.env.local (Development):** -```env -MODULITE_CACHE_ENABLED=false -MODULITE_CACHE_TTL=300 -MODULITE_LOGGING_ENABLED=true -MODULITE_FAIL_SILENTLY=false -``` - -**.env.production (Production):** -```env -MODULITE_CACHE_ENABLED=true -MODULITE_CACHE_TTL=0 -MODULITE_LOGGING_ENABLED=false -MODULITE_FAIL_SILENTLY=true -MODULITE_LAZY_DISCOVERY=true -``` - -**.env.testing (Testing):** -```env -MODULITE_CACHE_ENABLED=false -MODULITE_LOGGING_ENABLED=false -MODULITE_FAIL_SILENTLY=false -``` - -## Production Optimizations - -### Optimal Production Configuration - -```php -// config/modulite.php - Production optimizations -return [ - 'cache' => [ - 'enabled' => true, - 'ttl' => 0, // Never expires - 'auto_invalidate' => false, - ], - 'performance' => [ - 'lazy_discovery' => true, - 'memory_optimization' => [ - 'batch_size' => 100, - 'clear_stat_cache' => true, - 'gc_after_scan' => true, - ], - ], - 'error_handling' => [ - 'fail_silently' => true, - 'log_errors' => false, // Disable in production for performance - ], - 'logging' => [ - 'enabled' => false, // Disable for maximum performance - ], -]; -``` - -### Deployment Checklist - -1. **Set environment variables:** - ```bash - APP_DEBUG=false - MODULITE_CACHE_ENABLED=true - MODULITE_CACHE_TTL=0 - ``` - -2. **Run optimization commands:** - ```bash - php artisan config:cache - php artisan route:cache - php artisan view:cache - php artisan optimize - ``` - -3. **Verify cache is working:** - ```bash - php artisan modulite:status - php artisan modulite:benchmark --iterations=100 - ``` - -4. **Set proper file permissions:** - ```bash - chmod 755 bootstrap/cache - chown -R www-data:www-data bootstrap/cache - ``` - -### Monitoring & Maintenance - -**Cache monitoring:** -```bash -# Check cache status -php artisan modulite:status - -# Clear cache if needed -php artisan modulite:clear-cache - -# Rebuild cache -php artisan optimize -``` - -**Performance monitoring:** -```bash -# Benchmark performance -php artisan modulite:benchmark --iterations=1000 - -# Monitor memory usage -php artisan modulite:status --vvv -``` - ---- - -This configuration guide covers all aspects of Modulite configuration. For specific use cases or advanced configurations, refer to the examples in each section or check the [troubleshooting guide](TROUBLESHOOTING.md). diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md deleted file mode 100644 index ceb920f..0000000 --- a/docs/EXAMPLES.md +++ /dev/null @@ -1,1117 +0,0 @@ -# ๐ŸŽจ Usage Examples & Best Practices - -This guide provides comprehensive examples of using Modulite in real-world scenarios, following Laravel and Filament best practices. - -## ๐Ÿ“‹ Table of Contents - -- [Basic Panel Setup](#basic-panel-setup) -- [Multi-Panel Applications](#multi-panel-applications) -- [Component Organization](#component-organization) -- [Advanced Configurations](#advanced-configurations) -- [Enterprise Patterns](#enterprise-patterns) -- [Performance Optimization](#performance-optimization) -- [Testing Strategies](#testing-strategies) - - -## Basic Panel Setup - -### Simple Admin Panel - -The most basic setup for an admin panel: - -```php -default() - ->id('admin') - ->path('/admin') - ->login() - ->colors([ - 'primary' => '#1f2937', - 'gray' => '#6b7280', - ]) - ->discoverResources( - in: module_path('Admin', 'Filament/Admin/Resources'), - for: 'Modules\\Admin\\Filament\\Admin\\Resources' - ) - ->discoverPages( - in: module_path('Admin', 'Filament/Admin/Pages'), - for: 'Modules\\Admin\\Filament\\Admin\\Pages' - ) - ->pages([ - 'dashboard' => \Modules\Admin\Filament\Admin\Pages\Dashboard::class, - ]) - ->widgets([ - Widgets\AccountWidget::class, - Widgets\FilamentInfoWidget::class, - ]) - ->middleware([ - 'web', - 'auth', - \Modules\Admin\Http\Middleware\AdminMiddleware::class, - ]) - ->authMiddleware([ - 'auth', - \Modules\Admin\Http\Middleware\AdminAuthMiddleware::class, - ]); - } -} -``` - -### User Management Panel - -A specialized panel for user management: - -```php -id('users') - ->path('/users') - ->login() - ->registration() - ->passwordReset() - ->colors([ - 'primary' => '#3b82f6', - ]) - ->font('Inter') - ->favicon(asset('images/favicon.ico')) - ->brandName('User Management') - ->brandLogo(asset('images/user-logo.svg')) - ->viteTheme('resources/css/filament/user/theme.css') - ->discoverResources( - in: module_path('User', 'Filament/User/Resources'), - for: 'Modules\\User\\Filament\\User\\Resources' - ) - ->resources([ - \Modules\User\Filament\User\Resources\UserResource::class, - \Modules\User\Filament\User\Resources\RoleResource::class, - ]) - ->pages([ - \Modules\User\Filament\User\Pages\Profile::class, - \Modules\User\Filament\User\Pages\Settings::class, - ]) - ->navigationGroups([ - 'User Management', - 'Security', - 'Reports', - ]); - } -} -``` - -## Multi-Panel Applications - -### E-commerce Platform Example - -A complete e-commerce platform with multiple specialized panels: - -#### 1. Admin Panel (Core Management) - -```php -default() - ->id('admin') - ->path('/admin') - ->login() - ->colors(['primary' => '#dc2626']) - ->brandName('E-Shop Admin') - ->discoverResources( - in: module_path('Core', 'Filament/Admin/Resources'), - for: 'Modules\\Core\\Filament\\Admin\\Resources' - ) - ->navigationGroups([ - 'Dashboard', - 'User Management', - 'Content Management', - 'System', - 'Reports', - ]) - ->middleware([ - 'web', - 'auth', - \Modules\Core\Http\Middleware\AdminMiddleware::class, - ]); - } -} -``` - -#### 2. Shop Management Panel - -```php -id('shop') - ->path('/shop/manage') - ->login() - ->colors(['primary' => '#059669']) - ->brandName('Shop Manager') - ->discoverResources( - in: module_path('Shop', 'Filament/Shop/Resources'), - for: 'Modules\\Shop\\Filament\\Shop\\Resources' - ) - ->resources([ - \Modules\Shop\Filament\Shop\Resources\ProductResource::class, - \Modules\Shop\Filament\Shop\Resources\CategoryResource::class, - \Modules\Shop\Filament\Shop\Resources\OrderResource::class, - \Modules\Shop\Filament\Shop\Resources\InventoryResource::class, - ]) - ->pages([ - \Modules\Shop\Filament\Shop\Pages\Dashboard::class, - \Modules\Shop\Filament\Shop\Pages\Analytics::class, - ]) - ->widgets([ - \Modules\Shop\Filament\Shop\Widgets\SalesOverview::class, - \Modules\Shop\Filament\Shop\Widgets\TopProducts::class, - \Modules\Shop\Filament\Shop\Widgets\RecentOrders::class, - ]) - ->navigationGroups([ - 'Catalog', - 'Orders', - 'Inventory', - 'Analytics', - 'Settings', - ]); - } -} -``` - -#### 3. Customer Service Panel - -```php -id('support') - ->path('/support') - ->login() - ->colors(['primary' => '#7c3aed']) - ->brandName('Customer Support') - ->discoverResources( - in: module_path('Support', 'Filament/Support/Resources'), - for: 'Modules\\Support\\Filament\\Support\\Resources' - ) - ->resources([ - \Modules\Support\Filament\Support\Resources\TicketResource::class, - \Modules\Support\Filament\Support\Resources\CustomerResource::class, - \Modules\Support\Filament\Support\Resources\KnowledgeBaseResource::class, - ]) - ->pages([ - \Modules\Support\Filament\Support\Pages\Dashboard::class, - \Modules\Support\Filament\Support\Pages\LiveChat::class, - ]) - ->widgets([ - \Modules\Support\Filament\Support\Widgets\TicketStats::class, - \Modules\Support\Filament\Support\Widgets\ResponseTime::class, - ]) - ->middleware([ - 'web', - 'auth', - \Modules\Support\Http\Middleware\SupportAgentMiddleware::class, - ]); - } -} -``` - -### Environment-Specific Panels - -#### Development Panel (Local Only) - -```php -id('debug') - ->path('/debug') - ->colors(['primary' => '#f59e0b']) - ->brandName('Debug Panel') - ->resources([ - \Modules\Debug\Filament\Debug\Resources\LogResource::class, - \Modules\Debug\Filament\Debug\Resources\QueueJobResource::class, - \Modules\Debug\Filament\Debug\Resources\CacheResource::class, - ]) - ->pages([ - \Modules\Debug\Filament\Debug\Pages\SystemInfo::class, - \Modules\Debug\Filament\Debug\Pages\DatabaseQueries::class, - \Modules\Debug\Filament\Debug\Pages\ModuliteStatus::class, - ]) - ->middleware(['web']) - ->authGuard(null); // No auth required in development - } -} -``` - -#### Staging Panel (Testing Environment) - -```php -id('staging') - ->path('/staging') - ->login() - ->colors(['primary' => '#f97316']) - ->brandName('Staging Environment') - ->brandLogo(asset('images/staging-logo.svg')) - ->resources([ - \Modules\Testing\Filament\Staging\Resources\TestDataResource::class, - \Modules\Testing\Filament\Staging\Resources\TestResultResource::class, - ]) - ->pages([ - \Modules\Testing\Filament\Staging\Pages\TestRunner::class, - \Modules\Testing\Filament\Staging\Pages\DataSeeder::class, - ]) - ->middleware([ - 'web', - 'auth', - \Modules\Testing\Http\Middleware\StagingMiddleware::class, - ]); - } -} -``` - -## Component Organization - -### Modular Resource Structure - -#### User Resource Example - -```php -schema([ - Forms\Components\Section::make('Personal Information') - ->schema([ - Forms\Components\TextInput::make('name') - ->required() - ->maxLength(255), - Forms\Components\TextInput::make('email') - ->email() - ->required() - ->unique(ignoreRecord: true) - ->maxLength(255), - Forms\Components\DateTimePicker::make('email_verified_at'), - ]) - ->columns(2), - - Forms\Components\Section::make('Security') - ->schema([ - Forms\Components\TextInput::make('password') - ->password() - ->dehydrated(fn ($state) => filled($state)) - ->required(fn (string $context): bool => $context === 'create'), - Forms\Components\Select::make('roles') - ->relationship('roles', 'name') - ->multiple() - ->preload(), - ]), - - Forms\Components\Section::make('Status') - ->schema([ - Forms\Components\Toggle::make('is_active') - ->default(true), - Forms\Components\DateTimePicker::make('last_login_at') - ->disabled(), - ]) - ->columns(2), - ]); - } - - public static function table(Table $table): Table - { - return $table - ->columns([ - Tables\Columns\ImageColumn::make('avatar') - ->circular(), - Tables\Columns\TextColumn::make('name') - ->searchable() - ->sortable(), - Tables\Columns\TextColumn::make('email') - ->searchable() - ->sortable(), - Tables\Columns\TextColumn::make('roles.name') - ->badge(), - Tables\Columns\IconColumn::make('is_active') - ->boolean(), - Tables\Columns\TextColumn::make('last_login_at') - ->dateTime() - ->sortable(), - Tables\Columns\TextColumn::make('created_at') - ->dateTime() - ->sortable() - ->toggleable(isToggledHiddenByDefault: true), - ]) - ->filters([ - Tables\Filters\TernaryFilter::make('is_active'), - Tables\Filters\SelectFilter::make('roles') - ->relationship('roles', 'name') - ->multiple(), - Tables\Filters\Filter::make('verified') - ->query(fn ($query) => $query->whereNotNull('email_verified_at')), - ]) - ->actions([ - Tables\Actions\ViewAction::make(), - Tables\Actions\EditAction::make(), - Tables\Actions\DeleteAction::make(), - ]) - ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - Tables\Actions\BulkAction::make('activate') - ->action(fn ($records) => $records->each->update(['is_active' => true])) - ->icon('heroicon-o-check-circle'), - Tables\Actions\BulkAction::make('deactivate') - ->action(fn ($records) => $records->each->update(['is_active' => false])) - ->icon('heroicon-o-x-circle'), - ]), - ]); - } - - public static function getRelations(): array - { - return [ - RelationManagers\RolesRelationManager::class, - RelationManagers\PermissionsRelationManager::class, - RelationManagers\ActivityRelationManager::class, - ]; - } - - public static function getPages(): array - { - return [ - 'index' => Pages\ListUsers::route('/'), - 'create' => Pages\CreateUser::route('/create'), - 'view' => Pages\ViewUser::route('/{record}'), - 'edit' => Pages\EditUser::route('/{record}/edit'), - ]; - } - - public static function getGlobalSearchEloquentQuery(): Builder - { - return parent::getGlobalSearchEloquentQuery()->with(['roles']); - } - - public static function getGloballySearchableAttributes(): array - { - return ['name', 'email']; - } - - public static function getGlobalSearchResultDetails(Model $record): array - { - return [ - 'Email' => $record->email, - 'Roles' => $record->roles->pluck('name')->join(', '), - ]; - } -} -``` - -### Custom Page Example - -```php -icon('heroicon-o-arrow-down-tray') - ->action('exportData'), - \Filament\Actions\Action::make('refresh') - ->icon('heroicon-o-arrow-path') - ->action('refreshData'), - ]; - } - - public function exportData(): void - { - // Export logic - $this->notify('success', 'Data exported successfully!'); - } - - public function refreshData(): void - { - // Refresh logic - $this->dispatch('refreshWidgets'); - $this->notify('success', 'Data refreshed!'); - } -} -``` - -### Widget Examples - -#### Stats Overview Widget - -```php -description('32% increase') - ->descriptionIcon('heroicon-m-arrow-trending-up') - ->color('success'), - - Stat::make('Active Orders', Order::where('status', 'active')->count()) - ->description('12% increase') - ->descriptionIcon('heroicon-m-arrow-trending-up') - ->color('info'), - - Stat::make('Revenue', '$' . number_format(Order::sum('total'), 2)) - ->description('7% decrease') - ->descriptionIcon('heroicon-m-arrow-trending-down') - ->color('danger'), - ]; - } -} -``` - -#### Chart Widget - -```php -where('created_at', '>=', now()->subDays(30)) - ->groupBy('date') - ->orderBy('date') - ->get(); - - return [ - 'datasets' => [ - [ - 'label' => 'Revenue', - 'data' => $data->pluck('revenue'), - 'backgroundColor' => '#3b82f6', - 'borderColor' => '#3b82f6', - ], - ], - 'labels' => $data->pluck('date'), - ]; - } - - protected function getType(): string - { - return 'line'; - } -} -``` - -## Advanced Configurations - -### Custom Base Panel Provider - -Create a base panel provider for consistent configuration: - -```php -id($id) - ->path($path) - ->login() - ->colors([ - 'primary' => Color::Blue, - 'gray' => Color::Slate, - ]) - ->font('Inter') - ->brandName(config('app.name')) - ->brandLogo(asset('images/logo.svg')) - ->favicon(asset('images/favicon.ico')) - ->darkMode(true) - ->sidebarCollapsibleOnDesktop() - ->navigationGroups($this->getNavigationGroups()) - ->middleware($this->getBaseMiddleware()) - ->authMiddleware($this->getAuthMiddleware()) - ->globalSearchKeyBindings(['command+k', 'ctrl+k']) - ->viteTheme($this->getViteTheme()); - } - - /** - * Get navigation groups for this panel - */ - protected function getNavigationGroups(): array - { - return [ - 'Dashboard', - 'Management', - 'Reports', - 'Settings', - ]; - } - - /** - * Get base middleware stack - */ - protected function getBaseMiddleware(): array - { - return [ - 'web', - 'auth', - \Foundation\Base\Http\Middleware\BaseMiddleware::class, - ]; - } - - /** - * Get authentication middleware - */ - protected function getAuthMiddleware(): array - { - return [ - 'auth', - ]; - } - - /** - * Get Vite theme path - */ - protected function getViteTheme(): string - { - return 'resources/css/filament/theme.css'; - } -} -``` - -### Using Custom Base Provider - -```php -configureBasePanel($panel, 'admin', '/admin') - ->default() - ->discoverResources( - in: module_path('Admin', 'Filament/Admin/Resources'), - for: 'Modules\\Admin\\Filament\\Admin\\Resources' - ) - ->pages([ - \Modules\Admin\Filament\Admin\Pages\Dashboard::class, - ]) - ->widgets([ - \Modules\Admin\Filament\Admin\Widgets\AdminStats::class, - ]); - } - - protected function getNavigationGroups(): array - { - return [ - 'Dashboard', - 'User Management', - 'Content', - 'System', - 'Reports', - ]; - } - - protected function getAuthMiddleware(): array - { - return [ - 'auth', - \Modules\Admin\Http\Middleware\AdminMiddleware::class, - ]; - } -} -``` - -### Conditional Panel Registration - -```php -shouldRegisterPanel()) { - return $panel; - } - - return $panel - ->id('enterprise') - ->path('/enterprise') - ->login() - ->colors(['primary' => '#6366f1']) - ->brandName('Enterprise Dashboard') - ->resources([ - \Modules\Enterprise\Filament\Enterprise\Resources\LicenseResource::class, - \Modules\Enterprise\Filament\Enterprise\Resources\AdvancedAnalyticsResource::class, - ]); - } - - private function shouldRegisterPanel(): bool - { - return config('features.enterprise_enabled', false) && - app(\App\Services\LicenseService::class)->isValid(); - } -} -``` - -## Enterprise Patterns - -### Multi-Tenant Panel Architecture - -```php -id('tenant') - ->path('/tenant') - ->login() - ->tenant(\App\Models\Team::class) - ->tenantRegistration() - ->tenantProfile() - ->colors(['primary' => '#10b981']) - ->discoverResources( - in: module_path('Tenant', 'Filament/Tenant/Resources'), - for: 'Modules\\Tenant\\Filament\\Tenant\\Resources' - ) - ->middleware([ - 'web', - 'auth', - \Stancl\Tenancy\Middleware\InitializeTenancyByDomain::class, - \Stancl\Tenancy\Middleware\PreventAccessFromCentralDomains::class, - ]) - ->tenantMiddleware([ - \Modules\Tenant\Http\Middleware\TenantMiddleware::class, - ]); - } -} -``` - -### API-Driven Panel - -```php -id('api') - ->path('/api-manager') - ->login() - ->colors(['primary' => '#8b5cf6']) - ->brandName('API Manager') - ->resources([ - \Modules\Api\Filament\Api\Resources\ApiKeyResource::class, - \Modules\Api\Filament\Api\Resources\EndpointResource::class, - \Modules\Api\Filament\Api\Resources\RequestLogResource::class, - ]) - ->pages([ - \Modules\Api\Filament\Api\Pages\ApiDocumentation::class, - \Modules\Api\Filament\Api\Pages\RateLimiting::class, - ]) - ->widgets([ - \Modules\Api\Filament\Api\Widgets\ApiUsageStats::class, - \Modules\Api\Filament\Api\Widgets\ResponseTimeChart::class, - ]) - ->middleware([ - 'web', - 'auth', - \Modules\Api\Http\Middleware\ApiManagerMiddleware::class, - ]); - } -} -``` - -## Performance Optimization - -### Optimized Configuration for Large Applications - -```php - [ - 'enabled' => true, - 'ttl' => 0, // Never expires in production - 'file' => base_path('bootstrap/cache/modulite.php'), - 'memory_cache' => [ - 'enabled' => true, - 'max_items' => 500, - ], - ], - - 'performance' => [ - 'lazy_discovery' => true, - 'memory_optimization' => [ - 'batch_size' => 50, - 'clear_stat_cache' => true, - 'gc_after_scan' => true, - ], - ], - - 'panels' => [ - 'scanning' => [ - 'max_depth' => 3, - 'excluded_directories' => [ - 'tests', 'migrations', 'seeders', 'factories', - '.git', 'node_modules', 'vendor', 'storage', - 'bootstrap/cache', 'public', 'database', - ], - ], - ], - - 'error_handling' => [ - 'fail_silently' => true, - 'log_errors' => false, - ], - - 'logging' => [ - 'enabled' => false, - ], -]; -``` - -### Custom Cache Strategy - -```php -prefix . $key); - return $value !== null ? unserialize($value) : $default; - } - - public function put(string $key, mixed $value, ?int $ttl = null): bool - { - $serialized = serialize($value); - if ($ttl) { - return Redis::setex($this->prefix . $key, $ttl, $serialized); - } - return Redis::set($this->prefix . $key, $serialized); - } - - // Implement other interface methods... -} -``` - -Register custom cache service: - -```php -isProduction()) { - $this->app->singleton(CacheManagerInterface::class, ModuliteCacheService::class); - } -} -``` - -## Testing Strategies - -### Unit Testing Panel Discovery - -```php - false]); - } - - public function test_discovers_panels_with_attribute() - { - $scanner = app(PanelScannerService::class); - $panels = $scanner->discoverPanels(); - - $this->assertContains( - 'Modules\\Admin\\Providers\\Filament\\Panels\\AdminPanelProvider', - $panels - ); - } - - public function test_respects_environment_constraints() - { - app()->instance('env', 'production'); - - $scanner = app(PanelScannerService::class); - $panels = $scanner->discoverPanels(); - - // Development-only panels should not be discovered - $this->assertNotContains( - 'Modules\\Debug\\Providers\\Filament\\Panels\\DebugPanelProvider', - $panels - ); - } - - public function test_sorts_panels_by_priority() - { - $scanner = app(PanelScannerService::class); - $panels = $scanner->discoverPanels(); - - // Verify panels are sorted by priority - $adminIndex = array_search('AdminPanelProvider', $panels); - $userIndex = array_search('UserPanelProvider', $panels); - - $this->assertLessThan($userIndex, $adminIndex); - } -} -``` - -### Feature Testing with Multiple Panels - -```php -admin()->create(); - - $response = $this->actingAs($admin)->get('/admin'); - - $response->assertStatus(200); - $response->assertSee('Admin Dashboard'); - } - - public function test_user_panel_requires_proper_permissions() - { - $user = User::factory()->create(); - - $response = $this->actingAs($user)->get('/users'); - - $response->assertStatus(403); - } - - public function test_debug_panel_only_available_in_development() - { - app()->instance('env', 'production'); - - $response = $this->get('/debug'); - - $response->assertStatus(404); - } -} -``` - ---- - -This comprehensive examples guide demonstrates real-world usage patterns for Modulite, from basic setups to enterprise-grade applications. Each example follows Laravel and Filament best practices while leveraging Modulite's powerful auto-discovery features. diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md deleted file mode 100644 index 3315fe6..0000000 --- a/docs/INSTALLATION.md +++ /dev/null @@ -1,771 +0,0 @@ -# ๐Ÿš€ Installation & Setup Guide - -Complete guide to installing and configuring Modulite in your Laravel application. - -## ๐Ÿ“‹ Table of Contents - -- [Requirements](#requirements) -- [Quick Installation](#quick-installation) -- [Step-by-Step Installation](#step-by-step-installation) -- [Configuration Setup](#configuration-setup) -- [Directory Structure](#directory-structure) -- [First Panel Creation](#first-panel-creation) -- [Environment Configuration](#environment-configuration) -- [Framework Integration](#framework-integration) -- [Verification & Testing](#verification--testing) -- [Troubleshooting Installation](#troubleshooting-installation) - -## Requirements - -### System Requirements - -- **PHP**: 8.2 or higher -- **Laravel**: 10.0 or higher -- **Filament**: 4.0 or higher - -### Recommended Packages - -- **nwidart/laravel-modules**: 11.0+ (for modular applications) -- **Laravel Sanctum**: For API authentication (if using API panels) -- **Laravel Telescope**: For development debugging (optional) - -### PHP Extensions - -Required PHP extensions: -- `mbstring` -- `openssl` -- `PDO` -- `Tokenizer` -- `XML` -- `Ctype` -- `JSON` -- `BCMath` - -### Server Requirements - -**Development:** -- Memory: 512MB minimum -- Disk: 100MB free space -- PHP execution time: 60 seconds - -**Production:** -- Memory: 1GB recommended -- Disk: 500MB free space -- OPcache enabled -- Fast storage (SSD recommended) - -## Quick Installation - -For experienced developers who want to get started quickly: - -```bash -# 1. Install Modulite -composer require panicdevs/modulite - -# 2. Publish configuration (optional) -php artisan vendor:publish --tag=modulite-config - -# 3. Create your first panel -php artisan make:filament-panel admin - -# 4. Add the attribute to your panel provider -# Add #[FilamentPanel] to your AdminPanelProvider class - -# 5. Verify installation -php artisan modulite:status -``` - -## Step-by-Step Installation - -### Step 1: Install via Composer - -Add Modulite to your Laravel project: - -```bash -composer require panicdevs/modulite -``` - -**For development/testing environments:** -```bash -composer require panicdevs/modulite --dev -``` - -### Step 2: Verify Package Registration - -Modulite uses Laravel's auto-discovery feature. Verify it's registered: - -```bash -php artisan package:discover -``` - -You should see Modulite in the discovered packages list. - -### Step 3: Publish Configuration (Optional) - -Publish the configuration file to customize Modulite's behavior: - -```bash -php artisan vendor:publish --tag=modulite-config -``` - -This creates `config/modulite.php` with default settings. - -**Available publish tags:** -- `modulite-config` - Configuration file only -- `modulite` - All publishable assets - -### Step 4: Install Filament (if not already installed) - -If you don't have Filament installed: - -```bash -composer require filament/filament -php artisan filament:install --panels -``` - -### Step 5: Install nwidart/laravel-modules (Recommended) - -For modular applications: - -```bash -composer require nwidart/laravel-modules -php artisan vendor:publish --provider="Nwidart\Modules\LaravelModulesServiceProvider" -``` - -Configure modules in `config/modules.php`: - -```php -'paths' => [ - 'modules' => base_path('modules'), - 'assets' => public_path('modules'), - 'migration' => base_path('database/migrations'), - 'generator' => [ - 'config' => ['path' => 'Config', 'generate' => true], - 'command' => ['path' => 'Console', 'generate' => true], - 'migration' => ['path' => 'Database/Migrations', 'generate' => true], - 'seeder' => ['path' => 'Database/Seeders', 'generate' => true], - 'factory' => ['path' => 'Database/Factories', 'generate' => true], - 'model' => ['path' => 'Entities', 'generate' => true], - 'controller' => ['path' => 'Http/Controllers', 'generate' => true], - 'filter' => ['path' => 'Http/Middleware', 'generate' => true], - 'request' => ['path' => 'Http/Requests', 'generate' => true], - 'provider' => ['path' => 'Providers', 'generate' => true], - 'assets' => ['path' => 'Resources/assets', 'generate' => true], - 'lang' => ['path' => 'Resources/lang', 'generate' => true], - 'views' => ['path' => 'Resources/views', 'generate' => true], - 'test' => ['path' => 'Tests', 'generate' => true], - 'repository' => ['path' => 'Repositories', 'generate' => false], - 'event' => ['path' => 'Events', 'generate' => false], - 'listener' => ['path' => 'Listeners', 'generate' => false], - 'policies' => ['path' => 'Policies', 'generate' => false], - 'rules' => ['path' => 'Rules', 'generate' => false], - 'jobs' => ['path' => 'Jobs', 'generate' => false], - 'emails' => ['path' => 'Emails', 'generate' => false], - 'notifications' => ['path' => 'Notifications', 'generate' => false], - 'resource' => ['path' => 'Transformers', 'generate' => false], - ], -], -``` - -## Configuration Setup - -### Basic Configuration - -Edit `config/modulite.php` to match your application structure: - -```php - [ - 'locations' => [ - 'modules/*/Providers/Filament/Panels', // For nwidart modules - 'app/Filament/Panels', // For app-level panels - ], - 'patterns' => [ - 'files' => ['*PanelProvider.php'], - 'classes' => ['*PanelProvider'], - ], - ], - - 'cache' => [ - 'enabled' => env('MODULITE_CACHE_ENABLED', !app()->hasDebugModeEnabled()), - 'ttl' => env('MODULITE_CACHE_TTL', 3600), - ], - - 'performance' => [ - 'lazy_discovery' => env('MODULITE_LAZY_DISCOVERY', true), - ], -]; -``` - -### Environment Configuration - -Add environment variables to your `.env` file: - -```env -# Modulite Configuration -MODULITE_CACHE_ENABLED=true -MODULITE_CACHE_TTL=3600 -MODULITE_LAZY_DISCOVERY=true -MODULITE_LOGGING_ENABLED=false -``` - -### Advanced Configuration - -For complex applications, you might need custom configurations: - -```php -// config/modulite.php -return [ - 'panels' => [ - 'locations' => [ - 'modules/*/Providers/Filament/Panels', - 'app/Domains/*/Infrastructure/Filament/Panels', // DDD structure - 'packages/*/src/Filament/Panels', // Custom packages - ], - 'validation' => [ - 'strict_inheritance' => false, - 'allow_custom_base_classes' => true, - ], - 'scanning' => [ - 'max_depth' => 5, - 'excluded_directories' => [ - 'tests', 'vendor', 'node_modules', '.git', - 'storage', 'bootstrap/cache', 'public', - ], - ], - ], - - 'components' => [ - 'locations' => [ - 'modules/*/Filament/{panel}/Resources', - 'modules/*/Filament/{panel}/Pages', - 'modules/*/Filament/{panel}/Widgets', - ], - 'types' => [ - 'resources' => ['enabled' => true], - 'pages' => ['enabled' => true], - 'widgets' => ['enabled' => true], - ], - ], - - 'performance' => [ - 'memory_optimization' => [ - 'batch_size' => 100, - 'gc_after_scan' => true, - ], - ], -]; -``` - -## Directory Structure - -### Standard nwidart Structure - -Create the following directory structure for your modules: - -``` -modules/ -โ”œโ”€โ”€ Admin/ -โ”‚ โ”œโ”€โ”€ Providers/ -โ”‚ โ”‚ โ””โ”€โ”€ Filament/ -โ”‚ โ”‚ โ””โ”€โ”€ Panels/ -โ”‚ โ”‚ โ””โ”€โ”€ AdminPanelProvider.php -โ”‚ โ””โ”€โ”€ Filament/ -โ”‚ โ””โ”€โ”€ Admin/ -โ”‚ โ”œโ”€โ”€ Resources/ -โ”‚ โ”œโ”€โ”€ Pages/ -โ”‚ โ””โ”€โ”€ Widgets/ -โ”œโ”€โ”€ User/ -โ”‚ โ”œโ”€โ”€ Providers/ -โ”‚ โ”‚ โ””โ”€โ”€ Filament/ -โ”‚ โ”‚ โ””โ”€โ”€ Panels/ -โ”‚ โ”‚ โ””โ”€โ”€ UserPanelProvider.php -โ”‚ โ””โ”€โ”€ Filament/ -โ”‚ โ””โ”€โ”€ User/ -โ”‚ โ”œโ”€โ”€ Resources/ -โ”‚ โ”‚ โ””โ”€โ”€ UserResource.php -โ”‚ โ”œโ”€โ”€ Pages/ -โ”‚ โ””โ”€โ”€ Widgets/ -โ””โ”€โ”€ Blog/ - โ”œโ”€โ”€ Providers/ - โ”‚ โ””โ”€โ”€ Filament/ - โ”‚ โ””โ”€โ”€ Panels/ - โ”‚ โ””โ”€โ”€ BlogPanelProvider.php - โ””โ”€โ”€ Filament/ - โ””โ”€โ”€ Blog/ - โ”œโ”€โ”€ Resources/ - โ”‚ โ””โ”€โ”€ PostResource.php - โ”œโ”€โ”€ Pages/ - โ””โ”€โ”€ Widgets/ -``` - -### Alternative Structures - -**Domain-Driven Design:** -``` -app/ -โ””โ”€โ”€ Domains/ - โ”œโ”€โ”€ User/ - โ”‚ โ””โ”€โ”€ Infrastructure/ - โ”‚ โ””โ”€โ”€ Filament/ - โ”‚ โ””โ”€โ”€ Panels/ - โ”‚ โ””โ”€โ”€ UserPanelProvider.php - โ””โ”€โ”€ Blog/ - โ””โ”€โ”€ Infrastructure/ - โ””โ”€โ”€ Filament/ - โ””โ”€โ”€ Panels/ - โ””โ”€โ”€ BlogPanelProvider.php -``` - -**Foundation Modules:** -``` -foundation/ -โ”œโ”€โ”€ Core/ -โ”‚ โ””โ”€โ”€ Providers/ -โ”‚ โ””โ”€โ”€ Filament/ -โ”‚ โ””โ”€โ”€ Panels/ -โ”‚ โ””โ”€โ”€ CorePanelProvider.php -โ””โ”€โ”€ Auth/ - โ””โ”€โ”€ Providers/ - โ””โ”€โ”€ Filament/ - โ””โ”€โ”€ Panels/ - โ””โ”€โ”€ AuthPanelProvider.php -``` - -### Create Required Directories - -```bash -# For nwidart modules structure -mkdir -p modules/{Admin,User,Blog}/Providers/Filament/Panels -mkdir -p modules/{Admin,User,Blog}/Filament/{Admin,User,Blog}/{Resources,Pages,Widgets} - -# For app-level panels -mkdir -p app/Filament/Panels - -# Ensure proper permissions -chmod -R 755 modules/ -chmod -R 755 app/Filament/ -``` - -## First Panel Creation - -### Step 1: Create a Module (if using nwidart) - -```bash -php artisan module:make Admin -``` - -### Step 2: Create Panel Provider - -```bash -# Using Filament command -php artisan make:filament-panel admin - -# Or create manually -mkdir -p modules/Admin/Providers/Filament/Panels -``` - -### Step 3: Implement Panel Provider - -Create `modules/Admin/Providers/Filament/Panels/AdminPanelProvider.php`: - -```php -default() - ->id('admin') - ->path('/admin') - ->login() - ->colors([ - 'primary' => '#1f2937', - ]) - ->discoverResources( - in: module_path('Admin', 'Filament/Admin/Resources'), - for: 'Modules\\Admin\\Filament\\Admin\\Resources' - ) - ->discoverPages( - in: module_path('Admin', 'Filament/Admin/Pages'), - for: 'Modules\\Admin\\Filament\\Admin\\Pages' - ) - ->pages([ - 'dashboard' => \Modules\Admin\Filament\Admin\Pages\Dashboard::class, - ]) - ->widgets([ - Widgets\AccountWidget::class, - Widgets\FilamentInfoWidget::class, - ]) - ->middleware([ - 'web', - 'auth', - ]) - ->authMiddleware([ - 'auth', - ]); - } -} -``` - -### Step 4: Create Dashboard Page - -Create `modules/Admin/Filament/Admin/Pages/Dashboard.php`: - -```php - [ - 'modulite.cache', -], - -'flush' => [ - 'modulite.cache', -], -``` - -### Laravel Horizon - -If using Laravel Horizon, ensure queue workers are restarted after Modulite updates: - -```php -// app/Providers/HorizonServiceProvider.php -use PanicDevs\Modulite\Events\ModuliteUpdated; - -protected function gate() -{ - Gate::define('viewHorizon', function ($user) { - return in_array($user->email, [ - 'admin@example.com', - ]); - }); - - // Restart workers when Modulite cache changes - Event::listen(ModuliteUpdated::class, function () { - Artisan::call('horizon:terminate'); - }); -} -``` - -### Laravel Telescope - -Configure Telescope to monitor Modulite operations: - -```php -// config/telescope.php -'watchers' => [ - TelescopeWatchers\CacheWatcher::class => [ - 'enabled' => env('TELESCOPE_CACHE_WATCHER', true), - 'hidden' => [ - 'modulite:*', // Hide Modulite cache operations if too verbose - ], - ], -], -``` - - - -## Verification & Testing - -### Step 1: Check Modulite Status - -```bash -php artisan modulite:status -``` - -Expected output: -``` -Modulite Status Report -=================== - -Configuration: -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Setting โ”‚ Value โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ Cache Enabled โ”‚ โœ“ โ”‚ -โ”‚ Lazy Discovery โ”‚ โœ“ โ”‚ -โ”‚ Logging Enabled โ”‚ โœ— โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - -Panel Discovery: -Discovered 1 panels: - โ€ข AdminPanelProvider -``` - -### Step 2: Test Panel Access - -Visit your panel in the browser: - -``` -http://your-app.test/admin -``` - -You should see the Filament login page. - -### Step 3: Run Benchmark - -Test performance: - -```bash -php artisan modulite:benchmark --iterations=50 -``` - -### Step 4: Verify Cache Operations - -```bash -# Clear cache -php artisan modulite:clear-cache - -# Check cache file -ls -la bootstrap/cache/modulite.php - -# Force discovery -php artisan modulite:discover-panels --verbose -``` - -### Step 5: Test in Different Environments - -```bash -# Test with cache disabled -APP_ENV=local MODULITE_CACHE_ENABLED=false php artisan modulite:status - -# Test with cache enabled -APP_ENV=production MODULITE_CACHE_ENABLED=true php artisan modulite:status -``` - -## Troubleshooting Installation - -### Common Installation Issues - -#### 1. Composer Install Fails - -**Error:** "Could not find package panicdevs/modulite" - -**Solution:** -```bash -# Clear composer cache -composer clear-cache - -# Update composer -composer self-update - -# Try installing again -composer require panicdevs/modulite -``` - -#### 2. File Permissions - -**Error:** "Permission denied" when accessing cache - -**Solution:** -```bash -# Fix permissions -sudo chown -R $USER:www-data bootstrap/cache -chmod -R 775 bootstrap/cache - -# For production -sudo chown -R www-data:www-data bootstrap/cache -chmod -R 755 bootstrap/cache -``` - -#### 3. Namespace Issues - -**Error:** "Class not found" for panel providers - -**Solution:** -```bash -# Regenerate autoload -composer dump-autoload - -# Check PSR-4 autoloading in composer.json -{ - "autoload": { - "psr-4": { - "App\\": "app/", - "Modules\\": "modules/" - } - } -} -``` - -#### 4. Configuration Cache Issues - -**Error:** Configuration not loading correctly - -**Solution:** -```bash -# Clear configuration cache -php artisan config:clear - -# Republish configuration -php artisan vendor:publish --tag=modulite-config --force - -# Recache configuration -php artisan config:cache -``` - -### Validation Commands - -Run these commands to validate your installation: - -```bash -# 1. Check PHP requirements -php -v -php -m | grep -E "(mbstring|openssl|pdo|tokenizer|xml)" - -# 2. Check Laravel version -php artisan --version - -# 3. Check Filament installation -php artisan list | grep filament - -# 4. Check nwidart modules (if using) -php artisan module:list - -# 5. Verify Modulite registration -php artisan package:discover | grep modulite - -# 6. Test Modulite functionality -php artisan modulite:status --vvv -``` - -### Performance Validation - -```bash -# Test discovery performance -time php artisan modulite:discover-panels - -# Benchmark with cache -php artisan modulite:benchmark --warm-cache --iterations=100 - -# Check memory usage -php -d memory_limit=256M artisan modulite:status -``` - -### Getting Help - -If you encounter issues: - -1. **Check the logs:** - ```bash - tail -f storage/logs/laravel.log - ``` - -2. **Enable debug mode:** - ```env - APP_DEBUG=true - MODULITE_LOGGING_ENABLED=true - MODULITE_LOG_LEVEL=debug - ``` - -3. **Run diagnostics:** - ```bash - php artisan modulite:status --vvv - php artisan about - ``` - -4. **Create a GitHub issue** with: - - Laravel version - - PHP version - - Modulite configuration - - Error messages - - Steps to reproduce - ---- - -Congratulations! You've successfully installed and configured Modulite. Next, explore the [Configuration Guide](CONFIGURATION.md) for advanced setup options or check the [Examples](EXAMPLES.md) for real-world usage patterns. diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index e458d8e..0000000 --- a/docs/README.md +++ /dev/null @@ -1,306 +0,0 @@ -# ๐Ÿ“š Modulite Documentation - -Welcome to the comprehensive documentation for Modulite - the automatic Filament Panel Provider discovery package for modular Laravel applications. - -## ๐Ÿš€ Quick Links - -- **[Main README](../README.md)** - Overview and quick start -- **[Installation Guide](INSTALLATION.md)** - Complete setup instructions -- **[Configuration Guide](CONFIGURATION.md)** - All configuration options -- **[Usage Examples](EXAMPLES.md)** - Real-world examples and patterns -- **[API Reference](API_REFERENCE.md)** - Complete API documentation -- **[Troubleshooting](TROUBLESHOOTING.md)** - Common issues and solutions -- **[Contributing](../CONTRIBUTING.md)** - How to contribute -- **[Changelog](../CHANGELOG.md)** - Version history - -## ๐Ÿ“– Documentation Overview - -### Getting Started - -1. **[Installation Guide](INSTALLATION.md)** - - System requirements - - Step-by-step installation - - Environment configuration - - First panel creation - - Verification and testing - -2. **[Configuration Guide](CONFIGURATION.md)** - - Panel discovery configuration - - Component discovery settings - - Cache configuration - - Performance optimization - - Environment-specific settings - -### Usage & Examples - -3. **[Usage Examples](EXAMPLES.md)** - - Basic panel setup - - Multi-panel applications - - Component organization - - Advanced configurations - - Enterprise patterns - - Performance optimization - - Testing strategies - - Deployment examples - -### Reference - -4. **[API Reference](API_REFERENCE.md)** - - Attributes (`#[FilamentPanel]`) - - Interfaces (`CacheManagerInterface`, `PanelScannerInterface`) - - Services (`UnifiedCacheManager`, `PanelScannerService`) - - Console commands - - Exceptions - - Configuration schema - - Helper functions - -### Support - -5. **[Troubleshooting](TROUBLESHOOTING.md)** - - Quick diagnostic commands - - Common issues and solutions - - Cache troubleshooting - - Panel discovery issues - - Performance problems - - Environment-specific issues - - Advanced debugging - - FAQ - -## ๐ŸŽฏ Documentation by Use Case - -### I'm New to Modulite -Start with the [Main README](../README.md) for an overview, then follow the [Installation Guide](INSTALLATION.md). - -### I Want to Set Up My First Panel -1. [Installation Guide](INSTALLATION.md) - Install and configure -2. [Usage Examples](EXAMPLES.md#basic-panel-setup) - Create your first panel -3. [Troubleshooting](TROUBLESHOOTING.md) - If you run into issues - -### I Need to Configure for Production -1. [Configuration Guide](CONFIGURATION.md#production-optimizations) - Production settings -2. [Usage Examples](EXAMPLES.md#performance-optimization) - Performance patterns -3. [Installation Guide](INSTALLATION.md#environment-configuration) - Environment setup - -### I'm Building a Multi-Panel Application -1. [Usage Examples](EXAMPLES.md#multi-panel-applications) - Multi-panel patterns -2. [Configuration Guide](CONFIGURATION.md#panel-discovery-configuration) - Panel configuration -3. [API Reference](API_REFERENCE.md#filamentpanel) - Attribute options - -### I'm Working with Components -1. [Configuration Guide](CONFIGURATION.md#component-discovery-configuration) - Component setup -2. [Usage Examples](EXAMPLES.md#component-organization) - Organization patterns -3. [API Reference](API_REFERENCE.md#componentdiscoveryservice) - Component API - -### I Need to Debug Issues -1. [Troubleshooting](TROUBLESHOOTING.md) - Common solutions -2. [API Reference](API_REFERENCE.md#console-commands) - Debug commands -3. [Configuration Guide](CONFIGURATION.md#logging--debugging) - Logging setup - -### I Want to Contribute -1. [Contributing Guide](../CONTRIBUTING.md) - How to contribute -2. [API Reference](API_REFERENCE.md) - Code structure -3. [Changelog](../CHANGELOG.md) - Recent changes - -## ๐Ÿ—๏ธ Architecture Overview - -### Core Components - -```mermaid -graph TD - A[ModuliteServiceProvider] --> B[PanelScannerService] - A --> C[ComponentDiscoveryService] - A --> D[UnifiedCacheManager] - - B --> E[FilamentPanel Attribute] - C --> F[Component Discovery] - D --> G[File-based Cache] - - E --> H[Panel Registration] - F --> I[Component Registration] - G --> J[Performance Optimization] -``` - -### Key Features - -- **๐Ÿ” Auto-Discovery**: Automatically finds and registers Filament panels -- **โšก Performance**: Multi-layer caching with production optimizations -- **๐Ÿ—๏ธ Modular**: Built for nwidart/laravel-modules and modular applications -- **๐ŸŽฏ Attribute-Based**: Clean registration with `#[FilamentPanel]` attribute -- **๐Ÿ“Š Component Discovery**: Auto-discover Resources, Pages, and Widgets -- **๐Ÿ›ก๏ธ Production Ready**: Robust error handling and enterprise features - -### Integration Points - -- **Laravel Service Container**: Proper dependency injection -- **Filament Panel System**: Seamless panel registration -- **nwidart/laravel-modules**: Module system integration -- **Laravel Cache**: Intelligent caching strategies -- **Artisan Commands**: Development and debugging tools - -## ๐Ÿ“ Code Examples - -### Basic Usage - -```php -default() - ->id('admin') - ->path('/admin') - ->login(); - } -} -``` - -### Advanced Configuration - -```php -#[FilamentPanel( - priority: 100, - environment: 'production', - autoRegister: true -)] -class ProductionPanelProvider extends PanelProvider -{ - // Panel implementation -} -``` - -### Service Usage - -```php -use PanicDevs\Modulite\Contracts\PanelScannerInterface; - -$scanner = app(PanelScannerInterface::class); -$panels = $scanner->discoverPanels(); - -foreach ($panels as $panelClass) { - app()->register($panelClass); -} -``` - -## ๐Ÿ”ง Development Tools - -### Console Commands - -```bash -# Check Modulite status -php artisan modulite:status - -# Clear cache -php artisan modulite:clear-cache - -# Benchmark performance -php artisan modulite:benchmark - -# Discover panels manually -php artisan modulite:discover-panels --verbose - -# Discover components -php artisan modulite:discover-components admin -``` - -### Configuration - -```php -// config/modulite.php -return [ - 'panels' => [ - 'locations' => ['modules/*/Providers/Filament/Panels'], - 'patterns' => ['*PanelProvider.php'], - ], - 'cache' => [ - 'enabled' => true, - 'ttl' => 0, // Never expires in production - ], - 'performance' => [ - 'lazy_discovery' => true, - ], -]; -``` - -## ๐ŸŒ Environment Support - -### Development -- Auto-invalidation on file changes -- Detailed logging and debugging -- Hot reloading support - -### Staging -- Balanced performance and debugging -- Configurable cache TTL -- Error logging without breaking execution - -### Production -- Maximum performance optimization -- Persistent caching (TTL=0) -- Silent error handling -- Minimal logging overhead - -## ๐Ÿ“Š Performance - -### Benchmarks (Production Environment) - -**Cached Operations:** -- **Cache Read**: 0ms average -- **Panel Discovery**: 0ms average (fully cached) -- **Component Discovery**: 0ms average - -**Cache Simulation (500 iterations):** -- **Panel Discovery**: 1.054ms average -- **Performance**: Sub-millisecond for all cached operations - -### Optimization Features - -- **Lazy Discovery**: Defer scanning until needed -- **Multi-layer Caching**: File + memory caching -- **Memory Management**: Batch processing for large codebases -- **OPcache Compatible**: Works with PHP OPcache -- **Production Optimized**: Zero-overhead in production - -## ๐Ÿค Community & Support - -### Getting Help - -- **๐Ÿ› Bug Reports**: [GitHub Issues](https://github.com/panicdevs/modulite/issues) -- **๐Ÿ’ฌ Discussions**: [GitHub Discussions](https://github.com/panicdevs/modulite/discussions) -- **๐Ÿ“ง Email**: [support@panicdevs.agency](mailto:support@panicdevs.agency) -- **๐Ÿ’ฐ Sponsor**: [GitHub Sponsors](https://github.com/sponsors/panicdevs) - -### Contributing - -We welcome contributions! See our [Contributing Guide](../CONTRIBUTING.md) for: - -- Code of conduct -- Development setup -- Pull request process -- Issue guidelines -- Testing requirements - -### Recognition - -- All contributors are recognized in our [README](../README.md) -- Major contributors highlighted in release notes -- Community contributors featured in documentation - -## ๐Ÿ“œ License - -Modulite is open-sourced software licensed under the [MIT license](../LICENSE). - ---- - -**Built with โค๏ธ by [PanicDevs](https://panicdevs.agency)** - -For questions about this documentation, please [open an issue](https://github.com/panicdevs/modulite/issues) or contact us at [docs@panicdevs.agency](mailto:docs@panicdevs.agency). diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md deleted file mode 100644 index 72a0176..0000000 --- a/docs/TROUBLESHOOTING.md +++ /dev/null @@ -1,804 +0,0 @@ -# ๐Ÿ”ง Troubleshooting Guide - -This comprehensive troubleshooting guide will help you resolve common issues with Modulite and optimize its performance. - -## ๐Ÿ“‹ Table of Contents - -- [Quick Diagnostic Commands](#quick-diagnostic-commands) -- [Common Issues](#common-issues) -- [Cache Issues](#cache-issues) -- [Panel Discovery Issues](#panel-discovery-issues) -- [Component Discovery Issues](#component-discovery-issues) -- [Performance Issues](#performance-issues) -- [Memory Issues](#memory-issues) -- [Environment-Specific Issues](#environment-specific-issues) -- [Advanced Debugging](#advanced-debugging) -- [FAQ](#frequently-asked-questions) - -## Quick Diagnostic Commands - -Before diving into specific issues, run these commands to get an overview: - -```bash -# Check Modulite status -php artisan modulite:status --vvv - -# Clear and rebuild cache -php artisan modulite:clear-cache -php artisan optimize - -# Test performance -php artisan modulite:benchmark - -# Check for panels manually -php artisan modulite:discover-panels --verbose - -# Check Laravel optimization -php artisan optimize:clear -php artisan config:cache -``` - -## Common Issues - -### โŒ Panels Not Being Discovered - -**Symptoms:** -- Filament panels don't appear -- Getting "Panel not found" errors -- `php artisan modulite:status` shows 0 panels - -**Diagnostic Steps:** - -1. **Check file location and naming:** - ```bash - # Verify your panel files are in the configured locations - find modules/ -name "*PanelProvider.php" -type f - find foundation/ -name "*PanelProvider.php" -type f - ``` - -2. **Verify the attribute is present:** - ```php - // โœ… Correct - #[FilamentPanel] - class AdminPanelProvider extends PanelProvider - - // โŒ Missing attribute - class AdminPanelProvider extends PanelProvider - ``` - -3. **Check namespace and class name:** - ```php - // File: modules/User/Providers/Filament/Panels/AdminPanelProvider.php - namespace Modules\User\Providers\Filament\Panels; // โœ… Correct namespace - - class AdminPanelProvider extends PanelProvider // โœ… Correct class name - ``` - -4. **Verify configuration paths:** - ```bash - php artisan config:show modulite.panels.locations - ``` - -**Solutions:** - -1. **Fix file structure:** - ```bash - # Expected structure - modules/ - โ””โ”€โ”€ YourModule/ - โ””โ”€โ”€ Providers/ - โ””โ”€โ”€ Filament/ - โ””โ”€โ”€ Panels/ - โ””โ”€โ”€ YourPanelProvider.php - ``` - -2. **Add missing attribute:** - ```php - use PanicDevs\Modulite\Attributes\FilamentPanel; - - #[FilamentPanel] - class YourPanelProvider extends PanelProvider - ``` - -3. **Update configuration if using custom paths:** - ```php - // config/modulite.php - 'panels' => [ - 'locations' => [ - 'your/custom/path/*/Panels', // Add your custom path - ], - ], - ``` - -### โŒ Filament Panel Provider Not Registered - -**Symptoms:** -- Panel discovered but not working -- Routes not registered -- Panel configuration not applied - -**Diagnostic Steps:** - -1. **Check if panel class is valid:** - ```bash - php artisan tinker - >>> class_exists('Modules\Admin\Providers\Filament\Panels\AdminPanelProvider') - >>> (new ReflectionClass('Modules\Admin\Providers\Filament\Panels\AdminPanelProvider'))->isInstantiable() - ``` - -2. **Verify panel method exists:** - ```php - public function panel(Panel $panel): Panel - { - return $panel->id('admin'); // Must return configured panel - } - ``` - -**Solutions:** - -1. **Ensure proper inheritance:** - ```php - use Filament\PanelProvider; - - class AdminPanelProvider extends PanelProvider // โœ… - ``` - -2. **Implement required methods:** - ```php - public function panel(Panel $panel): Panel - { - return $panel - ->id('admin') - ->path('/admin'); - } - ``` - -### โŒ Multiple Panel Registration - -**Symptoms:** -- Same panel registered multiple times -- Conflicting panel configurations -- Routes collision errors - -**Solutions:** - -1. **Check for duplicate files:** - ```bash - find . -name "*AdminPanel*" -type f - ``` - -2. **Ensure unique panel IDs:** - ```php - // Each panel must have unique ID - return $panel->id('admin'); // โœ… - return $panel->id('manager'); // โœ… - return $panel->id('admin'); // โŒ Duplicate - ``` - -3. **Use priority to control registration order:** - ```php - #[FilamentPanel(priority: 100)] - class MainPanelProvider extends PanelProvider - - #[FilamentPanel(priority: 50)] - class SecondaryPanelProvider extends PanelProvider - ``` - -## Cache Issues - -### โŒ Cache Not Working - -**Symptoms:** -- Slow panel loading -- Changes not reflected immediately -- `modulite:status` shows cache disabled - -**Diagnostic Steps:** - -1. **Check cache configuration:** - ```bash - php artisan config:show modulite.cache - ``` - -2. **Verify cache file permissions:** - ```bash - ls -la bootstrap/cache/modulite.php - ``` - -3. **Check cache directory writability:** - ```bash - touch bootstrap/cache/test.txt && rm bootstrap/cache/test.txt - ``` - -**Solutions:** - -1. **Enable cache:** - ```env - MODULITE_CACHE_ENABLED=true - ``` - -2. **Fix permissions:** - ```bash - chmod 755 bootstrap/cache - chown -R www-data:www-data bootstrap/cache # For production - ``` - -3. **Recreate cache directory:** - ```bash - mkdir -p bootstrap/cache - chmod 755 bootstrap/cache - ``` - -### โŒ Stale Cache Issues - -**Symptoms:** -- Changes to panels not reflected -- Old panel configurations still active -- New panels not discovered - -**Solutions:** - -1. **Clear Modulite cache:** - ```bash - php artisan modulite:clear-cache - ``` - -2. **Clear all Laravel caches:** - ```bash - php artisan optimize:clear - ``` - -3. **Enable auto-invalidation in development:** - ```env - # .env.local - MODULITE_CACHE_ENABLED=false - # or - MODULITE_CACHE_TTL=300 # 5 minutes - ``` - -### โŒ Cache File Corruption - -**Symptoms:** -- Syntax errors in cache file -- PHP parse errors -- Cache loading failures - -**Solutions:** - -1. **Delete corrupted cache file:** - ```bash - rm bootstrap/cache/modulite.php - ``` - -2. **Rebuild cache:** - ```bash - php artisan optimize - php artisan modulite:status - ``` - -3. **Check for file system issues:** - ```bash - # Check disk space - df -h - - # Check for file system errors - dmesg | grep -i error - ``` - -## Panel Discovery Issues - -### โŒ Scanning Timeout - -**Symptoms:** -- Discovery process takes too long -- Memory exhaustion during scanning -- Timeout errors - -**Solutions:** - -1. **Reduce scan depth:** - ```php - 'scanning' => [ - 'max_depth' => 3, // Reduce from default 5 - ] - ``` - -2. **Add more excluded directories:** - ```php - 'excluded_directories' => [ - 'tests', - 'vendor', - 'node_modules', - 'storage', - 'public', - 'database/migrations', - '.git', - '.idea', - '.vscode', - ] - ``` - -3. **Enable memory optimization:** - ```php - 'performance' => [ - 'memory_optimization' => [ - 'batch_size' => 50, - 'gc_after_scan' => true, - ], - ] - ``` - -### โŒ Permission Denied Errors - -**Symptoms:** -- Cannot read module directories -- File access denied errors -- Scanner fails silently - -**Solutions:** - -1. **Fix directory permissions:** - ```bash - find modules/ -type d -exec chmod 755 {} \; - find modules/ -type f -exec chmod 644 {} \; - ``` - -2. **Check SELinux (if applicable):** - ```bash - setsebool -P httpd_can_network_connect 1 - ``` - -3. **Verify ownership:** - ```bash - chown -R www-data:www-data modules/ - ``` - -## Component Discovery Issues - -### โŒ Components Not Found - -**Symptoms:** -- Resources/Pages/Widgets not auto-registered -- Empty component discovery results - -**Diagnostic Steps:** - -1. **Check component file structure:** - ```bash - find modules/ -path "*/Filament/*/Resources/*.php" -type f - find modules/ -path "*/Filament/*/Pages/*.php" -type f - find modules/ -path "*/Filament/*/Widgets/*.php" -type f - ``` - -2. **Verify naming patterns:** - ```bash - # Should match *Resource.php, *Page.php, *Widget.php - ls modules/*/Filament/*/Resources/ - ``` - -**Solutions:** - -1. **Fix directory structure:** - ``` - modules/ - โ””โ”€โ”€ User/ - โ””โ”€โ”€ Filament/ - โ””โ”€โ”€ Admin/ # Panel name - โ”œโ”€โ”€ Resources/ - โ”‚ โ””โ”€โ”€ UserResource.php - โ”œโ”€โ”€ Pages/ - โ”‚ โ””โ”€โ”€ DashboardPage.php - โ””โ”€โ”€ Widgets/ - โ””โ”€โ”€ StatsWidget.php - ``` - -2. **Ensure proper inheritance:** - ```php - use Filament\Resources\Resource; - - class UserResource extends Resource // โœ… - ``` - -3. **Enable component types:** - ```php - 'components' => [ - 'types' => [ - 'resources' => ['enabled' => true], - 'pages' => ['enabled' => true], - 'widgets' => ['enabled' => true], - ], - ] - ``` - -### โŒ Components Not Registered to Panel - -**Symptoms:** -- Components discovered but not appearing in panel -- Panel navigation empty - -**Solutions:** - -1. **Verify panel configuration:** - ```php - public function panel(Panel $panel): Panel - { - return $panel - ->discoverResources(/* ... */) // Enable resource discovery - ->discoverPages(/* ... */) // Enable page discovery - ->discoverWidgets(/* ... */); // Enable widget discovery - } - ``` - -2. **Check component registration:** - ```php - // config/modulite.php - 'components' => [ - 'registration' => [ - 'auto_register' => true, // Enable auto-registration - ], - ] - ``` - -## Performance Issues - -### โŒ Slow Application Boot - -**Symptoms:** -- Application takes long to start -- High response times on first request -- Memory usage spikes - -**Solutions:** - -1. **Enable lazy discovery:** - ```env - MODULITE_LAZY_DISCOVERY=true - ``` - -2. **Optimize cache settings:** - ```env - MODULITE_CACHE_ENABLED=true - MODULITE_CACHE_TTL=0 - ``` - -3. **Use Laravel optimizations:** - ```bash - php artisan config:cache - php artisan route:cache - php artisan view:cache - ``` - -### โŒ High Memory Usage - -**Symptoms:** -- Memory limit exceeded errors -- Slow garbage collection -- System becomes unresponsive - -**Solutions:** - -1. **Enable memory optimization:** - ```php - 'performance' => [ - 'memory_optimization' => [ - 'batch_size' => 25, // Smaller batches - 'clear_stat_cache' => true, - 'gc_after_scan' => true, - ], - ] - ``` - -2. **Increase PHP memory limit temporarily:** - ```bash - php -d memory_limit=512M artisan modulite:status - ``` - -3. **Optimize scanning scope:** - ```php - 'scanning' => [ - 'max_depth' => 2, - 'excluded_directories' => [ - 'tests', 'vendor', 'node_modules', 'storage' - ], - ] - ``` - -### โŒ Slow Cache Operations - -**Symptoms:** -- Cache writes are slow -- File I/O bottlenecks -- High disk usage - -**Solutions:** - -1. **Use faster storage:** - ```php - // Move cache to RAM disk (Linux) - 'cache' => [ - 'file' => '/tmp/modulite.php', - ] - ``` - -2. **Optimize cache content:** - ```php - 'cache' => [ - 'memory_cache' => [ - 'enabled' => true, - 'max_items' => 500, // Reduce if memory constrained - ], - ] - ``` - -## Memory Issues - -### โŒ Memory Limit Exceeded - -**Error Message:** -``` -PHP Fatal error: Allowed memory size exhausted -``` - -**Solutions:** - -1. **Increase PHP memory limit:** - ```ini - ; php.ini - memory_limit = 512M - ``` - -2. **Optimize batch processing:** - ```php - 'performance' => [ - 'memory_optimization' => [ - 'batch_size' => 10, // Very small batches - 'gc_after_scan' => true, - ], - ] - ``` - -3. **Reduce scanning scope:** - ```php - 'panels' => [ - 'scanning' => [ - 'max_depth' => 1, // Only scan immediate subdirectories - ], - ] - ``` - -### โŒ Memory Leaks - -**Symptoms:** -- Memory usage grows over time -- Long-running processes consume increasing memory -- Eventual out-of-memory errors - -**Solutions:** - -1. **Enable garbage collection:** - ```php - 'performance' => [ - 'memory_optimization' => [ - 'gc_after_scan' => true, - 'clear_stat_cache' => true, - ], - ] - ``` - -2. **Use production-optimized settings:** - ```php - 'cache' => [ - 'memory_cache' => [ - 'max_items' => 100, // Limit in-memory cache - ], - ] - ``` - -## Environment-Specific Issues - -### ๐Ÿณ Docker/Sail Issues - -**Common Problems:** - -1. **File permission issues:** - ```bash - # In docker-compose.yml, ensure proper user mapping - user: "${WWWUSER:-1000}:${WWWGROUP:-1000}" - ``` - -2. **Volume mount performance:** - ```yaml - # Use delegated mounts for better performance on macOS - volumes: - - '.:/var/www/html:delegated' - ``` - -3. **Cache directory permissions:** - ```bash - docker exec -it laravel.test chmod -R 755 bootstrap/cache - ``` - -### โ˜๏ธ Production Server Issues - -**Common Problems:** - -1. **File ownership:** - ```bash - chown -R www-data:www-data /var/www/html - chmod -R 755 /var/www/html/bootstrap/cache - ``` - -2. **SELinux context:** - ```bash - setsebool -P httpd_exec_mem 1 - setsebool -P httpd_can_network_connect 1 - ``` - -3. **PHP configuration:** - ```ini - ; Production php.ini optimizations - opcache.enable=1 - opcache.memory_consumption=128 - opcache.max_accelerated_files=4000 - ``` - -### ๐Ÿงช Testing Environment Issues - -**Common Problems:** - -1. **Test isolation:** - ```php - // In test setup - protected function setUp(): void - { - parent::setUp(); - $this->app['config']->set('modulite.cache.enabled', false); - } - ``` - -2. **Test database:** - ```php - 'cache' => [ - 'file' => storage_path('framework/testing/modulite.php'), - ] - ``` - -## Advanced Debugging - -### Enable Debug Logging - -1. **Full debug configuration:** - ```env - MODULITE_LOGGING_ENABLED=true - MODULITE_LOG_LEVEL=debug - MODULITE_LOG_CHANNEL=stack - ``` - -2. **Custom log channel:** - ```php - // config/logging.php - 'channels' => [ - 'modulite' => [ - 'driver' => 'daily', - 'path' => storage_path('logs/modulite.log'), - 'level' => 'debug', - ], - ], - ``` - -3. **Check logs:** - ```bash - tail -f storage/logs/modulite.log - ``` - -### Profiling Performance - -1. **Use Xdebug profiler:** - ```ini - xdebug.mode=profile - xdebug.output_dir=/tmp/xdebug - ``` - -2. **Benchmark specific operations:** - ```bash - php artisan modulite:benchmark --iterations=10 --show-details - ``` - -3. **Memory profiling:** - ```php - // Add to panel discovery code for debugging - echo "Memory usage: " . memory_get_peak_usage(true) / 1024 / 1024 . "MB\n"; - ``` - -### Using Laravel Telescope - -1. **Install and configure Telescope:** - ```bash - composer require laravel/telescope - php artisan telescope:install - ``` - -2. **Monitor Modulite operations:** - - Cache operations - - Service provider registration - - File system operations - -## Frequently Asked Questions - -### Q: Why are my panels not being discovered? - -**A:** Check these common issues: -1. Missing `#[FilamentPanel]` attribute -2. File not in configured scan locations -3. Class naming doesn't match patterns -4. Cache is stale - clear with `php artisan modulite:clear-cache` - -### Q: How can I improve discovery performance? - -**A:** Try these optimizations: -1. Enable caching: `MODULITE_CACHE_ENABLED=true` -2. Use lazy discovery: `MODULITE_LAZY_DISCOVERY=true` -3. Reduce scan depth and exclude unnecessary directories -4. Use `php artisan optimize` for Laravel optimizations - -### Q: Can I use custom base classes for panels? - -**A:** Yes, set these configurations: -```php -'validation' => [ - 'strict_inheritance' => false, - 'allow_custom_base_classes' => true, -] -``` - -### Q: How do I debug discovery issues? - -**A:** Use these debugging tools: -1. `php artisan modulite:status --vvv` -2. Enable debug logging: `MODULITE_LOGGING_ENABLED=true` -3. Check specific locations: `php artisan modulite:discover-panels --verbose` - -### Q: Is Modulite compatible with Laravel Octane? - -**A:** Yes, but consider these points: -1. Enable static caching for better performance -2. Use memory optimization settings -3. Test thoroughly as worker processes persist - -### Q: How do I handle multi-tenant applications? - -**A:** For multi-tenancy: -1. Use environment-based panel registration -2. Implement custom conditions in `#[FilamentPanel]` -3. Consider tenant-specific cache keys - -### Q: Can I exclude specific modules from discovery? - -**A:** Yes, configure module exclusions: -```php -'modules' => [ - 'excluded_modules' => ['TestModule', 'DevModule'], -] -``` - -### Q: What's the impact on application performance? - -**A:** With proper configuration: -- **Development**: Minimal impact with caching disabled -- **Production**: Near-zero impact with persistent caching -- **Benchmark**: Use `php artisan modulite:benchmark` to measure - -### Q: How do I migrate from manual panel registration? - -**A:** Follow these steps: -1. Add `#[FilamentPanel]` attributes to existing providers -2. Remove manual registration from service providers -3. Test thoroughly with `php artisan modulite:status` -4. Clear caches and verify functionality - ---- - -For additional help, please: -1. Check the [Configuration Guide](CONFIGURATION.md) -2. Review the [API Reference](API_REFERENCE.md) -3. Open an issue on [GitHub](https://github.com/panicdevs/modulite/issues)