mirror of
https://github.com/torrentpier/torrentpier
synced 2025-08-21 22:03:49 -07:00
feat: implement Laravel-style CLI commands and enhance console architecture
- Introduced a new command structure for the TorrentPier CLI, named 'Dexter', following Laravel conventions. - Added base command class and several commands including ClearCacheCommand, InfoCommand, and MigrateCommand. - Integrated Symfony Console components for improved command handling and user interaction. - Updated console bootstrap process to register commands dynamically from the application container. - Removed legacy command structure to streamline CLI functionality and improve maintainability. This update modernizes the command-line interface, aligning it with Laravel practices while enhancing usability and extensibility.
This commit is contained in:
parent
3848c6f2c0
commit
314ceacbe7
13 changed files with 887 additions and 198 deletions
266
CLAUDE.md
266
CLAUDE.md
|
@ -10,38 +10,69 @@ TorrentPier is a BitTorrent tracker engine written in PHP, designed for hosting
|
|||
|
||||
- **PHP 8.3+** with modern features
|
||||
- **MySQL/MariaDB/Percona** database
|
||||
- **Nette Database** with temporary backward-compatible wrapper
|
||||
- **Nette Database** for data access (primary)
|
||||
- **Illuminate Database** for Eloquent ORM (optional)
|
||||
- **Illuminate Container** for dependency injection
|
||||
- **Illuminate Support** for collections and helpers
|
||||
- **Illuminate Collections** for enhanced collection handling
|
||||
- **Illuminate Events** for event-driven architecture
|
||||
- **Illuminate Routing** for Laravel-style routing
|
||||
- **Illuminate Validation** for request validation
|
||||
- **Illuminate HTTP** for request/response handling
|
||||
- **Composer** for dependency management
|
||||
- **Custom BitTorrent tracker** implementation
|
||||
- **Laravel-style MVC Architecture** with familiar patterns
|
||||
|
||||
## Key Directory Structure
|
||||
|
||||
- `/src/` - Modern PHP classes (PSR-4 autoloaded as `TorrentPier\`)
|
||||
- `/library/` - Core application logic and legacy code
|
||||
- `/admin/` - Administrative interface
|
||||
### Laravel-style Structure
|
||||
- `/app/` - Main application directory (PSR-4 autoloaded as `App\`)
|
||||
- `/Console/Commands/` - Artisan-style CLI commands for Dexter
|
||||
- `/Http/Controllers/` - Web, API, and Admin controllers
|
||||
- `/Http/Middleware/` - HTTP middleware
|
||||
- `/Http/Routing/` - Routing components (uses Illuminate Routing)
|
||||
- `/Models/` - Data models using Nette Database
|
||||
- `/Services/` - Business logic services
|
||||
- `/Providers/` - Service providers
|
||||
- `/Container/` - Container wrapper and extensions
|
||||
- `/Support/` - Helper classes and utilities
|
||||
- `/bootstrap/` - Application bootstrap files (app.php, container.php)
|
||||
- `/config/` - Laravel-style configuration files (app.php, database.php, etc.)
|
||||
- `/database/` - Migrations, seeders, factories
|
||||
- `/public/` - Web root with front controller (index.php)
|
||||
- `/resources/` - Views, language files, assets
|
||||
- `/routes/` - Route definitions (web.php, api.php, admin.php)
|
||||
- `/storage/` - Application storage (app/, framework/, logs/)
|
||||
- `dexter` - CLI interface
|
||||
|
||||
### Core Utilities & Legacy
|
||||
- `/src/` - Core utilities and services (PSR-4 autoloaded as `TorrentPier\`)
|
||||
- `/library/` - Legacy core application logic
|
||||
- `/controllers/` - Legacy PHP controllers (being migrated)
|
||||
- `/admin/` - Legacy administrative interface
|
||||
- `/bt/` - BitTorrent tracker functionality (announce.php, scrape.php)
|
||||
- `/styles/` - Templates, CSS, JS, images
|
||||
- `/internal_data/` - Cache, logs, compiled templates
|
||||
- `/install/` - Installation scripts and configuration examples
|
||||
- `/migrations/` - Database migration files (Phinx)
|
||||
- `/styles/` - Legacy templates, CSS, JS, images
|
||||
- `/internal_data/` - Legacy cache, logs, compiled templates
|
||||
|
||||
## Entry Points & Key Files
|
||||
|
||||
- `index.php` - Main forum homepage
|
||||
- `tracker.php` - Torrent search/browse interface
|
||||
### Modern Entry Points
|
||||
- `public/index.php` - Laravel-style front controller (web requests)
|
||||
- `dexter` - CLI interface (console commands)
|
||||
- `bootstrap/app.php` - Application bootstrap
|
||||
- `bootstrap/container.php` - Container setup and configuration
|
||||
- `bootstrap/console.php` - Console bootstrap
|
||||
|
||||
### Legacy Entry Points (Backward Compatibility)
|
||||
- `bt/announce.php` - BitTorrent announce endpoint
|
||||
- `bt/scrape.php` - BitTorrent scrape endpoint
|
||||
- `admin/index.php` - Administrative panel
|
||||
- `cron.php` - Background task runner (CLI only)
|
||||
- `install.php` - Installation script (CLI only)
|
||||
- `admin/index.php` - Legacy administrative panel
|
||||
- `cron.php` - Background task runner
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Installation & Setup
|
||||
```bash
|
||||
# Automated installation (CLI)
|
||||
php install.php
|
||||
|
||||
# Install dependencies
|
||||
composer install
|
||||
|
||||
|
@ -58,24 +89,82 @@ php cron.php
|
|||
### Code Quality
|
||||
The project uses **StyleCI** with PSR-2 preset for code style enforcement. StyleCI configuration is in `.styleci.yml` targeting `src/` directory.
|
||||
|
||||
## Modern Architecture Components
|
||||
## MVC Architecture Components
|
||||
|
||||
### Models (`/app/Models/`)
|
||||
- **Simple Active Record pattern** using Nette Database (primary)
|
||||
- **Eloquent ORM** available via Illuminate Database (optional)
|
||||
- Base `Model` class provides common CRUD operations
|
||||
- No complex ORM required, just straightforward database access
|
||||
- Example: `Torrent`, `User`, `Forum`, `Post` models
|
||||
|
||||
### Controllers (`/app/Http/Controllers/`)
|
||||
- **Thin controllers** that delegate to services
|
||||
- Organized by area: `Web/`, `Api/`, `Admin/`
|
||||
- `LegacyController` maintains backward compatibility
|
||||
- Base `Controller` class provides common methods
|
||||
|
||||
### Services (`/app/Services/`)
|
||||
- **Business logic layer** between controllers and models
|
||||
- Handles complex operations and workflows
|
||||
- Example: `TorrentService`, `AuthService`, `ForumService`
|
||||
- Injected via dependency injection
|
||||
|
||||
### Views (`/resources/views/`)
|
||||
- **PHP templates** (planning future Twig integration)
|
||||
- Organized by feature areas
|
||||
- Layouts for consistent structure
|
||||
- Partials for reusable components
|
||||
|
||||
## Infrastructure Components
|
||||
|
||||
### Database Layer (`/src/Database/`)
|
||||
- **Nette Database** replacing legacy SqlDb system
|
||||
- **Nette Database** for all data access (primary)
|
||||
- **Illuminate Database** available for Eloquent ORM features
|
||||
- Modern singleton pattern accessible via `DB()` function
|
||||
- Support for multiple database connections and debug functionality
|
||||
- **Breaking changes expected** during 3.0 migration to ORM-style queries
|
||||
- Direct SQL queries when needed
|
||||
|
||||
### Cache System (`/src/Cache/`)
|
||||
- **Unified caching** using Nette Caching internally
|
||||
- Replaces existing `CACHE()` and $datastore systems
|
||||
- Supports file, SQLite, memory, and Memcached storage
|
||||
- **API changes planned** for improved developer experience
|
||||
- Used by services and repositories
|
||||
|
||||
### Configuration Management
|
||||
- Environment-based config with `.env` files
|
||||
- **Illuminate Config** for Laravel-style configuration
|
||||
- Modern singleton `Config` class accessible via `config()` function
|
||||
- **Legacy config access will be removed** in favor of new patterns
|
||||
- Configuration files in `/config/` directory
|
||||
|
||||
### Event System (`/app/Events/` & `/app/Listeners/`)
|
||||
- **Illuminate Events** for decoupled, event-driven architecture
|
||||
- Event classes in `/app/Events/`
|
||||
- Listener classes in `/app/Listeners/`
|
||||
- Event-listener mappings in `EventServiceProvider`
|
||||
- Global `event()` helper function for dispatching events
|
||||
- Support for queued listeners (when queue system is configured)
|
||||
|
||||
### Routing System
|
||||
- **Illuminate Routing** for full Laravel-compatible routing (as of TorrentPier 3.0)
|
||||
- Route definitions in `/routes/` directory (web.php, api.php, admin.php)
|
||||
- Support for route groups, middleware, named routes, route model binding
|
||||
- Resource controllers and RESTful routing patterns
|
||||
- Custom `IlluminateRouter` wrapper for smooth Laravel integration
|
||||
- Legacy custom router preserved as `LegacyRouter` for reference
|
||||
- Backward compatibility maintained through `Router` alias
|
||||
|
||||
### Validation Layer
|
||||
- **Illuminate Validation** for robust input validation
|
||||
- Form request classes in `/app/Http/Requests/`
|
||||
- Base `FormRequest` class for common validation logic
|
||||
- Custom validation rules and messages
|
||||
- Automatic validation exception handling
|
||||
|
||||
### HTTP Layer
|
||||
- **Illuminate HTTP** for request/response handling
|
||||
- Middleware support for request filtering
|
||||
- JSON response helpers and content negotiation
|
||||
|
||||
## Configuration Files
|
||||
- `.env` - Environment variables (copy from `.env.example`)
|
||||
|
@ -89,16 +178,14 @@ The project uses **StyleCI** with PSR-2 preset for code style enforcement. Style
|
|||
- **GitHub Actions** for automated testing and deployment
|
||||
- **StyleCI** for code style enforcement
|
||||
- **Dependabot** for dependency updates
|
||||
- **FTP deployment** to demo environment
|
||||
|
||||
### Installation Methods
|
||||
1. **Automated**: `php install.php` (recommended)
|
||||
2. **Composer**: `composer create-project torrentpier/torrentpier`
|
||||
3. **Manual**: Git clone + `composer install` + database setup
|
||||
1. **Composer**: `composer create-project torrentpier/torrentpier`
|
||||
2. **Manual**: Git clone + `composer install`
|
||||
|
||||
## Database & Schema
|
||||
|
||||
- **Database migrations** managed via Phinx in `/migrations/` directory
|
||||
- **Database migrations** managed via Phinx in `/database/migrations/` directory
|
||||
- Initial schema: `20250619000001_initial_schema.php`
|
||||
- Initial seed data: `20250619000002_seed_initial_data.php`
|
||||
- UTF-8 (utf8mb4) character set required
|
||||
|
@ -118,25 +205,132 @@ php vendor/bin/phinx migrate --fake --configuration=phinx.php
|
|||
|
||||
## TorrentPier 3.0 Modernization Strategy
|
||||
|
||||
The TorrentPier 3.0 release represents a major architectural shift focused on:
|
||||
The TorrentPier 3.0 release represents a major architectural shift to Laravel-style MVC:
|
||||
|
||||
- **Laravel-style MVC Architecture**: Clean Model-View-Controller pattern
|
||||
- **Illuminate Container**: Laravel's dependency injection container
|
||||
- **Modern PHP practices**: PSR standards, namespaces, autoloading
|
||||
- **Clean architecture**: Separation of concerns, dependency injection
|
||||
- **Developer friendly**: Familiar Laravel patterns for easier contribution
|
||||
- **Performance improvements**: Optimized database queries, efficient caching
|
||||
- **Developer experience**: Better debugging, testing, and maintenance
|
||||
- **Breaking changes**: Legacy code removal and API modernization
|
||||
|
||||
**Important**: TorrentPier 3.0 will introduce breaking changes to achieve these modernization goals. Existing deployments should remain on 2.x versions until they're ready to migrate to the new architecture.
|
||||
|
||||
## Migration Path for 3.0
|
||||
## Current Architecture
|
||||
|
||||
- **Database layer**: Legacy SqlDb calls will be removed, migrate to new Database class
|
||||
- **Cache system**: Replace existing CACHE() and $datastore calls with new unified API
|
||||
- **Configuration**: Update legacy global $bb_cfg access to use config() singleton
|
||||
- **Templates**: Legacy template syntax may be deprecated in favor of modern Twig features
|
||||
- **Language system**: Update global $lang usage to new Language singleton methods
|
||||
### Container & Dependency Injection
|
||||
- **Illuminate Container**: Laravel's container for dependency injection
|
||||
- **Bootstrap**: Clean container setup in `/bootstrap/container.php`
|
||||
- **Service Providers**: Laravel-style providers in `/app/Providers/`
|
||||
- **Helper Functions**: Global helpers - `app()`, `config()`, `event()`
|
||||
|
||||
When working with this codebase, prioritize modern architecture patterns and clean code practices. Focus on the new systems in `/src/` directory rather than maintaining legacy compatibility.
|
||||
### MVC Structure
|
||||
- **Controllers**: All controllers in `/app/Http/Controllers/`
|
||||
- **Models**: Simple models in `/app/Models/` (using Nette Database or Eloquent)
|
||||
- **Services**: Business logic in `/app/Services/`
|
||||
- **Routes**: Laravel-style route definitions in `/routes/`
|
||||
- **Middleware**: HTTP middleware in `/app/Http/Middleware/`
|
||||
- **Events**: Event classes in `/app/Events/`
|
||||
- **Listeners**: Event listeners in `/app/Listeners/`
|
||||
- **Requests**: Form request validation in `/app/Http/Requests/`
|
||||
|
||||
### Migration Steps for New Features
|
||||
1. Create models in `/app/Models/` extending base `Model` class (or Eloquent models)
|
||||
2. Add business logic to services in `/app/Services/`
|
||||
3. Create form request classes in `/app/Http/Requests/` for validation
|
||||
4. Create thin controllers in `/app/Http/Controllers/`
|
||||
5. Define routes in `/routes/` files using Illuminate Routing syntax
|
||||
6. Create events in `/app/Events/` and listeners in `/app/Listeners/`
|
||||
7. Register event listeners in `EventServiceProvider`
|
||||
8. Use helper functions: `app()`, `config()`, `event()` for easy access
|
||||
|
||||
### What to Avoid
|
||||
- Don't use complex DDD patterns (aggregates, value objects)
|
||||
- Don't implement CQRS or event sourcing
|
||||
- Don't create repository interfaces (use concrete classes if needed)
|
||||
- Don't over-engineer - keep it simple and Laravel-like
|
||||
|
||||
When working with this codebase, prioritize simplicity and maintainability. New features should be built in the `/app/` directory using Laravel-style MVC patterns.
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Events and Listeners
|
||||
```php
|
||||
// Dispatch an event from a service
|
||||
use App\Events\UserRegistered;
|
||||
|
||||
event(new UserRegistered($user));
|
||||
|
||||
// Create an event listener
|
||||
class SendWelcomeEmail
|
||||
{
|
||||
public function handle(UserRegistered $event): void
|
||||
{
|
||||
// Send welcome email to $event->user
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Form Validation
|
||||
```php
|
||||
// Create a form request class
|
||||
class RegisterUserRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => 'required|email|unique:users',
|
||||
'username' => 'required|string|min:3|max:20',
|
||||
'password' => 'required|string|min:8',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Use in controller
|
||||
public function register(RegisterUserRequest $request): JsonResponse
|
||||
{
|
||||
// Request is automatically validated
|
||||
$validated = $request->validated();
|
||||
// ... create user
|
||||
}
|
||||
```
|
||||
|
||||
### Routing with Groups and Middleware
|
||||
```php
|
||||
// In routes/api.php
|
||||
$router->group(['prefix' => 'v1', 'middleware' => 'auth'], function () use ($router) {
|
||||
$router->resource('torrents', 'TorrentController');
|
||||
$router->get('stats', 'StatsController::index');
|
||||
});
|
||||
```
|
||||
|
||||
### Using Collections
|
||||
```php
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
$users = collect(User::all());
|
||||
$activeUsers = $users->filter(fn($user) => $user->isActive())
|
||||
->sortBy('last_seen')
|
||||
->take(10);
|
||||
```
|
||||
|
||||
### Middleware Usage
|
||||
```php
|
||||
// Apply middleware to routes
|
||||
$router->middleware(['auth', 'admin'])->group(function () use ($router) {
|
||||
$router->get('/admin/users', 'AdminController::users');
|
||||
});
|
||||
|
||||
// Create custom middleware
|
||||
class CustomMiddleware
|
||||
{
|
||||
public function handle(Request $request, \Closure $next)
|
||||
{
|
||||
// Middleware logic here
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Markdown File Guidelines
|
||||
|
||||
|
|
116
app/Console/Commands/ClearCacheCommand.php
Normal file
116
app/Console/Commands/ClearCacheCommand.php
Normal file
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* Clear Cache Command
|
||||
*
|
||||
* Clears application cache files
|
||||
*/
|
||||
class ClearCacheCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The command signature
|
||||
*/
|
||||
protected string $signature = 'cache:clear';
|
||||
|
||||
/**
|
||||
* The command description
|
||||
*/
|
||||
protected string $description = 'Clear application cache';
|
||||
|
||||
/**
|
||||
* Configure the command
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption(
|
||||
'force',
|
||||
'f',
|
||||
InputOption::VALUE_NONE,
|
||||
'Force clearing cache without confirmation'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the command
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$force = $this->option('force');
|
||||
|
||||
if (!$force && !$this->confirm('Are you sure you want to clear all cache?', true)) {
|
||||
$this->info('Cache clear cancelled.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info('Clearing application cache...');
|
||||
|
||||
$cleared = 0;
|
||||
|
||||
// Clear file cache
|
||||
$cacheDir = $this->app->make('path.base') . '/storage/framework/cache';
|
||||
if (is_dir($cacheDir)) {
|
||||
$files = glob($cacheDir . '/*');
|
||||
foreach ($files as $file) {
|
||||
if (is_file($file)) {
|
||||
unlink($file);
|
||||
$cleared++;
|
||||
}
|
||||
}
|
||||
$this->line("✓ File cache cleared ({$cleared} files)");
|
||||
}
|
||||
|
||||
// Clear view cache
|
||||
$viewCacheDir = $this->app->make('path.base') . '/storage/framework/views';
|
||||
if (is_dir($viewCacheDir)) {
|
||||
$viewFiles = glob($viewCacheDir . '/*');
|
||||
$viewCleared = 0;
|
||||
foreach ($viewFiles as $file) {
|
||||
if (is_file($file)) {
|
||||
unlink($file);
|
||||
$viewCleared++;
|
||||
}
|
||||
}
|
||||
$this->line("✓ View cache cleared ({$viewCleared} files)");
|
||||
$cleared += $viewCleared;
|
||||
}
|
||||
|
||||
// Clear legacy cache directories
|
||||
$legacyCacheDir = $this->app->make('path.base') . '/internal_data/cache';
|
||||
if (is_dir($legacyCacheDir)) {
|
||||
$legacyCleared = $this->clearDirectoryRecursive($legacyCacheDir);
|
||||
$this->line("✓ Legacy cache cleared ({$legacyCleared} files)");
|
||||
$cleared += $legacyCleared;
|
||||
}
|
||||
|
||||
$this->success("Cache cleared successfully! Total files removed: {$cleared}");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively clear a directory
|
||||
*/
|
||||
private function clearDirectoryRecursive(string $dir): int
|
||||
{
|
||||
$cleared = 0;
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
unlink($file->getRealPath());
|
||||
$cleared++;
|
||||
}
|
||||
}
|
||||
|
||||
return $cleared;
|
||||
}
|
||||
}
|
207
app/Console/Commands/Command.php
Normal file
207
app/Console/Commands/Command.php
Normal file
|
@ -0,0 +1,207 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Symfony\Component\Console\Command\Command as SymfonyCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Illuminate\Container\Container;
|
||||
|
||||
/**
|
||||
* Base Command Class
|
||||
*
|
||||
* Laravel-style base command class for TorrentPier console commands
|
||||
*/
|
||||
abstract class Command extends SymfonyCommand
|
||||
{
|
||||
/**
|
||||
* The command signature (Laravel-style)
|
||||
* Example: 'cache:clear {--force : Force clearing without confirmation}'
|
||||
*/
|
||||
protected string $signature = '';
|
||||
|
||||
/**
|
||||
* The command description
|
||||
*/
|
||||
protected string $description = '';
|
||||
|
||||
/**
|
||||
* Application container
|
||||
*/
|
||||
protected Container $app;
|
||||
|
||||
/**
|
||||
* Console input interface
|
||||
*/
|
||||
protected InputInterface $input;
|
||||
|
||||
/**
|
||||
* Console output interface
|
||||
*/
|
||||
protected OutputInterface $output;
|
||||
|
||||
/**
|
||||
* Symfony style interface
|
||||
*/
|
||||
protected SymfonyStyle $io;
|
||||
|
||||
/**
|
||||
* Create a new command instance
|
||||
*/
|
||||
public function __construct(?string $name = null)
|
||||
{
|
||||
// Parse signature if provided
|
||||
if ($this->signature) {
|
||||
$name = $this->parseSignature();
|
||||
}
|
||||
|
||||
parent::__construct($name);
|
||||
|
||||
// Set description
|
||||
if ($this->description) {
|
||||
$this->setDescription($this->description);
|
||||
}
|
||||
|
||||
// Get container instance
|
||||
$this->app = Container::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the command
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->output = $output;
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
|
||||
try {
|
||||
$result = $this->handle();
|
||||
return is_int($result) ? $result : self::SUCCESS;
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Command failed: ' . $e->getMessage());
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the command (implement in subclasses)
|
||||
*/
|
||||
abstract public function handle(): int;
|
||||
|
||||
/**
|
||||
* Parse Laravel-style signature
|
||||
*/
|
||||
protected function parseSignature(): string
|
||||
{
|
||||
// Simple signature parsing - just extract command name for now
|
||||
// Full Laravel signature parsing would be more complex
|
||||
$parts = explode(' ', trim($this->signature));
|
||||
return $parts[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an argument value
|
||||
*/
|
||||
protected function argument(?string $key = null): mixed
|
||||
{
|
||||
if ($key === null) {
|
||||
return $this->input->getArguments();
|
||||
}
|
||||
|
||||
return $this->input->getArgument($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an option value
|
||||
*/
|
||||
protected function option(?string $key = null): mixed
|
||||
{
|
||||
if ($key === null) {
|
||||
return $this->input->getOptions();
|
||||
}
|
||||
|
||||
return $this->input->getOption($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an info message
|
||||
*/
|
||||
protected function info(string $message): void
|
||||
{
|
||||
$this->io->info($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an error message
|
||||
*/
|
||||
protected function error(string $message): void
|
||||
{
|
||||
$this->io->error($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a warning message
|
||||
*/
|
||||
protected function warn(string $message): void
|
||||
{
|
||||
$this->io->warning($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a success message
|
||||
*/
|
||||
protected function success(string $message): void
|
||||
{
|
||||
$this->io->success($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a line of text
|
||||
*/
|
||||
protected function line(string $message): void
|
||||
{
|
||||
$this->output->writeln($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask a question
|
||||
*/
|
||||
protected function ask(string $question, ?string $default = null): ?string
|
||||
{
|
||||
return $this->io->ask($question, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask for confirmation
|
||||
*/
|
||||
protected function confirm(string $question, bool $default = false): bool
|
||||
{
|
||||
return $this->io->confirm($question, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user to select from a list of options
|
||||
*/
|
||||
protected function choice(string $question, array $choices, ?string $default = null): string
|
||||
{
|
||||
return $this->io->choice($question, $choices, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get application configuration
|
||||
*/
|
||||
protected function config(?string $key = null, mixed $default = null): mixed
|
||||
{
|
||||
$config = $this->app->make('config');
|
||||
|
||||
if ($key === null) {
|
||||
return $config;
|
||||
}
|
||||
|
||||
return $config->get($key, $default);
|
||||
}
|
||||
}
|
113
app/Console/Commands/InfoCommand.php
Normal file
113
app/Console/Commands/InfoCommand.php
Normal file
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
/**
|
||||
* Info Command
|
||||
*
|
||||
* Display TorrentPier system information
|
||||
*/
|
||||
class InfoCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The command signature
|
||||
*/
|
||||
protected string $signature = 'info';
|
||||
|
||||
/**
|
||||
* The command description
|
||||
*/
|
||||
protected string $description = 'Display system information';
|
||||
|
||||
/**
|
||||
* Handle the command
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$basePath = $this->app->make('path.base');
|
||||
|
||||
$this->line('');
|
||||
$this->line('<fg=cyan>TorrentPier System Information</>');
|
||||
$this->line('<fg=cyan>==============================</>');
|
||||
$this->line('');
|
||||
|
||||
// Application info
|
||||
$this->line('<fg=yellow>Application:</>');
|
||||
$this->line(' Name: TorrentPier');
|
||||
$this->line(' Version: 3.0-dev');
|
||||
$this->line(' Environment: ' . ($_ENV['APP_ENV'] ?? 'production'));
|
||||
$this->line(' Debug Mode: ' . (($_ENV['APP_DEBUG'] ?? false) ? 'enabled' : 'disabled'));
|
||||
$this->line('');
|
||||
|
||||
// Paths
|
||||
$this->line('<fg=yellow>Paths:</>');
|
||||
$this->line(' Base: ' . $basePath);
|
||||
$this->line(' App: ' . $this->app->make('path.app'));
|
||||
$this->line(' Config: ' . $this->app->make('path.config'));
|
||||
$this->line(' Storage: ' . $this->app->make('path.storage'));
|
||||
$this->line(' Public: ' . $this->app->make('path.public'));
|
||||
$this->line('');
|
||||
|
||||
// PHP info
|
||||
$this->line('<fg=yellow>PHP:</>');
|
||||
$this->line(' Version: ' . PHP_VERSION);
|
||||
$this->line(' SAPI: ' . PHP_SAPI);
|
||||
$this->line(' Memory Limit: ' . ini_get('memory_limit'));
|
||||
$this->line(' Max Execution Time: ' . ini_get('max_execution_time') . 's');
|
||||
$this->line('');
|
||||
|
||||
// Extensions
|
||||
$requiredExtensions = ['pdo', 'curl', 'gd', 'mbstring', 'openssl', 'zip'];
|
||||
$this->line('<fg=yellow>Required Extensions:</>');
|
||||
foreach ($requiredExtensions as $ext) {
|
||||
$status = extension_loaded($ext) ? '<fg=green>✓</>' : '<fg=red>✗</>';
|
||||
$this->line(" {$status} {$ext}");
|
||||
}
|
||||
$this->line('');
|
||||
|
||||
// File permissions
|
||||
$this->line('<fg=yellow>File Permissions:</>');
|
||||
$writablePaths = [
|
||||
'storage',
|
||||
'storage/app',
|
||||
'storage/framework',
|
||||
'storage/logs',
|
||||
'internal_data/cache',
|
||||
'data/uploads'
|
||||
];
|
||||
|
||||
foreach ($writablePaths as $path) {
|
||||
$fullPath = $basePath . '/' . $path;
|
||||
if (file_exists($fullPath)) {
|
||||
$writable = is_writable($fullPath);
|
||||
$status = $writable ? '<fg=green>✓</>' : '<fg=red>✗</>';
|
||||
$this->line(" {$status} {$path}");
|
||||
} else {
|
||||
$this->line(" <fg=yellow>?</> {$path} (not found)");
|
||||
}
|
||||
}
|
||||
$this->line('');
|
||||
|
||||
// Database
|
||||
try {
|
||||
if ($this->app->bound('config')) {
|
||||
$config = $this->app->make('config');
|
||||
$dbConfig = $config->get('database', []);
|
||||
|
||||
if (!empty($dbConfig)) {
|
||||
$this->line('<fg=yellow>Database:</>');
|
||||
$this->line(' Host: ' . ($dbConfig['host'] ?? 'not configured'));
|
||||
$this->line(' Database: ' . ($dbConfig['dbname'] ?? 'not configured'));
|
||||
$this->line(' Driver: ' . ($dbConfig['driver'] ?? 'not configured'));
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->line('<fg=yellow>Database:</> Configuration error');
|
||||
}
|
||||
|
||||
$this->line('');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
101
app/Console/Commands/MigrateCommand.php
Normal file
101
app/Console/Commands/MigrateCommand.php
Normal file
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* Migrate Command
|
||||
*
|
||||
* Run database migrations using Phinx
|
||||
*/
|
||||
class MigrateCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The command signature
|
||||
*/
|
||||
protected string $signature = 'migrate';
|
||||
|
||||
/**
|
||||
* The command description
|
||||
*/
|
||||
protected string $description = 'Run database migrations';
|
||||
|
||||
/**
|
||||
* Configure the command
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption(
|
||||
'fake',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Mark migrations as run without actually running them'
|
||||
)
|
||||
->addOption(
|
||||
'target',
|
||||
't',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Target migration version'
|
||||
)
|
||||
->addOption(
|
||||
'force',
|
||||
'f',
|
||||
InputOption::VALUE_NONE,
|
||||
'Force running migrations in production'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the command
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$basePath = $this->app->make('path.base');
|
||||
$phinxConfig = $basePath . '/phinx.php';
|
||||
|
||||
if (!file_exists($phinxConfig)) {
|
||||
$this->error('Phinx configuration file not found at: ' . $phinxConfig);
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('Running database migrations...');
|
||||
|
||||
// Build phinx command
|
||||
$command = 'cd ' . escapeshellarg($basePath) . ' && ';
|
||||
$command .= 'vendor/bin/phinx migrate';
|
||||
$command .= ' --configuration=' . escapeshellarg($phinxConfig);
|
||||
|
||||
if ($this->option('fake')) {
|
||||
$command .= ' --fake';
|
||||
}
|
||||
|
||||
if ($this->option('target')) {
|
||||
$command .= ' --target=' . escapeshellarg($this->option('target'));
|
||||
}
|
||||
|
||||
if ($this->option('force')) {
|
||||
$command .= ' --no-interaction';
|
||||
}
|
||||
|
||||
// Execute the command
|
||||
$output = [];
|
||||
$returnCode = 0;
|
||||
exec($command . ' 2>&1', $output, $returnCode);
|
||||
|
||||
// Display output
|
||||
foreach ($output as $line) {
|
||||
$this->line($line);
|
||||
}
|
||||
|
||||
if ($returnCode === 0) {
|
||||
$this->success('Migrations completed successfully!');
|
||||
} else {
|
||||
$this->error('Migration failed with exit code: ' . $returnCode);
|
||||
}
|
||||
|
||||
return $returnCode === 0 ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
}
|
47
bootstrap/console.php
Normal file
47
bootstrap/console.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Console Bootstrap
|
||||
*
|
||||
* Bootstrap the console application
|
||||
*/
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Symfony\Component\Console\Application;
|
||||
|
||||
// Only define DEXTER_BINARY if not already defined
|
||||
if (!defined('DEXTER_BINARY')) {
|
||||
define('DEXTER_BINARY', true);
|
||||
}
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
// Load container bootstrap
|
||||
require_once __DIR__ . '/container.php';
|
||||
|
||||
// Create the application container
|
||||
$container = createContainer(dirname(__DIR__));
|
||||
|
||||
// Create Symfony Console Application
|
||||
$app = new Application('TorrentPier Console', '3.0-dev');
|
||||
|
||||
// Get registered commands from the container
|
||||
try {
|
||||
if ($container->bound('console.commands')) {
|
||||
$commands = $container->make('console.commands');
|
||||
foreach ($commands as $command) {
|
||||
try {
|
||||
$app->add($container->make($command));
|
||||
} catch (BindingResolutionException $e) {
|
||||
// Skip commands that can't be resolved - console still works with built-in commands
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (BindingResolutionException $e) {
|
||||
// No commands registered or service binding failed - console still works with built-in commands
|
||||
}
|
||||
|
||||
// Return the console application
|
||||
return $app;
|
|
@ -79,6 +79,7 @@
|
|||
"php-curl-class/php-curl-class": "^12.0.0",
|
||||
"robmorgan/phinx": "^0.16.9",
|
||||
"samdark/sitemap": "2.4.1",
|
||||
"symfony/console": "^7.3",
|
||||
"symfony/mailer": "^7.3",
|
||||
"symfony/polyfill": "v1.32.0",
|
||||
"vlucas/phpdotenv": "^5.5",
|
||||
|
|
50
composer.lock
generated
50
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "b6064b3dc730bc6bf800fdf90be38b43",
|
||||
"content-hash": "697bc73a6378851ccaea6fb2c3c4f61d",
|
||||
"packages": [
|
||||
{
|
||||
"name": "arokettu/bencode",
|
||||
|
@ -2350,6 +2350,54 @@
|
|||
},
|
||||
"time": "2025-05-13T15:08:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/config",
|
||||
"version": "v12.19.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/config.git",
|
||||
"reference": "76824d4e853dba43359a201eba2422ab11e21e4d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/illuminate/config/zipball/76824d4e853dba43359a201eba2422ab11e21e4d",
|
||||
"reference": "76824d4e853dba43359a201eba2422ab11e21e4d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/collections": "^12.0",
|
||||
"illuminate/contracts": "^12.0",
|
||||
"php": "^8.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "12.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Illuminate\\Config\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "The Illuminate Config package.",
|
||||
"homepage": "https://laravel.com",
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2025-03-19T20:10:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/container",
|
||||
"version": "v12.19.3",
|
||||
|
|
21
dexter
Executable file
21
dexter
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Dexter - TorrentPier Command Line Interface
|
||||
*
|
||||
* Laravel-style CLI for running commands
|
||||
*/
|
||||
|
||||
// Load the bootstrap and get the application
|
||||
$app = require __DIR__ . '/bootstrap/console.php';
|
||||
|
||||
// Run the console application with proper error handling
|
||||
try {
|
||||
exit($app->run());
|
||||
} catch (\Exception $e) {
|
||||
fwrite(STDERR, "Error: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
|
@ -10,7 +10,7 @@ This document specifies the MVC (Model-View-Controller) architecture directory s
|
|||
# Laravel-style root structure
|
||||
/app/ # Application code (PSR-4: App\)
|
||||
├── Console/ # Console commands
|
||||
│ └── Commands/ # Artisan-style commands
|
||||
│ └── Commands/ # Artisan-style commands for Dexter
|
||||
├── Http/ # HTTP layer
|
||||
│ ├── Controllers/ # Controllers
|
||||
│ │ ├── Admin/ # Admin panel controllers
|
||||
|
@ -109,7 +109,7 @@ This document specifies the MVC (Model-View-Controller) architecture directory s
|
|||
.env # Environment variables
|
||||
.env.example # Environment example
|
||||
composer.json # Dependencies (App\ and TorrentPier\ namespaces)
|
||||
artisan # CLI interface
|
||||
dexter # CLI interface
|
||||
index.php # Legacy entry point (redirects to public/)
|
||||
```
|
||||
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Config;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
class ConfigSystemTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test that config helper function works
|
||||
*/
|
||||
public function testConfigHelper(): void
|
||||
{
|
||||
// Test getting the config repository
|
||||
$config = config();
|
||||
$this->assertInstanceOf(\Illuminate\Config\Repository::class, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting config values
|
||||
*/
|
||||
public function testGettingConfigValues(): void
|
||||
{
|
||||
// Assuming app.php config exists
|
||||
$appConfig = config('app');
|
||||
$this->assertIsArray($appConfig);
|
||||
|
||||
// Test with default value
|
||||
$nonExistent = config('non.existent.key', 'default');
|
||||
$this->assertEquals('default', $nonExistent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test setting config values
|
||||
*/
|
||||
public function testSettingConfigValues(): void
|
||||
{
|
||||
// Set a single value
|
||||
config(['test.key' => 'test value']);
|
||||
$this->assertEquals('test value', config('test.key'));
|
||||
|
||||
// Set multiple values
|
||||
config([
|
||||
'test.foo' => 'bar',
|
||||
'test.baz' => 'qux'
|
||||
]);
|
||||
|
||||
$this->assertEquals('bar', config('test.foo'));
|
||||
$this->assertEquals('qux', config('test.baz'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test dot notation access
|
||||
*/
|
||||
public function testDotNotationAccess(): void
|
||||
{
|
||||
config(['deeply.nested.config.value' => 'found it']);
|
||||
|
||||
$this->assertEquals('found it', config('deeply.nested.config.value'));
|
||||
$this->assertIsArray(config('deeply.nested'));
|
||||
$this->assertArrayHasKey('config', config('deeply.nested'));
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Events;
|
||||
|
||||
use App\Events\UserRegistered;
|
||||
use App\Events\TorrentUploaded;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EventSystemTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test that events can be dispatched
|
||||
*/
|
||||
public function testCanDispatchEvents(): void
|
||||
{
|
||||
// Create a mock listener
|
||||
$called = false;
|
||||
$eventData = null;
|
||||
|
||||
app('events')->listen(UserRegistered::class, function ($event) use (&$called, &$eventData) {
|
||||
$called = true;
|
||||
$eventData = $event;
|
||||
});
|
||||
|
||||
// Dispatch the event
|
||||
$event = new UserRegistered(
|
||||
userId: 123,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
registeredAt: new \DateTime('2025-01-01 12:00:00')
|
||||
);
|
||||
|
||||
event($event);
|
||||
|
||||
// Assert the listener was called
|
||||
$this->assertTrue($called);
|
||||
$this->assertInstanceOf(UserRegistered::class, $eventData);
|
||||
$this->assertEquals(123, $eventData->getUserId());
|
||||
$this->assertEquals('testuser', $eventData->getUsername());
|
||||
$this->assertEquals('test@example.com', $eventData->getEmail());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test event helper function
|
||||
*/
|
||||
public function testEventHelperFunction(): void
|
||||
{
|
||||
$listenerCalled = false;
|
||||
|
||||
app('events')->listen(TorrentUploaded::class, function () use (&$listenerCalled) {
|
||||
$listenerCalled = true;
|
||||
});
|
||||
|
||||
// Use the event() helper
|
||||
event(new TorrentUploaded(
|
||||
torrentId: 456,
|
||||
uploaderId: 789,
|
||||
torrentName: 'Test Torrent',
|
||||
size: 1024 * 1024 * 100, // 100MB
|
||||
uploadedAt: new \DateTime()
|
||||
));
|
||||
|
||||
$this->assertTrue($listenerCalled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that multiple listeners can be attached to an event
|
||||
*/
|
||||
public function testMultipleListeners(): void
|
||||
{
|
||||
$listener1Called = false;
|
||||
$listener2Called = false;
|
||||
|
||||
app('events')->listen(UserRegistered::class, function () use (&$listener1Called) {
|
||||
$listener1Called = true;
|
||||
});
|
||||
|
||||
app('events')->listen(UserRegistered::class, function () use (&$listener2Called) {
|
||||
$listener2Called = true;
|
||||
});
|
||||
|
||||
event(new UserRegistered(
|
||||
userId: 999,
|
||||
username: 'multitest',
|
||||
email: 'multi@test.com',
|
||||
registeredAt: new \DateTime()
|
||||
));
|
||||
|
||||
$this->assertTrue($listener1Called);
|
||||
$this->assertTrue($listener2Called);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue