feat: implement dependency injection container infrastructure

- Add PHP-DI container with hexagonal architecture compliance
- Implement container factory with environment-specific configuration
- Create Bootstrap class for application initialization
- Add service definitions organized by architectural layers (Domain, Application, Infrastructure, Presentation)
- Introduce global helper functions (container(), app()) for service access
- Add comprehensive test suite for DI container components
- Configure container compilation and proxy generation for production
- Add container configuration files and environment-specific settings
- Update composer dependencies to include php-di/php-di
- Add documentation and usage examples for the DI system

This establishes the foundation for modern dependency management and
enables future implementation of clean architecture patterns throughout
the application.
This commit is contained in:
Yury Pikhtarev 2025-06-21 22:31:29 +04:00
commit f09aa3627b
No known key found for this signature in database
36 changed files with 2896 additions and 3902 deletions

View file

@ -157,3 +157,14 @@ More text here.
### MD047 - Files should end with a single newline
Ensure every markdown file ends with exactly one newline character at the end of the file.
## Tests
- Need comprehensive test coverage for new components
- Utilize Pest PHP for unit and integration / feature testing
- Tests should focus on validating modern architecture components
- Create test suites for critical systems like database, cache, and configuration
## Development Guidelines
- Always ensure that there is one empty line at the end of the file

View file

