mirror of
https://github.com/torrentpier/torrentpier
synced 2025-08-22 06:13:58 -07:00
refactor!: remove legacy architecture and unused infrastructure
BREAKING CHANGE: Remove hexagonal architecture scaffolding, CI/CD workflows, test infrastructure, and legacy configuration files following Laravel migration. - Remove hexagonal architecture structure (Application, Domain, Infrastructure, Presentation layers) - Remove CI/CD workflows and release automation - Remove legacy web server configurations (.htaccess, nginx.conf, Caddyfile) - Remove test infrastructure and PHPUnit configuration - Remove development utilities and cleanup scripts - Remove extensive documentation scaffolding - Clean up unused configuration files
This commit is contained in:
parent
5dcc6f8cc3
commit
ae418d4b6a
82 changed files with 0 additions and 5057 deletions
|
@ -1,7 +0,0 @@
|
||||||
9766c534bddad8e82e6d19f9bad5cf70b9887f9a
|
|
||||||
92ce77ec0ec703c08a659419087a373f76e711f7
|
|
||||||
2d53efc945c7747be1755d0b66557a86bdc12cbd
|
|
||||||
602137b65129b817811b80975a369ebde3270c6d
|
|
||||||
4eb26ae37e1f4c82a45961517ffeb54c20200408
|
|
||||||
e59adce848a9e10ee5775254045cbbd915236b8b
|
|
||||||
9e0a64108d62236ab07b3f8d10e8c78269b8e1d1
|
|
|
@ -1,7 +0,0 @@
|
||||||
---
|
|
||||||
name: Feature / Enhancement request
|
|
||||||
about: Suggest an idea for TorrentPier
|
|
||||||
title: "[Feature]"
|
|
||||||
labels: [Feature, Enhancement]
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
80
.github/workflows/cd.yml
vendored
80
.github/workflows/cd.yml
vendored
|
@ -1,80 +0,0 @@
|
||||||
name: Continuous Deployment
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*.*.*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
generate-changelog:
|
|
||||||
name: Generate changelog
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
outputs:
|
|
||||||
release_body: ${{ steps.git-cliff.outputs.content }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Generate a changelog
|
|
||||||
uses: orhun/git-cliff-action@v4
|
|
||||||
id: git-cliff
|
|
||||||
with:
|
|
||||||
config: cliff.toml
|
|
||||||
args: -vv --latest --no-exec --github-repo ${{ github.repository }}
|
|
||||||
|
|
||||||
- name: Print the changelog
|
|
||||||
run: cat "${{ steps.git-cliff.outputs.changelog }}"
|
|
||||||
|
|
||||||
release:
|
|
||||||
name: Create release
|
|
||||||
needs: [ generate-changelog ]
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set the release version
|
|
||||||
shell: bash
|
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: '8.3'
|
|
||||||
|
|
||||||
- name: Install Composer dependencies
|
|
||||||
run: composer install --no-dev --no-progress --prefer-dist --optimize-autoloader
|
|
||||||
|
|
||||||
- name: Cleanup
|
|
||||||
run: php _cleanup.php && rm _cleanup.php
|
|
||||||
|
|
||||||
- name: Create archive
|
|
||||||
id: create-zip
|
|
||||||
run: |
|
|
||||||
ZIP_NAME="torrentpier-v${{ env.RELEASE_VERSION }}.zip"
|
|
||||||
zip -r "$ZIP_NAME" . -x ".git/*"
|
|
||||||
echo "ZIP_NAME=$ZIP_NAME" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Publish to GitHub
|
|
||||||
if: ${{ !contains(github.ref, '-') }}
|
|
||||||
uses: svenstaro/upload-release-action@v2
|
|
||||||
with:
|
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
file: ${{ steps.create-zip.outputs.ZIP_NAME }}
|
|
||||||
overwrite: true
|
|
||||||
tag: ${{ github.ref }}
|
|
||||||
release_name: "v${{ env.RELEASE_VERSION }}"
|
|
||||||
body: "${{ needs.generate-changelog.outputs.release_body }}"
|
|
||||||
|
|
||||||
- name: Publish to GitHub (pre-release)
|
|
||||||
if: ${{ contains(github.ref, '-') }}
|
|
||||||
uses: svenstaro/upload-release-action@v2
|
|
||||||
with:
|
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
file: ${{ steps.create-zip.outputs.ZIP_NAME }}
|
|
||||||
overwrite: true
|
|
||||||
tag: ${{ github.ref }}
|
|
||||||
release_name: "v${{ env.RELEASE_VERSION }}"
|
|
||||||
body: "${{ needs.generate-changelog.outputs.release_body }}"
|
|
||||||
prerelease: true
|
|
74
.github/workflows/ci.yml
vendored
74
.github/workflows/ci.yml
vendored
|
@ -1,74 +0,0 @@
|
||||||
name: Continuous Integration
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
nightly:
|
|
||||||
name: Nightly builds 📦
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code 🗳
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup PHP 🔩
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: '8.3'
|
|
||||||
|
|
||||||
- name: Install Composer dependencies 🪚
|
|
||||||
run: composer install --no-dev --no-progress --prefer-dist --optimize-autoloader
|
|
||||||
|
|
||||||
- name: Get commit hash 🔗
|
|
||||||
id: get-commit-hash
|
|
||||||
run: |
|
|
||||||
COMMIT_HASH=$(git rev-parse --short HEAD)
|
|
||||||
echo "COMMIT_HASH=$COMMIT_HASH" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Cleanup
|
|
||||||
run: php _cleanup.php && rm _cleanup.php
|
|
||||||
|
|
||||||
- name: Create archive 🗞
|
|
||||||
id: create-zip
|
|
||||||
run: |
|
|
||||||
ZIP_NAME="torrentpier-${{ steps.get-commit-hash.outputs.COMMIT_HASH }}.zip"
|
|
||||||
zip -r "$ZIP_NAME" . -x ".git/*"
|
|
||||||
echo "ZIP_NAME=$ZIP_NAME" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Upload Archive 📤
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: TorrentPier-master
|
|
||||||
path: ${{ steps.create-zip.outputs.ZIP_NAME }}
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
name: 🎉 Deploy
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- name: 🚚 Get latest code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: 🔩 Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: '8.3'
|
|
||||||
|
|
||||||
- name: 🖇 Install Composer dependencies
|
|
||||||
run: composer install --no-dev --no-progress --prefer-dist --optimize-autoloader
|
|
||||||
|
|
||||||
- name: 📂 Sync files
|
|
||||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
|
|
||||||
with:
|
|
||||||
server: ${{ secrets.FTP_SERVER }}
|
|
||||||
username: ${{ secrets.FTP_USERNAME }}
|
|
||||||
password: ${{ secrets.FTP_PASSWORD }}
|
|
||||||
server-dir: ${{ secrets.FTP_DIR }}
|
|
||||||
protocol: ${{ secrets.FTP_PROTOCOL }}
|
|
||||||
port: ${{ secrets.FTP_PORT }}
|
|
||||||
exclude: |
|
|
||||||
**/.git*
|
|
||||||
**/.git*/**
|
|
||||||
.env
|
|
57
.github/workflows/phpmd.yml
vendored
57
.github/workflows/phpmd.yml
vendored
|
@ -1,57 +0,0 @@
|
||||||
# This workflow uses actions that are not certified by GitHub.
|
|
||||||
# They are provided by a third-party and are governed by
|
|
||||||
# separate terms of service, privacy policy, and support
|
|
||||||
# documentation.
|
|
||||||
# PHPMD is a spin-off project of PHP Depend and
|
|
||||||
# aims to be a PHP equivalent of the well known Java tool PMD.
|
|
||||||
# What PHPMD does is: It takes a given PHP source code base
|
|
||||||
# and look for several potential problems within that source.
|
|
||||||
# These problems can be things like:
|
|
||||||
# Possible bugs
|
|
||||||
# Suboptimal code
|
|
||||||
# Overcomplicated expressions
|
|
||||||
# Unused parameters, methods, properties
|
|
||||||
# More details at https://phpmd.org/
|
|
||||||
|
|
||||||
name: PHPMD
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "master" ]
|
|
||||||
pull_request:
|
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ "master" ]
|
|
||||||
schedule:
|
|
||||||
- cron: '40 0 * * 3'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
PHPMD:
|
|
||||||
name: Run PHPMD scanning
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read # for checkout to fetch code
|
|
||||||
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
|
|
||||||
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@aa1fe473f9c687b6fb896056d771232c0bc41161
|
|
||||||
with:
|
|
||||||
coverage: none
|
|
||||||
tools: phpmd
|
|
||||||
|
|
||||||
- name: Run PHPMD
|
|
||||||
run: phpmd . sarif codesize --reportfile phpmd-results.sarif
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Upload analysis results to GitHub
|
|
||||||
uses: github/codeql-action/upload-sarif@v2
|
|
||||||
with:
|
|
||||||
sarif_file: phpmd-results.sarif
|
|
||||||
wait-for-processing: true
|
|
41
.github/workflows/schedule.yml
vendored
41
.github/workflows/schedule.yml
vendored
|
@ -1,41 +0,0 @@
|
||||||
name: Changelog generation
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * *'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
changelog:
|
|
||||||
name: Changelog generation
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: master
|
|
||||||
token: ${{ secrets.REPO_TOKEN }}
|
|
||||||
|
|
||||||
- name: Generate a changelog
|
|
||||||
uses: orhun/git-cliff-action@v4
|
|
||||||
id: git-cliff
|
|
||||||
with:
|
|
||||||
config: cliff.toml
|
|
||||||
args: v2.4.6-alpha.4.. --verbose
|
|
||||||
env:
|
|
||||||
OUTPUT: CHANGELOG.md
|
|
||||||
GITHUB_REPO: ${{ github.repository }}
|
|
||||||
|
|
||||||
- name: Print the changelog
|
|
||||||
run: cat "${{ steps.git-cliff.outputs.changelog }}"
|
|
||||||
|
|
||||||
- name: Commit changelog
|
|
||||||
run: |
|
|
||||||
git checkout master
|
|
||||||
git config --local user.name 'belomaxorka'
|
|
||||||
git config --local user.email 'roman25052006.kelesh@gmail.com'
|
|
||||||
set +e
|
|
||||||
git add CHANGELOG.md
|
|
||||||
git commit -m "changelog: Update CHANGELOG.md 📖"
|
|
||||||
git push https://${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git master
|
|
18
.htaccess
18
.htaccess
|
@ -1,18 +0,0 @@
|
||||||
## set default server charset
|
|
||||||
AddDefaultCharset UTF-8
|
|
||||||
|
|
||||||
## folder listing access control
|
|
||||||
Options All -Indexes
|
|
||||||
|
|
||||||
## sitemap and atom rewrite
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteRule ^sitemap.xml$ sitemap/sitemap.xml [L]
|
|
||||||
RewriteRule ^/internal_data/atom/(.*) /atom$1 [L]
|
|
||||||
|
|
||||||
## deny access to git folder
|
|
||||||
RedirectMatch 404 /\\.git(/|$)
|
|
||||||
|
|
||||||
## deny access to system files
|
|
||||||
<FilesMatch "\.(.*sql|tpl|db|inc|log|md|env)|(config|common).php$">
|
|
||||||
Require all denied
|
|
||||||
</FilesMatch>
|
|
57
_cleanup.php
57
_cleanup.php
|
@ -1,57 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* TorrentPier – Bull-powered BitTorrent tracker engine
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
|
|
||||||
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
|
|
||||||
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!defined('BB_ROOT')) {
|
|
||||||
define('BB_ROOT', __DIR__ . DIRECTORY_SEPARATOR);
|
|
||||||
define('BB_PATH', BB_ROOT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check CLI mode
|
|
||||||
if (PHP_SAPI != 'cli') {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!function_exists('removeFile')) {
|
|
||||||
// Get all constants
|
|
||||||
require_once BB_ROOT . 'library/defines.php';
|
|
||||||
|
|
||||||
// Include CLI functions
|
|
||||||
require INC_DIR . '/functions_cli.php';
|
|
||||||
}
|
|
||||||
|
|
||||||
$items = [
|
|
||||||
'.github',
|
|
||||||
'.cliffignore',
|
|
||||||
'.editorconfig',
|
|
||||||
'.gitignore',
|
|
||||||
'.styleci.yml',
|
|
||||||
'_release.php',
|
|
||||||
'CHANGELOG.md',
|
|
||||||
'CLAUDE.md',
|
|
||||||
'cliff.toml',
|
|
||||||
'CODE_OF_CONDUCT.md',
|
|
||||||
'CONTRIBUTING.md',
|
|
||||||
'crowdin.yml',
|
|
||||||
'HISTORY.md',
|
|
||||||
'phpunit.xml',
|
|
||||||
'README.md',
|
|
||||||
'SECURITY.md',
|
|
||||||
'tests',
|
|
||||||
'UPGRADE_GUIDE.md'
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$path = BB_ROOT . $item;
|
|
||||||
|
|
||||||
if (is_file($path)) {
|
|
||||||
removeFile($path);
|
|
||||||
} elseif (is_dir($path)) {
|
|
||||||
removeDir($path);
|
|
||||||
}
|
|
||||||
}
|
|
130
_release.php
130
_release.php
|
@ -1,130 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* TorrentPier – Bull-powered BitTorrent tracker engine
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
|
|
||||||
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
|
|
||||||
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
|
|
||||||
*/
|
|
||||||
|
|
||||||
define('BB_ROOT', __DIR__ . DIRECTORY_SEPARATOR);
|
|
||||||
define('BB_PATH', BB_ROOT);
|
|
||||||
|
|
||||||
// Check CLI mode
|
|
||||||
if (PHP_SAPI != 'cli') {
|
|
||||||
die('Please run <code style="background:#222;color:#00e01f;padding:2px 6px;border-radius:3px;">php ' . basename(__FILE__) . '</code> in CLI mode');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all constants
|
|
||||||
require_once BB_ROOT . 'library/defines.php';
|
|
||||||
|
|
||||||
// Include CLI functions
|
|
||||||
require INC_DIR . '/functions_cli.php';
|
|
||||||
|
|
||||||
// Welcoming message
|
|
||||||
out("--- Release creation tool ---\n", 'info');
|
|
||||||
|
|
||||||
$configFile = BB_PATH . '/library/config.php';
|
|
||||||
|
|
||||||
if (!is_file($configFile)) {
|
|
||||||
out('- Config file ' . basename($configFile) . ' not found', 'error');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
if (!is_readable($configFile)) {
|
|
||||||
out('- Config file ' . basename($configFile) . ' is not readable', 'error');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
if (!is_writable($configFile)) {
|
|
||||||
out('- Config file ' . basename($configFile) . ' is not writable', 'error');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ask for version
|
|
||||||
fwrite(STDOUT, 'Enter version number (e.g, v2.4.0): ');
|
|
||||||
$version = trim(fgets(STDIN));
|
|
||||||
|
|
||||||
if (empty($version)) {
|
|
||||||
out("- Version cannot be empty. Please enter a valid version number", 'error');
|
|
||||||
exit;
|
|
||||||
} else {
|
|
||||||
// Add 'v' prefix if missing
|
|
||||||
if (!str_starts_with($version, 'v')) {
|
|
||||||
$version = 'v' . $version;
|
|
||||||
}
|
|
||||||
|
|
||||||
out("- Using version: $version", 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ask for version emoji
|
|
||||||
fwrite(STDOUT, 'Enter version emoji: ');
|
|
||||||
$versionEmoji = trim(fgets(STDIN));
|
|
||||||
|
|
||||||
if (!empty($versionEmoji)) {
|
|
||||||
out("- Using version emoji: $versionEmoji", 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ask for release date or use today's date
|
|
||||||
fwrite(STDOUT, "Enter release date (e.g. 25-05-2025), leave empty to use today's date: ");
|
|
||||||
$date = trim(fgets(STDIN));
|
|
||||||
|
|
||||||
if (empty($date)) {
|
|
||||||
$date = date('d-m-Y');
|
|
||||||
out("- Using current date: $date", 'info');
|
|
||||||
} else {
|
|
||||||
// Validate date format (dd-mm-yyyy)
|
|
||||||
$dateObj = DateTime::createFromFormat('d-m-Y', $date);
|
|
||||||
if (!$dateObj || $dateObj->format('d-m-Y') !== $date) {
|
|
||||||
out("- Invalid date format. Expected format: DD-MM-YYYY", 'error');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
out("- Using date: $date", 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read config file content
|
|
||||||
$content = file_get_contents($configFile);
|
|
||||||
|
|
||||||
// Update version
|
|
||||||
$content = preg_replace(
|
|
||||||
"/\\\$bb_cfg\['tp_version'\]\s*=\s*'[^']*';/",
|
|
||||||
"\$bb_cfg['tp_version'] = '$version';",
|
|
||||||
$content
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update release date
|
|
||||||
$content = preg_replace(
|
|
||||||
"/\\\$bb_cfg\['tp_release_date'\]\s*=\s*'[^']*';/",
|
|
||||||
"\$bb_cfg['tp_release_date'] = '$date';",
|
|
||||||
$content
|
|
||||||
);
|
|
||||||
|
|
||||||
// Save updated config
|
|
||||||
$bytesWritten = file_put_contents($configFile, $content);
|
|
||||||
|
|
||||||
if ($bytesWritten === false) {
|
|
||||||
out("- Failed to write to config file", 'error');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($bytesWritten === 0) {
|
|
||||||
out("- Config file was not updated (0 bytes written)", 'error');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
out("\n- Config file has been updated!", 'success');
|
|
||||||
|
|
||||||
// Update CHANGELOG.md
|
|
||||||
runProcess('npx git-cliff v2.4.6-alpha.4.. --config cliff.toml --tag "' . $version . '" > CHANGELOG.md');
|
|
||||||
|
|
||||||
// Git add & commit
|
|
||||||
runProcess('git add -A && git commit -m "release: ' . escapeshellarg($version) . (!empty($versionEmoji) ? (' ' . $versionEmoji) : '') . '"');
|
|
||||||
|
|
||||||
// Git tag
|
|
||||||
runProcess("git tag -a \"$version\" -m \"Release $version\"");
|
|
||||||
runProcess("git tag -v \"$version\"");
|
|
||||||
|
|
||||||
// Git push
|
|
||||||
runProcess("git push origin master");
|
|
||||||
runProcess("git push origin $version");
|
|
||||||
|
|
||||||
out("\n- Release $version has been successfully prepared, committed and pushed!", 'success');
|
|
126
cliff.toml
126
cliff.toml
|
@ -1,126 +0,0 @@
|
||||||
# git-cliff ~ TorrentPier configuration file
|
|
||||||
# https://git-cliff.org/docs/configuration
|
|
||||||
#
|
|
||||||
# Lines starting with "#" are comments.
|
|
||||||
# Configuration options are organized into tables and keys.
|
|
||||||
# See documentation for more information on available options.
|
|
||||||
|
|
||||||
[remote.github]
|
|
||||||
owner = "torrentpier"
|
|
||||||
repo = "torrentpier"
|
|
||||||
|
|
||||||
[changelog]
|
|
||||||
# template for the changelog header
|
|
||||||
header = """
|
|
||||||
[](https://github.com/torrentpier)\n
|
|
||||||
# 📖 Change Log\n
|
|
||||||
"""
|
|
||||||
# template for the changelog body
|
|
||||||
# https://keats.github.io/tera/docs/#introduction
|
|
||||||
body = """
|
|
||||||
{%- macro remote_url() -%}
|
|
||||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
|
||||||
{%- endmacro -%}
|
|
||||||
|
|
||||||
{%- macro nightly_url() -%}
|
|
||||||
https://nightly.link/{{ remote.github.owner }}/{{ remote.github.repo }}/workflows/ci/master/TorrentPier-master
|
|
||||||
{%- endmacro -%}
|
|
||||||
|
|
||||||
{% macro print_commit(commit) -%}
|
|
||||||
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
|
|
||||||
{% if commit.breaking %}[**breaking**] {% endif %}\
|
|
||||||
{{ commit.message | upper_first }} - \
|
|
||||||
([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
|
|
||||||
{% endmacro -%}
|
|
||||||
|
|
||||||
{% if version %}\
|
|
||||||
{% if previous.version %}\
|
|
||||||
## [{{ version }}]\
|
|
||||||
({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) ({{ timestamp | date(format="%Y-%m-%d") }})
|
|
||||||
{% else %}\
|
|
||||||
## {{ version }} ({{ timestamp | date(format="%Y-%m-%d") }})
|
|
||||||
{% endif %}\
|
|
||||||
{% else %}\
|
|
||||||
## [nightly]({{ self::nightly_url() }})
|
|
||||||
{% endif %}\
|
|
||||||
|
|
||||||
{% for group, commits in commits | group_by(attribute="group") %}
|
|
||||||
### {{ group | striptags | trim | upper_first }}
|
|
||||||
{% for commit in commits
|
|
||||||
| filter(attribute="scope")
|
|
||||||
| sort(attribute="scope") %}
|
|
||||||
{{ self::print_commit(commit=commit) }}
|
|
||||||
{%- endfor %}
|
|
||||||
{% for commit in commits %}
|
|
||||||
{%- if not commit.scope -%}
|
|
||||||
{{ self::print_commit(commit=commit) }}
|
|
||||||
{% endif -%}
|
|
||||||
{% endfor -%}
|
|
||||||
{% endfor -%}
|
|
||||||
{%- if github -%}
|
|
||||||
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
|
||||||
## New Contributors ❤️
|
|
||||||
{% endif %}\
|
|
||||||
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
|
||||||
* @{{ contributor.username }} made their first contribution
|
|
||||||
{%- if contributor.pr_number %} in \
|
|
||||||
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
|
||||||
{%- endif %}
|
|
||||||
{%- endfor -%}
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
# template for the changelog footer
|
|
||||||
footer = """
|
|
||||||
"""
|
|
||||||
# remove the leading and trailing whitespace from the templates
|
|
||||||
trim = true
|
|
||||||
# postprocessors
|
|
||||||
postprocessors = [
|
|
||||||
{ pattern = '<REPO>', replace = "https://github.com/torrentpier/torrentpier" }, # replace repository URL
|
|
||||||
]
|
|
||||||
|
|
||||||
[git]
|
|
||||||
# parse the commits based on https://www.conventionalcommits.org
|
|
||||||
conventional_commits = true
|
|
||||||
# filter out the commits that are not conventional
|
|
||||||
filter_unconventional = true
|
|
||||||
# process each line of a commit as an individual commit
|
|
||||||
split_commits = false
|
|
||||||
# regex for preprocessing the commit messages
|
|
||||||
commit_preprocessors = [
|
|
||||||
# Replace issue numbers
|
|
||||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/pull/${2}))" },
|
|
||||||
# Check spelling of the commit with https://github.com/crate-ci/typos
|
|
||||||
# If the spelling is incorrect, it will be automatically fixed.
|
|
||||||
# { pattern = '.*', replace_command = 'typos --write-changes -' },
|
|
||||||
]
|
|
||||||
# regex for parsing and grouping commits
|
|
||||||
commit_parsers = [
|
|
||||||
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
|
|
||||||
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
|
||||||
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
|
|
||||||
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
|
||||||
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
|
||||||
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
|
||||||
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
|
||||||
{ message = "^ignore|^release|^changelog", skip = true },
|
|
||||||
{ message = "^chore|^ci|^misc", group = "<!-- 7 -->⚙️ Miscellaneous" },
|
|
||||||
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
|
|
||||||
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
|
|
||||||
{ message = "^crowdin|^crodwin", group = "<!-- 10 -->🈳 New translations" }, # crowdin pulls supporting
|
|
||||||
{ message = "^Composer", group = "<!-- 11 -->📦 Dependencies" }, # dependabot pulls supporting
|
|
||||||
{ message = "^rem|^drop|^removed", group = "<!-- 12 -->🗑️ Removed" },
|
|
||||||
{ message = ".*", group = "<!-- 13 -->💼 Other" },
|
|
||||||
]
|
|
||||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
|
||||||
protect_breaking_commits = false
|
|
||||||
# filter out the commits that are not matched by commit parsers
|
|
||||||
filter_commits = false
|
|
||||||
# regex for matching git tags
|
|
||||||
tag_pattern = "v[0-9].*"
|
|
||||||
# sort the tags topologically
|
|
||||||
topo_order = false
|
|
||||||
# sort the commits inside sections by oldest/newest order
|
|
||||||
sort_commits = "newest"
|
|
|
@ -1,53 +0,0 @@
|
||||||
# Application Configuration
|
|
||||||
|
|
||||||
System configuration files using PHP arrays for type safety and IDE support:
|
|
||||||
|
|
||||||
- **app.php**: Core application settings
|
|
||||||
- Site name, URL, timezone
|
|
||||||
- Debug mode, environment
|
|
||||||
- Feature flags and toggles
|
|
||||||
|
|
||||||
- **database.php**: Database connection settings
|
|
||||||
- Multiple connection definitions
|
|
||||||
- Read/write splitting configuration
|
|
||||||
- Connection pooling settings
|
|
||||||
|
|
||||||
- **cache.php**: Cache driver configurations
|
|
||||||
- Redis, Memcached, file-based settings
|
|
||||||
- TTL defaults per cache type
|
|
||||||
- Cache key prefixes
|
|
||||||
|
|
||||||
- **tracker.php**: BitTorrent tracker settings
|
|
||||||
- Announce intervals
|
|
||||||
- Peer limits
|
|
||||||
- Ratio requirements
|
|
||||||
|
|
||||||
- **environments/**: Environment-specific overrides
|
|
||||||
- Development, staging, production settings
|
|
||||||
- Local developer configurations
|
|
||||||
|
|
||||||
Example database configuration:
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
return [
|
|
||||||
'default' => env('DB_CONNECTION', 'mysql'),
|
|
||||||
|
|
||||||
'connections' => [
|
|
||||||
'mysql' => [
|
|
||||||
'driver' => 'mysql',
|
|
||||||
'host' => env('DB_HOST', '127.0.0.1'),
|
|
||||||
'port' => env('DB_PORT', 3306),
|
|
||||||
'database' => env('DB_DATABASE', 'tp'),
|
|
||||||
'username' => env('DB_USERNAME', 'root'),
|
|
||||||
'password' => env('DB_PASSWORD', ''),
|
|
||||||
'charset' => 'utf8mb4',
|
|
||||||
'collation' => 'utf8mb4_unicode_ci',
|
|
||||||
'options' => [
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
```
|
|
|
@ -1,26 +0,0 @@
|
||||||
<?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
|
|
||||||
],
|
|
||||||
];
|
|
|
@ -1,30 +0,0 @@
|
||||||
<?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'),
|
|
||||||
];
|
|
|
@ -1,99 +0,0 @@
|
||||||
<?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'
|
|
||||||
]);
|
|
|
@ -1,436 +0,0 @@
|
||||||
# Hexagonal Architecture Directory Structure Specification
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document specifies the new hexagonal architecture directory structure for TorrentPier 3.0. The structure follows Domain-Driven Design (DDD) principles and implements a clean separation of concerns through hexagonal architecture (ports and adapters pattern).
|
|
||||||
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
/src/
|
|
||||||
├── Domain/ # Core business logic - no framework dependencies
|
|
||||||
│ ├── Forum/ # Forum bounded context
|
|
||||||
│ │ ├── Model/ # Aggregates and entities
|
|
||||||
│ │ ├── ValueObject/ # Value objects (PostId, ThreadTitle, etc.)
|
|
||||||
│ │ ├── Repository/ # Repository interfaces
|
|
||||||
│ │ └── Exception/ # Domain-specific exceptions
|
|
||||||
│ ├── Tracker/ # BitTorrent tracker bounded context
|
|
||||||
│ │ ├── Model/ # Torrent, Peer aggregates
|
|
||||||
│ │ ├── ValueObject/ # InfoHash, PeerId, etc.
|
|
||||||
│ │ ├── Repository/ # Repository interfaces
|
|
||||||
│ │ └── Exception/ # Tracker-specific exceptions
|
|
||||||
│ ├── User/ # User management bounded context
|
|
||||||
│ │ ├── Model/ # User aggregate
|
|
||||||
│ │ ├── ValueObject/ # UserId, Email, Username
|
|
||||||
│ │ ├── Repository/ # User repository interface
|
|
||||||
│ │ └── Exception/ # Authentication/authorization exceptions
|
|
||||||
│ └── Shared/ # Shared kernel - minimal shared concepts
|
|
||||||
│ ├── Model/ # Base classes (AggregateRoot, Entity)
|
|
||||||
│ ├── ValueObject/ # Common value objects (Id, DateTime)
|
|
||||||
│ └── Event/ # Domain events base classes
|
|
||||||
│
|
|
||||||
├── Application/ # Application services - orchestration layer
|
|
||||||
│ ├── Forum/
|
|
||||||
│ │ ├── Command/ # Commands (CreatePost, LockThread)
|
|
||||||
│ │ ├── Query/ # Queries (GetThreadList, SearchPosts)
|
|
||||||
│ │ └── Handler/ # Command and query handlers
|
|
||||||
│ ├── Tracker/
|
|
||||||
│ │ ├── Command/ # Commands (RegisterTorrent, ProcessAnnounce)
|
|
||||||
│ │ ├── Query/ # Queries (GetPeerList, GetTorrentStats)
|
|
||||||
│ │ └── Handler/ # Command and query handlers
|
|
||||||
│ └── User/
|
|
||||||
│ ├── Command/ # Commands (RegisterUser, ChangePassword)
|
|
||||||
│ ├── Query/ # Queries (GetUserProfile, SearchUsers)
|
|
||||||
│ └── Handler/ # Command and query handlers
|
|
||||||
│
|
|
||||||
├── Infrastructure/ # External concerns and implementations
|
|
||||||
│ ├── Persistence/ # Data persistence layer
|
|
||||||
│ │ ├── Database/ # Database adapter and connection management
|
|
||||||
│ │ ├── Migration/ # Database migrations
|
|
||||||
│ │ └── Repository/ # Repository implementations
|
|
||||||
│ ├── Cache/ # Caching implementations
|
|
||||||
│ │ ├── Redis/ # Redis adapter
|
|
||||||
│ │ ├── Memcached/ # Memcached adapter
|
|
||||||
│ │ └── File/ # File-based cache adapter
|
|
||||||
│ ├── Email/ # Email service implementations
|
|
||||||
│ │ ├── Template/ # Email templates
|
|
||||||
│ │ └── Transport/ # SMTP, API transports
|
|
||||||
│ └── FileStorage/ # File storage abstractions
|
|
||||||
│ ├── Local/ # Local filesystem storage
|
|
||||||
│ └── S3/ # AWS S3 storage adapter
|
|
||||||
│
|
|
||||||
└── Presentation/ # User interface layer
|
|
||||||
├── Http/ # Web interface
|
|
||||||
│ ├── Controllers/ # HTTP controllers
|
|
||||||
│ │ ├── Admin/ # Admin panel controllers
|
|
||||||
│ │ ├── Api/ # REST API controllers
|
|
||||||
│ │ └── Web/ # Web UI controllers
|
|
||||||
│ ├── Middleware/ # HTTP middleware (auth, CORS, etc.)
|
|
||||||
│ ├── Requests/ # Request DTOs and validation
|
|
||||||
│ └── Responses/ # Response transformers
|
|
||||||
└── Cli/ # Command line interface
|
|
||||||
└── Commands/ # Console commands
|
|
||||||
|
|
||||||
# Additional directories (outside /src/)
|
|
||||||
/config/ # Application configuration
|
|
||||||
├── app.php # Main application settings
|
|
||||||
├── database.php # Database connections
|
|
||||||
├── cache.php # Cache drivers configuration
|
|
||||||
├── tracker.php # BitTorrent tracker settings
|
|
||||||
└── environments/ # Environment-specific overrides
|
|
||||||
|
|
||||||
/tests/ # Test suites (Pest)
|
|
||||||
├── Unit/ # Unit tests (mirrors src/ structure)
|
|
||||||
├── Feature/ # Feature/Integration tests
|
|
||||||
├── Pest.php # Pest configuration
|
|
||||||
└── TestCase.php # Base test case
|
|
||||||
```
|
|
||||||
|
|
||||||
## Directory README.md Templates
|
|
||||||
|
|
||||||
### Domain Layer READMEs
|
|
||||||
|
|
||||||
#### `/src/Domain/README.md`
|
|
||||||
```markdown
|
|
||||||
# Domain Layer
|
|
||||||
|
|
||||||
This directory contains the core business logic of TorrentPier. Code here should:
|
|
||||||
- Have no dependencies on frameworks or infrastructure
|
|
||||||
- Represent pure business rules and domain models
|
|
||||||
- Be testable in isolation
|
|
||||||
- Use only PHP language features and domain concepts
|
|
||||||
|
|
||||||
## Bounded Contexts
|
|
||||||
- **Forum**: Discussion forums, posts, threads
|
|
||||||
- **Tracker**: BitTorrent tracking, peers, torrents
|
|
||||||
- **User**: User management, authentication, profiles
|
|
||||||
- **Shared**: Minimal shared concepts between contexts
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `/src/Domain/Tracker/Model/README.md`
|
|
||||||
```markdown
|
|
||||||
# Tracker Domain Models
|
|
||||||
|
|
||||||
Contains aggregate roots and entities for the BitTorrent tracker:
|
|
||||||
- `Torrent`: Aggregate root for torrent management
|
|
||||||
- `Peer`: Entity representing a BitTorrent peer
|
|
||||||
- `TorrentStatistics`: Value object for torrent stats
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```php
|
|
||||||
class Torrent extends AggregateRoot
|
|
||||||
{
|
|
||||||
public function announce(Peer $peer, AnnounceEvent $event): void
|
|
||||||
{
|
|
||||||
// Business logic for handling announces
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `/src/Domain/Tracker/ValueObject/README.md`
|
|
||||||
```markdown
|
|
||||||
# Tracker Value Objects
|
|
||||||
|
|
||||||
Immutable objects representing domain concepts:
|
|
||||||
- `InfoHash`: 20-byte torrent identifier
|
|
||||||
- `PeerId`: Peer client identifier
|
|
||||||
- `Port`: Network port (1-65535)
|
|
||||||
- `BytesTransferred`: Upload/download bytes
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```php
|
|
||||||
final class InfoHash
|
|
||||||
{
|
|
||||||
private string $hash;
|
|
||||||
|
|
||||||
public function __construct(string $hash)
|
|
||||||
{
|
|
||||||
$this->guardAgainstInvalidHash($hash);
|
|
||||||
$this->hash = $hash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Application Layer READMEs
|
|
||||||
|
|
||||||
#### `/src/Application/README.md`
|
|
||||||
```markdown
|
|
||||||
# Application Layer
|
|
||||||
|
|
||||||
Contains application services that orchestrate domain objects to fulfill use cases.
|
|
||||||
- Commands: Write operations that change state
|
|
||||||
- Queries: Read operations for data retrieval
|
|
||||||
- Handlers: Process commands and queries
|
|
||||||
|
|
||||||
This layer should:
|
|
||||||
- Coordinate domain objects
|
|
||||||
- Handle transactions
|
|
||||||
- Dispatch domain events
|
|
||||||
- Not contain business logic
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `/src/Application/Tracker/Command/README.md`
|
|
||||||
```markdown
|
|
||||||
# Tracker Commands
|
|
||||||
|
|
||||||
Commands representing write operations:
|
|
||||||
- `RegisterTorrentCommand`: Register new torrent
|
|
||||||
- `UpdateTorrentCommand`: Modify torrent details
|
|
||||||
- `DeleteTorrentCommand`: Remove torrent from tracker
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```php
|
|
||||||
final class RegisterTorrentCommand
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public readonly string $infoHash,
|
|
||||||
public readonly int $uploaderId,
|
|
||||||
public readonly string $name,
|
|
||||||
public readonly int $size
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Infrastructure Layer READMEs
|
|
||||||
|
|
||||||
#### `/src/Infrastructure/README.md`
|
|
||||||
```markdown
|
|
||||||
# Infrastructure Layer
|
|
||||||
|
|
||||||
Technical implementations and external service adapters:
|
|
||||||
- Database persistence
|
|
||||||
- Caching mechanisms
|
|
||||||
- Email services
|
|
||||||
- File storage
|
|
||||||
- Third-party integrations
|
|
||||||
|
|
||||||
Infrastructure depends on domain, not vice versa.
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `/src/Infrastructure/Persistence/Repository/README.md`
|
|
||||||
```markdown
|
|
||||||
# Repository Implementations
|
|
||||||
|
|
||||||
Concrete implementations of domain repository interfaces:
|
|
||||||
- Uses database adapter for persistence
|
|
||||||
- Implements caching strategies
|
|
||||||
- Handles query optimization
|
|
||||||
- Supports multiple database backends
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```php
|
|
||||||
class TorrentRepository implements TorrentRepositoryInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private DatabaseAdapterInterface $db
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function findByInfoHash(InfoHash $infoHash): ?Torrent
|
|
||||||
{
|
|
||||||
// Database adapter implementation
|
|
||||||
$row = $this->db->select('torrents')
|
|
||||||
->where('info_hash', $infoHash->toString())
|
|
||||||
->first();
|
|
||||||
|
|
||||||
return $row ? $this->hydrateFromRow($row) : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Presentation Layer READMEs
|
|
||||||
|
|
||||||
#### `/src/Presentation/README.md`
|
|
||||||
```markdown
|
|
||||||
# Presentation Layer
|
|
||||||
|
|
||||||
User interface implementations:
|
|
||||||
- HTTP controllers for web and API
|
|
||||||
- CLI commands for console operations
|
|
||||||
- Request/response handling
|
|
||||||
- Input validation
|
|
||||||
- Output formatting
|
|
||||||
|
|
||||||
This layer translates between external format and application format.
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `/src/Presentation/Http/Controllers/Api/README.md`
|
|
||||||
```markdown
|
|
||||||
# API Controllers
|
|
||||||
|
|
||||||
RESTful API endpoints following OpenAPI specification:
|
|
||||||
- JSON request/response format
|
|
||||||
- Proper HTTP status codes
|
|
||||||
- HATEOAS where applicable
|
|
||||||
- Rate limiting aware
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```php
|
|
||||||
class UserController
|
|
||||||
{
|
|
||||||
public function register(RegisterRequest $request): JsonResponse
|
|
||||||
{
|
|
||||||
$command = new RegisterUserCommand(
|
|
||||||
$request->getUsername(),
|
|
||||||
$request->getEmail(),
|
|
||||||
$request->getPassword()
|
|
||||||
);
|
|
||||||
|
|
||||||
$userId = $this->commandBus->handle($command);
|
|
||||||
|
|
||||||
return new JsonResponse([
|
|
||||||
'id' => $userId,
|
|
||||||
'username' => $request->getUsername()
|
|
||||||
], Response::HTTP_CREATED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `/src/Presentation/Http/Controllers/Admin/README.md`
|
|
||||||
```markdown
|
|
||||||
# Admin Panel Controllers
|
|
||||||
|
|
||||||
Administrative interface controllers with enhanced security:
|
|
||||||
- Role-based access control (RBAC)
|
|
||||||
- Audit logging for all actions
|
|
||||||
- Additional authentication checks
|
|
||||||
- Administrative dashboards and reports
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```php
|
|
||||||
class AdminUserController
|
|
||||||
{
|
|
||||||
public function index(Request $request): Response
|
|
||||||
{
|
|
||||||
$query = new GetUsersQuery(
|
|
||||||
page: $request->getPage(),
|
|
||||||
filters: $request->getFilters()
|
|
||||||
);
|
|
||||||
|
|
||||||
$users = $this->queryBus->handle($query);
|
|
||||||
|
|
||||||
return $this->render('admin/users/index', [
|
|
||||||
'users' => $users,
|
|
||||||
'filters' => $request->getFilters()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `/config/README.md`
|
|
||||||
```markdown
|
|
||||||
# Application Configuration
|
|
||||||
|
|
||||||
System configuration files using PHP arrays for type safety and IDE support:
|
|
||||||
|
|
||||||
- **app.php**: Core application settings
|
|
||||||
- Site name, URL, timezone
|
|
||||||
- Debug mode, environment
|
|
||||||
- Feature flags and toggles
|
|
||||||
|
|
||||||
- **database.php**: Database connection settings
|
|
||||||
- Multiple connection definitions
|
|
||||||
- Read/write splitting configuration
|
|
||||||
- Connection pooling settings
|
|
||||||
|
|
||||||
- **cache.php**: Cache driver configurations
|
|
||||||
- Redis, Memcached, file-based settings
|
|
||||||
- TTL defaults per cache type
|
|
||||||
- Cache key prefixes
|
|
||||||
|
|
||||||
- **tracker.php**: BitTorrent tracker settings
|
|
||||||
- Announce intervals
|
|
||||||
- Peer limits
|
|
||||||
- Ratio requirements
|
|
||||||
|
|
||||||
- **environments/**: Environment-specific overrides
|
|
||||||
- Development, staging, production settings
|
|
||||||
- Local developer configurations
|
|
||||||
|
|
||||||
Example database configuration:
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
return [
|
|
||||||
'default' => env('DB_CONNECTION', 'mysql'),
|
|
||||||
|
|
||||||
'connections' => [
|
|
||||||
'mysql' => [
|
|
||||||
'driver' => 'mysql',
|
|
||||||
'host' => env('DB_HOST', '127.0.0.1'),
|
|
||||||
'port' => env('DB_PORT', 3306),
|
|
||||||
'database' => env('DB_DATABASE', 'tp'),
|
|
||||||
'username' => env('DB_USERNAME', 'root'),
|
|
||||||
'password' => env('DB_PASSWORD', ''),
|
|
||||||
'charset' => 'utf8mb4',
|
|
||||||
'collation' => 'utf8mb4_unicode_ci',
|
|
||||||
'options' => [
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Order
|
|
||||||
|
|
||||||
1. **Phase 1: Foundation**
|
|
||||||
|
|
||||||
- Create directory structure
|
|
||||||
- Set up base classes in Domain/Shared
|
|
||||||
- Configure dependency injection
|
|
||||||
|
|
||||||
2. **Phase 2: Domain Modeling**
|
|
||||||
|
|
||||||
- Implement core aggregates
|
|
||||||
- Create value objects
|
|
||||||
- Define repository interfaces
|
|
||||||
|
|
||||||
3. **Phase 3: Application Services**
|
|
||||||
|
|
||||||
- Create commands and queries
|
|
||||||
- Implement handlers
|
|
||||||
- Set up event dispatching
|
|
||||||
|
|
||||||
4. **Phase 4: Infrastructure**
|
|
||||||
|
|
||||||
- Implement repositories
|
|
||||||
- Configure database adapter
|
|
||||||
- Set up caching
|
|
||||||
|
|
||||||
5. **Phase 5: Presentation**
|
|
||||||
|
|
||||||
- Create controllers
|
|
||||||
- Implement middleware
|
|
||||||
- Build CLI commands
|
|
||||||
|
|
||||||
## Migration Strategy
|
|
||||||
|
|
||||||
- Existing code remains in current locations
|
|
||||||
- New features built in hexagonal architecture
|
|
||||||
- Gradual migration using strangler fig pattern
|
|
||||||
- Legacy adapters bridge old and new code
|
|
||||||
- Feature flags control rollout
|
|
||||||
|
|
||||||
## Key Principles
|
|
||||||
|
|
||||||
1. **Dependency Rule**: Dependencies point inward (Presentation → Application → Domain)
|
|
||||||
2. **Domain Isolation**: Business logic has no framework dependencies
|
|
||||||
3. **Interface Segregation**: Small, focused interfaces
|
|
||||||
4. **CQRS**: Separate read and write models
|
|
||||||
5. **Event-Driven**: Domain events for cross-context communication
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
- **Domain**: Pure unit tests, no mocks needed
|
|
||||||
- **Application**: Unit tests with mocked repositories
|
|
||||||
- **Infrastructure**: Integration tests with real services
|
|
||||||
- **Presentation**: E2E tests for user journeys
|
|
||||||
|
|
||||||
## Notes for Developers
|
|
||||||
|
|
||||||
- Start reading code from the Domain layer
|
|
||||||
- Business rules live in aggregates, not services
|
|
||||||
- Use value objects for type safety
|
|
||||||
- Prefer composition over inheritance
|
|
||||||
- Keep bounded contexts loosely coupled
|
|
|
@ -1,27 +0,0 @@
|
||||||
# Example Caddy configuration for TorrentPier
|
|
||||||
|
|
||||||
example.com {
|
|
||||||
root * /path/to/root
|
|
||||||
encode gzip zstd
|
|
||||||
php_fastcgi unix//run/php/php-fpm.sock
|
|
||||||
try_files {path} {path}/ /index.php?{query}
|
|
||||||
file_server
|
|
||||||
|
|
||||||
@blocked {
|
|
||||||
path /install/* /internal_data/* /library/*
|
|
||||||
path /.ht* /.en*
|
|
||||||
path /.git/*
|
|
||||||
path *.sql *.tpl *.db *.inc *.log *.md
|
|
||||||
}
|
|
||||||
respond @blocked 404
|
|
||||||
|
|
||||||
redir /sitemap.xml /sitemap/sitemap.xml
|
|
||||||
|
|
||||||
@html_css_js {
|
|
||||||
path *.html *.css *.js *.json *.xml *.txt
|
|
||||||
}
|
|
||||||
header @html_css_js Content-Type "{mime}; charset=utf-8"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Refer to the Caddy docs for more information:
|
|
||||||
# https://caddyserver.com/docs/caddyfile
|
|
|
@ -1,39 +0,0 @@
|
||||||
# Example nginx configuration for TorrentPier
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80; # port
|
|
||||||
server_name example.com; # your domain
|
|
||||||
root /path/to/root; # folder with TorrentPier installed
|
|
||||||
index index.php;
|
|
||||||
charset utf-8;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files \$uri \$uri/ /index.php?\$args;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ \/(install|internal_data|library)\/ {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ /\.(ht|en) {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ /\.git {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ \.(.*sql|tpl|db|inc|log|md)$ {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
rewrite ^/sitemap.xml$ /sitemap/sitemap.xml;
|
|
||||||
|
|
||||||
location ~ \.php$ {
|
|
||||||
include fastcgi_params;
|
|
||||||
fastcgi_pass unix:/run/php/php-fpm.sock;
|
|
||||||
fastcgi_index index.php;
|
|
||||||
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
|
|
||||||
include fastcgi_params;
|
|
||||||
}
|
|
||||||
}
|
|
18
phpunit.xml
18
phpunit.xml
|
@ -1,18 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
|
||||||
bootstrap="vendor/autoload.php"
|
|
||||||
colors="true"
|
|
||||||
>
|
|
||||||
<testsuites>
|
|
||||||
<testsuite name="Test Suite">
|
|
||||||
<directory suffix="Test.php">./tests</directory>
|
|
||||||
</testsuite>
|
|
||||||
</testsuites>
|
|
||||||
<source>
|
|
||||||
<include>
|
|
||||||
<directory>app</directory>
|
|
||||||
<directory>src</directory>
|
|
||||||
</include>
|
|
||||||
</source>
|
|
||||||
</phpunit>
|
|
|
@ -1,11 +0,0 @@
|
||||||
# Forum Commands
|
|
||||||
|
|
||||||
Commands representing write operations:
|
|
||||||
|
|
||||||
- `CreateThreadCommand`: Start new discussion
|
|
||||||
- `CreatePostCommand`: Reply to thread
|
|
||||||
- `EditPostCommand`: Modify existing post
|
|
||||||
- `LockThreadCommand`: Lock thread from replies
|
|
||||||
- `DeletePostCommand`: Remove post
|
|
||||||
|
|
||||||
Commands are simple DTOs containing operation data.
|
|
|
@ -1,9 +0,0 @@
|
||||||
# Forum Handlers
|
|
||||||
|
|
||||||
Command and query handlers for forum operations:
|
|
||||||
|
|
||||||
- `CreateThreadHandler`: Handles thread creation
|
|
||||||
- `CreatePostHandler`: Handles post creation
|
|
||||||
- `GetThreadListHandler`: Retrieves thread listings
|
|
||||||
|
|
||||||
Handlers coordinate between domain and infrastructure layers.
|
|
|
@ -1,10 +0,0 @@
|
||||||
# Forum Queries
|
|
||||||
|
|
||||||
Read-only queries for forum data:
|
|
||||||
|
|
||||||
- `GetThreadListQuery`: Paginated thread listing
|
|
||||||
- `GetPostsByThreadQuery`: Thread posts with pagination
|
|
||||||
- `SearchPostsQuery`: Full-text post search
|
|
||||||
- `GetForumStatsQuery`: Forum statistics
|
|
||||||
|
|
||||||
Queries return DTOs optimized for presentation.
|
|
|
@ -1,14 +0,0 @@
|
||||||
# Application Layer
|
|
||||||
|
|
||||||
Contains application services that orchestrate domain objects to fulfill use cases.
|
|
||||||
|
|
||||||
- Commands: Write operations that change state
|
|
||||||
- Queries: Read operations for data retrieval
|
|
||||||
- Handlers: Process commands and queries
|
|
||||||
|
|
||||||
This layer should:
|
|
||||||
|
|
||||||
- Coordinate domain objects
|
|
||||||
- Handle transactions
|
|
||||||
- Dispatch domain events
|
|
||||||
- Not contain business logic
|
|
|
@ -1,21 +0,0 @@
|
||||||
# Tracker Commands
|
|
||||||
|
|
||||||
Commands representing write operations:
|
|
||||||
|
|
||||||
- `RegisterTorrentCommand`: Register new torrent
|
|
||||||
- `UpdateTorrentCommand`: Modify torrent details
|
|
||||||
- `DeleteTorrentCommand`: Remove torrent from tracker
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```php
|
|
||||||
final class RegisterTorrentCommand
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public readonly string $infoHash,
|
|
||||||
public readonly int $uploaderId,
|
|
||||||
public readonly string $name,
|
|
||||||
public readonly int $size
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,9 +0,0 @@
|
||||||
# Tracker Handlers
|
|
||||||
|
|
||||||
Command and query handlers for tracker operations:
|
|
||||||
|
|
||||||
- `RegisterTorrentHandler`: Processes torrent registration
|
|
||||||
- `GetTorrentDetailsHandler`: Retrieves torrent information
|
|
||||||
- `UpdateTorrentHandler`: Handles torrent updates
|
|
||||||
|
|
||||||
Note: High-performance announce operations bypass this layer.
|
|
|
@ -1,10 +0,0 @@
|
||||||
# Tracker Queries
|
|
||||||
|
|
||||||
Read-only queries for tracker data:
|
|
||||||
|
|
||||||
- `GetTorrentDetailsQuery`: Single torrent information
|
|
||||||
- `GetPeerListQuery`: Active peers for torrent
|
|
||||||
- `GetTorrentStatsQuery`: Download/upload statistics
|
|
||||||
- `SearchTorrentsQuery`: Browse and filter torrents
|
|
||||||
|
|
||||||
Optimized for high-performance read operations.
|
|
|
@ -1,11 +0,0 @@
|
||||||
# User Commands
|
|
||||||
|
|
||||||
Commands for user management operations:
|
|
||||||
|
|
||||||
- `RegisterUserCommand`: New user registration
|
|
||||||
- `ChangePasswordCommand`: Password update
|
|
||||||
- `UpdateProfileCommand`: Profile modifications
|
|
||||||
- `ActivateUserCommand`: Account activation
|
|
||||||
- `BanUserCommand`: User suspension
|
|
||||||
|
|
||||||
These commands trigger domain events for cross-context synchronization.
|
|
|
@ -1,9 +0,0 @@
|
||||||
# User Handlers
|
|
||||||
|
|
||||||
Command and query handlers for user operations:
|
|
||||||
|
|
||||||
- `RegisterUserHandler`: Processes user registration
|
|
||||||
- `ChangePasswordHandler`: Handles password changes
|
|
||||||
- `GetUserProfileHandler`: Retrieves user profiles
|
|
||||||
|
|
||||||
Handlers manage transactions and event dispatching.
|
|
|
@ -1,10 +0,0 @@
|
||||||
# User Queries
|
|
||||||
|
|
||||||
Read operations for user data:
|
|
||||||
|
|
||||||
- `GetUserProfileQuery`: User profile details
|
|
||||||
- `SearchUsersQuery`: User search with filters
|
|
||||||
- `GetUserStatisticsQuery`: Upload/download stats
|
|
||||||
- `GetUserPermissionsQuery`: Authorization data
|
|
||||||
|
|
||||||
Returns DTOs suitable for presentation layer.
|
|
|
@ -1,422 +0,0 @@
|
||||||
# Unified Cache System
|
|
||||||
|
|
||||||
A modern, unified caching solution for TorrentPier 3.0 that uses **Nette Caching** internally. **Breaking changes**: This replaces the legacy Cache and Datastore APIs and requires code migration.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Unified Cache System addresses the complexity and duplication in TorrentPier's caching architecture by:
|
|
||||||
|
|
||||||
- **Unifying** Cache and Datastore systems into a single, coherent solution
|
|
||||||
- **Modernizing** the caching layer with Nette's advanced features and breaking changes for better architecture
|
|
||||||
- **Reducing** complexity and maintenance overhead
|
|
||||||
- **Improving** performance with efficient singleton pattern and advanced features
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
|
|
||||||
1. **UnifiedCacheSystem** - Main singleton orchestrator following TorrentPier's architectural patterns
|
|
||||||
2. **CacheManager** - Cache interface using Nette Caching internally with singleton pattern
|
|
||||||
3. **DatastoreManager** - Datastore interface that uses CacheManager internally for unified functionality
|
|
||||||
|
|
||||||
### Singleton Architecture
|
|
||||||
|
|
||||||
The system follows TorrentPier's consistent singleton pattern, similar to `config()`, `dev()`, `censor()`, and `DB()`:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Main singleton instance
|
|
||||||
TorrentPier\Cache\UnifiedCacheSystem::getInstance(config()->all());
|
|
||||||
|
|
||||||
// Clean global functions with proper return types
|
|
||||||
function CACHE(string $cache_name): \TorrentPier\Cache\CacheManager
|
|
||||||
function datastore(): \TorrentPier\Cache\DatastoreManager
|
|
||||||
|
|
||||||
// Usage (exactly like before)
|
|
||||||
$cache = CACHE('bb_cache');
|
|
||||||
$datastore = datastore();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Benefits
|
|
||||||
|
|
||||||
- ✅ **Single Source of Truth**: One caching system instead of two separate ones
|
|
||||||
- ✅ **Modern Foundation**: Built on Nette Caching v3.3 with all its advanced features
|
|
||||||
- ✅ **Zero Breaking Changes**: All existing `CACHE()` and `$datastore` calls work unchanged
|
|
||||||
- ✅ **Consistent Architecture**: Proper singleton pattern matching other TorrentPier services
|
|
||||||
- ✅ **Advanced Features**: Dependencies, tags, bulk operations, memoization, output buffering
|
|
||||||
- ✅ **Better Debugging**: Unified debug interface with compatibility for Dev.php
|
|
||||||
- ✅ **Performance**: 456,647+ operations per second with efficient memory usage
|
|
||||||
- ✅ **Clean Architecture**: No redundant configuration logic, single storage creation path
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Basic Cache Operations (100% Backward Compatible)
|
|
||||||
|
|
||||||
```php
|
|
||||||
// All existing cache calls work exactly the same
|
|
||||||
$cache = CACHE('bb_cache');
|
|
||||||
$value = $cache->get('key');
|
|
||||||
$cache->set('key', $value, 3600);
|
|
||||||
$cache->rm('key');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Datastore Operations (100% Backward Compatible)
|
|
||||||
|
|
||||||
```php
|
|
||||||
// All existing datastore calls work exactly the same
|
|
||||||
$datastore = datastore();
|
|
||||||
$forums = $datastore->get('cat_forums');
|
|
||||||
$datastore->store('custom_data', $data);
|
|
||||||
$datastore->update(['cat_forums', 'stats']);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Advanced Nette Caching Features
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Get cache manager for advanced features
|
|
||||||
$cache = CACHE('bb_cache');
|
|
||||||
|
|
||||||
// Load with callback (compute if not cached)
|
|
||||||
$value = $cache->load('expensive_key', function() {
|
|
||||||
return expensive_computation();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cache with time expiration
|
|
||||||
$cache->save('key', $value, [
|
|
||||||
\Nette\Caching\Cache::Expire => '1 hour'
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Cache with file dependencies
|
|
||||||
$cache->save('config', $data, [
|
|
||||||
\Nette\Caching\Cache::Files => ['/path/to/config.php']
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Memoize function calls
|
|
||||||
$result = $cache->call('expensive_function', $param1, $param2);
|
|
||||||
|
|
||||||
// Bulk operations
|
|
||||||
$values = $cache->bulkLoad(['key1', 'key2', 'key3'], function($key) {
|
|
||||||
return "computed_value_for_$key";
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean by tags (requires SQLite storage)
|
|
||||||
$cache->clean([\Nette\Caching\Cache::Tags => ['user-123']]);
|
|
||||||
|
|
||||||
// Output buffering
|
|
||||||
$content = $cache->capture('output_key', function() {
|
|
||||||
echo "This content will be cached";
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Datastore Advanced Features
|
|
||||||
|
|
||||||
```php
|
|
||||||
$datastore = datastore();
|
|
||||||
|
|
||||||
// All standard operations work
|
|
||||||
$forums = $datastore->get('cat_forums');
|
|
||||||
$datastore->store('custom_data', $data);
|
|
||||||
|
|
||||||
// Access underlying CacheManager for advanced features
|
|
||||||
$manager = $datastore->getCacheManager();
|
|
||||||
$value = $manager->load('complex_data', function() {
|
|
||||||
return build_complex_data();
|
|
||||||
}, [
|
|
||||||
\Nette\Caching\Cache::Expire => '30 minutes',
|
|
||||||
\Nette\Caching\Cache::Tags => ['forums', 'categories']
|
|
||||||
]);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration & Initialization
|
|
||||||
|
|
||||||
### Automatic Integration
|
|
||||||
|
|
||||||
The system integrates seamlessly in `library/includes/functions.php`:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Singleton initialization (done once)
|
|
||||||
TorrentPier\Cache\UnifiedCacheSystem::getInstance(config()->all());
|
|
||||||
|
|
||||||
// Global functions provide backward compatibility
|
|
||||||
function CACHE(string $cache_name): \TorrentPier\Cache\CacheManager {
|
|
||||||
return TorrentPier\Cache\UnifiedCacheSystem::getInstance()->getCache($cache_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function datastore(): \TorrentPier\Cache\DatastoreManager {
|
|
||||||
return TorrentPier\Cache\UnifiedCacheSystem::getInstance()->getDatastore(config()->get('datastore_type', 'file'));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debug Compatibility
|
|
||||||
|
|
||||||
The system maintains full compatibility with Dev.php debugging:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Dev.php can access debug information via magic __get() methods
|
|
||||||
$cache = CACHE('bb_cache');
|
|
||||||
$debug_info = $cache->dbg; // Array of operations
|
|
||||||
$engine_name = $cache->engine; // Storage engine name
|
|
||||||
$total_time = $cache->sql_timetotal; // Total operation time
|
|
||||||
|
|
||||||
$datastore = datastore();
|
|
||||||
$datastore_debug = $datastore->dbg; // Datastore debug info
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The system uses existing configuration seamlessly:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// library/config.php
|
|
||||||
$bb_cfg['cache'] = [
|
|
||||||
'db_dir' => realpath(BB_ROOT) . '/internal_data/cache/filecache/',
|
|
||||||
'prefix' => 'tp_',
|
|
||||||
'engines' => [
|
|
||||||
'bb_cache' => ['file'], // Uses Nette FileStorage
|
|
||||||
'session_cache' => ['sqlite'], // Uses Nette SQLiteStorage
|
|
||||||
'tr_cache' => ['file'], // Uses Nette FileStorage
|
|
||||||
// ... other caches
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
$bb_cfg['datastore_type'] = 'file'; // Uses Nette FileStorage
|
|
||||||
```
|
|
||||||
|
|
||||||
## Storage Types
|
|
||||||
|
|
||||||
### Supported Storage Types
|
|
||||||
|
|
||||||
| Legacy Type | Nette Storage | Features |
|
|
||||||
|------------|---------------|----------|
|
|
||||||
| `file` | `FileStorage` | File-based, persistent, dependencies |
|
|
||||||
| `sqlite` | `SQLiteStorage` | Database, supports tags and complex dependencies |
|
|
||||||
| `memory` | `MemoryStorage` | In-memory, fastest, non-persistent |
|
|
||||||
| `memcached` | `MemcachedStorage` | Distributed memory, high-performance |
|
|
||||||
|
|
||||||
### Storage Features Comparison
|
|
||||||
|
|
||||||
| Feature | FileStorage | SQLiteStorage | MemoryStorage | MemcachedStorage |
|
|
||||||
|---------|-------------|---------------|---------------|------------------|
|
|
||||||
| Persistence | ✅ | ✅ | ❌ | ✅ |
|
|
||||||
| File Dependencies | ✅ | ✅ | ✅ | ✅ |
|
|
||||||
| Tags | ❌ | ✅ | ✅ | ❌ |
|
|
||||||
| Callbacks | ✅ | ✅ | ✅ | ✅ |
|
|
||||||
| Bulk Operations | ✅ | ✅ | ✅ | ✅ |
|
|
||||||
| Performance | High | Medium | Highest | Very High |
|
|
||||||
| Distributed | ❌ | ❌ | ❌ | ✅ |
|
|
||||||
|
|
||||||
## Migration Guide
|
|
||||||
|
|
||||||
### Zero Migration Required
|
|
||||||
|
|
||||||
All existing code continues to work without any modifications:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// ✅ This works exactly as before - no changes needed
|
|
||||||
$cache = CACHE('bb_cache');
|
|
||||||
$forums = $datastore->get('cat_forums');
|
|
||||||
|
|
||||||
// ✅ All debug functionality preserved
|
|
||||||
global $CACHES;
|
|
||||||
foreach ($CACHES->obj as $cache_name => $cache_obj) {
|
|
||||||
echo "Cache: $cache_name\n";
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Enhanced Capabilities for New Code
|
|
||||||
|
|
||||||
New code can take advantage of advanced features:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// ✅ Enhanced caching with dependencies and tags
|
|
||||||
$cache = CACHE('bb_cache');
|
|
||||||
$forums = $cache->load('forums_with_stats', function() {
|
|
||||||
return build_forums_with_statistics();
|
|
||||||
}, [
|
|
||||||
\Nette\Caching\Cache::Expire => '1 hour',
|
|
||||||
\Nette\Caching\Cache::Files => ['/path/to/forums.config'],
|
|
||||||
\Nette\Caching\Cache::Tags => ['forums', 'statistics']
|
|
||||||
]);
|
|
||||||
|
|
||||||
// ✅ Function memoization
|
|
||||||
$expensive_result = $cache->call('calculate_user_stats', $user_id);
|
|
||||||
|
|
||||||
// ✅ Output buffering
|
|
||||||
$rendered_page = $cache->capture("page_$page_id", function() {
|
|
||||||
include_template('complex_page.php');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Benefits
|
|
||||||
|
|
||||||
### Benchmarks
|
|
||||||
|
|
||||||
- **456,647+ operations per second** in production testing
|
|
||||||
- **Singleton efficiency**: Each cache namespace instantiated only once
|
|
||||||
- **Memory optimization**: Shared storage and efficient instance management
|
|
||||||
- **Nette optimizations**: Advanced algorithms for cache invalidation and cleanup
|
|
||||||
|
|
||||||
### Advanced Features Performance
|
|
||||||
|
|
||||||
- **Bulk Operations**: Load multiple keys in single operation
|
|
||||||
- **Memoization**: Automatic function result caching with parameter-based keys
|
|
||||||
- **Dependencies**: Smart cache invalidation based on files, time, or custom logic
|
|
||||||
- **Output Buffering**: Cache generated output directly without intermediate storage
|
|
||||||
|
|
||||||
## Critical Issues Resolved
|
|
||||||
|
|
||||||
### Sessions Compatibility
|
|
||||||
|
|
||||||
**Issue**: Legacy cache returns `false` for missing values, Nette returns `null`
|
|
||||||
**Solution**: CacheManager->get() returns `$result ?? false` for backward compatibility
|
|
||||||
|
|
||||||
### Debug Integration
|
|
||||||
|
|
||||||
**Issue**: Dev.php expected `->db` property on cache objects for debug logging
|
|
||||||
**Solution**: Added `__get()` magic methods returning compatible debug objects with `dbg[]`, `engine`, `sql_timetotal` properties
|
|
||||||
|
|
||||||
### Architecture Consistency
|
|
||||||
|
|
||||||
**Issue**: Inconsistent initialization pattern compared to other TorrentPier singletons
|
|
||||||
**Solution**: Converted to proper singleton pattern with `getInstance()` method and clean global functions
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
### Architecture Flow (Refactored)
|
|
||||||
|
|
||||||
**Clean, Non-Redundant Architecture:**
|
|
||||||
```
|
|
||||||
UnifiedCacheSystem (singleton)
|
|
||||||
├── _buildStorage() → Creates Nette Storage instances directly
|
|
||||||
├── get_cache_obj() → Returns CacheManager with pre-built storage
|
|
||||||
└── getDatastore() → Returns DatastoreManager with pre-built storage
|
|
||||||
|
|
||||||
CacheManager (receives pre-built Storage)
|
|
||||||
├── Constructor receives: Storage instance + minimal config
|
|
||||||
├── No redundant initializeStorage() switch statement
|
|
||||||
└── Focuses purely on cache operations
|
|
||||||
|
|
||||||
DatastoreManager (uses CacheManager internally)
|
|
||||||
├── Constructor receives: Storage instance + minimal config
|
|
||||||
├── Uses CacheManager internally for unified functionality
|
|
||||||
└── Maintains datastore-specific methods and compatibility
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits of Refactored Architecture:**
|
|
||||||
- **Single Source of Truth**: Only UnifiedCacheSystem creates storage instances
|
|
||||||
- **No Redundancy**: Eliminated duplicate switch statements and configuration parsing
|
|
||||||
- **Cleaner Separation**: CacheManager focuses on caching, not storage creation
|
|
||||||
- **Impossible Path Bugs**: Storage is pre-built, no configuration mismatches possible
|
|
||||||
- **Better Maintainability**: One place to modify storage creation logic
|
|
||||||
|
|
||||||
### Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/Cache/
|
|
||||||
├── CacheManager.php # Cache interface with Nette Caching + singleton pattern
|
|
||||||
├── DatastoreManager.php # Datastore interface using CacheManager internally
|
|
||||||
├── UnifiedCacheSystem.php # Main singleton orchestrator + storage factory
|
|
||||||
└── README.md # This documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
### Removed Development Files
|
|
||||||
|
|
||||||
The following development and testing files were removed after successful integration:
|
|
||||||
- `Example.php` - Migration examples (no longer needed)
|
|
||||||
- `Integration.php` - Testing utilities (production-ready)
|
|
||||||
- `cache_test.php` - Performance testing script (completed)
|
|
||||||
|
|
||||||
### Key Features Achieved
|
|
||||||
|
|
||||||
1. **100% Backward Compatibility**: All existing APIs work unchanged
|
|
||||||
2. **Modern Foundation**: Built on stable, well-tested Nette Caching v3.3
|
|
||||||
3. **Advanced Features**: Dependencies, tags, bulk operations, memoization, output buffering
|
|
||||||
4. **Efficient Singletons**: Memory-efficient instance management following TorrentPier patterns
|
|
||||||
5. **Unified Debugging**: Consistent debug interface compatible with Dev.php
|
|
||||||
6. **Production Ready**: Comprehensive error handling, validation, and performance optimization
|
|
||||||
7. **Clean Architecture**: Eliminated redundant configuration logic and switch statements
|
|
||||||
8. **Single Storage Source**: All storage creation centralized in UnifiedCacheSystem
|
|
||||||
|
|
||||||
### Architectural Consistency
|
|
||||||
|
|
||||||
Following TorrentPier's established patterns:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Consistent with other singletons
|
|
||||||
config() -> Config::getInstance()
|
|
||||||
dev() -> Dev::getInstance()
|
|
||||||
censor() -> Censor::getInstance()
|
|
||||||
DB() -> DB::getInstance()
|
|
||||||
CACHE() -> UnifiedCacheSystem::getInstance()->getCache()
|
|
||||||
datastore() -> UnifiedCacheSystem::getInstance()->getDatastore()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing & Verification
|
|
||||||
|
|
||||||
### Backward Compatibility Verified
|
|
||||||
|
|
||||||
```php
|
|
||||||
// ✅ All existing functionality preserved
|
|
||||||
$cache = CACHE('bb_cache');
|
|
||||||
assert($cache->set('test', 'value', 60) === true);
|
|
||||||
assert($cache->get('test') === 'value');
|
|
||||||
assert($cache->rm('test') === true);
|
|
||||||
|
|
||||||
$datastore = datastore();
|
|
||||||
$datastore->store('test_item', ['data' => 'test']);
|
|
||||||
assert($datastore->get('test_item')['data'] === 'test');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Advanced Features Verified
|
|
||||||
|
|
||||||
```php
|
|
||||||
// ✅ Nette features working correctly
|
|
||||||
$cache = CACHE('advanced_test');
|
|
||||||
|
|
||||||
// Memoization
|
|
||||||
$result1 = $cache->call('expensive_function', 'param');
|
|
||||||
$result2 = $cache->call('expensive_function', 'param'); // From cache
|
|
||||||
|
|
||||||
// Dependencies
|
|
||||||
$cache->save('file_dependent', $data, [
|
|
||||||
\Nette\Caching\Cache::Files => [__FILE__]
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Bulk operations
|
|
||||||
$values = $cache->bulkLoad(['key1', 'key2'], function($key) {
|
|
||||||
return "value_$key";
|
|
||||||
});
|
|
||||||
|
|
||||||
// Performance: 456,647+ ops/sec verified
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debug Functionality Verified
|
|
||||||
|
|
||||||
```php
|
|
||||||
// ✅ Dev.php integration working
|
|
||||||
$cache = CACHE('bb_cache');
|
|
||||||
$debug = $cache->dbg; // Returns array of operations
|
|
||||||
$engine = $cache->engine; // Returns storage type
|
|
||||||
$time = $cache->sql_timetotal; // Returns total time
|
|
||||||
|
|
||||||
// ✅ Singleton behavior verified
|
|
||||||
$instance1 = TorrentPier\Cache\UnifiedCacheSystem::getInstance();
|
|
||||||
$instance2 = TorrentPier\Cache\UnifiedCacheSystem::getInstance();
|
|
||||||
assert($instance1 === $instance2); // Same instance
|
|
||||||
```
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
### Planned Storage Implementations
|
|
||||||
- Redis storage adapter for Nette
|
|
||||||
- Memcached storage adapter for Nette
|
|
||||||
- APCu storage adapter for Nette
|
|
||||||
|
|
||||||
### Advanced Features Roadmap
|
|
||||||
- Distributed caching support
|
|
||||||
- Cache warming and preloading
|
|
||||||
- Advanced metrics and monitoring
|
|
||||||
- Multi-tier caching strategies
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
This unified cache system represents a significant architectural improvement in TorrentPier while ensuring seamless backward compatibility and providing a robust foundation for future enhancements. The clean singleton pattern, advanced Nette Caching features, and comprehensive debug support make it a production-ready replacement for the legacy Cache and Datastore systems.
|
|
|
@ -1,362 +0,0 @@
|
||||||
# TorrentPier Database Layer
|
|
||||||
|
|
||||||
This directory contains the new database layer for TorrentPier 3.0 that uses Nette Database internally. **Breaking changes**: This replaces the legacy SqlDb interface and requires code migration.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The new database system has completely replaced the legacy SqlDb/Dbs system and provides:
|
|
||||||
|
|
||||||
- **Modern API** - New `DB()->method()` calls with improved functionality
|
|
||||||
- **Nette Database integration** - Modern, efficient database layer under the hood
|
|
||||||
- **Singleton pattern** - Efficient connection management
|
|
||||||
- **Complete feature parity** - All original functionality preserved
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Classes
|
|
||||||
|
|
||||||
1. **`Database`** - Main singleton database class using Nette Database Connection
|
|
||||||
2. **`DatabaseFactory`** - Factory that has completely replaced the legacy SqlDb/Dbs system
|
|
||||||
3. **`DatabaseDebugger`** - Dedicated debug functionality extracted from Database class
|
|
||||||
4. **`DebugSelection`** - Debug-enabled wrapper for Nette Database Selection
|
|
||||||
|
|
||||||
### Key Features
|
|
||||||
|
|
||||||
- **Singleton Pattern**: Ensures single database connection per server configuration
|
|
||||||
- **Multiple Database Support**: Handles multiple database servers via DatabaseFactory
|
|
||||||
- **Raw SQL Support**: Uses Nette Database's Connection class (SQL way) for minimal code impact
|
|
||||||
- **Complete Error Handling**: Maintains existing error handling behavior
|
|
||||||
- **Full Debug Support**: Preserves all debugging, logging, and explain functionality
|
|
||||||
- **Performance Tracking**: Query timing and slow query detection
|
|
||||||
- **Clean Architecture**: Debug functionality extracted to dedicated DatabaseDebugger class
|
|
||||||
- **Modular Design**: Single responsibility principle with separate debug and database concerns
|
|
||||||
|
|
||||||
## Implementation Status
|
|
||||||
|
|
||||||
- ✅ **Complete Replacement**: Legacy SqlDb/Dbs classes have been removed from the codebase
|
|
||||||
- ✅ **Backward Compatibility**: All existing `DB()->method()` calls work unchanged
|
|
||||||
- ✅ **Debug System**: Full explain(), logging, and performance tracking
|
|
||||||
- ✅ **Error Handling**: Complete error handling with sql_error() support
|
|
||||||
- ✅ **Connection Management**: Singleton pattern with proper initialization
|
|
||||||
- ✅ **Clean Architecture**: Debug functionality extracted to dedicated classes
|
|
||||||
- ✅ **Class Renaming**: Renamed DB → Database, DbFactory → DatabaseFactory for consistency
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Standard Database Operations
|
|
||||||
```php
|
|
||||||
// All existing code works unchanged
|
|
||||||
$user = DB()->fetch_row("SELECT * FROM users WHERE id = ?", 123);
|
|
||||||
$users = DB()->fetch_rowset("SELECT * FROM users");
|
|
||||||
$affected = DB()->affected_rows();
|
|
||||||
|
|
||||||
// Raw queries
|
|
||||||
$result = DB()->sql_query("UPDATE users SET status = ? WHERE id = ?", 1, 123);
|
|
||||||
|
|
||||||
// Data building
|
|
||||||
$data = ['name' => 'John', 'email' => 'john@example.com'];
|
|
||||||
$sql = DB()->build_array('INSERT', $data);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Multiple Database Servers
|
|
||||||
```php
|
|
||||||
// Access different database servers
|
|
||||||
$main_db = DB('db'); // Main database
|
|
||||||
$tracker_db = DB('tr'); // Tracker database
|
|
||||||
$stats_db = DB('stats'); // Statistics database
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
```php
|
|
||||||
$result = DB()->sql_query("SELECT * FROM users");
|
|
||||||
if (!$result) {
|
|
||||||
$error = DB()->sql_error();
|
|
||||||
echo "Error: " . $error['message'];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The database configuration is handled through the existing TorrentPier config system:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Initialized in common.php
|
|
||||||
TorrentPier\Database\DatabaseFactory::init(
|
|
||||||
config()->get('db'), // Database configurations
|
|
||||||
config()->get('db_alias', []) // Database aliases
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
- **Efficient Connections**: Singleton pattern prevents connection overhead
|
|
||||||
- **Modern Database Layer**: Nette Database v3.2 optimizations
|
|
||||||
- **Resource Management**: Automatic cleanup and proper connection handling
|
|
||||||
|
|
||||||
### Maintainability
|
|
||||||
- **Modern Codebase**: Uses current PHP standards and type declarations
|
|
||||||
- **Better Architecture**: Clean separation of concerns
|
|
||||||
- **Nette Ecosystem**: Part of actively maintained Nette framework
|
|
||||||
|
|
||||||
### Reliability
|
|
||||||
- **Proven Technology**: Nette Database is battle-tested
|
|
||||||
- **Regular Updates**: Automatic security and bug fixes through composer
|
|
||||||
- **Type Safety**: Better error detection and IDE support
|
|
||||||
|
|
||||||
## Debugging Features
|
|
||||||
|
|
||||||
All original debugging features are preserved and enhanced:
|
|
||||||
|
|
||||||
### Query Logging
|
|
||||||
- SQL query logging with timing
|
|
||||||
- Slow query detection and logging
|
|
||||||
- Memory usage tracking
|
|
||||||
|
|
||||||
### Debug Information
|
|
||||||
```php
|
|
||||||
// Enable debugging (same as before)
|
|
||||||
DB()->debug('start');
|
|
||||||
// ... run queries ...
|
|
||||||
DB()->debug('stop');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Explain Functionality
|
|
||||||
```php
|
|
||||||
// Explain queries (same interface as before)
|
|
||||||
DB()->explain('start');
|
|
||||||
DB()->explain('display');
|
|
||||||
```
|
|
||||||
|
|
||||||
## Technical Details
|
|
||||||
|
|
||||||
### Nette Database Integration
|
|
||||||
- Uses Nette Database **Connection** class (SQL way)
|
|
||||||
- Maintains raw SQL approach for minimal migration impact
|
|
||||||
- PDO-based with proper parameter binding
|
|
||||||
|
|
||||||
### Compatibility Layer
|
|
||||||
- All original method signatures preserved
|
|
||||||
- Same return types and behavior
|
|
||||||
- Error handling matches original implementation
|
|
||||||
|
|
||||||
### Connection Management
|
|
||||||
- Single connection per database server
|
|
||||||
- Lazy connection initialization
|
|
||||||
- Proper connection cleanup
|
|
||||||
|
|
||||||
## Migration Notes
|
|
||||||
|
|
||||||
This is a **complete replacement** that maintains 100% backward compatibility:
|
|
||||||
|
|
||||||
1. **No Code Changes Required**: All existing `DB()->method()` calls work unchanged
|
|
||||||
2. **Same Configuration**: Uses existing database configuration
|
|
||||||
3. **Same Behavior**: Error handling, return values, and debugging work identically
|
|
||||||
4. **Enhanced Performance**: Better connection management and modern database layer
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- **Nette Database v3.2**: Already included in composer.json
|
|
||||||
- **PHP 8.0+**: Required for type declarations and modern features
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
- `Database.php` - Main database class with full backward compatibility
|
|
||||||
- `DatabaseFactory.php` - Factory for managing database instances
|
|
||||||
- `DatabaseDebugger.php` - Dedicated debug functionality class
|
|
||||||
- `DebugSelection.php` - Debug-enabled Nette Selection wrapper
|
|
||||||
- `README.md` - This documentation
|
|
||||||
|
|
||||||
## Future Enhancement: Gradual Migration to Nette Explorer
|
|
||||||
|
|
||||||
While the current implementation uses Nette Database's **Connection** class (SQL way) for maximum compatibility, TorrentPier can gradually migrate to **Nette Database Explorer** for more modern ORM-style database operations.
|
|
||||||
|
|
||||||
### Phase 1: Hybrid Approach
|
|
||||||
|
|
||||||
Add Explorer support alongside existing Connection-based methods:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Current Connection-based approach (maintains compatibility)
|
|
||||||
$users = DB()->fetch_rowset("SELECT * FROM users WHERE status = ?", 1);
|
|
||||||
|
|
||||||
// New Explorer-based approach (added gradually)
|
|
||||||
$users = DB()->table('users')->where('status', 1)->fetchAll();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 2: Explorer Method Examples
|
|
||||||
|
|
||||||
#### Basic Table Operations
|
|
||||||
```php
|
|
||||||
// Select operations
|
|
||||||
$user = DB()->table('users')->get(123); // Get by ID
|
|
||||||
$users = DB()->table('users')->where('status', 1)->fetchAll(); // Where condition
|
|
||||||
$count = DB()->table('users')->where('status', 1)->count(); // Count records
|
|
||||||
|
|
||||||
// Insert operations
|
|
||||||
$user_id = DB()->table('users')->insert([
|
|
||||||
'username' => 'john',
|
|
||||||
'email' => 'john@example.com',
|
|
||||||
'reg_time' => time()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Update operations
|
|
||||||
DB()->table('users')
|
|
||||||
->where('id', 123)
|
|
||||||
->update(['last_visit' => time()]);
|
|
||||||
|
|
||||||
// Delete operations
|
|
||||||
DB()->table('users')
|
|
||||||
->where('status', 0)
|
|
||||||
->delete();
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Advanced Explorer Features
|
|
||||||
```php
|
|
||||||
// Joins and relationships
|
|
||||||
$posts = DB()->table('posts')
|
|
||||||
->select('posts.*, users.username')
|
|
||||||
->where('posts.forum_id', 5)
|
|
||||||
->order('posts.post_time DESC')
|
|
||||||
->limit(20)
|
|
||||||
->fetchAll();
|
|
||||||
|
|
||||||
// Aggregations
|
|
||||||
$stats = DB()->table('torrents')
|
|
||||||
->select('forum_id, COUNT(*) as total, SUM(size) as total_size')
|
|
||||||
->where('approved', 1)
|
|
||||||
->group('forum_id')
|
|
||||||
->fetchAll();
|
|
||||||
|
|
||||||
// Subqueries
|
|
||||||
$active_users = DB()->table('users')
|
|
||||||
->where('last_visit > ?', time() - 86400)
|
|
||||||
->where('id IN', DB()->table('posts')
|
|
||||||
->select('user_id')
|
|
||||||
->where('post_time > ?', time() - 86400)
|
|
||||||
)
|
|
||||||
->fetchAll();
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Working with Related Data
|
|
||||||
```php
|
|
||||||
// One-to-many relationships
|
|
||||||
$user = DB()->table('users')->get(123);
|
|
||||||
$user_posts = $user->related('posts')->order('post_time DESC');
|
|
||||||
|
|
||||||
// Many-to-many through junction table
|
|
||||||
$torrent = DB()->table('torrents')->get(456);
|
|
||||||
$seeders = $torrent->related('bt_tracker', 'torrent_id')
|
|
||||||
->where('seeder', 'yes')
|
|
||||||
->select('user_id');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 3: Migration Strategy
|
|
||||||
|
|
||||||
#### Step-by-Step Conversion
|
|
||||||
1. **Identify Patterns**: Find common SQL patterns in the codebase
|
|
||||||
2. **Create Helpers**: Build wrapper methods for complex queries
|
|
||||||
3. **Test Incrementally**: Convert one module at a time
|
|
||||||
4. **Maintain Compatibility**: Keep both approaches during transition
|
|
||||||
|
|
||||||
#### Example Migration Pattern
|
|
||||||
```php
|
|
||||||
// Before: Raw SQL
|
|
||||||
$result = DB()->sql_query("
|
|
||||||
SELECT t.*, u.username
|
|
||||||
FROM torrents t
|
|
||||||
JOIN users u ON t.poster_id = u.user_id
|
|
||||||
WHERE t.forum_id = ? AND t.approved = 1
|
|
||||||
ORDER BY t.reg_time DESC
|
|
||||||
LIMIT ?
|
|
||||||
", $forum_id, $limit);
|
|
||||||
|
|
||||||
$torrents = [];
|
|
||||||
while ($row = DB()->sql_fetchrow($result)) {
|
|
||||||
$torrents[] = $row;
|
|
||||||
}
|
|
||||||
|
|
||||||
// After: Explorer ORM
|
|
||||||
$torrents = DB()->table('torrents')
|
|
||||||
->alias('t')
|
|
||||||
->select('t.*, u.username')
|
|
||||||
->where('t.forum_id', $forum_id)
|
|
||||||
->where('t.approved', 1)
|
|
||||||
->order('t.reg_time DESC')
|
|
||||||
->limit($limit)
|
|
||||||
->fetchAll();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 4: Advanced Explorer Features
|
|
||||||
|
|
||||||
#### Custom Repository Classes
|
|
||||||
```php
|
|
||||||
// Create specialized repository classes
|
|
||||||
class TorrentRepository
|
|
||||||
{
|
|
||||||
private $db;
|
|
||||||
|
|
||||||
public function __construct($db)
|
|
||||||
{
|
|
||||||
$this->db = $db;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getApprovedByForum($forum_id, $limit = 20)
|
|
||||||
{
|
|
||||||
return $this->db->table('torrents')
|
|
||||||
->where('forum_id', $forum_id)
|
|
||||||
->where('approved', 1)
|
|
||||||
->order('reg_time DESC')
|
|
||||||
->limit($limit)
|
|
||||||
->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTopSeeded($limit = 10)
|
|
||||||
{
|
|
||||||
return $this->db->table('torrents')
|
|
||||||
->where('approved', 1)
|
|
||||||
->order('seeders DESC')
|
|
||||||
->limit($limit)
|
|
||||||
->fetchAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage
|
|
||||||
$torrentRepo = new TorrentRepository(DB());
|
|
||||||
$popular = $torrentRepo->getTopSeeded();
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Database Events and Caching
|
|
||||||
```php
|
|
||||||
// Add caching layer
|
|
||||||
$cached_result = DB()->table('users')
|
|
||||||
->where('status', 1)
|
|
||||||
->cache('active_users', 3600) // Cache for 1 hour
|
|
||||||
->fetchAll();
|
|
||||||
|
|
||||||
// Database events for logging
|
|
||||||
DB()->onQuery[] = function ($query, $parameters, $time) {
|
|
||||||
if ($time > 1.0) { // Log slow queries
|
|
||||||
error_log("Slow query ({$time}s): $query");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Benefits of Explorer Migration
|
|
||||||
|
|
||||||
#### Developer Experience
|
|
||||||
- **Fluent Interface**: Chainable method calls
|
|
||||||
- **IDE Support**: Better autocomplete and type hints
|
|
||||||
- **Less SQL**: Reduced raw SQL writing
|
|
||||||
- **Built-in Security**: Automatic parameter binding
|
|
||||||
|
|
||||||
#### Code Quality
|
|
||||||
- **Readable Code**: Self-documenting query building
|
|
||||||
- **Reusable Patterns**: Common queries become methods
|
|
||||||
- **Type Safety**: Better error detection
|
|
||||||
- **Testing**: Easier to mock and test
|
|
||||||
|
|
||||||
#### Performance
|
|
||||||
- **Query Optimization**: Explorer can optimize queries
|
|
||||||
- **Lazy Loading**: Load related data only when needed
|
|
||||||
- **Connection Pooling**: Better resource management
|
|
||||||
- **Caching Integration**: Built-in caching support
|
|
|
@ -1,8 +0,0 @@
|
||||||
# Forum Domain Exceptions
|
|
||||||
|
|
||||||
Domain-specific exceptions for forum operations:
|
|
||||||
|
|
||||||
- `ThreadLockedException`: Attempt to post in locked thread
|
|
||||||
- `PostEditTimeExpiredException`: Edit time limit exceeded
|
|
||||||
- `ForumAccessDeniedException`: Insufficient permissions
|
|
||||||
- `InvalidPostContentException`: Post validation failed
|
|
|
@ -1,15 +0,0 @@
|
||||||
# Forum Domain Models
|
|
||||||
|
|
||||||
Contains aggregate roots and entities for the forum system:
|
|
||||||
|
|
||||||
- `Forum`: Forum category aggregate
|
|
||||||
- `Thread`: Discussion thread aggregate root
|
|
||||||
- `Post`: Individual post entity
|
|
||||||
- `Attachment`: File attachment entity
|
|
||||||
|
|
||||||
Business rules enforced at this level:
|
|
||||||
|
|
||||||
- Post editing time limits
|
|
||||||
- Thread locking rules
|
|
||||||
- Forum access permissions
|
|
||||||
- Post moderation workflow
|
|
|
@ -1,9 +0,0 @@
|
||||||
# Forum Repository Interfaces
|
|
||||||
|
|
||||||
Repository interfaces for forum aggregates:
|
|
||||||
|
|
||||||
- `ForumRepositoryInterface`: Forum aggregate persistence
|
|
||||||
- `ThreadRepositoryInterface`: Thread aggregate persistence
|
|
||||||
- `PostRepositoryInterface`: Post queries (read-only)
|
|
||||||
|
|
||||||
These interfaces define contracts that infrastructure must implement.
|
|
|
@ -1,10 +0,0 @@
|
||||||
# Forum Value Objects
|
|
||||||
|
|
||||||
Immutable objects representing forum concepts:
|
|
||||||
|
|
||||||
- `PostId`: Unique post identifier
|
|
||||||
- `ThreadTitle`: Thread title with validation
|
|
||||||
- `PostContent`: Formatted post content
|
|
||||||
- `ForumSlug`: URL-friendly forum identifier
|
|
||||||
|
|
||||||
These objects ensure type safety and encapsulate validation rules.
|
|
|
@ -1,15 +0,0 @@
|
||||||
# Domain Layer
|
|
||||||
|
|
||||||
This directory contains the core business logic of TorrentPier. Code here should:
|
|
||||||
|
|
||||||
- Have no dependencies on frameworks or infrastructure
|
|
||||||
- Represent pure business rules and domain models
|
|
||||||
- Be testable in isolation
|
|
||||||
- Use only PHP language features and domain concepts
|
|
||||||
|
|
||||||
## Bounded Contexts
|
|
||||||
|
|
||||||
- **Forum**: Discussion forums, posts, threads
|
|
||||||
- **Tracker**: BitTorrent tracking, peers, torrents
|
|
||||||
- **User**: User management, authentication, profiles
|
|
||||||
- **Shared**: Minimal shared concepts between contexts
|
|
|
@ -1,15 +0,0 @@
|
||||||
# Domain Events
|
|
||||||
|
|
||||||
Base classes and interfaces for domain event system:
|
|
||||||
|
|
||||||
- `DomainEvent`: Base event interface
|
|
||||||
- `EventRecording`: Trait for aggregate event recording
|
|
||||||
- `AggregateHistory`: Event sourcing support
|
|
||||||
|
|
||||||
Example events:
|
|
||||||
|
|
||||||
- `UserRegisteredEvent`
|
|
||||||
- `TorrentUploadedEvent`
|
|
||||||
- `ThreadCreatedEvent`
|
|
||||||
|
|
||||||
Events enable loose coupling between bounded contexts.
|
|
|
@ -1,10 +0,0 @@
|
||||||
# Shared Domain Exceptions
|
|
||||||
|
|
||||||
Base exception classes used across all bounded contexts:
|
|
||||||
|
|
||||||
- `DomainException`: Base domain exception
|
|
||||||
- `InvalidArgumentException`: Invalid input validation
|
|
||||||
- `EntityNotFoundException`: Generic entity not found
|
|
||||||
- `BusinessRuleViolationException`: Business rule violations
|
|
||||||
|
|
||||||
These provide common exception handling patterns without coupling contexts.
|
|
|
@ -1,10 +0,0 @@
|
||||||
# Shared Domain Models
|
|
||||||
|
|
||||||
Base classes and interfaces used across bounded contexts:
|
|
||||||
|
|
||||||
- `AggregateRoot`: Base class for all aggregates
|
|
||||||
- `Entity`: Base class for entities
|
|
||||||
- `DomainEvent`: Interface for domain events
|
|
||||||
- `ValueObject`: Base value object interface
|
|
||||||
|
|
||||||
These provide common functionality while maintaining context boundaries.
|
|
|
@ -1,9 +0,0 @@
|
||||||
# Shared Repository Interfaces
|
|
||||||
|
|
||||||
Common repository patterns and base interfaces:
|
|
||||||
|
|
||||||
- `RepositoryInterface`: Base repository contract
|
|
||||||
- `ReadOnlyRepositoryInterface`: Query-only operations
|
|
||||||
- `AggregateRepositoryInterface`: Aggregate-specific methods
|
|
||||||
|
|
||||||
These define common persistence patterns without implementation details.
|
|
|
@ -1,10 +0,0 @@
|
||||||
# Shared Value Objects
|
|
||||||
|
|
||||||
Common value objects used across contexts:
|
|
||||||
|
|
||||||
- `Id`: Generic identifier base class
|
|
||||||
- `DateTime`: Immutable datetime wrapper
|
|
||||||
- `Money`: Monetary value representation
|
|
||||||
- `Percentage`: Percentage value (0-100)
|
|
||||||
|
|
||||||
These are minimal shared concepts that don't violate bounded context principles.
|
|
|
@ -1,8 +0,0 @@
|
||||||
# Tracker Domain Exceptions
|
|
||||||
|
|
||||||
Domain-specific exceptions for tracker operations:
|
|
||||||
|
|
||||||
- `InvalidInfoHashException`: Malformed info hash
|
|
||||||
- `InactiveTorrentException`: Torrent not active
|
|
||||||
- `PeerLimitExceededException`: Too many peers
|
|
||||||
- `InvalidAnnounceException`: Protocol violation
|
|
|
@ -1,19 +0,0 @@
|
||||||
# Tracker Domain Models
|
|
||||||
|
|
||||||
Contains aggregate roots and entities for the BitTorrent tracker:
|
|
||||||
|
|
||||||
- `Torrent`: Aggregate root for torrent management
|
|
||||||
- `Peer`: Entity representing a BitTorrent peer
|
|
||||||
- `TorrentStatistics`: Value object for torrent stats
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```php
|
|
||||||
class Torrent extends AggregateRoot
|
|
||||||
{
|
|
||||||
public function announce(Peer $peer, AnnounceEvent $event): void
|
|
||||||
{
|
|
||||||
// Business logic for handling announces
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,9 +0,0 @@
|
||||||
# Tracker Repository Interfaces
|
|
||||||
|
|
||||||
Repository interfaces for tracker aggregates:
|
|
||||||
|
|
||||||
- `TorrentRepositoryInterface`: Torrent aggregate persistence
|
|
||||||
- `PeerRepositoryInterface`: Peer data access
|
|
||||||
- `TrackerStatsRepositoryInterface`: Statistics queries
|
|
||||||
|
|
||||||
These define persistence contracts without implementation details.
|
|
|
@ -1,23 +0,0 @@
|
||||||
# Tracker Value Objects
|
|
||||||
|
|
||||||
Immutable objects representing domain concepts:
|
|
||||||
|
|
||||||
- `InfoHash`: 20-byte torrent identifier
|
|
||||||
- `PeerId`: Peer client identifier
|
|
||||||
- `Port`: Network port (1-65535)
|
|
||||||
- `BytesTransferred`: Upload/download bytes
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```php
|
|
||||||
final class InfoHash
|
|
||||||
{
|
|
||||||
private string $hash;
|
|
||||||
|
|
||||||
public function __construct(string $hash)
|
|
||||||
{
|
|
||||||
$this->guardAgainstInvalidHash($hash);
|
|
||||||
$this->hash = $hash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,9 +0,0 @@
|
||||||
# User Domain Exceptions
|
|
||||||
|
|
||||||
Domain-specific exceptions for user operations:
|
|
||||||
|
|
||||||
- `UserNotFoundException`: User not found
|
|
||||||
- `DuplicateUsernameException`: Username already taken
|
|
||||||
- `InvalidCredentialsException`: Authentication failed
|
|
||||||
- `UserNotActivatedException`: Account not activated
|
|
||||||
- `PasswordComplexityException`: Weak password
|
|
|
@ -1,15 +0,0 @@
|
||||||
# User Domain Models
|
|
||||||
|
|
||||||
Contains user management aggregates and entities:
|
|
||||||
|
|
||||||
- `User`: User aggregate root
|
|
||||||
- `UserProfile`: User profile information
|
|
||||||
- `UserCredentials`: Authentication credentials
|
|
||||||
- `UserPermissions`: Authorization rules
|
|
||||||
|
|
||||||
Business rules:
|
|
||||||
|
|
||||||
- Password complexity requirements
|
|
||||||
- Username uniqueness
|
|
||||||
- Email verification workflow
|
|
||||||
- Account activation/deactivation
|
|
|
@ -1,10 +0,0 @@
|
||||||
# User Repository Interface
|
|
||||||
|
|
||||||
Repository interface for user aggregate:
|
|
||||||
|
|
||||||
- `UserRepositoryInterface`: User persistence and retrieval
|
|
||||||
- `findById(UserId $id): ?User`
|
|
||||||
- `findByUsername(Username $username): ?User`
|
|
||||||
- `findByEmail(Email $email): ?User`
|
|
||||||
- `save(User $user): void`
|
|
||||||
- `delete(User $user): void`
|
|
|
@ -1,11 +0,0 @@
|
||||||
# User Value Objects
|
|
||||||
|
|
||||||
Immutable objects for user domain:
|
|
||||||
|
|
||||||
- `UserId`: Unique user identifier
|
|
||||||
- `Username`: Username with validation
|
|
||||||
- `Email`: Email address with format validation
|
|
||||||
- `HashedPassword`: Secure password representation
|
|
||||||
- `UserRole`: User role enumeration
|
|
||||||
|
|
||||||
These ensure data integrity and type safety.
|
|
|
@ -1,9 +0,0 @@
|
||||||
# File Cache Adapter
|
|
||||||
|
|
||||||
File system-based caching implementation:
|
|
||||||
|
|
||||||
- Simple file-based storage
|
|
||||||
- Suitable for single-server deployments
|
|
||||||
- Directory structure optimization
|
|
||||||
- Lock file support for concurrency
|
|
||||||
- Automatic garbage collection
|
|
|
@ -1,9 +0,0 @@
|
||||||
# Memcached Cache Adapter
|
|
||||||
|
|
||||||
Memcached-based caching implementation:
|
|
||||||
|
|
||||||
- Simple key-value caching
|
|
||||||
- Distributed cache support
|
|
||||||
- Automatic key expiration
|
|
||||||
- Connection pooling
|
|
||||||
- Consistent hashing for distribution
|
|
|
@ -1,9 +0,0 @@
|
||||||
# Redis Cache Adapter
|
|
||||||
|
|
||||||
Redis-based caching implementation:
|
|
||||||
|
|
||||||
- High-performance key-value storage
|
|
||||||
- Support for data structures (lists, sets, hashes)
|
|
||||||
- TTL management
|
|
||||||
- Pub/sub for cache invalidation
|
|
||||||
- Cluster support for scalability
|
|
|
@ -1,171 +0,0 @@
|
||||||
# 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,
|
|
||||||
// ],
|
|
||||||
// ];
|
|
||||||
```
|
|
|
@ -1,16 +0,0 @@
|
||||||
# Email Templates
|
|
||||||
|
|
||||||
Email template management:
|
|
||||||
|
|
||||||
- HTML and plain text templates
|
|
||||||
- Template variables and placeholders
|
|
||||||
- Multi-language support
|
|
||||||
- Template caching
|
|
||||||
- Preview functionality
|
|
||||||
|
|
||||||
Common templates:
|
|
||||||
|
|
||||||
- User registration confirmation
|
|
||||||
- Password reset
|
|
||||||
- Torrent notifications
|
|
||||||
- Forum post notifications
|
|
|
@ -1,10 +0,0 @@
|
||||||
# Email Transport
|
|
||||||
|
|
||||||
Email delivery mechanisms:
|
|
||||||
|
|
||||||
- `SmtpTransport`: Traditional SMTP delivery
|
|
||||||
- `SendgridTransport`: SendGrid API integration
|
|
||||||
- `MailgunTransport`: Mailgun API integration
|
|
||||||
- `NullTransport`: Testing/development transport
|
|
||||||
|
|
||||||
Supports queuing and retry mechanisms.
|
|
|
@ -1,11 +0,0 @@
|
||||||
# Local File Storage
|
|
||||||
|
|
||||||
Local filesystem storage implementation:
|
|
||||||
|
|
||||||
- Direct disk storage
|
|
||||||
- Directory structure management
|
|
||||||
- File permissions handling
|
|
||||||
- Atomic file operations
|
|
||||||
- Cleanup and maintenance
|
|
||||||
|
|
||||||
Used for torrent files, avatars, and attachments.
|
|
|
@ -1,11 +0,0 @@
|
||||||
# S3 File Storage
|
|
||||||
|
|
||||||
Amazon S3 (or compatible) storage implementation:
|
|
||||||
|
|
||||||
- Cloud object storage
|
|
||||||
- Pre-signed URLs for direct uploads
|
|
||||||
- CDN integration support
|
|
||||||
- Lifecycle policies
|
|
||||||
- Multi-region support
|
|
||||||
|
|
||||||
Provides scalable storage for large deployments.
|
|
|
@ -1,9 +0,0 @@
|
||||||
# Database Adapter
|
|
||||||
|
|
||||||
Database connection and query building layer:
|
|
||||||
|
|
||||||
- `DatabaseAdapterInterface`: Common database operations
|
|
||||||
- `QueryBuilder`: Fluent query construction
|
|
||||||
- `ConnectionPool`: Connection management
|
|
||||||
|
|
||||||
Provides abstraction over raw database access.
|
|
|
@ -1,10 +0,0 @@
|
||||||
# Database Migrations
|
|
||||||
|
|
||||||
Schema version control and migration management:
|
|
||||||
|
|
||||||
- Migration files for schema changes
|
|
||||||
- Seed data for initial setup
|
|
||||||
- Rollback support
|
|
||||||
- Migration history tracking
|
|
||||||
|
|
||||||
Uses Phinx or similar migration tool.
|
|
|
@ -1,29 +0,0 @@
|
||||||
# Repository Implementations
|
|
||||||
|
|
||||||
Concrete implementations of domain repository interfaces:
|
|
||||||
|
|
||||||
- Uses database adapter for persistence
|
|
||||||
- Implements caching strategies
|
|
||||||
- Handles query optimization
|
|
||||||
- Supports multiple database backends
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```php
|
|
||||||
class TorrentRepository implements TorrentRepositoryInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private DatabaseAdapterInterface $db
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function findByInfoHash(InfoHash $infoHash): ?Torrent
|
|
||||||
{
|
|
||||||
// Database adapter implementation
|
|
||||||
$row = $this->db->select('torrents')
|
|
||||||
->where('info_hash', $infoHash->toString())
|
|
||||||
->first();
|
|
||||||
|
|
||||||
return $row ? $this->hydrateFromRow($row) : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,11 +0,0 @@
|
||||||
# Infrastructure Layer
|
|
||||||
|
|
||||||
Technical implementations and external service adapters:
|
|
||||||
|
|
||||||
- Database persistence
|
|
||||||
- Caching mechanisms
|
|
||||||
- Email services
|
|
||||||
- File storage
|
|
||||||
- Third-party integrations
|
|
||||||
|
|
||||||
Infrastructure depends on domain, not vice versa.
|
|
|
@ -1,10 +0,0 @@
|
||||||
# CLI Commands
|
|
||||||
|
|
||||||
Console commands for administrative tasks:
|
|
||||||
|
|
||||||
- `UserCreateCommand`: Create users from CLI
|
|
||||||
- `CacheClearCommand`: Clear cache stores
|
|
||||||
- `MigrateCommand`: Run database migrations
|
|
||||||
- `CronCommand`: Execute scheduled tasks
|
|
||||||
|
|
||||||
Built using Symfony Console component.
|
|
|
@ -1,30 +0,0 @@
|
||||||
# Admin Panel Controllers
|
|
||||||
|
|
||||||
Administrative interface controllers with enhanced security:
|
|
||||||
|
|
||||||
- Role-based access control (RBAC)
|
|
||||||
- Audit logging for all actions
|
|
||||||
- Additional authentication checks
|
|
||||||
- Administrative dashboards and reports
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```php
|
|
||||||
class AdminUserController
|
|
||||||
{
|
|
||||||
public function index(Request $request): Response
|
|
||||||
{
|
|
||||||
$query = new GetUsersQuery(
|
|
||||||
page: $request->getPage(),
|
|
||||||
filters: $request->getFilters()
|
|
||||||
);
|
|
||||||
|
|
||||||
$users = $this->queryBus->handle($query);
|
|
||||||
|
|
||||||
return $this->render('admin/users/index', [
|
|
||||||
'users' => $users,
|
|
||||||
'filters' => $request->getFilters()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,31 +0,0 @@
|
||||||
# API Controllers
|
|
||||||
|
|
||||||
RESTful API endpoints following OpenAPI specification:
|
|
||||||
|
|
||||||
- JSON request/response format
|
|
||||||
- Proper HTTP status codes
|
|
||||||
- HATEOAS where applicable
|
|
||||||
- Rate limiting aware
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```php
|
|
||||||
class UserController
|
|
||||||
{
|
|
||||||
public function register(RegisterRequest $request): JsonResponse
|
|
||||||
{
|
|
||||||
$command = new RegisterUserCommand(
|
|
||||||
$request->getUsername(),
|
|
||||||
$request->getEmail(),
|
|
||||||
$request->getPassword()
|
|
||||||
);
|
|
||||||
|
|
||||||
$userId = $this->commandBus->handle($command);
|
|
||||||
|
|
||||||
return new JsonResponse([
|
|
||||||
'id' => $userId,
|
|
||||||
'username' => $request->getUsername()
|
|
||||||
], Response::HTTP_CREATED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,16 +0,0 @@
|
||||||
# Web Controllers
|
|
||||||
|
|
||||||
Traditional web interface controllers:
|
|
||||||
|
|
||||||
- HTML response generation
|
|
||||||
- Template rendering
|
|
||||||
- Form handling
|
|
||||||
- Session management
|
|
||||||
- CSRF protection
|
|
||||||
|
|
||||||
Controllers for:
|
|
||||||
|
|
||||||
- Forum browsing and posting
|
|
||||||
- Torrent browsing and downloading
|
|
||||||
- User profiles and settings
|
|
||||||
- Search functionality
|
|
|
@ -1,12 +0,0 @@
|
||||||
# HTTP Middleware
|
|
||||||
|
|
||||||
Request/response pipeline middleware:
|
|
||||||
|
|
||||||
- `AuthenticationMiddleware`: User authentication
|
|
||||||
- `AuthorizationMiddleware`: Permission checks
|
|
||||||
- `CsrfProtectionMiddleware`: CSRF token validation
|
|
||||||
- `RateLimitMiddleware`: Request throttling
|
|
||||||
- `LocalizationMiddleware`: Language detection
|
|
||||||
- `CorsMiddleware`: Cross-origin resource sharing
|
|
||||||
|
|
||||||
Middleware follows PSR-15 standard.
|
|
|
@ -1,25 +0,0 @@
|
||||||
# HTTP Requests
|
|
||||||
|
|
||||||
Request objects and validation:
|
|
||||||
|
|
||||||
- Request DTOs with validation rules
|
|
||||||
- Type-safe access to request data
|
|
||||||
- File upload handling
|
|
||||||
- Input sanitization
|
|
||||||
- Custom validation rules
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```php
|
|
||||||
class RegisterRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'username' => ['required', 'string', 'min:3', 'max:20', 'unique:users'],
|
|
||||||
'email' => ['required', 'email', 'unique:users'],
|
|
||||||
'password' => ['required', 'string', 'min:8', 'confirmed'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,11 +0,0 @@
|
||||||
# HTTP Responses
|
|
||||||
|
|
||||||
Response transformation and formatting:
|
|
||||||
|
|
||||||
- Response factories
|
|
||||||
- JSON transformers
|
|
||||||
- View presenters
|
|
||||||
- Error response formatting
|
|
||||||
- Content negotiation
|
|
||||||
|
|
||||||
Ensures consistent API responses and proper HTTP semantics.
|
|
|
@ -1,11 +0,0 @@
|
||||||
# Presentation Layer
|
|
||||||
|
|
||||||
User interface implementations:
|
|
||||||
|
|
||||||
- HTTP controllers for web and API
|
|
||||||
- CLI commands for console operations
|
|
||||||
- Request/response handling
|
|
||||||
- Input validation
|
|
||||||
- Output formatting
|
|
||||||
|
|
||||||
This layer translates between external format and application format.
|
|
|
@ -1,132 +0,0 @@
|
||||||
# TorrentPier Whoops Enhanced Error Reporting
|
|
||||||
|
|
||||||
This directory contains enhanced Whoops error handlers specifically designed for TorrentPier to provide better debugging information when database errors occur.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### Enhanced Database Error Reporting
|
|
||||||
|
|
||||||
The enhanced Whoops handlers provide comprehensive database information when errors occur:
|
|
||||||
|
|
||||||
1. **Current SQL Query** - Shows the exact query that caused the error
|
|
||||||
2. **Recent Query History** - Displays the last 5 SQL queries executed
|
|
||||||
3. **Database Connection Status** - Connection state, server info, database name
|
|
||||||
4. **Error Context** - PDO error codes, exception details, source location
|
|
||||||
5. **TorrentPier Environment** - Debug mode status, system information
|
|
||||||
|
|
||||||
### Components
|
|
||||||
|
|
||||||
#### EnhancedPrettyPageHandler
|
|
||||||
|
|
||||||
Extends Whoops' default `PrettyPageHandler` to include:
|
|
||||||
- **Database Information** table with connection details and current query
|
|
||||||
- **Recent SQL Queries** table showing query history with timing
|
|
||||||
- **TorrentPier Environment** table with system status
|
|
||||||
|
|
||||||
#### DatabaseErrorHandler
|
|
||||||
|
|
||||||
Specialized handler that:
|
|
||||||
- Adds database context to exception stack frames
|
|
||||||
- Identifies database-related code in the call stack
|
|
||||||
- Collects comprehensive database state information
|
|
||||||
- Formats SQL queries for readable display
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
The enhanced handlers are automatically activated when `DBG_USER` is enabled in TorrentPier configuration.
|
|
||||||
|
|
||||||
### Automatic Integration
|
|
||||||
|
|
||||||
```php
|
|
||||||
// In src/Dev.php - automatically configured
|
|
||||||
$prettyPageHandler = new \TorrentPier\Whoops\EnhancedPrettyPageHandler();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Error Logging
|
|
||||||
|
|
||||||
Database errors are now automatically logged even when they occur in Nette Database layer:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Enhanced error handling in Database.php
|
|
||||||
try {
|
|
||||||
$row = $result->fetch();
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
// Log the error including the query that caused it
|
|
||||||
$this->debugger->log_error($e);
|
|
||||||
throw $e; // Re-throw for Whoops display
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Information Displayed
|
|
||||||
|
|
||||||
When a database error occurs, Whoops will now show:
|
|
||||||
|
|
||||||
### Database Information
|
|
||||||
- Connection Status: Connected/Disconnected
|
|
||||||
- Database Server: Host and port information
|
|
||||||
- Selected Database: Current database name
|
|
||||||
- Database Engine: MySQL/PostgreSQL/etc.
|
|
||||||
- Total Queries: Number of queries executed
|
|
||||||
- Total Query Time: Cumulative execution time
|
|
||||||
- Current Query: The SQL that caused the error
|
|
||||||
- Last Database Error: Error code and message
|
|
||||||
- PDO Driver: Database driver information
|
|
||||||
- Server Version: Database server version
|
|
||||||
|
|
||||||
### Recent SQL Queries
|
|
||||||
- **Query #1-5**: Last 5 queries executed
|
|
||||||
- SQL: Formatted query text
|
|
||||||
- Time: Execution time in seconds
|
|
||||||
- Source: File and line where query originated
|
|
||||||
- Info: Additional query information
|
|
||||||
- Memory: Memory usage if available
|
|
||||||
|
|
||||||
### TorrentPier Environment
|
|
||||||
- Application Environment: local/production/etc.
|
|
||||||
- Debug Mode: Enabled/Disabled
|
|
||||||
- SQL Debug: Enabled/Disabled
|
|
||||||
- TorrentPier Version: Current version
|
|
||||||
- Config Loaded: Configuration status
|
|
||||||
- Cache System: Availability status
|
|
||||||
- Language System: Status and encoding
|
|
||||||
- Template System: Twig-based availability
|
|
||||||
- Execution Time: Request processing time
|
|
||||||
- Peak Memory: Maximum memory used
|
|
||||||
- Current Memory: Current memory usage
|
|
||||||
- Request Method: GET/POST/etc.
|
|
||||||
- Request URI: Current page
|
|
||||||
- User Agent: Browser information
|
|
||||||
- Remote IP: Client IP address
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The enhanced handlers respect TorrentPier's debug configuration:
|
|
||||||
|
|
||||||
- `DBG_USER`: Must be enabled to show enhanced error pages
|
|
||||||
- `SQL_DEBUG`: Enables SQL query logging and timing
|
|
||||||
- `APP_ENV`: Determines environment-specific features
|
|
||||||
|
|
||||||
## Logging
|
|
||||||
|
|
||||||
Database errors are now logged in multiple locations:
|
|
||||||
|
|
||||||
1. **PHP Error Log**: Basic error message
|
|
||||||
2. **TorrentPier bb_log**: Detailed error with context (`database_errors.log`)
|
|
||||||
3. **Whoops Log**: Complete error details (`php_whoops.log`)
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
The enhanced handlers maintain security by:
|
|
||||||
- Only showing detailed information when `DBG_USER` is enabled
|
|
||||||
- Using Whoops' blacklist for sensitive data
|
|
||||||
- Logging detailed information to files (not user-accessible)
|
|
||||||
- Providing generic error messages to non-debug users
|
|
||||||
|
|
||||||
## Integration with TorrentPier 3.0
|
|
||||||
|
|
||||||
All enhancements are:
|
|
||||||
|
|
||||||
- **Integrated** with the new TorrentPier 3.0 architecture
|
|
||||||
- **Modern** - designed for the rewritten codebase
|
|
||||||
- **Optional** - only activated in debug mode
|
|
||||||
- **Safe** - no security implications for production use
|
|
|
@ -1,193 +0,0 @@
|
||||||
<?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,95 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Test Case
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| The closure you provide to your test functions is always bound to a specific PHPUnit test
|
|
||||||
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
|
|
||||||
| need to change it using the "pest()" function to bind a different classes or traits.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
pest()->extend(Tests\TestCase::class)->in('Feature');
|
|
||||||
pest()->extend(Tests\TestCase::class)->in('Unit');
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Expectations
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| When you're writing tests, you often need to check that values meet certain conditions. The
|
|
||||||
| "expect()" function gives you access to a set of "expectations" methods that you can use
|
|
||||||
| to assert different things. Of course, you may extend the Expectation API at any time.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
expect()->extend('toBeOne', function () {
|
|
||||||
return $this->toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Functions
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
|
|
||||||
| project that you don't want to repeat in every file. Here you can also expose helpers as
|
|
||||||
| global functions to help you to reduce the number of lines of code in your test files.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performance Testing Helpers
|
|
||||||
*/
|
|
||||||
function measureExecutionTime(callable $callback): float
|
|
||||||
{
|
|
||||||
$start = microtime(true);
|
|
||||||
$callback();
|
|
||||||
return microtime(true) - $start;
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectExecutionTimeUnder(callable $callback, float $maxSeconds): void
|
|
||||||
{
|
|
||||||
$time = measureExecutionTime($callback);
|
|
||||||
expect($time)->toBeLessThan($maxSeconds, "Execution took {$time}s, expected under {$maxSeconds}s");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* File System Helpers
|
|
||||||
*/
|
|
||||||
function createTempDirectory(): string
|
|
||||||
{
|
|
||||||
$tempDir = sys_get_temp_dir() . '/torrentpier_test_' . uniqid();
|
|
||||||
mkdir($tempDir, 0755, true);
|
|
||||||
return $tempDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeTempDirectory(string $dir): void
|
|
||||||
{
|
|
||||||
if (is_dir($dir)) {
|
|
||||||
$files = array_diff(scandir($dir), ['.', '..']);
|
|
||||||
foreach ($files as $file) {
|
|
||||||
$path = $dir . '/' . $file;
|
|
||||||
is_dir($path) ? removeTempDirectory($path) : unlink($path);
|
|
||||||
}
|
|
||||||
rmdir($dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception Testing Helpers
|
|
||||||
*/
|
|
||||||
function expectException(callable $callback, string $exceptionClass, ?string $message = null): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$callback();
|
|
||||||
fail("Expected exception $exceptionClass was not thrown");
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
expect($e)->toBeInstanceOf($exceptionClass);
|
|
||||||
if ($message) {
|
|
||||||
expect($e->getMessage())->toContain($message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
433
tests/README.md
433
tests/README.md
|
@ -1,433 +0,0 @@
|
||||||
# 🧪 TorrentPier 3.0 Testing Infrastructure
|
|
||||||
|
|
||||||
This document outlines the testing infrastructure for TorrentPier 3.0, built using **Pest PHP** and following the hexagonal architecture principles outlined in the project specification.
|
|
||||||
|
|
||||||
## 📖 Table of Contents
|
|
||||||
|
|
||||||
- [Overview](#overview)
|
|
||||||
- [Hexagonal Architecture Testing](#hexagonal-architecture-testing)
|
|
||||||
- [Test Organization](#test-organization)
|
|
||||||
- [DI Container Testing](#di-container-testing)
|
|
||||||
- [Testing Patterns](#testing-patterns)
|
|
||||||
- [Test Execution](#test-execution)
|
|
||||||
- [Best Practices](#best-practices)
|
|
||||||
|
|
||||||
## 🎯 Overview
|
|
||||||
|
|
||||||
TorrentPier 3.0's testing suite is designed following the hexagonal architecture testing strategy:
|
|
||||||
|
|
||||||
- **Domain**: Pure unit tests, no mocks needed
|
|
||||||
- **Application**: Unit tests with mocked repositories
|
|
||||||
- **Infrastructure**: Integration tests with real services
|
|
||||||
- **Presentation**: E2E tests for user journeys
|
|
||||||
|
|
||||||
### Core Testing Principles
|
|
||||||
|
|
||||||
1. **Architecture-Driven**: Tests follow the hexagonal architecture layers
|
|
||||||
2. **Phase-Aligned**: Testing matches the 5-phase implementation strategy
|
|
||||||
3. **Clean Slate**: No legacy dependencies, modern PHP 8.3+ testing
|
|
||||||
4. **Infrastructure First**: Focus on foundational DI container testing
|
|
||||||
5. **Future-Ready**: Structure prepared for upcoming domain/application layers
|
|
||||||
|
|
||||||
## 🏗️ Hexagonal Architecture Testing
|
|
||||||
|
|
||||||
### Testing Strategy by Layer
|
|
||||||
|
|
||||||
#### Domain Layer Testing (Phase 2 - Future)
|
|
||||||
```php
|
|
||||||
// Pure unit tests, no framework dependencies
|
|
||||||
it('validates business rules without external dependencies', function () {
|
|
||||||
$user = new User(new UserId(1), new Email('test@example.com'));
|
|
||||||
expect($user->canPost())->toBeTrue();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Application Layer Testing (Phase 3 - Future)
|
|
||||||
```php
|
|
||||||
// Unit tests with mocked repositories
|
|
||||||
it('handles user registration command', function () {
|
|
||||||
$mockRepo = Mockery::mock(UserRepositoryInterface::class);
|
|
||||||
$handler = new RegisterUserHandler($mockRepo);
|
|
||||||
|
|
||||||
$command = new RegisterUserCommand('john', 'john@example.com');
|
|
||||||
$handler->handle($command);
|
|
||||||
|
|
||||||
$mockRepo->shouldHaveReceived('save');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Infrastructure Layer Testing (Phase 1 - Current)
|
|
||||||
```php
|
|
||||||
// Integration tests with real services
|
|
||||||
it('creates container with real PHP-DI integration', function () {
|
|
||||||
$container = ContainerFactory::create();
|
|
||||||
expect($container)->toBeInstanceOf(Container::class);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Presentation Layer Testing (Phase 5 - Future)
|
|
||||||
```php
|
|
||||||
// E2E tests for user journeys
|
|
||||||
it('handles API request end-to-end', function () {
|
|
||||||
$response = $this->post('/api/users', ['name' => 'John']);
|
|
||||||
expect($response->status())->toBe(201);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📁 Test Organization
|
|
||||||
|
|
||||||
### Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
tests/
|
|
||||||
├── README.md # This documentation
|
|
||||||
├── Pest.php # Clean Pest configuration
|
|
||||||
├── TestCase.php # Enhanced base test case with DI utilities
|
|
||||||
├── Unit/Infrastructure/DependencyInjection/ # DI Container tests (Phase 1)
|
|
||||||
│ ├── ContainerTest.php # Container wrapper tests
|
|
||||||
│ ├── ContainerFactoryTest.php # Factory functionality tests
|
|
||||||
│ ├── BootstrapTest.php # Application bootstrapping tests
|
|
||||||
│ ├── ServiceProviderTest.php # Service provider interface tests
|
|
||||||
│ └── Definitions/ # Layer-specific definition tests
|
|
||||||
│ ├── DomainDefinitionsTest.php
|
|
||||||
│ ├── ApplicationDefinitionsTest.php
|
|
||||||
│ ├── InfrastructureDefinitionsTest.php
|
|
||||||
│ └── PresentationDefinitionsTest.php
|
|
||||||
└── Feature/ # Integration tests
|
|
||||||
└── ContainerIntegrationTest.php # End-to-end container tests
|
|
||||||
```
|
|
||||||
|
|
||||||
### Future Structure (As Phases Are Implemented)
|
|
||||||
|
|
||||||
```
|
|
||||||
tests/
|
|
||||||
├── Unit/
|
|
||||||
│ ├── Domain/ # Phase 2: Pure business logic tests
|
|
||||||
│ │ ├── User/
|
|
||||||
│ │ ├── Forum/
|
|
||||||
│ │ └── Tracker/
|
|
||||||
│ ├── Application/ # Phase 3: Use case orchestration tests
|
|
||||||
│ │ ├── User/
|
|
||||||
│ │ ├── Forum/
|
|
||||||
│ │ └── Tracker/
|
|
||||||
│ ├── Infrastructure/ # Phase 4: External service integration tests
|
|
||||||
│ │ ├── Persistence/
|
|
||||||
│ │ ├── Cache/
|
|
||||||
│ │ └── Email/
|
|
||||||
│ └── Presentation/ # Phase 5: Interface layer tests
|
|
||||||
│ ├── Http/
|
|
||||||
│ └── Cli/
|
|
||||||
└── Feature/ # Cross-layer integration tests
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛠️ DI Container Testing
|
|
||||||
|
|
||||||
### Current Implementation (Phase 1)
|
|
||||||
|
|
||||||
The DI container is the foundation of TorrentPier 3.0's architecture. Our tests ensure:
|
|
||||||
|
|
||||||
#### Container Wrapper Testing
|
|
||||||
```php
|
|
||||||
// tests/Unit/Infrastructure/DependencyInjection/ContainerTest.php
|
|
||||||
it('implements PSR-11 ContainerInterface', function () {
|
|
||||||
expect($this->container)->toBeInstanceOf(\Psr\Container\ContainerInterface::class);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can resolve autowired classes', function () {
|
|
||||||
$result = $this->container->get(stdClass::class);
|
|
||||||
expect($result)->toBeInstanceOf(stdClass::class);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws NotFoundExceptionInterface for non-existent services', function () {
|
|
||||||
expect(fn() => $this->container->get('non.existent.service'))
|
|
||||||
->toThrow(NotFoundExceptionInterface::class);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Factory Configuration Testing
|
|
||||||
```php
|
|
||||||
// tests/Unit/Infrastructure/DependencyInjection/ContainerFactoryTest.php
|
|
||||||
it('applies configuration correctly', function () {
|
|
||||||
$config = [
|
|
||||||
'environment' => 'testing',
|
|
||||||
'autowiring' => true,
|
|
||||||
'definitions' => [
|
|
||||||
'test.service' => \DI\factory(fn() => 'test_value'),
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
$container = ContainerFactory::create($config);
|
|
||||||
expect($container->get('test.service'))->toBe('test_value');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Bootstrap Integration Testing
|
|
||||||
```php
|
|
||||||
// tests/Unit/Infrastructure/DependencyInjection/BootstrapTest.php
|
|
||||||
it('loads configuration from multiple sources', function () {
|
|
||||||
$rootPath = $this->createTestRootDirectory();
|
|
||||||
$this->createTestConfigFiles($rootPath, [
|
|
||||||
'env' => ['APP_ENV' => 'testing'],
|
|
||||||
'services' => ['config.service' => \DI\factory(fn() => 'merged_config')],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$container = Bootstrap::init($rootPath);
|
|
||||||
expect($container->get('config.service'))->toBe('merged_config');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Utilities
|
|
||||||
|
|
||||||
#### Enhanced TestCase
|
|
||||||
```php
|
|
||||||
// tests/TestCase.php
|
|
||||||
abstract class TestCase extends BaseTestCase
|
|
||||||
{
|
|
||||||
protected function createTestContainer(array $config = []): Container
|
|
||||||
{
|
|
||||||
$defaultConfig = [
|
|
||||||
'environment' => 'testing',
|
|
||||||
'autowiring' => true,
|
|
||||||
'definitions' => [],
|
|
||||||
];
|
|
||||||
|
|
||||||
return ContainerFactory::create(array_merge($defaultConfig, $config));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function assertCanResolve(Container $container, string $serviceId): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($container->has($serviceId));
|
|
||||||
$this->assertNotNull($container->get($serviceId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎨 Testing Patterns
|
|
||||||
|
|
||||||
### 1. Infrastructure Integration Testing
|
|
||||||
```php
|
|
||||||
// Real service integration (current phase)
|
|
||||||
it('integrates with real PHP-DI container', function () {
|
|
||||||
$container = $this->createTestContainer([
|
|
||||||
'definitions' => [
|
|
||||||
'real.service' => \DI\autowire(stdClass::class),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$service = $container->get('real.service');
|
|
||||||
expect($service)->toBeInstanceOf(stdClass::class);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Configuration-Driven Testing
|
|
||||||
```php
|
|
||||||
// Environment-based configuration
|
|
||||||
it('adapts to different environments', function () {
|
|
||||||
$prodContainer = $this->createTestContainer(['environment' => 'production']);
|
|
||||||
$devContainer = $this->createTestContainer(['environment' => 'development']);
|
|
||||||
|
|
||||||
expect($prodContainer)->toBeInstanceOf(Container::class);
|
|
||||||
expect($devContainer)->toBeInstanceOf(Container::class);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Service Provider Testing
|
|
||||||
```php
|
|
||||||
// Modular service registration
|
|
||||||
it('registers services through providers', function () {
|
|
||||||
$provider = new class implements ServiceProvider {
|
|
||||||
public function register(Container $container): void {
|
|
||||||
$container->getWrappedContainer()->set('provider.service', 'registered');
|
|
||||||
}
|
|
||||||
public function boot(Container $container): void {}
|
|
||||||
};
|
|
||||||
|
|
||||||
$container = $this->createTestContainer();
|
|
||||||
$provider->register($container);
|
|
||||||
|
|
||||||
expect($container->get('provider.service'))->toBe('registered');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Layer Definition Testing
|
|
||||||
```php
|
|
||||||
// Architectural layer compliance
|
|
||||||
it('follows domain layer principles', function () {
|
|
||||||
$definitions = DomainDefinitions::getDefinitions();
|
|
||||||
|
|
||||||
// Domain definitions should be empty in Phase 1
|
|
||||||
expect($definitions)->toBe([]);
|
|
||||||
|
|
||||||
// Structure should be prepared for Phase 2
|
|
||||||
expect($definitions)->toBeArray();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Test Execution
|
|
||||||
|
|
||||||
### Running Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run all tests
|
|
||||||
./vendor/bin/pest
|
|
||||||
|
|
||||||
# Run DI container tests specifically
|
|
||||||
./vendor/bin/pest tests/Unit/Infrastructure/DependencyInjection/
|
|
||||||
|
|
||||||
# Run integration tests
|
|
||||||
./vendor/bin/pest tests/Feature/
|
|
||||||
|
|
||||||
# Run with coverage
|
|
||||||
./vendor/bin/pest --coverage
|
|
||||||
|
|
||||||
# Run specific test file
|
|
||||||
./vendor/bin/pest tests/Unit/Infrastructure/DependencyInjection/ContainerTest.php
|
|
||||||
```
|
|
||||||
|
|
||||||
### Performance Testing
|
|
||||||
```bash
|
|
||||||
# Measure container bootstrap performance
|
|
||||||
./vendor/bin/pest --filter="performance"
|
|
||||||
|
|
||||||
# Container creation should be fast
|
|
||||||
expectExecutionTimeUnder(fn() => Bootstrap::init($rootPath), 1.0);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Best Practices
|
|
||||||
|
|
||||||
### 1. Phase-Aligned Testing
|
|
||||||
```php
|
|
||||||
// Current Phase 1: Test infrastructure only
|
|
||||||
it('provides foundation for future phases', function () {
|
|
||||||
$container = $this->createTestContainer();
|
|
||||||
|
|
||||||
// Infrastructure works now
|
|
||||||
expect($container)->toBeInstanceOf(Container::class);
|
|
||||||
|
|
||||||
// Ready for future domain services
|
|
||||||
expect($container->has(stdClass::class))->toBeTrue();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Architecture Compliance
|
|
||||||
```php
|
|
||||||
// Ensure clean architectural boundaries
|
|
||||||
it('keeps domain layer pure', function () {
|
|
||||||
$definitions = DomainDefinitions::getDefinitions();
|
|
||||||
|
|
||||||
// Domain should have no infrastructure dependencies
|
|
||||||
expect($definitions)->toBeArray();
|
|
||||||
|
|
||||||
// Future domain services will be dependency-free
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Configuration Testing
|
|
||||||
```php
|
|
||||||
// Test multiple configuration sources
|
|
||||||
it('merges configuration correctly', function () {
|
|
||||||
$rootPath = $this->createTestRootDirectory();
|
|
||||||
$this->createTestConfigFiles($rootPath, [
|
|
||||||
'container' => ['autowiring' => true],
|
|
||||||
'services' => ['test.service' => \DI\factory(fn() => 'test')],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$container = Bootstrap::init($rootPath, [
|
|
||||||
'definitions' => ['runtime.service' => \DI\factory(fn() => 'runtime')],
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect($container->get('test.service'))->toBe('test');
|
|
||||||
expect($container->get('runtime.service'))->toBe('runtime');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Error Handling
|
|
||||||
```php
|
|
||||||
// Comprehensive error testing
|
|
||||||
it('provides meaningful error messages', function () {
|
|
||||||
$container = $this->createTestContainer();
|
|
||||||
|
|
||||||
try {
|
|
||||||
$container->get('missing.service');
|
|
||||||
fail('Expected exception');
|
|
||||||
} catch (RuntimeException $e) {
|
|
||||||
expect($e->getMessage())->toContain('missing.service');
|
|
||||||
expect($e->getMessage())->toContain('not found in container');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Current Implementation Status
|
|
||||||
|
|
||||||
### ✅ Phase 1 Complete: Infrastructure Foundation
|
|
||||||
|
|
||||||
- **DI Container**: Fully tested container wrapper with PSR-11 compliance
|
|
||||||
- **Factory Pattern**: Comprehensive configuration and creation testing
|
|
||||||
- **Bootstrap Process**: Environment loading and configuration merging
|
|
||||||
- **Service Providers**: Modular service registration interface
|
|
||||||
- **Helper Functions**: Global container access with proper error handling
|
|
||||||
- **Layer Definitions**: Prepared structure for all architectural layers
|
|
||||||
|
|
||||||
### 🔄 Testing Coverage
|
|
||||||
|
|
||||||
- **Container Core**: 100% coverage of wrapper functionality
|
|
||||||
- **Configuration**: All config sources and merging scenarios tested
|
|
||||||
- **Error Handling**: Complete PSR-11 exception compliance
|
|
||||||
- **Integration**: End-to-end bootstrap and usage scenarios
|
|
||||||
- **Performance**: Container creation and resolution timing validation
|
|
||||||
|
|
||||||
### 🔮 Future Phase Testing
|
|
||||||
|
|
||||||
As TorrentPier 3.0 phases are implemented:
|
|
||||||
|
|
||||||
#### Phase 2: Domain Layer
|
|
||||||
```php
|
|
||||||
// Domain entity testing (future)
|
|
||||||
it('validates user business rules', function () {
|
|
||||||
$user = new User(UserId::generate(), new Email('test@example.com'));
|
|
||||||
expect($user->isActive())->toBeTrue();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Phase 3: Application Layer
|
|
||||||
```php
|
|
||||||
// Command handler testing (future)
|
|
||||||
it('processes registration command', function () {
|
|
||||||
$handler = app(RegisterUserHandler::class);
|
|
||||||
$command = new RegisterUserCommand('john', 'john@example.com');
|
|
||||||
|
|
||||||
$userId = $handler->handle($command);
|
|
||||||
expect($userId)->toBeInstanceOf(UserId::class);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Phase 4: Infrastructure Layer
|
|
||||||
```php
|
|
||||||
// Repository integration testing (future)
|
|
||||||
it('persists user through repository', function () {
|
|
||||||
$repository = app(UserRepositoryInterface::class);
|
|
||||||
$user = User::create('john', 'john@example.com');
|
|
||||||
|
|
||||||
$repository->save($user);
|
|
||||||
expect($repository->findById($user->getId()))->not->toBeNull();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Phase 5: Presentation Layer
|
|
||||||
```php
|
|
||||||
// Controller integration testing (future)
|
|
||||||
it('handles user registration via API', function () {
|
|
||||||
$response = $this->postJson('/api/users', [
|
|
||||||
'username' => 'john',
|
|
||||||
'email' => 'john@example.com',
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect($response->status())->toBe(201);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**TorrentPier 3.0 Testing Philosophy**: Tests serve as both validation and documentation of the hexagonal architecture. Each layer has distinct testing strategies that ensure clean separation of concerns and maintainable code.
|
|
||||||
|
|
||||||
For questions about testing patterns or contributions, refer to the [TorrentPier GitHub repository](https://github.com/torrentpier/torrentpier) or the hexagonal architecture specification at `/docs/specs/hexagonal-architecture-spec.md`.
|
|
|
@ -1,120 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests;
|
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase as BaseTestCase;
|
|
||||||
use TorrentPier\Infrastructure\DependencyInjection\Bootstrap;
|
|
||||||
use TorrentPier\Infrastructure\DependencyInjection\Container;
|
|
||||||
use TorrentPier\Infrastructure\DependencyInjection\ContainerFactory;
|
|
||||||
|
|
||||||
abstract class TestCase extends BaseTestCase
|
|
||||||
{
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
// Reset container state for each test
|
|
||||||
Bootstrap::reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function tearDown(): void
|
|
||||||
{
|
|
||||||
// Clean up container state
|
|
||||||
Bootstrap::reset();
|
|
||||||
|
|
||||||
parent::tearDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a test container with optional custom configuration
|
|
||||||
*/
|
|
||||||
protected function createTestContainer(array $config = []): Container
|
|
||||||
{
|
|
||||||
$defaultConfig = [
|
|
||||||
'environment' => 'testing',
|
|
||||||
'autowiring' => true,
|
|
||||||
'definitions' => [],
|
|
||||||
];
|
|
||||||
|
|
||||||
return ContainerFactory::create(array_merge($defaultConfig, $config));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a container with custom service definitions
|
|
||||||
*/
|
|
||||||
protected function createContainerWithDefinitions(array $definitions): Container
|
|
||||||
{
|
|
||||||
return $this->createTestContainer([
|
|
||||||
'definitions' => $definitions,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a temporary test root directory
|
|
||||||
*/
|
|
||||||
protected function createTestRootDirectory(): string
|
|
||||||
{
|
|
||||||
$tempDir = createTempDirectory();
|
|
||||||
|
|
||||||
// Create basic directory structure
|
|
||||||
mkdir($tempDir . '/config', 0755, true);
|
|
||||||
mkdir($tempDir . '/internal_data/cache', 0755, true);
|
|
||||||
|
|
||||||
return $tempDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create test configuration files
|
|
||||||
*/
|
|
||||||
protected function createTestConfigFiles(string $rootPath, array $configs = []): void
|
|
||||||
{
|
|
||||||
$configPath = $rootPath . '/config';
|
|
||||||
|
|
||||||
// Create container.php
|
|
||||||
if (isset($configs['container'])) {
|
|
||||||
file_put_contents(
|
|
||||||
$configPath . '/container.php',
|
|
||||||
'<?php return ' . var_export($configs['container'], true) . ';'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create services.php - simplified approach for testing
|
|
||||||
if (isset($configs['services'])) {
|
|
||||||
$servicesContent = "<?php\n\nuse function DI\\factory;\n\nreturn [\n";
|
|
||||||
foreach ($configs['services'] as $key => $value) {
|
|
||||||
if (is_string($value)) {
|
|
||||||
$servicesContent .= " '$key' => factory(function () { return '$value'; }),\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$servicesContent .= "];\n";
|
|
||||||
|
|
||||||
file_put_contents($configPath . '/services.php', $servicesContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create .env file
|
|
||||||
if (isset($configs['env'])) {
|
|
||||||
$envContent = '';
|
|
||||||
foreach ($configs['env'] as $key => $value) {
|
|
||||||
$envContent .= "$key=$value\n";
|
|
||||||
}
|
|
||||||
file_put_contents($rootPath . '/.env', $envContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert that a service can be resolved from the container
|
|
||||||
*/
|
|
||||||
protected function assertCanResolve(Container $container, string $serviceId): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($container->has($serviceId), "Container should have service: $serviceId");
|
|
||||||
$this->assertNotNull($container->get($serviceId), "Should be able to resolve service: $serviceId");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert that a service cannot be resolved from the container
|
|
||||||
*/
|
|
||||||
protected function assertCannotResolve(Container $container, string $serviceId): void
|
|
||||||
{
|
|
||||||
$this->assertFalse($container->has($serviceId), "Container should not have service: $serviceId");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,190 +0,0 @@
|
||||||
<?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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,206 +0,0 @@
|
||||||
<?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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,165 +0,0 @@
|
||||||
<?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'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,100 +0,0 @@
|
||||||
<?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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,84 +0,0 @@
|
||||||
<?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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,159 +0,0 @@
|
||||||
<?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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,147 +0,0 @@
|
||||||
<?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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,144 +0,0 @@
|
||||||
<?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']);
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Add table
Add a link
Reference in a new issue