mirror of
https://github.com/torrentpier/torrentpier
synced 2025-08-21 22:03:49 -07:00
feat: implement dependency injection container infrastructure (#1997)
* 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. * refactor(tests): update exception handling to use Throwable - Changed exception handling in Pest.php to catch Throwable instead of Exception. - Updated BootstrapTest.php and ContainerTest.php to expect Throwable for error handling in tests. - Refactored tests to utilize expectException helper for cleaner syntax and improved readability.
This commit is contained in:
parent
39bc5977e3
commit
5b5bf49f4e
36 changed files with 2891 additions and 3902 deletions
11
CLAUDE.md
11
CLAUDE.md
|
@ -157,3 +157,14 @@ More text here.
|
||||||
|
|
||||||
### MD047 - Files should end with a single newline
|
### MD047 - Files should end with a single newline
|
||||||
Ensure every markdown file ends with exactly one newline character at the end of the file.
|
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
|
||||||
|
|
|
@ -67,6 +67,7 @@
|
||||||
"nette/caching": "^3.3",
|
"nette/caching": "^3.3",
|
||||||
"nette/database": "^3.2",
|
"nette/database": "^3.2",
|
||||||
"php-curl-class/php-curl-class": "^12.0.0",
|
"php-curl-class/php-curl-class": "^12.0.0",
|
||||||
|
"php-di/php-di": "^7.0",
|
||||||
"robmorgan/phinx": "^0.16.9",
|
"robmorgan/phinx": "^0.16.9",
|
||||||
"samdark/sitemap": "2.4.1",
|
"samdark/sitemap": "2.4.1",
|
||||||
"symfony/mailer": "^7.3",
|
"symfony/mailer": "^7.3",
|
||||||
|
@ -82,7 +83,10 @@
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"TorrentPier\\": "src/"
|
"TorrentPier\\": "src/"
|
||||||
}
|
},
|
||||||
|
"files": [
|
||||||
|
"src/helpers.php"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|
191
composer.lock
generated
191
composer.lock
generated
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "26f36d2312e2eabf3ed5ff36391cc050",
|
"content-hash": "57713d8849e71683b70d934a81f7e18c",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "arokettu/bencode",
|
"name": "arokettu/bencode",
|
||||||
|
@ -1879,6 +1879,67 @@
|
||||||
],
|
],
|
||||||
"time": "2024-09-11T14:15:04+00:00"
|
"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",
|
"name": "league/color-extractor",
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
|
@ -2740,6 +2801,134 @@
|
||||||
},
|
},
|
||||||
"time": "2025-03-25T18:04:16+00:00"
|
"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",
|
"name": "phpoption/phpoption",
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
|
|
26
config/container.php
Normal file
26
config/container.php
Normal 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
|
||||||
|
],
|
||||||
|
];
|
0
config/environments/.keep
Normal file
0
config/environments/.keep
Normal file
30
config/services.php
Normal file
30
config/services.php
Normal 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'),
|
||||||
|
];
|
99
docs/examples/di-container-usage.php
Normal file
99
docs/examples/di-container-usage.php
Normal 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'
|
||||||
|
]);
|
89
src/Infrastructure/DependencyInjection/Bootstrap.php
Normal file
89
src/Infrastructure/DependencyInjection/Bootstrap.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
49
src/Infrastructure/DependencyInjection/Container.php
Normal file
49
src/Infrastructure/DependencyInjection/Container.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
93
src/Infrastructure/DependencyInjection/ContainerFactory.php
Normal file
93
src/Infrastructure/DependencyInjection/ContainerFactory.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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');
|
||||||
|
// }),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'),
|
||||||
|
// };
|
||||||
|
// }),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
171
src/Infrastructure/DependencyInjection/README.md
Normal file
171
src/Infrastructure/DependencyInjection/README.md
Normal 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,
|
||||||
|
// ],
|
||||||
|
// ];
|
||||||
|
```
|
24
src/Infrastructure/DependencyInjection/ServiceProvider.php
Normal file
24
src/Infrastructure/DependencyInjection/ServiceProvider.php
Normal 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
50
src/helpers.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
193
tests/Feature/ContainerIntegrationTest.php
Normal file
193
tests/Feature/ContainerIntegrationTest.php
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,5 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
test('example', function () {
|
|
||||||
expect(true)->toBeTrue();
|
|
||||||
});
|
|
447
tests/Pest.php
447
tests/Pest.php
|
@ -12,6 +12,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
pest()->extend(Tests\TestCase::class)->in('Feature');
|
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);
|
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
|
| 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
|
* 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");
|
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
|
* 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)) {
|
try {
|
||||||
eval("function $functionName() { return " . var_export($returnValue, true) . "; }");
|
$callback();
|
||||||
|
fail("Expected exception $exceptionClass was not thrown");
|
||||||
|
} catch (Throwable $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();
|
|
||||||
|
|
918
tests/README.md
918
tests/README.md
File diff suppressed because it is too large
Load diff
|
@ -3,8 +3,118 @@
|
||||||
namespace Tests;
|
namespace Tests;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase as BaseTestCase;
|
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
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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
|
|
||||||
});
|
|
190
tests/Unit/Infrastructure/DependencyInjection/BootstrapTest.php
Normal file
190
tests/Unit/Infrastructure/DependencyInjection/BootstrapTest.php
Normal 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(Throwable::class);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
165
tests/Unit/Infrastructure/DependencyInjection/ContainerTest.php
Normal file
165
tests/Unit/Infrastructure/DependencyInjection/ContainerTest.php
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
<?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 () {
|
||||||
|
expectException(
|
||||||
|
fn() => $this->container->get('non.existent.service'),
|
||||||
|
NotFoundExceptionInterface::class,
|
||||||
|
'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 () {
|
||||||
|
expectException(
|
||||||
|
fn() => $this->container->get('missing.service'),
|
||||||
|
NotFoundExceptionInterface::class,
|
||||||
|
'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');
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectException(
|
||||||
|
fn() => $container->get('service.a'),
|
||||||
|
ContainerExceptionInterface::class,
|
||||||
|
'Circular dependency'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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']);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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>& {TEST_VAR} <script></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>& safe content <script></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'])");
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
Loading…
Add table
Add a link
Reference in a new issue