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:
Yury Pikhtarev 2025-06-23 00:55:30 +04:00
commit 314ceacbe7
No known key found for this signature in database
13 changed files with 887 additions and 198 deletions

266
CLAUDE.md
View file

@ -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

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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
View 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;

View file

@ -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
View file

@ -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
View 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);
}

View file

@ -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/)
```

View file

@ -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'));
}
}

View file

@ -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);
}
}