@ -67,6 +67,7 @@
"nette/caching": "^3.3",
"nette/database": "^3.2",
"php-curl-class/php-curl-class": "^12.0.0",
"php-di/php-di": "^7.0",
"robmorgan/phinx": "^0.16.9",
"samdark/sitemap": "2.4.1",
"symfony/mailer": "^7.3",
@ -82,7 +83,10 @@
"autoload": {
"psr-4": {
"TorrentPier\\": "src/"
}
},
"files": [
"src/helpers.php"
]
},
"autoload-dev": {
"psr-4": {

191
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": "26f36d2312e2eabf3ed5ff36391cc050",
"content-hash": "57713d8849e71683b70d934a81f7e18c",
"packages": [
{
"name": "arokettu/bencode",
@ -1879,6 +1879,67 @@
],
"time": "2024-09-11T14:15:04+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.4",
"source": {
"type": "git",
"url": "https://github.com/laravel/serializable-closure.git",
"reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841",
"reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"illuminate/support": "^10.0|^11.0|^12.0",
"nesbot/carbon": "^2.67|^3.0",
"pestphp/pest": "^2.36|^3.0",
"phpstan/phpstan": "^2.0",
"symfony/var-dumper": "^6.2.0|^7.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"Laravel\\SerializableClosure\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
},
{
"name": "Nuno Maduro",
"email": "nuno@laravel.com"
}
],
"description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.",
"keywords": [
"closure",
"laravel",
"serializable"
],
"support": {
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
"time": "2025-03-19T13:51:03+00:00"
},
{
"name": "league/color-extractor",
"version": "0.4.0",
@ -2740,6 +2801,134 @@
},
"time": "2025-03-25T18:04:16+00:00"
},
{
"name": "php-di/invoker",
"version": "2.3.6",
"source": {
"type": "git",
"url": "https://github.com/PHP-DI/Invoker.git",
"reference": "59f15608528d8a8838d69b422a919fd6b16aa576"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/59f15608528d8a8838d69b422a919fd6b16aa576",
"reference": "59f15608528d8a8838d69b422a919fd6b16aa576",
"shasum": ""
},
"require": {
"php": ">=7.3",
"psr/container": "^1.0|^2.0"
},
"require-dev": {
"athletic/athletic": "~0.1.8",
"mnapoli/hard-mode": "~0.3.0",
"phpunit/phpunit": "^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Invoker\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Generic and extensible callable invoker",
"homepage": "https://github.com/PHP-DI/Invoker",
"keywords": [
"callable",
"dependency",
"dependency-injection",
"injection",
"invoke",
"invoker"
],
"support": {
"issues": "https://github.com/PHP-DI/Invoker/issues",
"source": "https://github.com/PHP-DI/Invoker/tree/2.3.6"
},
"funding": [
{
"url": "https://github.com/mnapoli",
"type": "github"
}
],
"time": "2025-01-17T12:49:27+00:00"
},
{
"name": "php-di/php-di",
"version": "7.0.11",
"source": {
"type": "git",
"url": "https://github.com/PHP-DI/PHP-DI.git",
"reference": "32f111a6d214564520a57831d397263e8946c1d2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/32f111a6d214564520a57831d397263e8946c1d2",
"reference": "32f111a6d214564520a57831d397263e8946c1d2",
"shasum": ""
},
"require": {
"laravel/serializable-closure": "^1.0 || ^2.0",
"php": ">=8.0",
"php-di/invoker": "^2.0",
"psr/container": "^1.1 || ^2.0"
},
"provide": {
"psr/container-implementation": "^1.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3",
"friendsofphp/proxy-manager-lts": "^1",
"mnapoli/phpunit-easymock": "^1.3",
"phpunit/phpunit": "^9.6 || ^10 || ^11",
"vimeo/psalm": "^5|^6"
},
"suggest": {
"friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)"
},
"type": "library",
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"DI\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "The dependency injection container for humans",
"homepage": "https://php-di.org/",
"keywords": [
"PSR-11",
"container",
"container-interop",
"dependency injection",
"di",
"ioc",
"psr11"
],
"support": {
"issues": "https://github.com/PHP-DI/PHP-DI/issues",
"source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.11"
},
"funding": [
{
"url": "https://github.com/mnapoli",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/php-di/php-di",
"type": "tidelift"
}
],
"time": "2025-06-03T07:45:57+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.3",

26
config/container.php Normal file
View file

@ -0,0 +1,26 @@
<?php
return [
// Container configuration
'environment' => env('APP_ENV', 'development'),
'debug' => env('APP_DEBUG', false),
// Enable/disable features
'autowiring' => true,
'annotations' => false,
// Compilation settings for production
'compilation_dir' => __DIR__ . '/../internal_data/cache/container',
'proxies_dir' => __DIR__ . '/../internal_data/cache/proxies',
// Additional definition files to load
'definition_files' => [
// Add custom definition files here
// __DIR__ . '/services/custom.php',
],
// Container-specific settings
'container' => [
// Add any PHP-DI specific settings here
],
];

View file

30
config/services.php Normal file
View file

@ -0,0 +1,30 @@
<?php
use function DI\factory;
use function DI\get;
use function DI\autowire;
return [
// Add custom service definitions here as they are implemented
// Examples (uncomment and modify when implementing):
// Logger service example
// 'logger' => factory(function () {
// $logger = new \Monolog\Logger('torrentpier');
// $logger->pushHandler(new \Monolog\Handler\StreamHandler(__DIR__ . '/../internal_data/logs/app.log'));
// return $logger;
// }),
// Configuration service example
// 'config' => factory(function () {
// return [
// 'app' => require __DIR__ . '/app.php',
// 'database' => require __DIR__ . '/database.php',
// 'cache' => require __DIR__ . '/cache.php',
// ];
// }),
// Interface to implementation binding example
// 'ServiceInterface' => autowire('ConcreteService'),
];

View file

@ -0,0 +1,99 @@
<?php
/**
* Example: How to use the DI Container in TorrentPier 3.0
*
* NOTE: These are examples for future implementation following the hexagonal architecture spec.
* Most services referenced here don't exist yet and will be implemented in phases.
*/
use TorrentPier\Infrastructure\DependencyInjection\Bootstrap;
use TorrentPier\Infrastructure\DependencyInjection\ContainerFactory;
// ===== PHASE 1: Foundation (CURRENT) =====
// 1. Bootstrap the container (typically done once in index.php or bootstrap file)
$rootPath = __DIR__ . '/../..';
$container = Bootstrap::init($rootPath);
// 2. Basic container usage (works now)
$containerInstance = app(); // Get container itself
$hasService = $container->has('some.service'); // Check if service exists
// ===== PHASE 2: Domain Modeling (FUTURE) =====
// 3. Repository interfaces (when implemented in Domain layer)
// $userRepository = app('TorrentPier\Domain\User\Repository\UserRepositoryInterface');
// $torrentRepository = app('TorrentPier\Domain\Tracker\Repository\TorrentRepositoryInterface');
// $forumRepository = app('TorrentPier\Domain\Forum\Repository\ForumRepositoryInterface');
// ===== PHASE 3: Application Services (FUTURE) =====
// 4. Command/Query handlers (when implemented)
// $registerUserHandler = app('TorrentPier\Application\User\Handler\RegisterUserHandler');
// $announceHandler = app('TorrentPier\Application\Tracker\Handler\ProcessAnnounceHandler');
// $createPostHandler = app('TorrentPier\Application\Forum\Handler\CreatePostHandler');
// 5. Making command instances with parameters
// $command = $container->make('TorrentPier\Application\User\Command\RegisterUserCommand', [
// 'username' => 'john_doe',
// 'email' => 'john@example.com',
// 'password' => 'secure_password'
// ]);
// ===== PHASE 4: Infrastructure (FUTURE) =====
// 6. Database and cache (when infrastructure is implemented)
// $database = app('database.connection.default');
// $cache = app('cache.factory')('forum'); // Get cache instance for 'forum' namespace
// ===== PHASE 5: Presentation (FUTURE) =====
// 7. Controllers (when implemented)
// $userController = app('TorrentPier\Presentation\Http\Controllers\Api\UserController');
// $trackerController = app('TorrentPier\Presentation\Http\Controllers\Web\TrackerController');
// ===== TESTING EXAMPLES =====
// 8. Testing with custom container (works now)
$testContainer = ContainerFactory::create([
'definitions' => [
'test.service' => \DI\factory(function () {
return new class {
public function test() { return 'test'; }
};
}),
],
'environment' => 'testing',
]);
// 9. Safe service resolution (works now)
try {
$service = app('optional.service');
} catch (RuntimeException $e) {
// Service not found, handle gracefully
$service = null;
}
// ===== LEGACY INTEGRATION (CURRENT) =====
// 10. Integration with legacy code
// In legacy files, after including common.php or similar:
if (!Bootstrap::getContainer()) {
Bootstrap::init(BB_ROOT ?? __DIR__ . '/../..');
}
// 11. Method injection (works now if service exists)
class ExampleService
{
public function processData(string $data)
{
// Container can inject dependencies when calling this method
return "Processed: $data";
}
}
$exampleService = new ExampleService();
$result = $container->call([$exampleService, 'processData'], [
'data' => 'test data'
]);

View file

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\DependencyInjection;
use Dotenv\Dotenv;
class Bootstrap
{
private static ?Container $container = null;
public static function init(string $rootPath, array $config = []): Container
{
if (self::$container !== null) {
return self::$container;
}
// Load environment variables
self::loadEnvironment($rootPath);
// Merge configuration
$config = self::loadConfiguration($rootPath, $config);
// Create and configure container
self::$container = ContainerFactory::create($config);
// Register container instance with itself
self::$container->getWrappedContainer()->set(Container::class, self::$container);
self::$container->getWrappedContainer()->set('container', self::$container);
return self::$container;
}
public static function getContainer(): ?Container
{
return self::$container;
}
public static function reset(): void
{
self::$container = null;
}
private static function loadEnvironment(string $rootPath): void
{
if (file_exists($rootPath . '/.env')) {
$dotenv = Dotenv::createImmutable($rootPath);
$dotenv->load();
}
}
private static function loadConfiguration(string $rootPath, array $config): array
{
// Load base configuration
$configPath = $rootPath . '/config';
// Container configuration
if (file_exists($configPath . '/container.php')) {
$containerConfig = require $configPath . '/container.php';
$config = array_merge($config, $containerConfig);
}
// Services configuration
if (file_exists($configPath . '/services.php')) {
$servicesConfig = require $configPath . '/services.php';
$config['definitions'] = array_merge(
$config['definitions'] ?? [],
$servicesConfig
);
}
// Database configuration
if (file_exists($configPath . '/database.php')) {
$config['database'] = require $configPath . '/database.php';
}
// Cache configuration
if (file_exists($configPath . '/cache.php')) {
$config['cache'] = require $configPath . '/cache.php';
}
// Environment from .env
$config['environment'] = $_ENV['APP_ENV'] ?? 'development';
$config['debug'] = filter_var($_ENV['APP_DEBUG'] ?? false, FILTER_VALIDATE_BOOLEAN);
return $config;
}
}

View file

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\DependencyInjection;
use DI\Container as DIContainer;
use DI\ContainerBuilder;
use Psr\Container\ContainerInterface;
class Container implements ContainerInterface
{
private DIContainer $container;
public function __construct(DIContainer $container)
{
$this->container = $container;
}
public function get(string $id): mixed
{
return $this->container->get($id);
}
public function has(string $id): bool
{
return $this->container->has($id);
}
public function make(string $name, array $parameters = []): mixed
{
return $this->container->make($name, $parameters);
}
public function call(callable $callable, array $parameters = []): mixed
{
return $this->container->call($callable, $parameters);
}
public function injectOn(object $instance): object
{
return $this->container->injectOn($instance);
}
public function getWrappedContainer(): DIContainer
{
return $this->container;
}
}

View file

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\DependencyInjection;
use DI\ContainerBuilder;
use TorrentPier\Infrastructure\DependencyInjection\Definitions\ApplicationDefinitions;
use TorrentPier\Infrastructure\DependencyInjection\Definitions\DomainDefinitions;
use TorrentPier\Infrastructure\DependencyInjection\Definitions\InfrastructureDefinitions;
use TorrentPier\Infrastructure\DependencyInjection\Definitions\PresentationDefinitions;
class ContainerFactory
{
public static function create(array $config = []): Container
{
$builder = new ContainerBuilder();
// Configure container settings
self::configureContainer($builder, $config);
// Add definitions from all layers
self::addDefinitions($builder, $config);
// Build the container
$diContainer = $builder->build();
$container = new Container($diContainer);
// Register and boot service providers
self::registerProviders($container, $config);
return $container;
}
private static function configureContainer(ContainerBuilder $builder, array $config): void
{
// Enable compilation in production for better performance
$isProduction = ($config['environment'] ?? 'development') === 'production';
if ($isProduction) {
$builder->enableCompilation($config['compilation_dir'] ?? __DIR__ . '/../../../internal_data/cache/container');
$builder->writeProxiesToFile(true, $config['proxies_dir'] ?? __DIR__ . '/../../../internal_data/cache/proxies');
}
// Enable autowiring by default
$builder->useAutowiring($config['autowiring'] ?? true);
}
private static function addDefinitions(ContainerBuilder $builder, array $config): void
{
// Add config definitions first
if (isset($config['definitions'])) {
$builder->addDefinitions($config['definitions']);
}
// Add layer-specific definitions
$builder->addDefinitions(DomainDefinitions::getDefinitions());
$builder->addDefinitions(ApplicationDefinitions::getDefinitions());
$builder->addDefinitions(InfrastructureDefinitions::getDefinitions($config));
$builder->addDefinitions(PresentationDefinitions::getDefinitions());
// Add custom definition files if provided
if (isset($config['definition_files'])) {
foreach ($config['definition_files'] as $file) {
if (file_exists($file)) {
$builder->addDefinitions($file);
}
}
}
}
private static function registerProviders(Container $container, array $config): void
{
$providers = $config['providers'] ?? [];
// Instantiate providers
$instances = [];
foreach ($providers as $providerClass) {
if (class_exists($providerClass)) {
$provider = new $providerClass();
if ($provider instanceof ServiceProvider) {
$instances[] = $provider;
$provider->register($container);
}
}
}
// Boot all providers after registration
foreach ($instances as $provider) {
$provider->boot($container);
}
}
}

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\DependencyInjection\Definitions;
use DI;
use Psr\Container\ContainerInterface;
class ApplicationDefinitions
{
public static function getDefinitions(): array
{
return [
// Command Bus
// 'CommandBusInterface' => DI\factory(function (ContainerInterface $c) {
// return new CommandBus($c);
// }),
// Query Bus
// 'QueryBusInterface' => DI\factory(function (ContainerInterface $c) {
// return new QueryBus($c);
// }),
// Event Dispatcher
// 'EventDispatcherInterface' => DI\factory(function (ContainerInterface $c) {
// return new EventDispatcher();
// }),
// Application Services
// These typically orchestrate domain objects and handle use cases
// Forum Handlers
// 'TorrentPier\Application\Forum\Handler\CreatePostHandler' => DI\autowire(),
// 'TorrentPier\Application\Forum\Handler\GetThreadListHandler' => DI\autowire(),
// Tracker Handlers
// 'TorrentPier\Application\Tracker\Handler\RegisterTorrentHandler' => DI\autowire(),
// 'TorrentPier\Application\Tracker\Handler\ProcessAnnounceHandler' => DI\autowire(),
// User Handlers
// 'TorrentPier\Application\User\Handler\RegisterUserHandler' => DI\autowire(),
// 'TorrentPier\Application\User\Handler\AuthenticateUserHandler' => DI\autowire(),
];
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\DependencyInjection\Definitions;
use DI;
use Psr\Container\ContainerInterface;
class DomainDefinitions
{
public static function getDefinitions(): array
{
return [
// Domain services should not depend on infrastructure
// These are typically created by factories in the application layer
// Example domain service factory definitions:
// 'TorrentPier\Domain\Forum\Repository\ForumRepositoryInterface' => DI\factory(function (ContainerInterface $c) {
// return $c->get('TorrentPier\Infrastructure\Persistence\Repository\ForumRepository');
// }),
// 'TorrentPier\Domain\Tracker\Repository\TorrentRepositoryInterface' => DI\factory(function (ContainerInterface $c) {
// return $c->get('TorrentPier\Infrastructure\Persistence\Repository\TorrentRepository');
// }),
// 'TorrentPier\Domain\User\Repository\UserRepositoryInterface' => DI\factory(function (ContainerInterface $c) {
// return $c->get('TorrentPier\Infrastructure\Persistence\Repository\UserRepository');
// }),
];
}
}

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\DependencyInjection\Definitions;
use DI;
use Nette\Caching\Cache;
use Nette\Caching\Storages\FileStorage;
use Nette\Caching\Storages\MemcachedStorage;
use Nette\Caching\Storages\SQLiteStorage;
use Nette\Database\Connection;
use Psr\Container\ContainerInterface;
class InfrastructureDefinitions
{
public static function getDefinitions(array $config = []): array
{
return [
// TODO: Add infrastructure service definitions as they are implemented
// Example: Database Connection (implement when Nette Database integration is ready)
// 'database.connection.default' => DI\factory(function () use ($config) {
// $dbConfig = $config['database'] ?? [];
// $dsn = sprintf(
// 'mysql:host=%s;port=%s;dbname=%s;charset=%s',
// $dbConfig['host'] ?? '127.0.0.1',
// $dbConfig['port'] ?? 3306,
// $dbConfig['database'] ?? 'tp',
// $dbConfig['charset'] ?? 'utf8mb4'
// );
//
// return new Connection(
// $dsn,
// $dbConfig['username'] ?? 'root',
// $dbConfig['password'] ?? ''
// );
// }),
// Example: Cache Storage (implement when cache infrastructure is ready)
// 'cache.storage' => DI\factory(function () use ($config) {
// $cacheConfig = $config['cache'] ?? [];
// $driver = $cacheConfig['driver'] ?? 'file';
//
// switch ($driver) {
// case 'memcached':
// $memcached = new \Memcached();
// $memcached->addServer(
// $cacheConfig['memcached']['host'] ?? '127.0.0.1',
// $cacheConfig['memcached']['port'] ?? 11211
// );
// return new MemcachedStorage($memcached);
//
// case 'sqlite':
// return new SQLiteStorage($cacheConfig['sqlite']['path'] ?? __DIR__ . '/../../../../internal_data/cache/cache.db');
//
// case 'file':
// default:
// return new FileStorage($cacheConfig['file']['path'] ?? __DIR__ . '/../../../../internal_data/cache');
// }
// }),
// Example: Repository Implementations (implement when repositories are created)
// 'TorrentPier\Infrastructure\Persistence\Repository\ForumRepository' => DI\autowire()
// ->constructorParameter('connection', DI\get('database.connection.default'))
// ->constructorParameter('cache', DI\get('cache.factory')),
// Example: Email Service (implement when email infrastructure is ready)
// 'EmailServiceInterface' => DI\factory(function (ContainerInterface $c) use ($config) {
// $emailConfig = $config['email'] ?? [];
// return new SmtpEmailService($emailConfig);
// }),
// Example: File Storage (implement when file storage abstraction is ready)
// 'FileStorageInterface' => DI\factory(function (ContainerInterface $c) use ($config) {
// $storageConfig = $config['storage'] ?? [];
// return match ($storageConfig['driver'] ?? 'local') {
// 's3' => new S3FileStorage($storageConfig['s3']),
// default => new LocalFileStorage($storageConfig['local']['path'] ?? __DIR__ . '/../../../../internal_data/uploads'),
// };
// }),
];
}
}

View file

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\DependencyInjection\Definitions;
use DI;
use Psr\Container\ContainerInterface;
class PresentationDefinitions
{
public static function getDefinitions(): array
{
return [
// HTTP Controllers
// Controllers are typically autowired with their dependencies
// Web Controllers
// 'TorrentPier\Presentation\Http\Controllers\Web\HomeController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Web\ForumController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Web\TrackerController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Web\UserController' => DI\autowire(),
// API Controllers
// 'TorrentPier\Presentation\Http\Controllers\Api\UserController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Api\TorrentController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Api\ForumController' => DI\autowire(),
// Admin Controllers
// 'TorrentPier\Presentation\Http\Controllers\Admin\DashboardController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Admin\UserController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Admin\ForumController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Admin\TrackerController' => DI\autowire(),
// Middleware
// 'AuthenticationMiddleware' => DI\autowire('TorrentPier\Presentation\Http\Middleware\AuthenticationMiddleware'),
// 'AuthorizationMiddleware' => DI\autowire('TorrentPier\Presentation\Http\Middleware\AuthorizationMiddleware'),
// 'CorsMiddleware' => DI\autowire('TorrentPier\Presentation\Http\Middleware\CorsMiddleware'),
// 'RateLimitMiddleware' => DI\autowire('TorrentPier\Presentation\Http\Middleware\RateLimitMiddleware'),
// CLI Commands
// 'TorrentPier\Presentation\Cli\Commands\CacheCommand' => DI\autowire(),
// 'TorrentPier\Presentation\Cli\Commands\MigrateCommand' => DI\autowire(),
// 'TorrentPier\Presentation\Cli\Commands\SeedCommand' => DI\autowire(),
// 'TorrentPier\Presentation\Cli\Commands\TrackerCommand' => DI\autowire(),
// View/Response Transformers
// 'JsonResponseTransformer' => DI\autowire('TorrentPier\Presentation\Http\Responses\JsonResponseTransformer'),
// 'HtmlResponseTransformer' => DI\autowire('TorrentPier\Presentation\Http\Responses\HtmlResponseTransformer'),
];
}
}

View file

@ -0,0 +1,171 @@
# Dependency Injection Infrastructure
This directory contains the dependency injection container implementation using PHP-DI, following hexagonal architecture principles.
## Architecture Overview
The DI container is placed in the Infrastructure layer because:
- Dependency injection is a technical implementation detail
- It handles wiring and bootstrapping, not business logic
- The domain layer remains pure without framework dependencies
## Structure
```
DependencyInjection/
├── Container.php # Wrapper around PHP-DI container
├── ContainerFactory.php # Factory for building configured containers
├── Bootstrap.php # Application bootstrapper
└── Definitions/ # Service definitions by layer
├── DomainDefinitions.php
├── ApplicationDefinitions.php
├── InfrastructureDefinitions.php
└── PresentationDefinitions.php
```
## Usage
### Basic Bootstrap (Works Now)
```php
use TorrentPier\Infrastructure\DependencyInjection\Bootstrap;
// Initialize the container
$container = Bootstrap::init(__DIR__ . '/../..');
// Basic usage
$containerInstance = app(); // Get container itself
$hasService = $container->has('some.service'); // Check if service exists
```
### Manual Container Creation (Works Now)
```php
use TorrentPier\Infrastructure\DependencyInjection\ContainerFactory;
$config = [
'environment' => 'production',
'definitions' => [
'custom.service' => \DI\factory(function () {
return new CustomService();
}),
],
];
$container = ContainerFactory::create($config);
```
### Future Usage (When Services Are Implemented)
```php
// These will work when the respective layers are implemented:
// $userRepository = $container->get(UserRepositoryInterface::class);
// $commandBus = $container->get(CommandBusInterface::class);
```
## Service Definitions
Services are organized by architectural layer following the hexagonal architecture spec:
### Domain Layer (`DomainDefinitions.php`)
- Repository interface mappings (when implemented in Phase 2)
- Domain service factories
- No direct infrastructure dependencies
### Application Layer (`ApplicationDefinitions.php`)
- Command/Query buses (when implemented in Phase 3)
- Command/Query handlers
- Event dispatcher
- Application services
### Infrastructure Layer (`InfrastructureDefinitions.php`)
- Database connections (when Nette Database integration is ready)
- Cache implementations (when cache infrastructure is ready)
- Repository implementations (when implemented in Phase 4)
- External service adapters
### Presentation Layer (`PresentationDefinitions.php`)
- HTTP controllers (when implemented in Phase 5)
- CLI commands
- Middleware
- Response transformers
**Note**: Most definitions are currently commented out as examples until the actual services are implemented according to the implementation phases.
## Configuration
Configuration is loaded from multiple sources:
1. **Environment Variables** (`.env` file)
2. **Configuration Files** (`/config/*.php`)
3. **Runtime Configuration** (passed to factory)
### Production Optimization
In production mode, the container:
- Compiles definitions for performance
- Generates proxies for lazy loading
- Caches resolved dependencies
Enable by setting `APP_ENV=production` in your `.env` file.
## Best Practices
1. **Use Interfaces**: Define interfaces in domain, implement in infrastructure
2. **Explicit Definitions**: Prefer explicit over magic for complex services
3. **Layer Separation**: Keep definitions organized by architectural layer
4. **Lazy Loading**: Use factories for expensive services
5. **Immutable Services**: Services should be stateless and immutable
## Example Service Registration
### Current Usage (Works Now)
```php
// In services.php or custom definitions
return [
'custom.service' => \DI\factory(function () {
return new CustomService();
}),
'test.service' => \DI\autowire(TestService::class),
];
```
### Future Examples (When Infrastructure Is Ready)
```php
// These will be uncommented when the services are implemented:
// UserRepositoryInterface::class => autowire(UserRepository::class)
// ->constructorParameter('connection', get('database.connection.default'))
// ->constructorParameter('cache', get('cache.factory')),
//
// 'email.service' => factory(function (ContainerInterface $c) {
// $config = $c->get('config')['email'];
// return new SmtpEmailService($config);
// }),
```
## Testing
For testing, create a test container with mocked services:
```php
// Current testing approach (works now)
$testConfig = [
'definitions' => [
'test.service' => \DI\factory(function () {
return new MockTestService();
}),
],
'environment' => 'testing',
];
$container = ContainerFactory::create($testConfig);
// Future testing (when services are implemented)
// $testConfig = [
// 'definitions' => [
// UserRepositoryInterface::class => $mockUserRepository,
// EmailServiceInterface::class => $mockEmailService,
// ],
// ];
```

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\DependencyInjection;
interface ServiceProvider
{
/**
* Register services in the container
*
* @param Container $container
* @return void
*/
public function register(Container $container): void;
/**
* Bootstrap services after all providers have been registered
*
* @param Container $container
* @return void
*/
public function boot(Container $container): void;
}

50
src/helpers.php Normal file
View file

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use TorrentPier\Infrastructure\DependencyInjection\Bootstrap;
use TorrentPier\Infrastructure\DependencyInjection\Container;
if (!function_exists('container')) {
/**
* Get the dependency injection container instance
*
* @return Container|null
*/
function container(): ?Container
{
return Bootstrap::getContainer();
}
}
if (!function_exists('app')) {
/**
* Get a service from the container or the container itself
*
* @param string|null $id Service identifier
* @return mixed
* @throws RuntimeException If container is not initialized or service not found
*/
function app(?string $id = null): mixed
{
$container = container();
if ($container === null) {
throw new RuntimeException('Container has not been initialized. Call Bootstrap::init() first.');
}
if ($id === null) {
return $container;
}
try {
return $container->get($id);
} catch (NotFoundExceptionInterface $e) {
throw new RuntimeException("Service '$id' not found in container: " . $e->getMessage(), 0, $e);
} catch (ContainerExceptionInterface $e) {
throw new RuntimeException("Container error while resolving '$id': " . $e->getMessage(), 0, $e);
}
}
}

View file

@ -0,0 +1,193 @@
<?php
use TorrentPier\Infrastructure\DependencyInjection\Bootstrap;
use TorrentPier\Infrastructure\DependencyInjection\Container;
describe('Container Integration', function () {
afterEach(function () {
Bootstrap::reset();
});
it('can bootstrap the container with real configuration', function () {
$rootPath = $this->createTestRootDirectory();
$this->createTestConfigFiles($rootPath, [
'services' => [
'integration.test' => 'integration_value',
],
]);
$container = Bootstrap::init($rootPath);
expect($container)->toBeInstanceOf(Container::class);
expect($container->get('integration.test'))->toBe('integration_value');
removeTempDirectory($rootPath);
});
it('integrates with global helper functions', function () {
$rootPath = $this->createTestRootDirectory();
$this->createTestConfigFiles($rootPath, [
'services' => [
'helper.test' => 'helper_value',
],
]);
Bootstrap::init($rootPath);
// Test container() helper
expect(container())->toBeInstanceOf(Container::class);
// Test app() helper without parameter
expect(app())->toBeInstanceOf(Container::class);
// Test app() helper with service ID
expect(app('helper.test'))->toBe('helper_value');
removeTempDirectory($rootPath);
});
it('handles missing services gracefully in helpers', function () {
$rootPath = $this->createTestRootDirectory();
Bootstrap::init($rootPath);
// Should throw RuntimeException for missing service
expect(fn() => app('missing.service'))
->toThrow(RuntimeException::class)
->toThrow('not found in container');
removeTempDirectory($rootPath);
});
it('supports autowiring for simple classes', function () {
$rootPath = $this->createTestRootDirectory();
$container = Bootstrap::init($rootPath);
// Should be able to autowire stdClass
expect($container->has(stdClass::class))->toBeTrue();
expect($container->get(stdClass::class))->toBeInstanceOf(stdClass::class);
removeTempDirectory($rootPath);
});
it('loads all architectural layer definitions', function () {
$rootPath = $this->createTestRootDirectory();
$container = Bootstrap::init($rootPath);
// Container should be created successfully with all layer definitions loaded
// Even though most definitions are commented out, the loading should work
expect($container)->toBeInstanceOf(Container::class);
// Container should have itself registered
expect($container->get(Container::class))->toBe($container);
expect($container->get('container'))->toBe($container);
removeTempDirectory($rootPath);
});
it('supports environment-based configuration', function () {
$rootPath = $this->createTestRootDirectory();
$this->createTestConfigFiles($rootPath, [
'container' => [
'environment' => 'production',
'compilation_dir' => $rootPath . '/internal_data/cache/container',
'proxies_dir' => $rootPath . '/internal_data/cache/proxies',
],
]);
$container = Bootstrap::init($rootPath);
expect($container)->toBeInstanceOf(Container::class);
removeTempDirectory($rootPath);
});
it('supports service provider registration', function () {
$testProviderClass = new class implements \TorrentPier\Infrastructure\DependencyInjection\ServiceProvider {
public static bool $wasRegistered = false;
public static bool $wasBooted = false;
public function register(\TorrentPier\Infrastructure\DependencyInjection\Container $container): void
{
self::$wasRegistered = true;
$container->getWrappedContainer()->set('provider.test', 'provider_registered');
}
public function boot(\TorrentPier\Infrastructure\DependencyInjection\Container $container): void
{
self::$wasBooted = true;
}
};
$rootPath = $this->createTestRootDirectory();
$this->createTestConfigFiles($rootPath, [
'container' => [
'providers' => [get_class($testProviderClass)],
],
]);
$container = Bootstrap::init($rootPath);
expect($testProviderClass::$wasRegistered)->toBeTrue();
expect($testProviderClass::$wasBooted)->toBeTrue();
expect($container->get('provider.test'))->toBe('provider_registered');
removeTempDirectory($rootPath);
});
it('handles configuration file loading priority', function () {
$rootPath = $this->createTestRootDirectory();
$this->createTestConfigFiles($rootPath, [
'services' => [
'priority.test' => \DI\factory(function () {
return 'from_services_file';
}),
],
]);
// Initialize with runtime config that should override file config
$container = Bootstrap::init($rootPath, [
'definitions' => [
'priority.test' => \DI\factory(function () {
return 'from_runtime_config';
}),
'runtime.only' => \DI\factory(function () {
return 'runtime_value';
}),
],
]);
// Runtime config should override file config
expect($container->get('priority.test'))->toBe('from_runtime_config');
expect($container->get('runtime.only'))->toBe('runtime_value');
removeTempDirectory($rootPath);
});
it('provides meaningful error messages', function () {
$rootPath = $this->createTestRootDirectory();
Bootstrap::init($rootPath);
try {
app('definitely.missing.service');
fail('Expected exception to be thrown');
} catch (RuntimeException $e) {
expect($e->getMessage())->toContain('definitely.missing.service');
expect($e->getMessage())->toContain('not found in container');
}
removeTempDirectory($rootPath);
});
it('supports performance measurement', function () {
$rootPath = $this->createTestRootDirectory();
$time = measureExecutionTime(function () use ($rootPath) {
Bootstrap::init($rootPath);
});
// Container initialization should be reasonably fast
expect($time)->toBeLessThan(1.0); // Should take less than 1 second
removeTempDirectory($rootPath);
});
});

View file

@ -1,5 +0,0 @@
<?php
test('example', function () {
expect(true)->toBeTrue();
});

View file

@ -12,6 +12,7 @@
*/
pest()->extend(Tests\TestCase::class)->in('Feature');
pest()->extend(Tests\TestCase::class)->in('Unit');
/*
|--------------------------------------------------------------------------
@ -28,22 +29,6 @@ expect()->extend('toBeOne', function () {
return $this->toBe(1);
});
expect()->extend('toBeValidDatabaseConfig', function () {
$requiredKeys = ['dbhost', 'dbport', 'dbname', 'dbuser', 'dbpasswd', 'charset', 'persist'];
foreach ($requiredKeys as $key) {
if (!array_key_exists($key, $this->value)) {
return $this->toBeNull("Missing required config key: $key");
}
}
return $this->toBeArray();
});
expect()->extend('toHaveDebugInfo', function () {
return $this->toHaveKeys(['sql', 'src', 'file', 'line', 'time']);
});
/*
|--------------------------------------------------------------------------
| Functions
@ -55,236 +40,6 @@ expect()->extend('toHaveDebugInfo', function () {
|
*/
use Nette\Caching\Storages\MemoryStorage;
use Nette\Database\Connection;
use Nette\Database\ResultSet;
use TorrentPier\Cache\CacheManager;
use TorrentPier\Cache\DatastoreManager;
use TorrentPier\Cache\UnifiedCacheSystem;
use TorrentPier\Database\Database;
use TorrentPier\Database\DatabaseDebugger;
/**
* Test Environment Setup
*/
function setupTestEnvironment(): void
{
// Define test constants if not already defined
if (!defined('BB_ROOT')) {
define('BB_ROOT', __DIR__ . '/../');
}
if (!defined('INC_DIR')) {
define('INC_DIR', BB_ROOT . 'library/includes');
}
if (!defined('SQL_PREPEND_SRC')) {
define('SQL_PREPEND_SRC', true);
}
if (!defined('SQL_CALC_QUERY_TIME')) {
define('SQL_CALC_QUERY_TIME', true);
}
if (!defined('SQL_LOG_SLOW_QUERIES')) {
define('SQL_LOG_SLOW_QUERIES', false);
}
if (!defined('LOG_SEPR')) {
define('LOG_SEPR', ' | ');
}
if (!defined('LOG_LF')) {
define('LOG_LF', "\n");
}
}
/**
* Database Test Configuration
*/
function getTestDatabaseConfig(): array
{
return [
'dbhost' => 'localhost',
'dbport' => 3306,
'dbname' => 'test_torrentpier',
'dbuser' => 'test_user',
'dbpasswd' => 'test_password',
'charset' => 'utf8mb4',
'persist' => false
];
}
function getInvalidDatabaseConfig(): array
{
return [
'dbhost' => 'nonexistent.host',
'dbport' => 9999,
'dbname' => 'invalid_db',
'dbuser' => 'invalid_user',
'dbpasswd' => 'invalid_password',
'charset' => 'utf8mb4',
'persist' => false
];
}
/**
* Mock Database Components
*/
function mockDatabase(): Database
{
$mock = Mockery::mock(Database::class);
$mock->shouldReceive('init')->andReturn(true);
$mock->shouldReceive('connect')->andReturn(true);
$mock->shouldReceive('sql_query')->andReturn(mockResultSet());
$mock->shouldReceive('num_rows')->andReturn(1);
$mock->shouldReceive('affected_rows')->andReturn(1);
$mock->shouldReceive('sql_nextid')->andReturn(123);
$mock->shouldReceive('close')->andReturn(true);
return $mock;
}
function mockResultSet(): ResultSet
{
$mock = Mockery::mock(ResultSet::class);
// For testing purposes, just return null to indicate empty result set
// This avoids complex Row object mocking and type issues
$mock->shouldReceive('fetch')->andReturn(null);
$mock->shouldReceive('getRowCount')->andReturn(0);
return $mock;
}
function mockConnection(): Connection
{
$mock = Mockery::mock(Connection::class);
$mock->shouldReceive('query')->andReturn(mockResultSet());
$mock->shouldReceive('getInsertId')->andReturn(123);
$mock->shouldReceive('getPdo')->andReturn(mockPdo());
return $mock;
}
function mockPdo(): PDO
{
$mock = Mockery::mock(PDO::class);
$mock->shouldReceive('prepare')->andReturn(mockPdoStatement());
$mock->shouldReceive('errorInfo')->andReturn(['00000', null, null]);
return $mock;
}
function mockPdoStatement(): PDOStatement
{
$mock = Mockery::mock(PDOStatement::class);
$mock->shouldReceive('execute')->andReturn(true);
$mock->shouldReceive('fetch')->andReturn(['id' => 1, 'name' => 'test']);
$mock->shouldReceive('fetchAll')->andReturn([['id' => 1, 'name' => 'test']]);
return $mock;
}
function mockDatabaseDebugger(): DatabaseDebugger
{
$mockDb = mockDatabase();
$mock = Mockery::mock(DatabaseDebugger::class, [$mockDb]);
$mock->shouldReceive('debug')->andReturn(true);
$mock->shouldReceive('debug_find_source')->andReturn('test.php(123)');
$mock->shouldReceive('log_query')->andReturn(true);
$mock->shouldReceive('log_error')->andReturn(true);
return $mock;
}
/**
* Mock Cache Components
*/
function mockCacheManager(): CacheManager
{
$mock = Mockery::mock(CacheManager::class);
$mock->shouldReceive('get')->andReturn('test_value');
$mock->shouldReceive('set')->andReturn(true);
$mock->shouldReceive('rm')->andReturn(true);
$mock->shouldReceive('load')->andReturn('test_value');
$mock->shouldReceive('save')->andReturn(true);
$mock->shouldReceive('clean')->andReturn(true);
return $mock;
}
function mockDatastoreManager(): DatastoreManager
{
$mock = Mockery::mock(DatastoreManager::class);
$mock->shouldReceive('get')->andReturn(['test' => 'data']);
$mock->shouldReceive('store')->andReturn(true);
$mock->shouldReceive('update')->andReturn(true);
$mock->shouldReceive('rm')->andReturn(true);
$mock->shouldReceive('clean')->andReturn(true);
return $mock;
}
function mockMemoryStorage(): MemoryStorage
{
return new MemoryStorage();
}
/**
* Test Data Factories
*/
function createTestUser(array $overrides = []): array
{
return array_merge([
'id' => 1,
'username' => 'testuser',
'email' => 'test@example.com',
'active' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
], $overrides);
}
function createTestTorrent(array $overrides = []): array
{
return array_merge([
'id' => 1,
'info_hash' => 'test_hash_' . uniqid(),
'name' => 'Test Torrent',
'size' => 1048576,
'seeders' => 5,
'leechers' => 2,
'completed' => 10
], $overrides);
}
function createTestCacheConfig(): array
{
return [
'prefix' => 'test_',
'engine' => 'Memory',
'enabled' => true,
'ttl' => 3600
];
}
/**
* Exception Testing Helpers
*/
function expectException(callable $callback, string $exceptionClass, ?string $message = null): void
{
try {
$callback();
fail("Expected exception $exceptionClass was not thrown");
} catch (Exception $e) {
expect($e)->toBeInstanceOf($exceptionClass);
if ($message) {
expect($e->getMessage())->toContain($message);
}
}
}
/**
* Performance Testing Helpers
*/
@ -301,150 +56,6 @@ function expectExecutionTimeUnder(callable $callback, float $maxSeconds): void
expect($time)->toBeLessThan($maxSeconds, "Execution took {$time}s, expected under {$maxSeconds}s");
}
/**
* Database Query Testing Helpers
*/
function createSelectQuery(array $options = []): array
{
return array_merge([
'SELECT' => '*',
'FROM' => 'test_table',
'WHERE' => '1=1',
'ORDER BY' => 'id ASC',
'LIMIT' => '10'
], $options);
}
function createInsertQuery(array $data = []): array
{
$defaultData = ['name' => 'test', 'value' => 'test_value'];
return [
'INSERT' => 'test_table',
'VALUES' => array_merge($defaultData, $data)
];
}
function createUpdateQuery(array $data = [], string $where = 'id = 1'): array
{
$defaultData = ['updated_at' => date('Y-m-d H:i:s')];
return [
'UPDATE' => 'test_table',
'SET' => array_merge($defaultData, $data),
'WHERE' => $where
];
}
function createDeleteQuery(string $where = 'id = 1'): array
{
return [
'DELETE' => 'test_table',
'WHERE' => $where
];
}
/**
* Cache Testing Helpers
*/
function createTestCacheKey(string $suffix = ''): string
{
return 'test_key_' . uniqid() . ($suffix ? '_' . $suffix : '');
}
function createTestCacheValue(array $data = []): array
{
return array_merge([
'data' => 'test_value',
'timestamp' => time(),
'version' => '1.0'
], $data);
}
/**
* Debug Testing Helpers
*/
function createDebugEntry(array $overrides = []): array
{
return array_merge([
'sql' => 'SELECT * FROM test_table',
'src' => 'test.php(123)',
'file' => 'test.php',
'line' => '123',
'time' => 0.001,
'info' => 'Test query',
'mem_before' => 1024,
'mem_after' => 1024
], $overrides);
}
function assertDebugEntryValid(array $entry): void
{
expect($entry)->toHaveDebugInfo();
expect($entry['sql'])->toBeString();
expect($entry['time'])->toBeFloat();
expect($entry['src'])->toBeString();
}
/**
* Cleanup Helpers
*/
function cleanupSingletons(): void
{
// Reset database instances
if (class_exists(Database::class) && method_exists(Database::class, 'destroyInstances')) {
Database::destroyInstances();
}
// Reset cache instances
if (class_exists(UnifiedCacheSystem::class) && method_exists(UnifiedCacheSystem::class, 'destroyInstance')) {
UnifiedCacheSystem::destroyInstance();
}
// Close mockery
Mockery::close();
}
function resetGlobalState(): void
{
// Reset any global variables that might affect tests
$_COOKIE = [];
$_SESSION = [];
// Reset any global database connections
global $db;
$db = null;
// Initialize critical global variables needed by datastore builders
mockForumBitfieldMappings();
}
/**
* Mock forum bitfield mappings needed by datastore builders
* This prevents "Trying to access array offset on null" warnings in tests
*/
function mockForumBitfieldMappings(): void
{
global $bf;
if (!isset($bf) || !isset($bf['forum_perm'])) {
$bf = [];
$bf['forum_perm'] = [
'auth_view' => 0, // AUTH_VIEW
'auth_read' => 1, // AUTH_READ
'auth_mod' => 2, // AUTH_MOD
'auth_post' => 3, // AUTH_POST
'auth_reply' => 4, // AUTH_REPLY
'auth_edit' => 5, // AUTH_EDIT
'auth_delete' => 6, // AUTH_DELETE
'auth_sticky' => 7, // AUTH_STICKY
'auth_announce' => 8, // AUTH_ANNOUNCE
'auth_vote' => 9, // AUTH_VOTE
'auth_pollcreate' => 10, // AUTH_POLLCREATE
'auth_attachments' => 11, // AUTH_ATTACH
'auth_download' => 12, // AUTH_DOWNLOAD
];
}
}
/**
* File System Helpers
*/
@ -468,53 +79,17 @@ function removeTempDirectory(string $dir): void
}
/**
* Function Mocking Helpers
* Exception Testing Helpers
*/
function mockGlobalFunction(string $functionName, $returnValue): void
function expectException(callable $callback, string $exceptionClass, ?string $message = null): void
{
if (!function_exists($functionName)) {
eval("function $functionName() { return " . var_export($returnValue, true) . "; }");
try {
$callback();
fail("Expected exception $exceptionClass was not thrown");
} catch (Exception $e) {
expect($e)->toBeInstanceOf($exceptionClass);
if ($message) {
expect($e->getMessage())->toContain($message);
}
}
}
function mockDevFunction(): void
{
if (!function_exists('dev')) {
eval('
function dev() {
return new class {
public function checkSqlDebugAllowed() { return true; }
public function formatShortQuery($query, $escape = false) { return $query; }
};
}
');
}
}
function mockBbLogFunction(): void
{
if (!function_exists('bb_log')) {
eval('function bb_log($message, $file = "test", $append = true) { return true; }');
}
}
function mockHideBbPathFunction(): void
{
if (!function_exists('hide_bb_path')) {
eval('function hide_bb_path($path) { return basename($path); }');
}
}
function mockUtimeFunction(): void
{
if (!function_exists('utime')) {
eval('function utime() { return microtime(true); }');
}
}
// Initialize test environment when Pest loads
setupTestEnvironment();
mockDevFunction();
mockBbLogFunction();
mockHideBbPathFunction();
mockUtimeFunction();

File diff suppressed because it is too large Load diff

View file

@ -3,8 +3,118 @@
namespace Tests;
use PHPUnit\Framework\TestCase as BaseTestCase;
use TorrentPier\Infrastructure\DependencyInjection\Bootstrap;
use TorrentPier\Infrastructure\DependencyInjection\Container;
use TorrentPier\Infrastructure\DependencyInjection\ContainerFactory;
abstract class TestCase extends BaseTestCase
{
//
protected function setUp(): void
{
parent::setUp();
// Reset container state for each test
Bootstrap::reset();
}
protected function tearDown(): void
{
// Clean up container state
Bootstrap::reset();
parent::tearDown();
}
/**
* Create a test container with optional custom configuration
*/
protected function createTestContainer(array $config = []): Container
{
$defaultConfig = [
'environment' => 'testing',
'autowiring' => true,
'definitions' => [],
];
return ContainerFactory::create(array_merge($defaultConfig, $config));
}
/**
* Create a container with custom service definitions
*/
protected function createContainerWithDefinitions(array $definitions): Container
{
return $this->createTestContainer([
'definitions' => $definitions,
]);
}
/**
* Create a temporary test root directory
*/
protected function createTestRootDirectory(): string
{
$tempDir = createTempDirectory();
// Create basic directory structure
mkdir($tempDir . '/config', 0755, true);
mkdir($tempDir . '/internal_data/cache', 0755, true);
return $tempDir;
}
/**
* Create test configuration files
*/
protected function createTestConfigFiles(string $rootPath, array $configs = []): void
{
$configPath = $rootPath . '/config';
// Create container.php
if (isset($configs['container'])) {
file_put_contents(
$configPath . '/container.php',
'<?php return ' . var_export($configs['container'], true) . ';'
);
}
// Create services.php - simplified approach for testing
if (isset($configs['services'])) {
$servicesContent = "<?php\n\nuse function DI\\factory;\n\nreturn [\n";
foreach ($configs['services'] as $key => $value) {
if (is_string($value)) {
$servicesContent .= " '$key' => factory(function () { return '$value'; }),\n";
}
}
$servicesContent .= "];\n";
file_put_contents($configPath . '/services.php', $servicesContent);
}
// Create .env file
if (isset($configs['env'])) {
$envContent = '';
foreach ($configs['env'] as $key => $value) {
$envContent .= "$key=$value\n";
}
file_put_contents($rootPath . '/.env', $envContent);
}
}
/**
* Assert that a service can be resolved from the container
*/
protected function assertCanResolve(Container $container, string $serviceId): void
{
$this->assertTrue($container->has($serviceId), "Container should have service: $serviceId");
$this->assertNotNull($container->get($serviceId), "Should be able to resolve service: $serviceId");
}
/**
* Assert that a service cannot be resolved from the container
*/
protected function assertCannotResolve(Container $container, string $serviceId): void
{
$this->assertFalse($container->has($serviceId), "Container should not have service: $serviceId");
}
}

View file

@ -1,461 +0,0 @@
<?php
use Nette\Caching\Cache;
use Nette\Caching\Storage;
use Nette\Caching\Storages\FileStorage;
use Nette\Caching\Storages\MemoryStorage;
use TorrentPier\Cache\CacheManager;
describe('CacheManager Class', function () {
beforeEach(function () {
resetGlobalState();
mockDevFunction();
mockBbLogFunction();
mockHideBbPathFunction();
mockUtimeFunction();
// Create memory storage for testing
$this->storage = new MemoryStorage();
$this->config = createTestCacheConfig();
$this->cacheManager = CacheManager::getInstance('test_namespace', $this->storage, $this->config);
});
afterEach(function () {
cleanupSingletons();
});
describe('Singleton Pattern', function () {
it('creates singleton instance correctly', function () {
$manager1 = CacheManager::getInstance('test', $this->storage, $this->config);
$manager2 = CacheManager::getInstance('test', $this->storage, $this->config);
expect($manager1)->toBe($manager2);
});
it('creates different instances for different namespaces', function () {
$manager1 = CacheManager::getInstance('namespace1', $this->storage, $this->config);
$manager2 = CacheManager::getInstance('namespace2', $this->storage, $this->config);
expect($manager1)->not->toBe($manager2);
});
it('stores configuration correctly', function () {
expect($this->cacheManager->prefix)->toBe($this->config['prefix']);
expect($this->cacheManager->engine)->toBe($this->config['engine']);
});
it('initializes Nette Cache with correct namespace', function () {
$cache = $this->cacheManager->getCache();
expect($cache)->toBeInstanceOf(Cache::class);
});
});
describe('Basic Cache Operations', function () {
it('stores and retrieves values correctly', function () {
$key = 'test_key';
$value = 'test_value';
$result = $this->cacheManager->set($key, $value);
expect($result)->toBeTrue();
expect($this->cacheManager->get($key))->toBe($value);
});
it('returns false for non-existent keys', function () {
$result = $this->cacheManager->get('non_existent_key');
expect($result)->toBeFalse();
});
it('handles different data types', function () {
$testCases = [
['string_key', 'string_value'],
['int_key', 42],
['float_key', 3.14],
['bool_key', true],
['array_key', ['nested' => ['data' => 'value']]],
['object_key', (object)['property' => 'value']]
];
foreach ($testCases as [$key, $value]) {
$this->cacheManager->set($key, $value);
expect($this->cacheManager->get($key))->toBe($value);
}
});
it('respects TTL expiration', function () {
$key = 'ttl_test';
$value = 'expires_soon';
// Set with 1 second TTL
$this->cacheManager->set($key, $value, 1);
// Should be available immediately
expect($this->cacheManager->get($key))->toBe($value);
// Wait for expiration (simulate with manual cache clear for testing)
$this->cacheManager->clean([Cache::All => true]);
// Should be expired now
expect($this->cacheManager->get($key))->toBeFalse();
});
it('handles zero TTL as permanent storage', function () {
$key = 'permanent_key';
$value = 'permanent_value';
$this->cacheManager->set($key, $value, 0);
expect($this->cacheManager->get($key))->toBe($value);
});
});
describe('Cache Removal', function () {
beforeEach(function () {
// Set up test data
$this->cacheManager->set('key1', 'value1');
$this->cacheManager->set('key2', 'value2');
$this->cacheManager->set('key3', 'value3');
});
it('removes individual keys', function () {
$result = $this->cacheManager->rm('key1');
expect($result)->toBeTrue();
expect($this->cacheManager->get('key1'))->toBeFalse();
expect($this->cacheManager->get('key2'))->toBe('value2'); // Others should remain
});
it('removes all keys when null is passed', function () {
$result = $this->cacheManager->rm(null);
expect($result)->toBeTrue();
expect($this->cacheManager->get('key1'))->toBeFalse();
expect($this->cacheManager->get('key2'))->toBeFalse();
expect($this->cacheManager->get('key3'))->toBeFalse();
});
it('removes specific key using remove method', function () {
$this->cacheManager->remove('key2');
expect($this->cacheManager->get('key2'))->toBeFalse();
expect($this->cacheManager->get('key1'))->toBe('value1'); // Others should remain
});
});
describe('Advanced Nette Caching Features', function () {
it('loads with callback function', function () {
$key = 'callback_test';
$callbackExecuted = false;
$result = $this->cacheManager->load($key, function () use (&$callbackExecuted) {
$callbackExecuted = true;
return 'callback_result';
});
expect($result)->toBe('callback_result');
expect($callbackExecuted)->toBeTrue();
// Second call should use cached value
$callbackExecuted = false;
$result2 = $this->cacheManager->load($key);
expect($result2)->toBe('callback_result');
expect($callbackExecuted)->toBeFalse(); // Callback should not be executed again
});
it('saves with dependencies', function () {
$key = 'dependency_test';
$value = 'dependent_value';
$dependencies = [
Cache::Expire => '1 hour',
Cache::Tags => ['user', 'data']
];
expect(fn() => $this->cacheManager->save($key, $value, $dependencies))->not->toThrow(Exception::class);
expect($this->cacheManager->get($key))->toBe($value);
});
it('performs bulk loading', function () {
// Pre-populate some data
$this->cacheManager->set('bulk1', 'value1');
$this->cacheManager->set('bulk2', 'value2');
$keys = ['bulk1', 'bulk2', 'bulk3'];
$results = $this->cacheManager->bulkLoad($keys);
expect($results)->toBeArray();
expect($results)->toHaveCount(3);
});
it('memoizes function calls', function () {
// Test with string function name instead of closure to avoid serialization
$callCount = 0;
// Create a global counter for testing
$GLOBALS['test_call_count'] = 0;
// Define a named function that can be cached
if (!function_exists('test_expensive_function')) {
function test_expensive_function($param)
{
$GLOBALS['test_call_count']++;
return "result_$param";
}
}
// Reset counter
$GLOBALS['test_call_count'] = 0;
// For closures that can't be serialized, just test that the method exists
// and doesn't throw exceptions with simpler data
expect(method_exists($this->cacheManager, 'call'))->toBeTrue();
// Test with serializable function name
$result1 = $this->cacheManager->call('test_expensive_function', 'test');
expect($result1)->toBe('result_test');
expect($GLOBALS['test_call_count'])->toBe(1);
});
it('wraps functions for memoization', function () {
// Test that wrap method exists and is callable, but skip actual closure wrapping
// due to serialization limitations in test environment
expect(method_exists($this->cacheManager, 'wrap'))->toBeTrue();
// For actual wrapping test, use a simple approach that doesn't rely on closure serialization
if (!function_exists('test_double_function')) {
function test_double_function($x)
{
return $x * 2;
}
}
// Test with named function
$wrappedFunction = $this->cacheManager->wrap('test_double_function');
expect($wrappedFunction)->toBeCallable();
expect($wrappedFunction(5))->toBe(10);
});
it('captures output', function () {
// Output capture is complex in test environment, just verify method exists
expect(method_exists($this->cacheManager, 'capture'))->toBeTrue();
// Capture method may start output buffering which is hard to test cleanly
// Skip actual capture test to avoid buffer conflicts
expect(true)->toBeTrue();
});
});
describe('Cache Cleaning', function () {
beforeEach(function () {
// Set up test data with tags
$this->cacheManager->save('tagged1', 'value1', [Cache::Tags => ['tag1', 'tag2']]);
$this->cacheManager->save('tagged2', 'value2', [Cache::Tags => ['tag2', 'tag3']]);
$this->cacheManager->save('untagged', 'value3');
});
it('cleans cache by criteria', function () {
expect(fn() => $this->cacheManager->clean([Cache::All => true]))->not->toThrow(Exception::class);
// All items should be removed
expect($this->cacheManager->get('tagged1'))->toBeFalse();
expect($this->cacheManager->get('tagged2'))->toBeFalse();
expect($this->cacheManager->get('untagged'))->toBeFalse();
});
it('cleans cache by tags if supported', function () {
// This depends on the storage supporting tags
expect(fn() => $this->cacheManager->clean([Cache::Tags => ['tag1']]))->not->toThrow(Exception::class);
});
});
describe('Debug Functionality', function () {
it('initializes debug properties', function () {
expect($this->cacheManager->dbg_enabled)->toBeBool();
// Reset num_queries as it may have been incremented by previous operations
$this->cacheManager->num_queries = 0;
expect($this->cacheManager->num_queries)->toBe(0);
expect($this->cacheManager->dbg)->toBeArray();
});
it('tracks query count', function () {
$initialQueries = $this->cacheManager->num_queries;
$this->cacheManager->set('debug_test', 'value');
$this->cacheManager->get('debug_test');
expect($this->cacheManager->num_queries)->toBeGreaterThan($initialQueries);
});
it('captures debug information when enabled', function () {
$this->cacheManager->dbg_enabled = true;
$this->cacheManager->set('debug_key', 'debug_value');
if ($this->cacheManager->dbg_enabled) {
expect($this->cacheManager->dbg)->not->toBeEmpty();
}
});
it('finds debug source information', function () {
$source = $this->cacheManager->debug_find_source();
expect($source)->toBeString();
});
it('handles debug timing correctly', function () {
$this->cacheManager->dbg_enabled = true;
$this->cacheManager->debug('start', 'test_operation');
usleep(1000); // 1ms delay
$this->cacheManager->debug('stop');
expect($this->cacheManager->cur_query_time)->toBeFloat();
expect($this->cacheManager->cur_query_time)->toBeGreaterThan(0);
});
});
describe('Storage Integration', function () {
it('provides access to underlying storage', function () {
$storage = $this->cacheManager->getStorage();
expect($storage)->toBeInstanceOf(Storage::class);
// Note: Due to possible storage wrapping/transformation, check type instead of reference
expect($storage)->toBeInstanceOf(get_class($this->storage));
});
it('provides access to Nette Cache instance', function () {
$cache = $this->cacheManager->getCache();
expect($cache)->toBeInstanceOf(Cache::class);
});
it('works with different storage types', function () {
// Test with file storage
$tempDir = createTempDirectory();
$fileStorage = new FileStorage($tempDir);
$fileManager = CacheManager::getInstance('file_test', $fileStorage, $this->config);
$fileManager->set('file_key', 'file_value');
expect($fileManager->get('file_key'))->toBe('file_value');
removeTempDirectory($tempDir);
});
});
describe('Error Handling', function () {
it('handles storage errors gracefully', function () {
// Mock a storage that throws exceptions
$mockStorage = Mockery::mock(Storage::class);
$mockStorage->shouldReceive('write')->andThrow(new Exception('Storage error'));
$mockStorage->shouldReceive('lock')->andReturn(true);
$errorManager = CacheManager::getInstance('error_test', $mockStorage, $this->config);
// Only set() method has exception handling - get() method will throw
expect($errorManager->set('any_key', 'any_value'))->toBeFalse();
// Test that the method exists but note that get() doesn't handle storage errors
expect(method_exists($errorManager, 'get'))->toBeTrue();
});
it('handles invalid cache operations', function () {
// Test with null values - note that CacheManager converts null to false for backward compatibility
expect($this->cacheManager->set('null_test', null))->toBeTrue();
// Due to backward compatibility, null values are returned as false when not found
// But when explicitly stored as null, they should return null
$result = $this->cacheManager->get('null_test');
expect($result === null || $result === false)->toBeTrue();
});
});
describe('Magic Properties', function () {
it('provides legacy database property', function () {
$db = $this->cacheManager->__get('db');
expect($db)->toBeObject();
expect($db->dbg)->toBeArray();
expect($db->engine)->toBe($this->cacheManager->engine);
});
it('throws exception for invalid properties', function () {
expect(fn() => $this->cacheManager->__get('invalid_property'))
->toThrow(InvalidArgumentException::class);
});
});
describe('Performance Testing', function () {
it('handles high-volume operations efficiently')
->group('performance')
->expect(function () {
return measureExecutionTime(function () {
for ($i = 0; $i < 1000; $i++) {
$this->cacheManager->set("perf_key_$i", "value_$i");
$this->cacheManager->get("perf_key_$i");
}
});
})
->toBeLessThan(1.0); // 1 second for 1000 operations
it('maintains consistent performance across operations', function () {
$times = [];
for ($i = 0; $i < 10; $i++) {
$time = measureExecutionTime(function () use ($i) {
$this->cacheManager->set("consistency_$i", "value_$i");
$this->cacheManager->get("consistency_$i");
});
$times[] = $time;
}
$averageTime = array_sum($times) / count($times);
expect($averageTime)->toBeLessThan(0.01); // 10ms average
});
});
describe('Memory Usage', function () {
it('handles large datasets efficiently', function () {
$largeData = array_fill(0, 1000, str_repeat('x', 1000)); // 1MB of data
expect(fn() => $this->cacheManager->set('large_data', $largeData))->not->toThrow(Exception::class);
expect($this->cacheManager->get('large_data'))->toBe($largeData);
});
it('handles concurrent cache operations', function () {
// Simulate concurrent operations
$keys = [];
for ($i = 0; $i < 100; $i++) {
$key = "concurrent_$i";
$keys[] = $key;
$this->cacheManager->set($key, "value_$i");
}
// Verify all operations completed successfully
foreach ($keys as $i => $key) {
expect($this->cacheManager->get($key))->toBe("value_$i");
}
});
});
describe('Backward Compatibility', function () {
it('maintains legacy Cache API compatibility', function () {
// Test that all legacy methods exist and work
expect(method_exists($this->cacheManager, 'get'))->toBeTrue();
expect(method_exists($this->cacheManager, 'set'))->toBeTrue();
expect(method_exists($this->cacheManager, 'rm'))->toBeTrue();
// Test legacy behavior
expect($this->cacheManager->get('non_existent'))->toBeFalse(); // Returns false, not null
});
it('provides backward compatible debug properties', function () {
expect(property_exists($this->cacheManager, 'num_queries'))->toBeTrue();
expect(property_exists($this->cacheManager, 'dbg'))->toBeTrue();
expect(property_exists($this->cacheManager, 'dbg_enabled'))->toBeTrue();
});
});
});

View file

@ -1,516 +0,0 @@
<?php
use Nette\Caching\Cache;
use Nette\Caching\Storage;
use Nette\Caching\Storages\MemoryStorage;
use TorrentPier\Cache\CacheManager;
use TorrentPier\Cache\DatastoreManager;
describe('DatastoreManager Class', function () {
beforeEach(function () {
resetGlobalState();
mockDevFunction();
mockBbLogFunction();
mockHideBbPathFunction();
mockUtimeFunction();
// Create memory storage for testing
$this->storage = new MemoryStorage();
$this->config = createTestCacheConfig();
$this->datastore = DatastoreManager::getInstance($this->storage, $this->config);
});
afterEach(function () {
cleanupSingletons();
});
describe('Singleton Pattern', function () {
it('creates singleton instance correctly', function () {
$manager1 = DatastoreManager::getInstance($this->storage, $this->config);
$manager2 = DatastoreManager::getInstance($this->storage, $this->config);
expect($manager1)->toBe($manager2);
});
it('initializes with correct configuration', function () {
expect($this->datastore->engine)->toBe($this->config['engine']);
expect($this->datastore->dbg_enabled)->toBeBool();
});
it('creates underlying cache manager', function () {
$cacheManager = $this->datastore->getCacheManager();
expect($cacheManager)->toBeInstanceOf(CacheManager::class);
});
});
describe('Known Items Configuration', function () {
it('defines known datastore items', function () {
expect($this->datastore->known_items)->toBeArray();
expect($this->datastore->known_items)->not->toBeEmpty();
});
it('includes essential datastore items', function () {
$essentialItems = [
'cat_forums',
'censor',
'moderators',
'stats',
'ranks',
'ban_list',
'attach_extensions',
'smile_replacements'
];
foreach ($essentialItems as $item) {
expect($this->datastore->known_items)->toHaveKey($item);
expect($this->datastore->known_items[$item])->toBeString();
expect($this->datastore->known_items[$item])->toContain('.php');
}
});
it('maps items to builder scripts', function () {
expect($this->datastore->known_items['cat_forums'])->toBe('build_cat_forums.php');
expect($this->datastore->known_items['censor'])->toBe('build_censor.php');
expect($this->datastore->known_items['moderators'])->toBe('build_moderators.php');
});
});
describe('Data Storage and Retrieval', function () {
it('stores and retrieves data correctly', function () {
$testData = ['test' => 'value', 'number' => 42];
$result = $this->datastore->store('test_item', $testData);
expect($result)->toBeTrue();
expect($this->datastore->get('test_item'))->toBe($testData);
});
it('handles different data types', function () {
$testCases = [
['string_item', 'string_value'],
['int_item', 42],
['float_item', 3.14],
['bool_item', true],
['array_item', ['nested' => ['data' => 'value']]],
['object_item', (object)['property' => 'value']]
];
foreach ($testCases as [$key, $value]) {
$this->datastore->store($key, $value);
expect($this->datastore->get($key))->toBe($value);
}
});
it('stores data permanently without TTL', function () {
$this->datastore->store('permanent_item', 'permanent_value');
// Data should persist (no TTL applied)
expect($this->datastore->get('permanent_item'))->toBe('permanent_value');
});
it('updates existing data', function () {
$this->datastore->store('update_test', 'original_value');
$this->datastore->store('update_test', 'updated_value');
expect($this->datastore->get('update_test'))->toBe('updated_value');
});
});
describe('Queue Management', function () {
it('enqueues items for batch loading', function () {
$items = ['item1', 'item2', 'item3'];
$this->datastore->enqueue($items);
expect($this->datastore->queued_items)->toContain('item1', 'item2', 'item3');
});
it('avoids duplicate items in queue', function () {
$this->datastore->enqueue(['item1', 'item2']);
$this->datastore->enqueue(['item2', 'item3']); // item2 is duplicate
expect($this->datastore->queued_items)->toHaveCount(3);
expect(array_count_values($this->datastore->queued_items)['item2'])->toBe(1);
});
it('skips already loaded items', function () {
// Pre-load data
$this->datastore->store('loaded_item', 'loaded_value');
$this->datastore->enqueue(['loaded_item', 'new_item']);
expect($this->datastore->queued_items)->not->toContain('loaded_item');
expect($this->datastore->queued_items)->toContain('new_item');
});
it('triggers fetch of queued items automatically', function () {
// Create a scenario where item is in cache but not in memory
$testItem = 'test_item';
// First store the item to put it in cache and memory
$this->datastore->store($testItem, 'test_value');
// Manually clear from memory to simulate cache-only state
unset($this->datastore->data[$testItem]);
// Directly enqueue the item
$this->datastore->queued_items = [$testItem];
// Verify item is queued
expect($this->datastore->queued_items)->toContain($testItem);
// Manually call _fetch_from_store to simulate the cache retrieval part
$this->datastore->_fetch_from_store();
// Verify the item was loaded back from cache into memory
expect($this->datastore->data)->toHaveKey($testItem);
expect($this->datastore->data[$testItem])->toBe('test_value');
// Now manually clear the queue (simulating what _fetch() does)
$this->datastore->queued_items = [];
// Verify queue is cleared
expect($this->datastore->queued_items)->toBeEmpty();
});
});
describe('Memory Management', function () {
it('removes data from memory cache', function () {
$this->datastore->store('memory_test1', 'value1');
$this->datastore->store('memory_test2', 'value2');
$this->datastore->rm('memory_test1');
// Should be removed from memory but might still be in cache
expect($this->datastore->data)->not->toHaveKey('memory_test1');
expect($this->datastore->data)->toHaveKey('memory_test2');
});
it('removes multiple items from memory', function () {
$this->datastore->store('multi1', 'value1');
$this->datastore->store('multi2', 'value2');
$this->datastore->store('multi3', 'value3');
$this->datastore->rm(['multi1', 'multi3']);
expect($this->datastore->data)->not->toHaveKey('multi1');
expect($this->datastore->data)->toHaveKey('multi2');
expect($this->datastore->data)->not->toHaveKey('multi3');
});
});
describe('Cache Cleaning', function () {
it('cleans all datastore cache', function () {
$this->datastore->store('clean_test', 'value');
expect(fn() => $this->datastore->clean())->not->toThrow(Exception::class);
});
it('cleans cache by criteria', function () {
expect(fn() => $this->datastore->cleanByCriteria([Cache::All => true]))->not->toThrow(Exception::class);
});
it('cleans cache by tags if supported', function () {
$tags = ['datastore', 'test'];
expect(fn() => $this->datastore->cleanByTags($tags))->not->toThrow(Exception::class);
});
});
describe('Advanced Nette Caching Features', function () {
it('loads with dependencies', function () {
$key = 'dependency_test';
$value = 'dependent_value';
$dependencies = [Cache::Expire => '1 hour'];
expect(fn() => $this->datastore->load($key, null, $dependencies))->not->toThrow(Exception::class);
});
it('saves with dependencies', function () {
$key = 'save_dependency_test';
$value = 'dependent_value';
$dependencies = [Cache::Tags => ['datastore']];
expect(fn() => $this->datastore->save($key, $value, $dependencies))->not->toThrow(Exception::class);
});
it('uses callback for loading missing data', function () {
$key = 'callback_test';
$callbackExecuted = false;
$result = $this->datastore->load($key, function () use (&$callbackExecuted) {
$callbackExecuted = true;
return ['generated' => 'data'];
});
expect($callbackExecuted)->toBeTrue();
expect($result)->toBe(['generated' => 'data']);
});
});
describe('Builder System Integration', function () {
it('tracks builder script directory', function () {
expect($this->datastore->ds_dir)->toBe('datastore');
});
it('builds items using known scripts', function () {
// Mock INC_DIR constant if not defined
if (!defined('INC_DIR')) {
define('INC_DIR', __DIR__ . '/../../../library/includes');
}
// We can't actually build items without the real files,
// but we can test the error handling
expect(fn() => $this->datastore->_build_item('non_existent_item'))
->toThrow(Exception::class);
});
it('updates specific datastore items', function () {
// Mock the update process (would normally rebuild from database)
expect(fn() => $this->datastore->update(['censor']))->not->toThrow(Exception::class);
});
it('updates all items when requested', function () {
expect(fn() => $this->datastore->update('all'))->not->toThrow(Exception::class);
});
});
describe('Bulk Operations', function () {
it('fetches multiple items from store', function () {
// Pre-populate data
$this->datastore->store('bulk1', 'value1');
$this->datastore->store('bulk2', 'value2');
$this->datastore->enqueue(['bulk1', 'bulk2', 'bulk3']);
expect(fn() => $this->datastore->_fetch_from_store())->not->toThrow(Exception::class);
});
it('handles bulk loading efficiently', function () {
// Setup bulk data directly in memory and cache
for ($i = 1; $i <= 10; $i++) {
$this->datastore->store("bulk_item_$i", "value_$i");
}
// Now test the fetching logic without building unknown items
$items = array_map(fn($i) => "bulk_item_$i", range(1, 10));
$this->datastore->queued_items = $items;
// Test the fetch_from_store part which should work fine
expect(fn() => $this->datastore->_fetch_from_store())->not->toThrow(Exception::class);
// Manually clear the queue since we're not testing the full _fetch()
$this->datastore->queued_items = [];
// Verify items are accessible
for ($i = 1; $i <= 10; $i++) {
expect($this->datastore->data["bulk_item_$i"])->toBe("value_$i");
}
});
});
describe('Debug Integration', function () {
it('updates debug counters from cache manager', function () {
$initialQueries = $this->datastore->num_queries;
$this->datastore->store('debug_test', 'value');
$this->datastore->get('debug_test');
expect($this->datastore->num_queries)->toBeGreaterThan($initialQueries);
});
it('tracks timing information', function () {
$initialTime = $this->datastore->sql_timetotal;
$this->datastore->store('timing_test', 'value');
expect($this->datastore->sql_timetotal)->toBeGreaterThanOrEqual($initialTime);
});
it('maintains debug arrays', function () {
expect($this->datastore->dbg)->toBeArray();
expect($this->datastore->dbg_id)->toBeInt();
});
});
describe('Source Debugging', function () {
it('finds debug caller information', function () {
$caller = $this->datastore->_debug_find_caller('enqueue');
expect($caller)->toBeString();
// Caller might return "caller not found" in test environment
expect($caller)->toBeString();
if (!str_contains($caller, 'not found')) {
expect($caller)->toContain('(');
expect($caller)->toContain(')');
}
});
it('handles missing caller gracefully', function () {
$caller = $this->datastore->_debug_find_caller('non_existent_function');
expect($caller)->toBe('caller not found');
});
});
describe('Magic Methods', function () {
it('delegates property access to cache manager', function () {
// Test accessing cache manager properties
expect($this->datastore->prefix)->toBeString();
expect($this->datastore->used)->toBeTrue();
});
it('delegates method calls to cache manager', function () {
// Test calling cache manager methods
expect(fn() => $this->datastore->bulkLoad(['test1', 'test2']))->not->toThrow(Exception::class);
});
it('provides legacy database property', function () {
$db = $this->datastore->__get('db');
expect($db)->toBeObject();
expect($db->dbg)->toBeArray();
expect($db->engine)->toBe($this->datastore->engine);
});
it('throws exception for invalid property access', function () {
expect(fn() => $this->datastore->__get('invalid_property'))
->toThrow(InvalidArgumentException::class);
});
it('throws exception for invalid method calls', function () {
expect(fn() => $this->datastore->__call('invalid_method', []))
->toThrow(BadMethodCallException::class);
});
});
describe('Tag Support Detection', function () {
it('detects tag support in storage', function () {
$supportsTagsBefore = $this->datastore->supportsTags();
expect($supportsTagsBefore)->toBeBool();
});
it('returns engine information', function () {
$engine = $this->datastore->getEngine();
expect($engine)->toBe($this->config['engine']);
});
});
describe('Performance Testing', function () {
it('handles high-volume datastore operations efficiently')
->group('performance')
->expect(function () {
return measureExecutionTime(function () {
for ($i = 0; $i < 100; $i++) {
$this->datastore->store("perf_item_$i", ['data' => "value_$i"]);
$this->datastore->get("perf_item_$i");
}
});
})
->toBeLessThan(0.5); // 500ms for 100 operations
it('efficiently handles bulk enqueue operations', function () {
$items = array_map(fn($i) => "bulk_perf_$i", range(1, 1000));
$time = measureExecutionTime(function () use ($items) {
$this->datastore->enqueue($items);
});
expect($time)->toBeLessThan(0.1); // 100ms for 1000 items
});
});
describe('Error Handling', function () {
it('handles missing builder scripts', function () {
// Test with non-existent item
expect(fn() => $this->datastore->_build_item('non_existent'))
->toThrow(Exception::class);
});
it('handles empty queue operations', function () {
$this->datastore->queued_items = [];
// Should handle empty queue gracefully - just test that queue is empty
expect($this->datastore->queued_items)->toBeEmpty();
});
});
describe('Memory Optimization', function () {
it('manages memory efficiently with large datasets', function () {
// Create large dataset
$largeData = array_fill(0, 1000, str_repeat('x', 1000)); // ~1MB
expect(fn() => $this->datastore->store('large_dataset', $largeData))->not->toThrow(Exception::class);
expect($this->datastore->get('large_dataset'))->toBe($largeData);
});
it('handles concurrent datastore operations', function () {
// Simulate concurrent operations
$operations = [];
for ($i = 0; $i < 50; $i++) {
$operations[] = function () use ($i) {
$this->datastore->store("concurrent_$i", ['id' => $i, 'data' => "value_$i"]);
return $this->datastore->get("concurrent_$i");
};
}
// Execute operations
foreach ($operations as $i => $operation) {
$result = $operation();
expect($result['id'])->toBe($i);
}
});
});
describe('Backward Compatibility', function () {
it('maintains legacy Datastore API compatibility', function () {
// Test that all legacy methods exist and work
expect(method_exists($this->datastore, 'get'))->toBeTrue();
expect(method_exists($this->datastore, 'store'))->toBeTrue();
expect(method_exists($this->datastore, 'update'))->toBeTrue();
expect(method_exists($this->datastore, 'rm'))->toBeTrue();
expect(method_exists($this->datastore, 'clean'))->toBeTrue();
});
it('provides backward compatible properties', function () {
expect(property_exists($this->datastore, 'data'))->toBeTrue();
expect(property_exists($this->datastore, 'queued_items'))->toBeTrue();
expect(property_exists($this->datastore, 'known_items'))->toBeTrue();
expect(property_exists($this->datastore, 'ds_dir'))->toBeTrue();
});
it('maintains reference semantics for get method', function () {
$testData = ['modifiable' => 'data'];
$this->datastore->store('reference_test', $testData);
$retrieved = &$this->datastore->get('reference_test');
$retrieved['modifiable'] = 'modified';
// Should maintain reference semantics
expect($this->datastore->data['reference_test']['modifiable'])->toBe('modified');
});
});
describe('Integration Features', function () {
it('integrates with cache manager debug features', function () {
$cacheManager = $this->datastore->getCacheManager();
expect($this->datastore->dbg_enabled)->toBe($cacheManager->dbg_enabled);
});
it('synchronizes debug counters properly', function () {
$initialQueries = $this->datastore->num_queries;
// Perform operations through datastore
$this->datastore->store('sync_test', 'value');
$this->datastore->get('sync_test');
// Counters should be synchronized
$cacheManager = $this->datastore->getCacheManager();
expect($this->datastore->num_queries)->toBe($cacheManager->num_queries);
});
});
});

View file

@ -1,90 +0,0 @@
<?php
use TorrentPier\Database\Database;
describe('Database Affected Rows Fix', function () {
beforeEach(function () {
// Reset singleton instances
Database::destroyInstances();
});
afterEach(function () {
Database::destroyInstances();
});
it('has affected_rows method that returns integer', function () {
// Create a simple mock to test the method exists
$db = Mockery::mock(Database::class)->makePartial();
expect(method_exists($db, 'affected_rows'))->toBeTrue();
// Mock the method to return a value
$db->shouldReceive('affected_rows')->andReturn(1);
$result = $db->affected_rows();
expect($result)->toBeInt();
expect($result)->toBe(1);
});
it('affected_rows method returns 0 initially', function () {
$db = Mockery::mock(Database::class)->makePartial();
$db->shouldReceive('affected_rows')->andReturn(0);
expect($db->affected_rows())->toBe(0);
});
it('affected_rows can track INSERT operations', function () {
$db = Mockery::mock(Database::class)->makePartial();
// Mock that INSERT affects 1 row
$db->shouldReceive('affected_rows')->andReturn(1);
expect($db->affected_rows())->toBe(1);
});
it('affected_rows can track UPDATE operations', function () {
$db = Mockery::mock(Database::class)->makePartial();
// Mock that UPDATE affects 3 rows
$db->shouldReceive('affected_rows')->andReturn(3);
expect($db->affected_rows())->toBe(3);
});
it('affected_rows can track DELETE operations', function () {
$db = Mockery::mock(Database::class)->makePartial();
// Mock that DELETE affects 2 rows
$db->shouldReceive('affected_rows')->andReturn(2);
expect($db->affected_rows())->toBe(2);
});
it('affected_rows returns 0 when no rows affected', function () {
$db = Mockery::mock(Database::class)->makePartial();
// Mock operation that affects no rows
$db->shouldReceive('affected_rows')->andReturn(0);
expect($db->affected_rows())->toBe(0);
});
it('validates Database class has last_affected_rows property', function () {
// Test that the Database class structure supports affected_rows tracking
$reflection = new ReflectionClass(Database::class);
expect($reflection->hasProperty('last_affected_rows'))->toBeTrue();
expect($reflection->hasMethod('affected_rows'))->toBeTrue();
});
it('validates fix is present in source code', function () {
// Simple source code validation to ensure fix is in place
$databaseSource = file_get_contents(__DIR__ . '/../../../src/Database/Database.php');
// Check that our fix is present: getRowCount() usage
expect($databaseSource)->toContain('getRowCount()');
// Check that the last_affected_rows property exists
expect($databaseSource)->toContain('last_affected_rows');
});
});

View file

@ -1,572 +0,0 @@
<?php
use TorrentPier\Database\Database;
use TorrentPier\Database\DatabaseDebugger;
describe('DatabaseDebugger Class', function () {
beforeEach(function () {
Database::destroyInstances();
resetGlobalState();
mockDevFunction();
mockBbLogFunction();
mockHideBbPathFunction();
// Set up test database instance
$this->db = Database::getInstance(getTestDatabaseConfig());
$this->db->connection = mockConnection();
$this->debugger = $this->db->debugger;
});
afterEach(function () {
cleanupSingletons();
});
describe('Initialization', function () {
it('initializes with database reference', function () {
// Test that debugger is properly constructed with database reference
expect($this->debugger)->toBeInstanceOf(DatabaseDebugger::class);
// Test that it has necessary public properties/methods
expect(property_exists($this->debugger, 'dbg_enabled'))->toBe(true);
expect(property_exists($this->debugger, 'dbg'))->toBe(true);
});
it('sets up debug configuration', function () {
expect($this->debugger->dbg_enabled)->toBeBool();
expect($this->debugger->do_explain)->toBeBool();
expect($this->debugger->slow_time)->toBeFloat();
});
it('initializes debug arrays', function () {
expect($this->debugger->dbg)->toBeArray();
expect($this->debugger->dbg_id)->toBe(0);
expect($this->debugger->legacy_queries)->toBeArray();
});
it('sets up timing properties', function () {
expect($this->debugger->sql_starttime)->toBeFloat();
expect($this->debugger->cur_query_time)->toBeFloat();
});
});
describe('Debug Configuration', function () {
it('enables debug based on dev settings', function () {
// Test that debug configuration is working
$originalEnabled = $this->debugger->dbg_enabled;
// Test that the debugger has debug configuration
expect($this->debugger->dbg_enabled)->toBeBool();
expect(isset($this->debugger->dbg_enabled))->toBe(true);
});
it('enables explain based on cookie', function () {
$_COOKIE['explain'] = '1';
// Test that explain functionality can be configured
expect(property_exists($this->debugger, 'do_explain'))->toBe(true);
expect($this->debugger->do_explain)->toBeBool();
unset($_COOKIE['explain']);
});
it('respects slow query time constants', function () {
if (!defined('SQL_SLOW_QUERY_TIME')) {
define('SQL_SLOW_QUERY_TIME', 5.0);
}
$debugger = new DatabaseDebugger($this->db);
expect($debugger->slow_time)->toBe(5.0);
});
});
describe('Debug Information Collection', function () {
beforeEach(function () {
$this->debugger->dbg_enabled = true;
$this->db->cur_query = 'SELECT * FROM test_table';
});
it('captures debug info on start', function () {
$this->debugger->debug('start');
expect($this->debugger->dbg[0])->toHaveKey('sql');
expect($this->debugger->dbg[0])->toHaveKey('src');
expect($this->debugger->dbg[0])->toHaveKey('file');
expect($this->debugger->dbg[0])->toHaveKey('line');
expect($this->debugger->dbg[0]['sql'])->toContain('SELECT * FROM test_table');
});
it('captures timing info on stop', function () {
$this->debugger->debug('start');
usleep(1000); // 1ms delay
$this->debugger->debug('stop');
expect($this->debugger->dbg[0])->toHaveKey('time');
expect($this->debugger->dbg[0]['time'])->toBeFloat();
expect($this->debugger->dbg[0]['time'])->toBeGreaterThan(0);
});
it('captures memory usage if available', function () {
// Mock sys function
if (!function_exists('sys')) {
eval('function sys($what) { return $what === "mem" ? 1024 : 0; }');
}
$this->debugger->debug('start');
$this->debugger->debug('stop');
expect($this->debugger->dbg[0])->toHaveKey('mem_before');
expect($this->debugger->dbg[0])->toHaveKey('mem_after');
});
it('increments debug ID after each query', function () {
$initialId = $this->debugger->dbg_id;
$this->debugger->debug('start');
$this->debugger->debug('stop');
expect($this->debugger->dbg_id)->toBe($initialId + 1);
});
it('handles multiple debug entries', function () {
// First query
$this->db->cur_query = 'SELECT 1';
$this->debugger->debug('start');
$this->debugger->debug('stop');
// Second query
$this->db->cur_query = 'SELECT 2';
$this->debugger->debug('start');
$this->debugger->debug('stop');
expect($this->debugger->dbg)->toHaveCount(2);
expect($this->debugger->dbg[0]['sql'])->toContain('SELECT 1');
expect($this->debugger->dbg[1]['sql'])->toContain('SELECT 2');
});
});
describe('Source Detection', function () {
it('finds debug source information', function () {
$source = $this->debugger->debug_find_source();
expect($source)->toBeString();
expect($source)->toContain('(');
expect($source)->toContain(')');
});
it('extracts file path only when requested', function () {
$file = $this->debugger->debug_find_source('file');
expect($file)->toBeString();
expect($file)->toContain('.php');
});
it('extracts line number only when requested', function () {
$line = $this->debugger->debug_find_source('line');
expect($line)->toBeString();
expect(is_numeric($line) || $line === '?')->toBeTrue();
});
it('returns "src disabled" when SQL_PREPEND_SRC is false', function () {
if (defined('SQL_PREPEND_SRC')) {
// Create new constant for this test
eval('define("TEST_SQL_PREPEND_SRC", false);');
}
// This test would need modification of the actual method to test properly
// For now, we'll test the positive case
$source = $this->debugger->debug_find_source();
expect($source)->not->toBe('src disabled');
});
it('skips Database-related files in stack trace', function () {
$source = $this->debugger->debug_find_source();
// Should not contain Database.php or DatabaseDebugger.php in the result
expect($source)->not->toContain('Database.php');
expect($source)->not->toContain('DatabaseDebugger.php');
});
});
describe('Nette Explorer Detection', function () {
it('detects Nette Explorer in call stack', function () {
// Create a mock trace that includes Nette Database classes
$trace = [
['class' => 'Nette\\Database\\Table\\Selection', 'function' => 'select'],
['class' => 'TorrentPier\\Database\\DebugSelection', 'function' => 'where'],
['file' => '/path/to/DatabaseTest.php', 'function' => 'testMethod']
];
$result = $this->debugger->detectNetteExplorerInTrace($trace);
expect($result)->toBeTrue();
});
it('detects Nette Explorer by SQL syntax patterns', function () {
$netteSQL = 'SELECT `id`, `name` FROM `users` WHERE (`active` = 1)';
$result = $this->debugger->detectNetteExplorerBySqlSyntax($netteSQL);
expect($result)->toBeTrue();
});
it('does not detect regular SQL as Nette Explorer', function () {
$regularSQL = 'SELECT id, name FROM users WHERE active = 1';
$result = $this->debugger->detectNetteExplorerBySqlSyntax($regularSQL);
expect($result)->toBeFalse();
});
it('marks queries as Nette Explorer when detected', function () {
$this->debugger->markAsNetteExplorerQuery();
expect($this->debugger->is_nette_explorer_query)->toBeTrue();
});
it('resets Nette Explorer flag after query completion', function () {
$this->debugger->markAsNetteExplorerQuery();
$this->debugger->resetNetteExplorerFlag();
expect($this->debugger->is_nette_explorer_query)->toBeFalse();
});
it('adds Nette Explorer marker to debug info', function () {
$this->debugger->dbg_enabled = true;
$this->debugger->markAsNetteExplorerQuery();
$this->db->cur_query = 'SELECT `id` FROM `users`';
$this->debugger->debug('start');
$this->debugger->debug('stop');
$debugEntry = $this->debugger->dbg[0];
expect($debugEntry['is_nette_explorer'])->toBeTrue();
expect($debugEntry['info'])->toContain('[Nette Explorer]');
});
});
describe('Query Logging', function () {
beforeEach(function () {
$this->db->DBS['log_counter'] = 0;
$this->db->DBS['log_file'] = 'test_queries';
});
it('prepares for query logging', function () {
$this->debugger->log_next_query(3, 'custom_log');
expect($this->db->DBS['log_counter'])->toBe(3);
expect($this->db->DBS['log_file'])->toBe('custom_log');
});
it('logs queries when enabled', function () {
$this->debugger->log_next_query(1);
$this->db->inited = true;
$this->db->cur_query = 'SELECT 1';
$this->debugger->cur_query_time = 0.001;
$this->debugger->sql_starttime = microtime(true);
// Should not throw
expect(fn() => $this->debugger->log_query())->not->toThrow(Exception::class);
});
it('logs slow queries when they exceed threshold', function () {
$this->debugger->slow_time = 0.001; // Very low threshold
$this->debugger->cur_query_time = 0.002; // Exceeds threshold
$this->db->cur_query = 'SELECT SLEEP(1)';
expect(fn() => $this->debugger->log_slow_query())->not->toThrow(Exception::class);
});
it('respects slow query cache setting', function () {
// Mock CACHE function
if (!function_exists('CACHE')) {
eval('
function CACHE($name) {
return new class {
public function get($key) { return true; } // Indicates not to log
};
}
');
}
$this->debugger->slow_time = 0.001;
$this->debugger->cur_query_time = 0.002;
// Should not log due to cache setting
expect(fn() => $this->debugger->log_slow_query())->not->toThrow(Exception::class);
});
});
describe('Error Logging', function () {
it('logs exceptions with detailed information', function () {
$exception = new Exception('Test database error', 1064);
expect(fn() => $this->debugger->log_error($exception))->not->toThrow(Exception::class);
});
it('logs PDO exceptions with specific details', function () {
$pdoException = new PDOException('Connection failed');
$pdoException->errorInfo = ['42000', 1045, 'Access denied'];
expect(fn() => $this->debugger->log_error($pdoException))->not->toThrow(Exception::class);
});
it('logs comprehensive context information', function () {
$this->db->cur_query = 'SELECT * FROM nonexistent_table';
$this->db->selected_db = 'test_db';
$this->db->db_server = 'test_server';
$exception = new Exception('Table does not exist');
expect(fn() => $this->debugger->log_error($exception))->not->toThrow(Exception::class);
});
it('handles empty or no-error states gracefully', function () {
// Mock sql_error to return no error
$this->db->connection = mockConnection();
expect(fn() => $this->debugger->log_error())->not->toThrow(Exception::class);
});
it('checks connection status during error logging', function () {
$this->db->connection = null; // No connection
$exception = new Exception('No connection');
expect(fn() => $this->debugger->log_error($exception))->not->toThrow(Exception::class);
});
});
describe('Legacy Query Tracking', function () {
it('logs legacy queries that needed compatibility fixes', function () {
$problematicQuery = 'SELECT t.*, f.* FROM table t, forum f';
$error = 'Found duplicate columns';
$this->debugger->logLegacyQuery($problematicQuery, $error);
expect($this->debugger->legacy_queries)->not->toBeEmpty();
expect($this->debugger->legacy_queries[0]['query'])->toBe($problematicQuery);
expect($this->debugger->legacy_queries[0]['error'])->toBe($error);
});
it('marks debug entries as legacy when logging', function () {
$this->debugger->dbg_enabled = true;
// Create a debug entry first
$this->db->cur_query = 'SELECT t.*, f.*';
$this->debugger->debug('start');
$this->debugger->debug('stop');
// Now log it as legacy
$this->debugger->logLegacyQuery('SELECT t.*, f.*', 'Duplicate columns');
$debugEntry = $this->debugger->dbg[0];
expect($debugEntry['is_legacy_query'])->toBeTrue();
expect($debugEntry['info'])->toContain('LEGACY COMPATIBILITY FIX APPLIED');
});
it('records detailed legacy query information', function () {
$query = 'SELECT * FROM old_table';
$error = 'Compatibility issue';
$this->debugger->logLegacyQuery($query, $error);
$entry = $this->debugger->legacy_queries[0];
expect($entry)->toHaveKey('query');
expect($entry)->toHaveKey('error');
expect($entry)->toHaveKey('source');
expect($entry)->toHaveKey('file');
expect($entry)->toHaveKey('line');
expect($entry)->toHaveKey('time');
});
});
describe('Explain Functionality', function () {
beforeEach(function () {
$this->debugger->do_explain = true;
$this->db->cur_query = 'SELECT * FROM users WHERE active = 1';
});
it('starts explain capture for SELECT queries', function () {
expect(fn() => $this->debugger->explain('start'))->not->toThrow(Exception::class);
});
it('converts UPDATE queries to SELECT for explain', function () {
$this->db->cur_query = 'UPDATE users SET status = 1 WHERE id = 5';
expect(fn() => $this->debugger->explain('start'))->not->toThrow(Exception::class);
});
it('converts DELETE queries to SELECT for explain', function () {
$this->db->cur_query = 'DELETE FROM users WHERE status = 0';
expect(fn() => $this->debugger->explain('start'))->not->toThrow(Exception::class);
});
it('generates explain output on stop', function () {
$this->debugger->explain_hold = '<table><tr><td>explain data</td></tr>';
$this->debugger->dbg_enabled = true;
// Create debug entry
$this->debugger->debug('start');
$this->debugger->debug('stop');
// Test that explain functionality works without throwing exceptions
expect(fn() => $this->debugger->explain('stop'))->not->toThrow(Exception::class);
// Verify that explain_out is a string (the explain functionality ran)
expect($this->debugger->explain_out)->toBeString();
// If there's any output, it should contain some HTML structure
if (!empty($this->debugger->explain_out)) {
expect($this->debugger->explain_out)->toContain('<table');
}
});
it('adds explain row formatting', function () {
// Test the explain row formatting functionality
$row = [
'table' => 'users',
'type' => 'ALL',
'rows' => '1000'
];
// Test that the explain method exists and can process row data
if (method_exists($this->debugger, 'explain')) {
expect(fn() => $this->debugger->explain('add_explain_row', false, $row))
->not->toThrow(Exception::class);
} else {
// If method doesn't exist, just verify our data structure
expect($row)->toHaveKey('table');
expect($row)->toHaveKey('type');
expect($row)->toHaveKey('rows');
}
});
});
describe('Performance Optimization', function () {
it('marks slow queries for ignoring when expected', function () {
// Test that the method exists and can be called without throwing
expect(fn() => $this->debugger->expect_slow_query(60, 5))->not->toThrow(Exception::class);
});
it('respects priority levels for slow query marking', function () {
// Test that the method handles multiple calls correctly
expect(fn() => $this->debugger->expect_slow_query(30, 10))->not->toThrow(Exception::class);
expect(fn() => $this->debugger->expect_slow_query(60, 5))->not->toThrow(Exception::class);
});
});
describe('Debug Statistics', function () {
it('provides debug statistics', function () {
// Generate some actual debug data to test stats
$this->debugger->dbg_enabled = true;
// Create some debug entries
$this->db->cur_query = 'SELECT 1';
$this->debugger->debug('start');
usleep(1000);
$this->debugger->debug('stop');
// Test that the stats method exists and returns expected structure
$result = method_exists($this->debugger, 'getDebugStats') ||
!empty($this->debugger->dbg);
expect($result)->toBe(true);
});
it('clears debug data when requested', function () {
// Add some debug data first
$this->debugger->dbg = [createDebugEntry()];
$this->debugger->legacy_queries = [['query' => 'test']];
$this->debugger->dbg_id = 5;
// Test that clear methods exist and work
if (method_exists($this->debugger, 'clearDebugData')) {
$this->debugger->clearDebugData();
expect($this->debugger->dbg)->toBeEmpty();
} else {
// Manual cleanup for testing
$this->debugger->dbg = [];
$this->debugger->legacy_queries = [];
$this->debugger->dbg_id = 0;
expect($this->debugger->dbg)->toBeEmpty();
expect($this->debugger->legacy_queries)->toBeEmpty();
expect($this->debugger->dbg_id)->toBe(0);
}
});
});
describe('Timing Accuracy', function () {
it('measures query execution time accurately', function () {
$this->debugger->debug('start');
$startTime = $this->debugger->sql_starttime;
usleep(2000); // 2ms delay
$this->debugger->debug('stop');
expect($this->debugger->cur_query_time)->toBeGreaterThan(0.001);
expect($this->debugger->cur_query_time)->toBeLessThan(0.1);
});
it('accumulates total SQL time correctly', function () {
$initialTotal = $this->db->sql_timetotal;
$this->debugger->debug('start');
usleep(1000);
$this->debugger->debug('stop');
expect($this->db->sql_timetotal)->toBeGreaterThan($initialTotal);
});
it('updates DBS statistics correctly', function () {
$initialDBS = $this->db->DBS['sql_timetotal'];
$this->debugger->debug('start');
usleep(1000);
$this->debugger->debug('stop');
expect($this->db->DBS['sql_timetotal'])->toBeGreaterThan($initialDBS);
});
});
describe('Edge Cases', function () {
it('handles debugging when query is null', function () {
$this->db->cur_query = null;
$this->debugger->dbg_enabled = true;
expect(fn() => $this->debugger->debug('start'))->not->toThrow(Exception::class);
expect(fn() => $this->debugger->debug('stop'))->not->toThrow(Exception::class);
});
it('handles debugging when connection is null', function () {
$this->db->connection = null;
expect(fn() => $this->debugger->log_error(new Exception('Test')))->not->toThrow(Exception::class);
});
it('handles missing global functions gracefully', function () {
// Test when bb_log function doesn't exist
if (function_exists('bb_log')) {
// We can't really undefine it, but we can test error handling
expect(fn() => $this->debugger->log_query())->not->toThrow(Exception::class);
}
});
it('handles empty debug arrays', function () {
// Reset to empty state
$this->debugger->dbg = [];
$this->debugger->dbg_id = 0;
// Test handling of empty arrays
expect($this->debugger->dbg)->toBeEmpty();
expect($this->debugger->dbg_id)->toBe(0);
// Test that debug operations still work with empty state
expect(fn() => $this->debugger->debug('start'))->not->toThrow(Exception::class);
});
});
});

View file

@ -1,730 +0,0 @@
<?php
use Nette\Database\Connection;
use Nette\Database\Explorer;
use Nette\Database\ResultSet;
use TorrentPier\Database\Database;
use TorrentPier\Database\DatabaseDebugger;
describe('Database Class', function () {
beforeEach(function () {
// Reset singleton instances between tests
Database::destroyInstances();
// Reset any global state
resetGlobalState();
// Mock required functions that might not exist in test environment
mockDevFunction();
mockBbLogFunction();
mockHideBbPathFunction();
});
afterEach(function () {
// Clean up after each test
cleanupSingletons();
});
describe('Singleton Pattern', function () {
it('creates singleton instance with valid configuration', function () {
$config = getTestDatabaseConfig();
$instance1 = Database::getInstance($config);
$instance2 = Database::getInstance();
expect($instance1)->toBe($instance2);
expect($instance1)->toBeInstanceOf(Database::class);
});
it('creates different instances for different servers', function () {
$config = getTestDatabaseConfig();
$dbInstance = Database::getServerInstance($config, 'db');
$trackerInstance = Database::getServerInstance($config, 'tracker');
expect($dbInstance)->not->toBe($trackerInstance);
expect($dbInstance)->toBeInstanceOf(Database::class);
expect($trackerInstance)->toBeInstanceOf(Database::class);
});
it('sets first server instance as default', function () {
$config = getTestDatabaseConfig();
$trackerInstance = Database::getServerInstance($config, 'tracker');
$defaultInstance = Database::getInstance();
expect($defaultInstance)->toBe($trackerInstance);
});
it('stores configuration correctly', function () {
$config = getTestDatabaseConfig();
$db = Database::getInstance($config);
expect($db->cfg)->toBe($config);
expect($db->db_server)->toBe('db');
expect($db->cfg_keys)->toContain('dbhost', 'dbport', 'dbname');
});
it('initializes debugger on construction', function () {
$config = getTestDatabaseConfig();
$db = Database::getInstance($config);
expect($db->debugger)->toBeInstanceOf(DatabaseDebugger::class);
});
});
describe('Configuration Validation', function () {
it('validates required configuration keys', function () {
$requiredKeys = ['dbhost', 'dbport', 'dbname', 'dbuser', 'dbpasswd', 'charset', 'persist'];
$config = getTestDatabaseConfig();
foreach ($requiredKeys as $key) {
expect($config)->toHaveKey($key);
}
});
it('validates configuration has correct structure', function () {
$config = getTestDatabaseConfig();
expect($config)->toBeValidDatabaseConfig();
});
it('handles missing configuration gracefully', function () {
$invalidConfig = ['dbhost' => 'localhost']; // Missing required keys
expect(function () use ($invalidConfig) {
Database::getInstance(array_values($invalidConfig));
})->toThrow(ValueError::class);
});
});
describe('Connection Management', function () {
it('initializes connection state correctly', function () {
$config = getTestDatabaseConfig();
$db = Database::getInstance($config);
expect($db->connection)->toBeNull();
expect($db->inited)->toBeFalse();
expect($db->num_queries)->toBe(0);
});
it('tracks initialization state', function () {
// Create a mock that doesn't try to connect to real database
$mockConnection = Mockery::mock(Connection::class);
$mockConnection->shouldReceive('connect')->andReturn(true);
$this->db = Mockery::mock(Database::class)->makePartial();
$this->db->shouldReceive('init')->andReturnNull();
$this->db->shouldReceive('connect')->andReturnNull();
$this->db->init(); // void method, just call it
expect(true)->toBeTrue(); // Just verify it completes without error
});
it('only initializes once', function () {
$this->db = Mockery::mock(Database::class)->makePartial();
$this->db->shouldReceive('init')->twice()->andReturnNull();
// Both calls should work
$this->db->init();
$this->db->init();
expect(true)->toBeTrue(); // Just verify both calls complete without error
});
it('handles connection errors gracefully', function () {
$invalidConfig = getInvalidDatabaseConfig();
$db = Database::getInstance($invalidConfig);
// Connection should fail with invalid config
expect(fn() => $db->connect())->toThrow(Exception::class);
});
});
describe('Query Execution', function () {
beforeEach(function () {
$this->db = Mockery::mock(Database::class)->makePartial();
$this->db->shouldReceive('init')->andReturnNull();
$this->db->num_queries = 0;
// Mock the debugger to prevent null pointer errors
$mockDebugger = Mockery::mock(\TorrentPier\Database\DatabaseDebugger::class);
$mockDebugger->shouldReceive('debug_find_source')->andReturn('test.php:123');
$mockDebugger->shouldReceive('debug')->andReturnNull();
$this->db->debugger = $mockDebugger;
});
it('executes SQL queries successfully', function () {
$query = 'SELECT * FROM users';
$mockResult = Mockery::mock(ResultSet::class);
$this->db->shouldReceive('sql_query')->with($query)->andReturn($mockResult);
$result = $this->db->sql_query($query);
expect($result)->toBeInstanceOf(ResultSet::class);
});
it('handles SQL query arrays', function () {
$queryArray = createSelectQuery();
$mockResult = Mockery::mock(ResultSet::class);
$this->db->shouldReceive('sql_query')->with(Mockery::type('array'))->andReturn($mockResult);
$result = $this->db->sql_query($queryArray);
expect($result)->toBeInstanceOf(ResultSet::class);
});
it('increments query counter correctly', function () {
$initialCount = $this->db->num_queries;
$mockResult = Mockery::mock(ResultSet::class);
$this->db->shouldReceive('sql_query')->andReturn($mockResult);
$this->db->shouldReceive('getQueryCount')->andReturn($initialCount + 1);
$this->db->sql_query('SELECT 1');
expect($this->db->getQueryCount())->toBe($initialCount + 1);
});
it('prepends debug source to queries when enabled', function () {
$query = 'SELECT * FROM users';
$mockResult = Mockery::mock(ResultSet::class);
$this->db->shouldReceive('sql_query')->with($query)->andReturn($mockResult);
// Mock the debug source prepending behavior
$result = $this->db->sql_query($query);
expect($result)->toBeInstanceOf(ResultSet::class);
});
it('handles query execution errors', function () {
$query = 'INVALID SQL';
$this->db->shouldReceive('sql_query')->with($query)
->andThrow(new Exception('SQL syntax error'));
expect(function () use ($query) {
$this->db->sql_query($query);
})->toThrow(Exception::class);
});
it('executes query wrapper with error handling', function () {
$this->db->shouldReceive('query_wrap')->andReturn(true);
$result = $this->db->query_wrap();
expect($result)->toBe(true);
});
});
describe('Result Processing', function () {
beforeEach(function () {
$this->db = Mockery::mock(Database::class)->makePartial();
$this->mockResult = Mockery::mock(ResultSet::class);
});
it('counts number of rows correctly', function () {
$this->mockResult->shouldReceive('getRowCount')->andReturn(5);
$this->db->shouldReceive('num_rows')->with($this->mockResult)->andReturn(5);
$count = $this->db->num_rows($this->mockResult);
expect($count)->toBe(5);
});
it('tracks affected rows', function () {
$this->db->shouldReceive('affected_rows')->andReturn(5);
$affected = $this->db->affected_rows();
expect($affected)->toBe(5);
});
it('fetches single row correctly', function () {
$mockRow = Mockery::mock(\Nette\Database\Row::class);
$mockRow->shouldReceive('toArray')->andReturn(['id' => 1, 'name' => 'test']);
$this->mockResult->shouldReceive('fetch')->andReturn($mockRow);
$this->db->shouldReceive('sql_fetchrow')->with($this->mockResult)
->andReturn(['id' => 1, 'name' => 'test']);
$row = $this->db->sql_fetchrow($this->mockResult);
expect($row)->toBe(['id' => 1, 'name' => 'test']);
});
it('fetches single field from row', function () {
$this->db->shouldReceive('sql_fetchfield')->with('name', 0, $this->mockResult)
->andReturn('test_value');
$value = $this->db->sql_fetchfield('name', 0, $this->mockResult);
expect($value)->toBe('test_value');
});
it('returns false for empty result', function () {
$this->mockResult->shouldReceive('fetch')->andReturn(null);
$this->db->shouldReceive('sql_fetchrow')->with($this->mockResult)->andReturn(false);
$row = $this->db->sql_fetchrow($this->mockResult);
expect($row)->toBe(false);
});
it('fetches multiple rows as rowset', function () {
$expectedRows = [
['id' => 1, 'name' => 'test1'],
['id' => 2, 'name' => 'test2']
];
$this->db->shouldReceive('sql_fetchrowset')->with($this->mockResult)
->andReturn($expectedRows);
$rowset = $this->db->sql_fetchrowset($this->mockResult);
expect($rowset)->toBe($expectedRows);
expect($rowset)->toHaveCount(2);
});
it('fetches rowset with field extraction', function () {
$expectedValues = ['test1', 'test2'];
$this->db->shouldReceive('sql_fetchrowset')->with($this->mockResult, 'name')
->andReturn($expectedValues);
$values = $this->db->sql_fetchrowset($this->mockResult, 'name');
expect($values)->toBe($expectedValues);
expect($values)->toHaveCount(2);
});
});
describe('SQL Building', function () {
beforeEach(function () {
$this->db = Mockery::mock(Database::class)->makePartial();
});
it('builds SELECT queries correctly', function () {
$sqlArray = [
'SELECT' => ['*'],
'FROM' => ['users'],
'WHERE' => ['active = 1']
];
$this->db->shouldReceive('build_sql')->with($sqlArray)
->andReturn('SELECT * FROM users WHERE active = 1');
$sql = $this->db->build_sql($sqlArray);
expect($sql)->toContain('SELECT *');
expect($sql)->toContain('FROM users');
expect($sql)->toContain('WHERE active = 1');
});
it('builds INSERT queries correctly', function () {
$sqlArray = [
'INSERT' => 'test_table',
'VALUES' => ['name' => 'John', 'email' => 'john@test.com']
];
$this->db->shouldReceive('build_sql')->with($sqlArray)
->andReturn("INSERT INTO test_table (name, email) VALUES ('John', 'john@test.com')");
$sql = $this->db->build_sql($sqlArray);
expect($sql)->toContain('INSERT INTO test_table');
expect($sql)->toContain('John');
expect($sql)->toContain('john@test.com');
});
it('builds UPDATE queries correctly', function () {
$sqlArray = [
'UPDATE' => 'test_table',
'SET' => ['name' => 'Jane'],
'WHERE' => ['id = 1']
];
$this->db->shouldReceive('build_sql')->with($sqlArray)
->andReturn("UPDATE test_table SET name = 'Jane' WHERE id = 1");
$sql = $this->db->build_sql($sqlArray);
expect($sql)->toContain('UPDATE test_table');
expect($sql)->toContain('Jane');
});
it('builds DELETE queries correctly', function () {
$sqlArray = [
'DELETE' => 'test_table',
'WHERE' => ['id = 1']
];
$this->db->shouldReceive('build_sql')->with($sqlArray)
->andReturn('DELETE FROM test_table WHERE id = 1');
$sql = $this->db->build_sql($sqlArray);
expect($sql)->toContain('DELETE FROM test_table');
expect($sql)->toContain('WHERE id = 1');
});
it('creates empty SQL array template', function () {
$emptyArray = $this->db->get_empty_sql_array();
expect($emptyArray)->toBeArray();
expect($emptyArray)->toHaveKey('SELECT');
expect($emptyArray)->toHaveKey('FROM');
expect($emptyArray)->toHaveKey('WHERE');
});
it('builds arrays with escaping', function () {
$data = ['name' => "O'Reilly", 'count' => 42];
$this->db->shouldReceive('build_array')->with('UPDATE', $data)
->andReturn("name = 'O\\'Reilly', count = 42");
$result = $this->db->build_array('UPDATE', $data);
expect($result)->toContain("O\\'Reilly");
expect($result)->toContain('42');
});
});
describe('Data Escaping', function () {
beforeEach(function () {
$this->db = Mockery::mock(Database::class)->makePartial();
});
it('escapes strings correctly', function () {
$testString = "O'Reilly & Associates";
$expected = "O\\'Reilly & Associates";
$this->db->shouldReceive('escape')->with($testString)->andReturn($expected);
$result = $this->db->escape($testString);
expect($result)->toBe($expected);
});
it('escapes with type checking', function () {
$this->db->shouldReceive('escape')->with(123, true)->andReturn('123');
$this->db->shouldReceive('escape')->with('test', true)->andReturn("'test'");
$intResult = $this->db->escape(123, true);
$stringResult = $this->db->escape('test', true);
expect($intResult)->toBe('123');
expect($stringResult)->toBe("'test'");
});
});
describe('Database Explorer Integration', function () {
beforeEach(function () {
$this->db = Mockery::mock(Database::class)->makePartial();
$mockExplorer = Mockery::mock(Explorer::class);
$mockSelection = Mockery::mock(\Nette\Database\Table\Selection::class);
$mockExplorer->shouldReceive('table')->andReturn($mockSelection);
$this->db->shouldReceive('getExplorer')->andReturn($mockExplorer);
});
it('provides table access through explorer', function () {
$mockSelection = Mockery::mock(\TorrentPier\Database\DebugSelection::class);
$this->db->shouldReceive('table')->with('users')->andReturn($mockSelection);
$selection = $this->db->table('users');
expect($selection)->toBeInstanceOf(\TorrentPier\Database\DebugSelection::class);
});
it('initializes explorer lazily', function () {
$mockSelection = Mockery::mock(\TorrentPier\Database\DebugSelection::class);
$this->db->shouldReceive('table')->with('posts')->andReturn($mockSelection);
// First access should initialize explorer
$selection1 = $this->db->table('posts');
$selection2 = $this->db->table('posts');
expect($selection1)->toBeInstanceOf(\TorrentPier\Database\DebugSelection::class);
expect($selection2)->toBeInstanceOf(\TorrentPier\Database\DebugSelection::class);
});
});
describe('Utility Methods', function () {
beforeEach(function () {
$this->db = Mockery::mock(Database::class)->makePartial();
});
it('gets next insert ID', function () {
$this->db->shouldReceive('sql_nextid')->andReturn(123);
$nextId = $this->db->sql_nextid();
expect($nextId)->toBe(123);
});
it('frees SQL result resources', function () {
$this->db->shouldReceive('sql_freeresult')->andReturnNull();
$this->db->sql_freeresult(); // void method, just call it
expect(true)->toBeTrue(); // Just verify it completes without error
});
it('closes database connection', function () {
$this->db->shouldReceive('close')->andReturnNull();
$this->db->close(); // void method, just call it
expect(true)->toBeTrue(); // Just verify it completes without error
});
it('provides database version information', function () {
$this->db->shouldReceive('get_version')->andReturn('8.0.25-MySQL');
$version = $this->db->get_version();
expect($version)->toBeString();
expect($version)->toContain('MySQL');
});
it('handles database errors', function () {
$expectedError = [
'code' => '42000',
'message' => 'Syntax error or access violation'
];
$this->db->shouldReceive('sql_error')->andReturn($expectedError);
$error = $this->db->sql_error();
expect($error)->toHaveKey('code');
expect($error)->toHaveKey('message');
expect($error['code'])->toBe('42000');
});
});
describe('Locking Mechanisms', function () {
beforeEach(function () {
$this->db = Mockery::mock(Database::class)->makePartial();
});
it('gets named locks', function () {
$lockName = 'test_lock';
$timeout = 10;
$this->db->shouldReceive('get_lock')->with($lockName, $timeout)->andReturn(1);
$result = $this->db->get_lock($lockName, $timeout);
expect($result)->toBe(1);
});
it('releases named locks', function () {
$lockName = 'test_lock';
$this->db->shouldReceive('release_lock')->with($lockName)->andReturn(1);
$result = $this->db->release_lock($lockName);
expect($result)->toBe(1);
});
it('checks if lock is free', function () {
$lockName = 'test_lock';
$this->db->shouldReceive('is_free_lock')->with($lockName)->andReturn(1);
$result = $this->db->is_free_lock($lockName);
expect($result)->toBe(1);
});
it('generates lock names correctly', function () {
$this->db->shouldReceive('get_lock_name')->with('test')->andReturn('BB_LOCK_test');
$lockName = $this->db->get_lock_name('test');
expect($lockName)->toContain('BB_LOCK_');
expect($lockName)->toContain('test');
});
});
describe('Shutdown Handling', function () {
beforeEach(function () {
$this->db = Mockery::mock(Database::class)->makePartial();
$this->db->shutdown = [];
});
it('adds shutdown queries', function () {
$query = 'UPDATE stats SET value = value + 1';
$this->db->shouldReceive('add_shutdown_query')->with($query)->andReturn(true);
$this->db->shouldReceive('getShutdownQueries')->andReturn([$query]);
$this->db->add_shutdown_query($query);
expect($this->db->getShutdownQueries())->toContain($query);
});
it('executes shutdown queries', function () {
$this->db->shouldReceive('add_shutdown_query')->with('SELECT 1');
$this->db->shouldReceive('exec_shutdown_queries')->andReturn(true);
$this->db->shouldReceive('getQueryCount')->andReturn(1);
$this->db->add_shutdown_query('SELECT 1');
$initialQueries = 0;
$this->db->exec_shutdown_queries();
expect($this->db->getQueryCount())->toBeGreaterThan($initialQueries);
});
it('clears shutdown queries after execution', function () {
$this->db->shouldReceive('add_shutdown_query')->with('SELECT 1');
$this->db->shouldReceive('exec_shutdown_queries')->andReturn(true);
$this->db->shouldReceive('getShutdownQueries')->andReturn([]);
$this->db->add_shutdown_query('SELECT 1');
$this->db->exec_shutdown_queries();
expect($this->db->getShutdownQueries())->toBeEmpty();
});
});
describe('Magic Methods', function () {
beforeEach(function () {
$this->db = Database::getInstance(getTestDatabaseConfig());
$this->db->debugger = mockDatabaseDebugger();
});
it('provides access to debugger properties via magic getter', function () {
$this->db->debugger->dbg_enabled = true;
$value = $this->db->__get('dbg_enabled');
expect($value)->toBeTrue();
});
it('checks property existence via magic isset', function () {
$exists = $this->db->__isset('dbg_enabled');
expect($exists)->toBeTrue();
});
it('returns false for non-existent properties', function () {
$exists = $this->db->__isset('non_existent_property');
expect($exists)->toBeFalse();
});
it('throws exception for invalid property access', function () {
expect(fn() => $this->db->__get('invalid_property'))
->toThrow(InvalidArgumentException::class);
});
});
describe('Performance Testing', function () {
beforeEach(function () {
$this->db = Database::getInstance(getTestDatabaseConfig());
$this->db->connection = mockConnection();
});
it('executes queries within acceptable time', function () {
expectExecutionTimeUnder(function () {
$this->db->sql_query('SELECT 1');
}, 0.01); // 10ms
});
it('handles multiple concurrent queries efficiently', function () {
expectExecutionTimeUnder(function () {
for ($i = 0; $i < 100; $i++) {
$this->db->sql_query("SELECT $i");
}
}, 0.1); // 100ms for 100 queries
});
});
describe('Error Handling', function () {
beforeEach(function () {
$this->db = Mockery::mock(Database::class)->makePartial();
});
it('handles connection errors gracefully', function () {
$invalidConfig = getInvalidDatabaseConfig();
$db = Database::getInstance($invalidConfig);
expect(fn() => $db->connect())->toThrow(Exception::class);
});
it('triggers error for query failures when using wrapper', function () {
// Mock sql_query to return null (indicating failure)
$this->db->shouldReceive('sql_query')->andReturn(null);
// Mock trigger_error to throw RuntimeException instead of calling bb_die
$this->db->shouldReceive('trigger_error')->andThrow(new \RuntimeException('Database Error'));
expect(fn() => $this->db->query('INVALID'))
->toThrow(\RuntimeException::class);
});
it('logs errors appropriately', function () {
$exception = new Exception('Test error');
// Should not throw when logging errors
$this->db->shouldReceive('logError')->with($exception)->andReturn(true);
expect(fn() => $this->db->logError($exception))
->not->toThrow(Exception::class);
});
});
describe('Legacy Compatibility', function () {
it('maintains backward compatibility with SqlDb interface', function () {
$db = Database::getInstance(getTestDatabaseConfig());
$db->connection = mockConnection();
// All these methods should exist and work
expect(method_exists($db, 'sql_query'))->toBeTrue();
expect(method_exists($db, 'sql_fetchrow'))->toBeTrue();
expect(method_exists($db, 'sql_fetchrowset'))->toBeTrue();
expect(method_exists($db, 'fetch_row'))->toBeTrue();
expect(method_exists($db, 'fetch_rowset'))->toBeTrue();
expect(method_exists($db, 'affected_rows'))->toBeTrue();
expect(method_exists($db, 'sql_nextid'))->toBeTrue();
});
it('maintains DBS statistics compatibility', function () {
$db = Database::getInstance(getTestDatabaseConfig());
expect($db->DBS)->toBeArray();
expect($db->DBS)->toHaveKey('num_queries');
expect($db->DBS)->toHaveKey('sql_timetotal');
});
});
});
// Performance test group
describe('Database Performance', function () {
beforeEach(function () {
$this->db = Database::getInstance(getTestDatabaseConfig());
$this->db->connection = mockConnection();
});
it('maintains singleton instance creation performance')
->group('performance')
->repeat(1000)
->expect(fn() => Database::getInstance())
->toBeInstanceOf(Database::class);
it('executes simple queries efficiently')
->group('performance')
->expect(function () {
return measureExecutionTime(fn() => $this->db->sql_query('SELECT 1'));
})
->toBeLessThan(0.001); // 1ms
});

View file

@ -0,0 +1,190 @@
<?php
use TorrentPier\Infrastructure\DependencyInjection\Bootstrap;
use TorrentPier\Infrastructure\DependencyInjection\Container;
describe('Bootstrap', function () {
beforeEach(function () {
// Ensure clean state for each test
Bootstrap::reset();
});
afterEach(function () {
Bootstrap::reset();
});
describe('init() method', function () {
it('initializes and returns a container', function () {
$rootPath = $this->createTestRootDirectory();
$container = Bootstrap::init($rootPath);
expect($container)->toBeInstanceOf(Container::class);
removeTempDirectory($rootPath);
});
it('returns the same container on subsequent calls', function () {
$rootPath = $this->createTestRootDirectory();
$container1 = Bootstrap::init($rootPath);
$container2 = Bootstrap::init($rootPath);
expect($container1)->toBe($container2);
removeTempDirectory($rootPath);
});
it('registers container instance with itself', function () {
$rootPath = $this->createTestRootDirectory();
$container = Bootstrap::init($rootPath);
expect($container->get(Container::class))->toBe($container);
expect($container->get('container'))->toBe($container);
removeTempDirectory($rootPath);
});
it('loads environment variables from .env file', function () {
$rootPath = $this->createTestRootDirectory();
$this->createTestConfigFiles($rootPath, [
'env' => [
'TEST_VAR' => 'test_value',
'APP_ENV' => 'testing',
],
]);
Bootstrap::init($rootPath);
expect($_ENV['TEST_VAR'] ?? null)->toBe('test_value');
expect($_ENV['APP_ENV'] ?? null)->toBe('testing');
removeTempDirectory($rootPath);
});
it('loads configuration from config files', function () {
$rootPath = $this->createTestRootDirectory();
$this->createTestConfigFiles($rootPath, [
'container' => [
'environment' => 'testing',
'autowiring' => true,
],
'services' => [
'test.service' => 'config_value',
],
]);
$container = Bootstrap::init($rootPath);
expect($container->get('test.service'))->toBe('config_value');
removeTempDirectory($rootPath);
});
it('handles missing config files gracefully', function () {
$rootPath = $this->createTestRootDirectory();
// Should not throw exception even without config files
$container = Bootstrap::init($rootPath);
expect($container)->toBeInstanceOf(Container::class);
removeTempDirectory($rootPath);
});
});
describe('getContainer() method', function () {
it('returns null when not initialized', function () {
expect(Bootstrap::getContainer())->toBeNull();
});
it('returns container after initialization', function () {
$rootPath = $this->createTestRootDirectory();
$container = Bootstrap::init($rootPath);
expect(Bootstrap::getContainer())->toBe($container);
removeTempDirectory($rootPath);
});
});
describe('reset() method', function () {
it('clears the container instance', function () {
$rootPath = $this->createTestRootDirectory();
Bootstrap::init($rootPath);
expect(Bootstrap::getContainer())->not->toBeNull();
Bootstrap::reset();
expect(Bootstrap::getContainer())->toBeNull();
removeTempDirectory($rootPath);
});
it('allows re-initialization after reset', function () {
$rootPath = $this->createTestRootDirectory();
$container1 = Bootstrap::init($rootPath);
Bootstrap::reset();
$container2 = Bootstrap::init($rootPath);
expect($container1)->not->toBe($container2);
expect($container2)->toBeInstanceOf(Container::class);
removeTempDirectory($rootPath);
});
});
describe('configuration loading', function () {
it('merges configuration from multiple sources', function () {
$rootPath = $this->createTestRootDirectory();
$this->createTestConfigFiles($rootPath, [
'env' => [
'APP_ENV' => 'production',
'APP_DEBUG' => 'false',
],
'container' => [
'autowiring' => true,
],
'services' => [
'config.service' => 'merged_config',
],
]);
$container = Bootstrap::init($rootPath, [
'definitions' => [
'runtime.service' => \DI\factory(function () {
return 'runtime_config';
}),
],
]);
expect($container->get('config.service'))->toBe('merged_config');
expect($container->get('runtime.service'))->toBe('runtime_config');
removeTempDirectory($rootPath);
});
it('sets default environment when no .env file exists', function () {
$rootPath = $this->createTestRootDirectory();
$container = Bootstrap::init($rootPath);
// Container should still be created successfully
expect($container)->toBeInstanceOf(Container::class);
removeTempDirectory($rootPath);
});
});
describe('error handling', function () {
it('handles invalid root path gracefully', function () {
// Should not throw fatal error for non-existent path
expect(function () {
Bootstrap::init('/non/existent/path');
})->not->toThrow(Error::class);
});
});
});

View file

@ -0,0 +1,206 @@
<?php
use TorrentPier\Infrastructure\DependencyInjection\Container;
use TorrentPier\Infrastructure\DependencyInjection\ContainerFactory;
use TorrentPier\Infrastructure\DependencyInjection\ServiceProvider;
describe('ContainerFactory', function () {
describe('create() method', function () {
it('creates a container instance', function () {
$container = ContainerFactory::create();
expect($container)->toBeInstanceOf(Container::class);
});
it('applies configuration correctly', function () {
$config = [
'environment' => 'testing',
'autowiring' => true,
'annotations' => false,
];
$container = ContainerFactory::create($config);
expect($container)->toBeInstanceOf(Container::class);
});
it('loads custom definitions', function () {
$config = [
'definitions' => [
'test.service' => \DI\factory(function () {
return 'test_value';
}),
],
];
$container = ContainerFactory::create($config);
expect($container->get('test.service'))->toBe('test_value');
});
it('configures autowiring when enabled', function () {
$config = ['autowiring' => true];
$container = ContainerFactory::create($config);
// Should be able to autowire stdClass
expect($container->has(stdClass::class))->toBeTrue();
});
it('loads definition files when specified', function () {
$tempDir = createTempDirectory();
$definitionFile = $tempDir . '/definitions.php';
file_put_contents($definitionFile, '<?php return [
"file.service" => \DI\factory(function () {
return "from_file";
}),
];');
$config = [
'definition_files' => [$definitionFile],
];
$container = ContainerFactory::create($config);
expect($container->get('file.service'))->toBe('from_file');
removeTempDirectory($tempDir);
});
it('handles non-existent definition files gracefully', function () {
$config = [
'definition_files' => ['/non/existent/file.php'],
];
// Should not throw an exception
$container = ContainerFactory::create($config);
expect($container)->toBeInstanceOf(Container::class);
});
});
describe('service providers', function () {
it('registers and boots service providers', function () {
$providerClass = new class implements ServiceProvider {
public static bool $registered = false;
public static bool $booted = false;
public function register(Container $container): void
{
self::$registered = true;
$container->getWrappedContainer()->set('provider.service', 'provider_value');
}
public function boot(Container $container): void
{
self::$booted = true;
}
};
$config = [
'providers' => [get_class($providerClass)],
];
$container = ContainerFactory::create($config);
expect($providerClass::$registered)->toBeTrue();
expect($providerClass::$booted)->toBeTrue();
expect($container->get('provider.service'))->toBe('provider_value');
});
it('handles invalid provider classes gracefully', function () {
$config = [
'providers' => ['NonExistentProvider'],
];
// Should not throw an exception
$container = ContainerFactory::create($config);
expect($container)->toBeInstanceOf(Container::class);
});
it('boots providers after all registrations', function () {
// Use a simpler approach without constructor dependencies
$testFile = sys_get_temp_dir() . '/provider_order_test.txt';
if (file_exists($testFile)) {
unlink($testFile);
}
$provider1Class = new class implements ServiceProvider {
public function register(Container $container): void
{
$testFile = sys_get_temp_dir() . '/provider_order_test.txt';
file_put_contents($testFile, "register1\n", FILE_APPEND);
}
public function boot(Container $container): void
{
$testFile = sys_get_temp_dir() . '/provider_order_test.txt';
file_put_contents($testFile, "boot1\n", FILE_APPEND);
}
};
$provider2Class = new class implements ServiceProvider {
public function register(Container $container): void
{
$testFile = sys_get_temp_dir() . '/provider_order_test.txt';
file_put_contents($testFile, "register2\n", FILE_APPEND);
}
public function boot(Container $container): void
{
$testFile = sys_get_temp_dir() . '/provider_order_test.txt';
file_put_contents($testFile, "boot2\n", FILE_APPEND);
}
};
$config = [
'providers' => [get_class($provider1Class), get_class($provider2Class)],
];
ContainerFactory::create($config);
// Read the order from the test file
$content = file_get_contents($testFile);
$lines = array_filter(explode("\n", trim($content)));
// All registrations should happen before any boots
expect($lines)->toBe(['register1', 'register2', 'boot1', 'boot2']);
// Clean up
unlink($testFile);
});
});
describe('environment configuration', function () {
it('enables compilation in production', function () {
$tempDir = createTempDirectory();
$config = [
'environment' => 'production',
'compilation_dir' => $tempDir . '/container',
'proxies_dir' => $tempDir . '/proxies',
];
$container = ContainerFactory::create($config);
expect($container)->toBeInstanceOf(Container::class);
removeTempDirectory($tempDir);
});
it('skips compilation in development', function () {
$config = [
'environment' => 'development',
];
$container = ContainerFactory::create($config);
expect($container)->toBeInstanceOf(Container::class);
});
});
describe('layer definitions integration', function () {
it('loads definitions from all architectural layers', function () {
$container = ContainerFactory::create();
// Container should be created successfully with all layer definitions
expect($container)->toBeInstanceOf(Container::class);
// Since most definitions are commented out, we just verify the container works
expect($container->has(stdClass::class))->toBeTrue();
});
});
});

View file

@ -0,0 +1,170 @@
<?php
use TorrentPier\Infrastructure\DependencyInjection\Container;
use TorrentPier\Infrastructure\DependencyInjection\ContainerFactory;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Container\ContainerExceptionInterface;
describe('Container', function () {
beforeEach(function () {
$this->container = $this->createTestContainer();
});
it('implements PSR-11 ContainerInterface', function () {
expect($this->container)->toBeInstanceOf(\Psr\Container\ContainerInterface::class);
});
describe('get() method', function () {
it('can resolve a simple service', function () {
$container = $this->createContainerWithDefinitions([
'test.service' => \DI\factory(function () {
return 'test_value';
}),
]);
$result = $container->get('test.service');
expect($result)->toBe('test_value');
});
it('can resolve autowired classes', function () {
$container = $this->createContainerWithDefinitions([
'test.class' => \DI\autowire(stdClass::class),
]);
$result = $container->get('test.class');
expect($result)->toBeInstanceOf(stdClass::class);
});
it('throws NotFoundExceptionInterface for non-existent services', function () {
try {
$this->container->get('non.existent.service');
fail('Expected exception to be thrown');
} catch (Exception $e) {
expect($e)->toBeInstanceOf(NotFoundExceptionInterface::class);
expect($e->getMessage())->toContain('non.existent.service');
}
});
it('returns same instance for singleton services', function () {
$container = $this->createContainerWithDefinitions([
'singleton.service' => \DI\factory(function () {
return new stdClass();
}),
]);
$instance1 = $container->get('singleton.service');
$instance2 = $container->get('singleton.service');
expect($instance1)->toBe($instance2);
});
});
describe('has() method', function () {
it('returns true for existing services', function () {
$container = $this->createContainerWithDefinitions([
'existing.service' => \DI\factory(function () {
return 'value';
}),
]);
expect($container->has('existing.service'))->toBeTrue();
});
it('returns false for non-existent services', function () {
expect($this->container->has('non.existent.service'))->toBeFalse();
});
it('returns true for autowirable classes', function () {
expect($this->container->has(stdClass::class))->toBeTrue();
});
});
describe('make() method', function () {
it('can make instances with parameters', function () {
$result = $this->container->make(stdClass::class);
expect($result)->toBeInstanceOf(stdClass::class);
});
it('creates new instances each time', function () {
$instance1 = $this->container->make(stdClass::class);
$instance2 = $this->container->make(stdClass::class);
expect($instance1)->not->toBe($instance2);
});
});
describe('call() method', function () {
it('can call closures with dependency injection', function () {
$result = $this->container->call(function (stdClass $class) {
return get_class($class);
});
expect($result)->toBe('stdClass');
});
it('can call methods with parameters', function () {
$service = new class {
public function test(string $param): string
{
return "Hello $param";
}
};
$result = $this->container->call([$service, 'test'], ['param' => 'World']);
expect($result)->toBe('Hello World');
});
});
describe('injectOn() method', function () {
it('returns the object after injection', function () {
$object = new stdClass();
$result = $this->container->injectOn($object);
expect($result)->toBe($object);
});
});
describe('getWrappedContainer() method', function () {
it('returns the underlying PHP-DI container', function () {
$wrapped = $this->container->getWrappedContainer();
expect($wrapped)->toBeInstanceOf(\DI\Container::class);
});
it('allows direct access to PHP-DI functionality', function () {
$wrapped = $this->container->getWrappedContainer();
$wrapped->set('direct.service', 'direct_value');
expect($this->container->get('direct.service'))->toBe('direct_value');
});
});
describe('error handling', function () {
it('provides meaningful error messages for missing services', function () {
try {
$this->container->get('missing.service');
fail('Expected NotFoundExceptionInterface to be thrown');
} catch (NotFoundExceptionInterface $e) {
expect($e->getMessage())->toContain('missing.service');
}
});
it('handles circular dependencies gracefully', function () {
$container = $this->createContainerWithDefinitions([
'service.a' => \DI\factory(function (\Psr\Container\ContainerInterface $c) {
return $c->get('service.b');
}),
'service.b' => \DI\factory(function (\Psr\Container\ContainerInterface $c) {
return $c->get('service.a');
}),
]);
try {
$container->get('service.a');
fail('Expected circular dependency exception');
} catch (Exception $e) {
expect($e)->toBeInstanceOf(ContainerExceptionInterface::class);
expect($e->getMessage())->toContain('Circular dependency');
}
});
});
});

View file

@ -0,0 +1,100 @@
<?php
use TorrentPier\Infrastructure\DependencyInjection\Definitions\ApplicationDefinitions;
describe('ApplicationDefinitions', function () {
describe('getDefinitions() method', function () {
it('returns an array', function () {
$definitions = ApplicationDefinitions::getDefinitions();
expect($definitions)->toBeArray();
});
it('returns empty array when no application services are implemented yet', function () {
$definitions = ApplicationDefinitions::getDefinitions();
// Since we're in Phase 1 and application services aren't implemented yet,
// the definitions should be empty (all examples are commented out)
expect($definitions)->toBe([]);
});
it('follows application layer principles', function () {
// Application layer should orchestrate domain objects
// This test verifies the structure is ready for future application services
$definitions = ApplicationDefinitions::getDefinitions();
// Should be an array (even if empty)
expect($definitions)->toBeArray();
// When application services are added, they should follow these principles:
// - Command and Query handlers
// - Application services that orchestrate domain logic
// - Event dispatchers
// - No direct infrastructure concerns
});
it('can be safely called multiple times', function () {
$definitions1 = ApplicationDefinitions::getDefinitions();
$definitions2 = ApplicationDefinitions::getDefinitions();
expect($definitions1)->toBe($definitions2);
});
it('is prepared for future command/query handlers', function () {
// This test documents the intended structure for Phase 3 implementation
$definitions = ApplicationDefinitions::getDefinitions();
expect($definitions)->toBeArray();
// Future command/query handlers will be registered like:
// 'TorrentPier\Application\User\Handler\RegisterUserHandler' => DI\autowire(),
// 'CommandBusInterface' => DI\factory(function (ContainerInterface $c) {
// return new CommandBus($c);
// }),
// For now, verify the method works without breaking
expect(count($definitions))->toBeGreaterThanOrEqual(0);
});
});
describe('architectural compliance', function () {
it('follows hexagonal architecture principles', function () {
// Application layer should orchestrate domain objects without infrastructure concerns
$definitions = ApplicationDefinitions::getDefinitions();
// Application definitions should focus on:
// 1. Command and Query handlers
// 2. Application services
// 3. Event dispatchers
// 4. Use case orchestration
expect($definitions)->toBeArray();
});
it('supports CQRS pattern', function () {
// Application layer should separate commands and queries
// This test ensures the structure supports CQRS implementation
$definitions = ApplicationDefinitions::getDefinitions();
// Future implementation will separate:
// - Command handlers (write operations)
// - Query handlers (read operations)
// - Command and Query buses
expect($definitions)->toBeArray();
});
it('prepares for event-driven architecture', function () {
// Application layer should support domain events
$definitions = ApplicationDefinitions::getDefinitions();
// Future event dispatcher will be registered here
// 'EventDispatcherInterface' => DI\factory(...)
expect($definitions)->toBeArray();
});
});
});

View file

@ -0,0 +1,84 @@
<?php
use TorrentPier\Infrastructure\DependencyInjection\Definitions\DomainDefinitions;
describe('DomainDefinitions', function () {
describe('getDefinitions() method', function () {
it('returns an array', function () {
$definitions = DomainDefinitions::getDefinitions();
expect($definitions)->toBeArray();
});
it('returns empty array when no domain services are implemented yet', function () {
$definitions = DomainDefinitions::getDefinitions();
// Since we're in Phase 1 and domain services aren't implemented yet,
// the definitions should be empty (all examples are commented out)
expect($definitions)->toBe([]);
});
it('follows domain layer principles', function () {
// Domain definitions should not contain infrastructure dependencies
// This test verifies the structure is ready for future domain services
$definitions = DomainDefinitions::getDefinitions();
// Should be an array (even if empty)
expect($definitions)->toBeArray();
// When domain services are added, they should follow these principles:
// - No framework dependencies
// - Repository interfaces mapped to implementations
// - Pure business logic services
});
it('can be safely called multiple times', function () {
$definitions1 = DomainDefinitions::getDefinitions();
$definitions2 = DomainDefinitions::getDefinitions();
expect($definitions1)->toBe($definitions2);
});
it('is prepared for future repository interface mappings', function () {
// This test documents the intended structure for Phase 2 implementation
$definitions = DomainDefinitions::getDefinitions();
expect($definitions)->toBeArray();
// Future repository interfaces will be mapped like:
// 'TorrentPier\Domain\User\Repository\UserRepositoryInterface' =>
// DI\factory(function (ContainerInterface $c) {
// return $c->get('TorrentPier\Infrastructure\Persistence\Repository\UserRepository');
// }),
// For now, verify the method works without breaking
expect(count($definitions))->toBeGreaterThanOrEqual(0);
});
});
describe('architectural compliance', function () {
it('follows hexagonal architecture principles', function () {
// Domain layer should have no infrastructure dependencies
// This test ensures the definition structure is correct
$definitions = DomainDefinitions::getDefinitions();
// Domain definitions should focus on:
// 1. Repository interface mappings
// 2. Domain service factories
// 3. No framework dependencies
expect($definitions)->toBeArray();
});
it('supports dependency injection inversion', function () {
// Domain interfaces should be mapped to infrastructure implementations
// following the dependency inversion principle
$definitions = DomainDefinitions::getDefinitions();
// Even though empty now, the structure supports proper DI mapping
expect($definitions)->toBeArray();
});
});
});

View file

@ -0,0 +1,159 @@
<?php
use TorrentPier\Infrastructure\DependencyInjection\Definitions\InfrastructureDefinitions;
describe('InfrastructureDefinitions', function () {
describe('getDefinitions() method', function () {
it('returns an array', function () {
$definitions = InfrastructureDefinitions::getDefinitions();
expect($definitions)->toBeArray();
});
it('accepts configuration parameter', function () {
$config = ['test' => 'value'];
$definitions = InfrastructureDefinitions::getDefinitions($config);
expect($definitions)->toBeArray();
});
it('returns empty array when no infrastructure services are implemented yet', function () {
$definitions = InfrastructureDefinitions::getDefinitions();
// Since we're in Phase 1 and infrastructure services aren't implemented yet,
// the definitions should be empty (all examples are commented out)
expect($definitions)->toBe([]);
});
it('follows infrastructure layer principles', function () {
// Infrastructure layer should handle external concerns
// This test verifies the structure is ready for future infrastructure services
$definitions = InfrastructureDefinitions::getDefinitions();
// Should be an array (even if empty)
expect($definitions)->toBeArray();
// When infrastructure services are added, they should follow these principles:
// - Database connections and repositories
// - Cache implementations
// - External service adapters
// - File storage systems
});
it('can be safely called multiple times', function () {
$definitions1 = InfrastructureDefinitions::getDefinitions();
$definitions2 = InfrastructureDefinitions::getDefinitions();
expect($definitions1)->toBe($definitions2);
});
it('can handle different configurations', function () {
$config1 = ['database' => ['host' => 'localhost']];
$config2 = ['cache' => ['driver' => 'redis']];
$definitions1 = InfrastructureDefinitions::getDefinitions($config1);
$definitions2 = InfrastructureDefinitions::getDefinitions($config2);
// Should handle different configs without breaking
expect($definitions1)->toBeArray();
expect($definitions2)->toBeArray();
});
it('is prepared for future database services', function () {
// This test documents the intended structure for Phase 4 implementation
$definitions = InfrastructureDefinitions::getDefinitions([
'database' => [
'host' => '127.0.0.1',
'port' => 3306,
'database' => 'tp',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
],
]);
expect($definitions)->toBeArray();
// Future database services will be registered like:
// 'database.connection.default' => DI\factory(function () use ($config) { ... }),
// Connection::class => DI\get('database.connection.default'),
// For now, verify the method works without breaking
expect(count($definitions))->toBeGreaterThanOrEqual(0);
});
it('is prepared for future cache services', function () {
$definitions = InfrastructureDefinitions::getDefinitions([
'cache' => [
'driver' => 'file',
'file' => ['path' => '/tmp/cache'],
],
]);
expect($definitions)->toBeArray();
// Future cache services will be registered like:
// 'cache.storage' => DI\factory(function () use ($config) { ... }),
// 'cache.factory' => DI\factory(function (ContainerInterface $c) { ... }),
});
});
describe('architectural compliance', function () {
it('follows hexagonal architecture principles', function () {
// Infrastructure layer should handle external concerns and adapters
$definitions = InfrastructureDefinitions::getDefinitions();
// Infrastructure definitions should focus on:
// 1. Database connections and persistence
// 2. Cache implementations
// 3. External service adapters
// 4. File storage systems
// 5. Third-party integrations
expect($definitions)->toBeArray();
});
it('supports dependency inversion', function () {
// Infrastructure should implement domain interfaces
$definitions = InfrastructureDefinitions::getDefinitions();
// Future repository implementations will be registered here:
// 'TorrentPier\Infrastructure\Persistence\Repository\UserRepository' => DI\autowire()
// ->constructorParameter('connection', DI\get('database.connection.default'))
expect($definitions)->toBeArray();
});
it('handles configuration-based service creation', function () {
// Infrastructure services should be configurable
$config = [
'database' => ['driver' => 'mysql'],
'cache' => ['driver' => 'redis'],
'storage' => ['driver' => 's3'],
];
$definitions = InfrastructureDefinitions::getDefinitions($config);
// Should handle configuration without breaking
expect($definitions)->toBeArray();
});
it('prepares for multiple database connections', function () {
$config = [
'database' => [
'default' => 'mysql',
'connections' => [
'mysql' => ['driver' => 'mysql'],
'sqlite' => ['driver' => 'sqlite'],
],
],
];
$definitions = InfrastructureDefinitions::getDefinitions($config);
expect($definitions)->toBeArray();
});
});
});

View file

@ -0,0 +1,147 @@
<?php
use TorrentPier\Infrastructure\DependencyInjection\Definitions\PresentationDefinitions;
describe('PresentationDefinitions', function () {
describe('getDefinitions() method', function () {
it('returns an array', function () {
$definitions = PresentationDefinitions::getDefinitions();
expect($definitions)->toBeArray();
});
it('returns empty array when no presentation services are implemented yet', function () {
$definitions = PresentationDefinitions::getDefinitions();
// Since we're in Phase 1 and presentation services aren't implemented yet,
// the definitions should be empty (all examples are commented out)
expect($definitions)->toBe([]);
});
it('follows presentation layer principles', function () {
// Presentation layer should handle user interface concerns
// This test verifies the structure is ready for future presentation services
$definitions = PresentationDefinitions::getDefinitions();
// Should be an array (even if empty)
expect($definitions)->toBeArray();
// When presentation services are added, they should follow these principles:
// - HTTP controllers for web and API interfaces
// - CLI commands for console operations
// - Middleware for request/response processing
// - Response transformers for output formatting
});
it('can be safely called multiple times', function () {
$definitions1 = PresentationDefinitions::getDefinitions();
$definitions2 = PresentationDefinitions::getDefinitions();
expect($definitions1)->toBe($definitions2);
});
it('is prepared for future HTTP controllers', function () {
// This test documents the intended structure for Phase 5 implementation
$definitions = PresentationDefinitions::getDefinitions();
expect($definitions)->toBeArray();
// Future HTTP controllers will be registered like:
// 'TorrentPier\Presentation\Http\Controllers\Web\HomeController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Api\UserController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Admin\DashboardController' => DI\autowire(),
// For now, verify the method works without breaking
expect(count($definitions))->toBeGreaterThanOrEqual(0);
});
it('is prepared for future CLI commands', function () {
$definitions = PresentationDefinitions::getDefinitions();
expect($definitions)->toBeArray();
// Future CLI commands will be registered like:
// 'TorrentPier\Presentation\Cli\Commands\CacheCommand' => DI\autowire(),
// 'TorrentPier\Presentation\Cli\Commands\MigrateCommand' => DI\autowire(),
});
it('is prepared for future middleware', function () {
$definitions = PresentationDefinitions::getDefinitions();
expect($definitions)->toBeArray();
// Future middleware will be registered like:
// 'AuthenticationMiddleware' => DI\autowire('TorrentPier\Presentation\Http\Middleware\AuthenticationMiddleware'),
// 'CorsMiddleware' => DI\autowire('TorrentPier\Presentation\Http\Middleware\CorsMiddleware'),
});
});
describe('architectural compliance', function () {
it('follows hexagonal architecture principles', function () {
// Presentation layer should handle user interface and external interfaces
$definitions = PresentationDefinitions::getDefinitions();
// Presentation definitions should focus on:
// 1. HTTP controllers (Web, API, Admin)
// 2. CLI commands
// 3. Middleware for request processing
// 4. Response transformers
// 5. Input validation and output formatting
expect($definitions)->toBeArray();
});
it('supports multiple interface types', function () {
// Presentation layer should support web, API, and CLI interfaces
$definitions = PresentationDefinitions::getDefinitions();
// Future implementation will include:
// - Web controllers for HTML responses
// - API controllers for JSON responses
// - Admin controllers for administrative interface
// - CLI commands for console operations
expect($definitions)->toBeArray();
});
it('prepares for middleware stack', function () {
// Presentation layer should support request/response middleware
$definitions = PresentationDefinitions::getDefinitions();
// Future middleware will handle:
// - Authentication and authorization
// - CORS headers
// - Rate limiting
// - Request validation
// - Response transformation
expect($definitions)->toBeArray();
});
it('supports dependency injection for controllers', function () {
// Controllers should have their dependencies injected
$definitions = PresentationDefinitions::getDefinitions();
// Future controllers will be autowired with dependencies:
// - Application services (command/query handlers)
// - Request validators
// - Response transformers
expect($definitions)->toBeArray();
});
it('prepares for different response formats', function () {
// Presentation layer should support multiple response formats
$definitions = PresentationDefinitions::getDefinitions();
// Future response transformers:
// 'JsonResponseTransformer' => DI\autowire(...),
// 'HtmlResponseTransformer' => DI\autowire(...),
expect($definitions)->toBeArray();
});
});
});

View file

@ -0,0 +1,144 @@
<?php
use TorrentPier\Infrastructure\DependencyInjection\Container;
use TorrentPier\Infrastructure\DependencyInjection\ServiceProvider;
describe('ServiceProvider interface', function () {
it('defines required methods', function () {
$reflection = new ReflectionClass(ServiceProvider::class);
expect($reflection->isInterface())->toBeTrue();
expect($reflection->hasMethod('register'))->toBeTrue();
expect($reflection->hasMethod('boot'))->toBeTrue();
});
it('register method has correct signature', function () {
$reflection = new ReflectionClass(ServiceProvider::class);
$method = $reflection->getMethod('register');
expect($method->isPublic())->toBeTrue();
expect($method->getParameters())->toHaveCount(1);
expect($method->getParameters()[0]->getType()?->getName())->toBe(Container::class);
expect($method->getReturnType()?->getName())->toBe('void');
});
it('boot method has correct signature', function () {
$reflection = new ReflectionClass(ServiceProvider::class);
$method = $reflection->getMethod('boot');
expect($method->isPublic())->toBeTrue();
expect($method->getParameters())->toHaveCount(1);
expect($method->getParameters()[0]->getType()?->getName())->toBe(Container::class);
expect($method->getReturnType()?->getName())->toBe('void');
});
});
describe('ServiceProvider implementation examples', function () {
it('can implement a basic service provider', function () {
$provider = new class implements ServiceProvider {
public function register(Container $container): void
{
$container->getWrappedContainer()->set('example.service', 'registered');
}
public function boot(Container $container): void
{
// Boot logic here
}
};
$container = $this->createTestContainer();
$provider->register($container);
expect($container->get('example.service'))->toBe('registered');
});
it('can implement a provider with complex services', function () {
$provider = new class implements ServiceProvider {
public function register(Container $container): void
{
$container->getWrappedContainer()->set('complex.service', \DI\factory(function () {
return new class {
public function getValue(): string
{
return 'complex_value';
}
};
}));
}
public function boot(Container $container): void
{
// Could perform additional setup here
$service = $container->get('complex.service');
// Setup complete
}
};
$container = $this->createTestContainer();
$provider->register($container);
$provider->boot($container);
$service = $container->get('complex.service');
expect($service->getValue())->toBe('complex_value');
});
it('can implement a provider that registers multiple services', function () {
$provider = new class implements ServiceProvider {
public function register(Container $container): void
{
$wrapped = $container->getWrappedContainer();
$wrapped->set('service.a', 'value_a');
$wrapped->set('service.b', 'value_b');
$wrapped->set('service.c', \DI\factory(function () {
return 'value_c';
}));
}
public function boot(Container $container): void
{
// Boot all registered services
}
};
$container = $this->createTestContainer();
$provider->register($container);
$provider->boot($container);
expect($container->get('service.a'))->toBe('value_a');
expect($container->get('service.b'))->toBe('value_b');
expect($container->get('service.c'))->toBe('value_c');
});
it('boot method can access services registered by register method', function () {
$bootedServices = [];
$provider = new class($bootedServices) implements ServiceProvider {
public function __construct(private array &$bootedServices)
{
}
public function register(Container $container): void
{
$container->getWrappedContainer()->set('bootable.service', 'registered_value');
}
public function boot(Container $container): void
{
$value = $container->get('bootable.service');
$this->bootedServices[] = $value;
}
};
$container = $this->createTestContainer();
$provider->register($container);
$provider->boot($container);
expect($bootedServices)->toBe(['registered_value']);
});
});

View file

@ -1,501 +0,0 @@
<?php
use TorrentPier\Legacy\Template;
beforeEach(function () {
// Setup test environment
setupTestEnvironment();
// Define required constants
if (!defined('CACHE_DIR')) {
define('CACHE_DIR', sys_get_temp_dir() . '/torrentpier_test_cache');
}
if (!defined('TEMPLATES_DIR')) {
define('TEMPLATES_DIR', sys_get_temp_dir() . '/torrentpier_test_templates');
}
if (!defined('XS_TPL_PREFIX')) {
define('XS_TPL_PREFIX', 'tpl__');
}
if (!defined('XS_TAG_NONE')) {
define('XS_TAG_NONE', 0);
}
if (!defined('XS_TAG_BEGIN')) {
define('XS_TAG_BEGIN', 1);
}
if (!defined('XS_TAG_END')) {
define('XS_TAG_END', 2);
}
if (!defined('XS_TAG_INCLUDE')) {
define('XS_TAG_INCLUDE', 3);
}
if (!defined('XS_TAG_IF')) {
define('XS_TAG_IF', 4);
}
if (!defined('XS_TAG_ELSE')) {
define('XS_TAG_ELSE', 5);
}
if (!defined('XS_TAG_ELSEIF')) {
define('XS_TAG_ELSEIF', 6);
}
if (!defined('XS_TAG_ENDIF')) {
define('XS_TAG_ENDIF', 7);
}
if (!defined('XS_TAG_BEGINELSE')) {
define('XS_TAG_BEGINELSE', 8);
}
// Mock required functions if they don't exist
if (!function_exists('clean_filename')) {
function clean_filename($fname)
{
static $s = ['\\', '/', ':', '*', '?', '"', '<', '>', '|', ' '];
return str_replace($s, '_', trim($fname));
}
}
if (!function_exists('config')) {
function config()
{
return new class {
public function get($key, $default = null)
{
// Return sensible defaults for template configuration
return match ($key) {
'xs_use_cache' => 0,
'default_lang' => 'en',
default => $default
};
}
};
}
}
// Create a temporary directory for templates and cache
$this->tempDir = createTempDirectory();
$this->templateDir = $this->tempDir . '/templates';
$this->cacheDir = $this->tempDir . '/cache';
mkdir($this->templateDir, 0755, true);
mkdir($this->cacheDir, 0755, true);
// Set up global language array for testing
global $lang;
$lang = [
'EXISTING_KEY' => 'This key exists',
'ANOTHER_KEY' => 'Another existing key'
];
// Create template instance
$this->template = new Template($this->templateDir);
$this->template->cachedir = $this->cacheDir . '/';
$this->template->use_cache = 0; // Disable caching for tests
});
afterEach(function () {
// Clean up
if (isset($this->tempDir)) {
removeTempDirectory($this->tempDir);
}
// Reset global state
resetGlobalState();
});
/**
* Execute a compiled template and return its output
*
* @param string $compiled The compiled template code
* @param array $variables Optional variables to set in scope (V array)
* @param array $additionalVars Optional additional variables to set in scope
* @return string The template output
*/
function executeTemplate(string $compiled, array $variables = [], array $additionalVars = []): string
{
ob_start();
global $lang;
$L = &$lang;
$V = $variables;
// Set any additional variables in scope
foreach ($additionalVars as $name => $value) {
$$name = $value;
}
// SECURITY NOTE: eval() is used intentionally here to execute compiled template code
// within a controlled test environment. While eval() poses security risks in production,
// its use is justified in this specific unit test scenario because:
// 1. We're testing the legacy template compilation system that generates PHP code
// 2. The input is controlled and comes from our own template compiler
// 3. This runs in an isolated test environment, not production
// 4. Testing the actual execution is necessary to verify template output correctness
// Future maintainers: Use extreme caution with eval() and avoid it in production code
eval('?>' . $compiled);
return ob_get_clean();
}
describe('Template Text Compilation - Graceful Fallback', function () {
it('shows missing language variables as original syntax', function () {
$template = '{L_MISSING_KEY}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled);
expect($output)->toBe('L_MISSING_KEY');
});
it('shows existing language variables correctly', function () {
$template = '{L_EXISTING_KEY}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled);
expect($output)->toBe('This key exists');
});
it('shows missing regular variables as original syntax', function () {
$template = '{MISSING_VAR}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled);
expect($output)->toBe('');
});
it('shows existing regular variables correctly', function () {
$template = '{EXISTING_VAR}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled, ['EXISTING_VAR' => 'This variable exists']);
expect($output)->toBe('This variable exists');
});
it('shows missing constants as original syntax', function () {
$template = '{#MISSING_CONSTANT#}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled);
expect($output)->toBe('');
});
it('shows existing constants correctly', function () {
// Define a test constant
if (!defined('TEST_CONSTANT')) {
define('TEST_CONSTANT', 'This constant exists');
}
$template = '{#TEST_CONSTANT#}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled);
expect($output)->toBe('This constant exists');
});
it('handles mixed existing and missing variables correctly', function () {
$template = '{L_EXISTING_KEY} - {L_MISSING_KEY} - {EXISTING_VAR} - {MISSING_VAR}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled, ['EXISTING_VAR' => 'Variable exists']);
expect($output)->toBe('This key exists - L_MISSING_KEY - Variable exists - ');
});
it('handles PHP variables correctly without fallback', function () {
$template = '{$test_var}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled, [], ['test_var' => 'PHP variable value']);
expect($output)->toBe('PHP variable value');
});
it('handles undefined PHP variables gracefully', function () {
$template = '{$undefined_var}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled);
// PHP variables that don't exist should show empty string (original behavior)
expect($output)->toBe('');
});
});
describe('Template Block Variable Fallback', function () {
it('shows missing block variables as empty string', function () {
$namespace = 'testblock';
$varname = 'MISSING_VAR';
$result = $this->template->generate_block_varref($namespace . '.', $varname);
// Block variables should show empty string when missing, not the variable name
$expectedFormat = "<?php echo isset(\$testblock_item['MISSING_VAR']) ? \$testblock_item['MISSING_VAR'] : ''; ?>";
expect($result)->toBe($expectedFormat);
});
it('generates correct PHP code for block variable fallback', function () {
$namespace = 'news';
$varname = 'TITLE';
$result = $this->template->generate_block_varref($namespace . '.', $varname);
// Block variables should show empty string when missing, not the variable name
$expectedFormat = "<?php echo isset(\$news_item['TITLE']) ? \$news_item['TITLE'] : ''; ?>";
expect($result)->toBe($expectedFormat);
});
});
describe('Compiled Code Verification', function () {
it('compiles language variables with proper fallback code', function () {
$template = '{L_MISSING_KEY}';
$compiled = $this->template->_compile_text($template);
// Verify the compiled PHP code contains the expected fallback logic
expect($compiled)->toContain("isset(\$L['MISSING_KEY'])");
expect($compiled)->toContain("'L_MISSING_KEY'");
});
it('compiles regular variables with proper fallback code', function () {
$template = '{MISSING_VAR}';
$compiled = $this->template->_compile_text($template);
// Verify the compiled PHP code contains the expected fallback logic
expect($compiled)->toContain("isset(\$V['MISSING_VAR'])");
expect($compiled)->toContain("''");
});
it('compiles constants with proper fallback code', function () {
$template = '{#MISSING_CONSTANT#}';
$compiled = $this->template->_compile_text($template);
// Verify the compiled PHP code contains the expected fallback logic
expect($compiled)->toContain("defined('MISSING_CONSTANT')");
expect($compiled)->toContain("''");
});
});
describe('Real-world Example - Admin Migrations', function () {
it('handles the original L_MIGRATIONS_FILE error gracefully', function () {
// The exact template that was causing the error
$template = '<td class="catHead" width="50%"><b>{L_MIGRATIONS_FILE}</b></td>';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled);
// Should show the fallback without braces instead of throwing an error
expect($output)->toContain('L_MIGRATIONS_FILE');
expect($output)->toContain('<td class="catHead"');
});
});
describe('Edge Cases and Robustness', function () {
it('handles empty variable names gracefully', function () {
$template = '{}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled);
// Empty braces should remain as literal text
expect($output)->toBe('{}');
});
it('handles variables with special characters in names', function () {
$template = '{VAR_WITH_UNDERSCORES} {VAR-WITH-DASHES} {VAR123NUMBERS}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled, [
'VAR_WITH_UNDERSCORES' => 'underscore value',
'VAR123NUMBERS' => 'number value'
]);
// Verify the compiled code contains proper fallback logic for special chars
expect($compiled)->toContain("isset(\$V['VAR_WITH_UNDERSCORES'])");
expect($compiled)->toContain("isset(\$V['VAR123NUMBERS'])");
// Underscores and numbers should work, dashes might not be valid variable names
expect($output)->toContain('underscore value');
expect($output)->toContain('number value');
});
it('handles HTML entities and special characters in template content', function () {
$template = '<div>&amp; {TEST_VAR} &lt;script&gt;</div>';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled, ['TEST_VAR' => 'safe content']);
// HTML entities should be preserved, variable should be substituted
expect($output)->toBe('<div>&amp; safe content &lt;script&gt;</div>');
// Verify fallback logic is present
expect($compiled)->toContain("isset(\$V['TEST_VAR'])");
});
it('handles quotes and escaping in variable values', function () {
$template = 'Value: {QUOTED_VAR}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled, [
'QUOTED_VAR' => 'Contains "quotes" and \'apostrophes\''
]);
expect($output)->toBe('Value: Contains "quotes" and \'apostrophes\'');
expect($compiled)->toContain("isset(\$V['QUOTED_VAR'])");
});
it('handles very long variable names', function () {
$longVarName = 'VERY_LONG_VARIABLE_NAME_THAT_TESTS_BUFFER_LIMITS_AND_PARSING_' . str_repeat('X', 100);
$template = '{' . $longVarName . '}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled, [$longVarName => 'long var value']);
expect($output)->toBe('long var value');
expect($compiled)->toContain("isset(\$V['$longVarName'])");
});
it('handles nested braces and malformed syntax', function () {
$template = '{{NESTED}} {UNCLOSED {NORMAL_VAR} }EXTRA}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled, ['NORMAL_VAR' => 'works']);
// Should handle the valid variable and leave malformed parts as literals
expect($output)->toContain('works');
expect($compiled)->toContain("isset(\$V['NORMAL_VAR'])");
});
it('handles empty string values with proper fallback', function () {
$template = 'Before:{EMPTY_VAR}:After';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled, ['EMPTY_VAR' => '']);
expect($output)->toBe('Before::After');
expect($compiled)->toContain("isset(\$V['EMPTY_VAR'])");
});
it('handles null and false values correctly', function () {
$template = 'Null:{NULL_VAR} False:{FALSE_VAR} Zero:{ZERO_VAR}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled, [
'NULL_VAR' => null,
'FALSE_VAR' => false,
'ZERO_VAR' => 0
]);
// PHP's string conversion: null='', false='', 0='0'
expect($output)->toBe('Null: False: Zero:0');
expect($compiled)->toContain("isset(\$V['NULL_VAR'])");
expect($compiled)->toContain("isset(\$V['FALSE_VAR'])");
expect($compiled)->toContain("isset(\$V['ZERO_VAR'])");
});
it('handles whitespace around variable names', function () {
$template = '{ SPACED_VAR } {NORMAL_VAR}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled, [
'SPACED_VAR' => 'should not work',
'NORMAL_VAR' => 'should work'
]);
// Spaces inside braces should make it not match as a variable pattern
expect($output)->toContain('should work');
expect($compiled)->toContain("isset(\$V['NORMAL_VAR'])");
});
it('handles multiple consecutive variables', function () {
$template = '{VAR1}{VAR2}{VAR3}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled, [
'VAR1' => 'A',
'VAR2' => 'B',
'VAR3' => 'C'
]);
expect($output)->toBe('ABC');
expect($compiled)->toContain("isset(\$V['VAR1'])");
expect($compiled)->toContain("isset(\$V['VAR2'])");
expect($compiled)->toContain("isset(\$V['VAR3'])");
});
it('handles variables with numeric suffixes', function () {
$template = '{VAR1} {VAR2} {VAR10} {VAR100}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled, [
'VAR1' => 'one',
'VAR2' => 'two',
'VAR10' => 'ten',
'VAR100' => 'hundred'
]);
expect($output)->toBe('one two ten hundred');
expect($compiled)->toContain("isset(\$V['VAR1'])");
expect($compiled)->toContain("isset(\$V['VAR2'])");
expect($compiled)->toContain("isset(\$V['VAR10'])");
expect($compiled)->toContain("isset(\$V['VAR100'])");
});
it('handles mixed case sensitivity correctly', function () {
$template = '{lowercase} {UPPERCASE} {MixedCase}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled, [
'lowercase' => 'lower',
'UPPERCASE' => 'upper',
'MixedCase' => 'mixed'
]);
expect($output)->toBe('lower upper mixed');
expect($compiled)->toContain("isset(\$V['lowercase'])");
expect($compiled)->toContain("isset(\$V['UPPERCASE'])");
expect($compiled)->toContain("isset(\$V['MixedCase'])");
});
it('handles language variables with special prefixes', function () {
global $lang;
$originalLang = $lang;
// Add some special test language variables
$lang['TEST_SPECIAL_CHARS'] = 'Special: &<>"\'';
$lang['TEST_UNICODE'] = 'Unicode: ñáéíóú';
$template = '{L_TEST_SPECIAL_CHARS} | {L_TEST_UNICODE} | {L_MISSING_SPECIAL}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled);
expect($output)->toBe('Special: &<>"\' | Unicode: ñáéíóú | L_MISSING_SPECIAL');
expect($compiled)->toContain("isset(\$L['TEST_SPECIAL_CHARS'])");
expect($compiled)->toContain("isset(\$L['TEST_UNICODE'])");
expect($compiled)->toContain("'L_MISSING_SPECIAL'");
// Restore original language array
$lang = $originalLang;
});
it('handles constants with edge case names', function () {
// Define some test constants with edge case names
if (!defined('TEST_CONST_123')) {
define('TEST_CONST_123', 'numeric suffix');
}
if (!defined('TEST_CONST_UNDERSCORE_')) {
define('TEST_CONST_UNDERSCORE_', 'trailing underscore');
}
$template = '{#TEST_CONST_123#} {#TEST_CONST_UNDERSCORE_#} {#UNDEFINED_CONST_EDGE#}';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled);
expect($output)->toBe('numeric suffix trailing underscore ');
expect($compiled)->toContain("defined('TEST_CONST_123')");
expect($compiled)->toContain("defined('TEST_CONST_UNDERSCORE_')");
expect($compiled)->toContain("defined('UNDEFINED_CONST_EDGE')");
});
it('handles complex nested HTML with variables', function () {
$template = '<table><tr><td>{CELL1}</td><td class="{CSS_CLASS}">{CELL2}</td></tr></table>';
$compiled = $this->template->_compile_text($template);
$output = executeTemplate($compiled, [
'CELL1' => 'First Cell',
'CSS_CLASS' => 'highlight',
'CELL2' => 'Second Cell'
]);
expect($output)->toBe('<table><tr><td>First Cell</td><td class="highlight">Second Cell</td></tr></table>');
expect($compiled)->toContain("isset(\$V['CELL1'])");
expect($compiled)->toContain("isset(\$V['CSS_CLASS'])");
expect($compiled)->toContain("isset(\$V['CELL2'])");
});
});