From d061a6748a04b11f016e330176897b630aaec22e Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Tue, 25 Aug 2026 21:26:09 +0600 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(config):=20Update=20.env.examp?= =?UTF-8?q?le=20and=20.gitattributes=20for=20clarity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 3 +- .gitattributes | 13 +- .github/ISSUE_TEMPLATE/bug_report.yml | 6 +- .github/ISSUE_TEMPLATE/ci_failure.yml | 8 +- .github/ISSUE_TEMPLATE/docs_improvement.yml | 10 +- .github/ISSUE_TEMPLATE/feature_request.yml | 10 +- .github/ISSUE_TEMPLATE/question.yml | 8 +- .github/PULL_REQUEST_TEMPLATE.md | 34 +- .github/PULL_REQUEST_TEMPLATE/bug_fix.md | 94 ++++ .../PULL_REQUEST_TEMPLATE/documentation.md | 51 ++ .github/PULL_REQUEST_TEMPLATE/feature.md | 106 ++++ .github/PULL_REQUEST_TEMPLATE/maintenance.md | 91 ++++ .github/PULL_REQUEST_TEMPLATE/performance.md | 99 ++++ .github/PULL_REQUEST_TEMPLATE/refactor.md | 108 ++++ .../security_reliability.md | 92 ++++ .github/workflows/security-standards.yml | 32 +- .gitignore | 6 +- README.md | 301 ++++++++--- app/Console/Commands/.gitkeep | 1 - bootstrap/app.php | 7 +- bootstrap/cache/{config => }/.gitignore | 0 bootstrap/cache/console/.gitignore | 2 - bootstrap/cache/routes/.gitignore | 2 - bootstrap/console.php | 10 - bootstrap/options.php | 9 - bootstrap/providers.php | 16 +- captainhook.json | 55 -- composer.json | 4 +- config/app.php | 58 +- config/auth.php | 97 ++-- config/ids.php | 208 ------- config/router.php | 8 +- deploy.sh | 3 +- infbyte | 52 +- routes/api.php | 7 +- routes/console.php | 2 +- routes/schedule.php | 2 +- routes/workers.php | 6 +- tests/Feature/ExampleTest.php | 12 +- tests/Feature/FrameworkRuntimeTest.php | 201 ++++--- tests/Feature/RouteCacheCliTest.php | 509 ++++++++++-------- tests/Feature/SkeletonDistributionTest.php | 39 +- 42 files changed, 1412 insertions(+), 970 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE/bug_fix.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/documentation.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/feature.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/maintenance.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/performance.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/refactor.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/security_reliability.md delete mode 100644 app/Console/Commands/.gitkeep rename bootstrap/cache/{config => }/.gitignore (100%) delete mode 100644 bootstrap/cache/console/.gitignore delete mode 100644 bootstrap/cache/routes/.gitignore delete mode 100644 bootstrap/console.php delete mode 100644 bootstrap/options.php delete mode 100644 captainhook.json delete mode 100644 config/ids.php diff --git a/.env.example b/.env.example index d6a84f9..260f7b5 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,6 @@ APP_CONTAINER_COMPILED_ACTIVATION=off ROUTER_MATCHER=fused -AUTH_IDS=random AUTH_STORAGE=memory AUTH_CACHE=array AUTH_PASSWORDS=native @@ -16,4 +15,4 @@ AUTH_TOKENS=simple AUTH_MFA=simple AUTH_NOTIFICATIONS=collect AUTH_PASSKEY=disabled -AUTH_TOKEN_SECRET=foundation-dev-secret +AUTH_TOKEN_SECRET= diff --git a/.gitattributes b/.gitattributes index 8d4d429..72cde45 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,6 @@ /.gitattributes export-ignore /captainhook.json export-ignore /composer.lock export-ignore -/plan.md export-ignore /CODE_OF_CONDUCT.md export-ignore /CONTRIBUTING.md export-ignore /SECURITY.md export-ignore @@ -16,6 +15,7 @@ /.github export-ignore /.idea export-ignore /.phpunit.cache export-ignore +/.phpforge-report export-ignore /.psalm-cache export-ignore /.route-cache-* export-ignore /.vscode export-ignore @@ -33,12 +33,11 @@ /app/Services export-ignore /app/Support export-ignore /bin export-ignore -/bootstrap/cache/config/*.php export-ignore -/bootstrap/cache/console/*.php export-ignore -/bootstrap/cache/container.php export-ignore -/bootstrap/cache/modules.php export-ignore -/bootstrap/cache/optimize.php export-ignore -/bootstrap/cache/routes/*.php export-ignore +/bootstrap/cache/*.php export-ignore +/bootstrap/cache/config export-ignore +/bootstrap/cache/console export-ignore +/bootstrap/cache/container export-ignore +/bootstrap/cache/routes export-ignore /database export-ignore /resources export-ignore /storage/app export-ignore diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 855c42d..5907267 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -57,7 +57,7 @@ body: id: reproduce attributes: label: Minimal reproduction - description: Provide the smallest code sample, command, configuration, or repository that reproduces the problem. + description: Provide the smallest code sample, command, configuration or repository that reproduces the problem. placeholder: | 1. Install or configure ... 2. Run ... @@ -108,7 +108,7 @@ body: id: environment attributes: label: Environment - description: Include the operating system, relevant extensions, dependency mode, runtime, and CI provider when applicable. + description: Include the operating system, relevant extensions, dependency mode, runtime and CI provider when applicable. placeholder: Ubuntu 24.04, locked dependencies, ext-json enabled, GitHub Actions... validations: required: true @@ -117,4 +117,4 @@ body: id: additional attributes: label: Additional context - description: Add related issues, screenshots, logs, workarounds, or other useful context. + description: Add related issues, screenshots, logs, workarounds or other useful context. diff --git a/.github/ISSUE_TEMPLATE/ci_failure.yml b/.github/ISSUE_TEMPLATE/ci_failure.yml index e1897bf..9c3883f 100644 --- a/.github/ISSUE_TEMPLATE/ci_failure.yml +++ b/.github/ISSUE_TEMPLATE/ci_failure.yml @@ -60,7 +60,7 @@ body: id: local_details attributes: label: Local reproduction details - description: Include the command, result, and any differences from CI. + description: Include the command, result and any differences from CI. placeholder: composer ic:ci fails locally with the same error... - type: input @@ -83,7 +83,7 @@ body: id: environment attributes: label: Runner and dependency environment - description: Include the runner OS, dependency mode, relevant extensions, matrix values, and PHPForge version when known. + description: Include the runner OS, dependency mode, relevant extensions, matrix values and PHPForge version when known. placeholder: ubuntu-latest, prefer-lowest, PHPForge 1.x, ext-json enabled... validations: required: true @@ -92,10 +92,10 @@ body: id: recent_changes attributes: label: Relevant recent changes - description: Mention dependency, configuration, workflow, or source changes that may be related. + description: Mention dependency, configuration, workflow or source changes that may be related. - type: textarea id: additional attributes: label: Additional context - description: Add related issues, screenshots, logs, or other useful context. + description: Add related issues, screenshots, logs or other useful context. diff --git a/.github/ISSUE_TEMPLATE/docs_improvement.yml b/.github/ISSUE_TEMPLATE/docs_improvement.yml index 13b4f60..2ea49e9 100644 --- a/.github/ISSUE_TEMPLATE/docs_improvement.yml +++ b/.github/ISSUE_TEMPLATE/docs_improvement.yml @@ -1,5 +1,5 @@ name: Documentation improvement -description: Report missing, outdated, unclear, or incorrect documentation +description: Report missing, outdated, unclear or incorrect documentation labels: - documentation body: @@ -21,7 +21,7 @@ body: id: location attributes: label: Documentation location - description: Provide the file path, section, symbol, or URL. + description: Provide the file path, section, symbol or URL. placeholder: README.md — Quick Start validations: required: true @@ -30,7 +30,7 @@ body: id: problem attributes: label: Problem - description: Explain what is missing, unclear, outdated, or incorrect. + description: Explain what is missing, unclear, outdated or incorrect. placeholder: The current documentation says or omits... validations: required: true @@ -48,11 +48,11 @@ body: id: suggestion attributes: label: Suggested improvement - description: Optionally propose wording, structure, examples, or references. + description: Optionally propose wording, structure, examples or references. placeholder: It may be clearer to... - type: textarea id: additional attributes: label: Additional context - description: Add related links, screenshots, discussions, or examples. + description: Add related links, screenshots, discussions or examples. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 22df828..bbee6d0 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -8,13 +8,13 @@ body: value: | Describe the problem or use case before proposing an implementation. - For substantial public API, architectural, or compatibility changes, discussion may be requested before implementation. + For substantial public API, architectural or compatibility changes, discussion may be requested before implementation. - type: textarea id: problem attributes: label: Problem or use case - description: Explain the limitation, repeated difficulty, or capability you need. + description: Explain the limitation, repeated difficulty or capability you need. placeholder: I need to... because... validations: required: true @@ -32,7 +32,7 @@ body: id: example attributes: label: Example usage - description: Optionally show the proposed API, configuration, command, or workflow. + description: Optionally show the proposed API, configuration, command or workflow. render: php - type: textarea @@ -45,10 +45,10 @@ body: id: compatibility attributes: label: Compatibility considerations - description: Mention possible public API, behavior, PHP-version, extension, platform, or dependency implications. + description: Mention possible public API, behavior, PHP-version, extension, platform or dependency implications. - type: textarea id: additional attributes: label: Additional context - description: Add related issues, prior art, links, benchmarks, or other supporting information. + description: Add related issues, prior art, links, benchmarks or other supporting information. diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml index 5a322e3..62e91a7 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -1,5 +1,5 @@ name: Question -description: Ask about usage, behavior, integration, or configuration +description: Ask about usage, behavior, integration or configuration labels: - question body: @@ -23,7 +23,7 @@ body: id: attempted attributes: label: What have you tried? - description: Include relevant code, commands, configuration, documentation, or approaches already checked. + description: Include relevant code, commands, configuration, documentation or approaches already checked. placeholder: I tried... validations: required: true @@ -46,11 +46,11 @@ body: id: environment attributes: label: Environment - description: Include package, PHP, Composer, OS, extensions, or CI details only when relevant. + description: Include package, PHP, Composer, OS, extensions or CI details only when relevant. placeholder: Package 2.4.1, PHP 8.4, Composer 2.9, Ubuntu 24.04... - type: textarea id: additional attributes: label: Additional context - description: Add related links, screenshots, or prior discussions. + description: Add related links, screenshots or prior discussions. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4d91309..dcab1f5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -32,26 +32,22 @@ Use `Closes #...`, `Fixes #...` or `Relates #...` where applicable. ## Validation -### Complete Suite - * [ ] `composer ic:ci` - +
Focused validation * [ ] `composer ic:test:syntax` @@ -68,21 +64,11 @@ Select every command that passed.
-### Coverage - -* [ ] Expected or newly introduced behavior -* [ ] Regression scenario -* [ ] Boundary and edge cases -* [ ] Failure and exception paths -* [ ] Public API compatibility -* [ ] Version, dependency, extension or platform-specific behavior -* [ ] Not applicable - @@ -95,16 +81,10 @@ is being made. * [ ] Relevant benchmarks were added or updated * [ ] Results were compared against a relevant baseline - -
-Benchmark commands - * [ ] `composer ic:benchmark` * [ ] `composer ic:bench:quick` * [ ] `composer ic:bench:chart` -
- ## Checklist @@ -140,4 +118,4 @@ version compatibility or security-sensitive logic. * [ ] Documentation, examples and type information were updated where required. * [ ] Performance claims are supported by reproducible benchmarks. * [ ] No credentials, secrets, personal data or sensitive debug output are included. -* [ ] I followed `CONTRIBUTING.md`. +* [ ] I followed `CONTRIBUTING.md` and the engineering principles. diff --git a/.github/PULL_REQUEST_TEMPLATE/bug_fix.md b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md new file mode 100644 index 0000000..66759b4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md @@ -0,0 +1,94 @@ +## Problem + +Describe the incorrect behavior, its impact and how it can be reproduced. + + + +## Root Cause + + + +## Fix + +Describe how the change corrects the problem and the expected behavior after the fix. + +## Behavior and Compatibility + +* [ ] Existing documented behavior was restored +* [ ] Existing undocumented behavior was corrected +* [ ] Public API remains compatible +* [ ] Public API or documented behavior changed +* [ ] Backward compatibility may be affected +* [ ] PHP, extension, platform or dependency requirements changed + + + +## Validation + +* [ ] `composer ic:ci` +* [ ] The original failure no longer reproduces +* [ ] A regression test was added or updated +* [ ] Relevant boundary and failure paths were tested + + + +
+Focused validation + + + +* [ ] `composer ic:test:syntax` +* [ ] `composer ic:test:code` +* [ ] `composer ic:test:lint` +* [ ] `composer ic:test:sniff` +* [ ] `composer ic:test:duplicates` +* [ ] `composer ic:test:probe` +* [ ] `composer ic:test:comments` +* [ ] `composer ic:test:architecture` +* [ ] `composer ic:test:static` +* [ ] `composer ic:test:security` +* [ ] `composer ic:test:refactor` + +
+ + + +## Review Focus + + + +## Checklist + +* [ ] The fix is focused and excludes unrelated changes. +* [ ] The fix addresses the root cause rather than only masking symptoms. +* [ ] Regression-prone behavior is covered by tests. +* [ ] Public API and backward-compatibility implications were considered. +* [ ] Documentation and examples were updated where required. +* [ ] No credentials, secrets, personal data or sensitive debug output are included. +* [ ] I followed `CONTRIBUTING.md` and the engineering principles. diff --git a/.github/PULL_REQUEST_TEMPLATE/documentation.md b/.github/PULL_REQUEST_TEMPLATE/documentation.md new file mode 100644 index 0000000..f983b85 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/documentation.md @@ -0,0 +1,51 @@ +## Documentation Change + +Describe what is being added, corrected, clarified or removed and why. + + + +## Affected Content + +* [ ] README or getting-started guidance +* [ ] API or reference documentation +* [ ] Configuration documentation +* [ ] Examples or tutorials +* [ ] Contribution or community documentation +* [ ] Changelog or release documentation +* [ ] Other + +## Verification + +* [ ] Links and references were checked +* [ ] Code examples were executed or otherwise verified +* [ ] Commands and configuration examples match current behavior +* [ ] Terminology is consistent with the project +* [ ] `composer ic:ci` +* [ ] No executable behavior changed + + + +## Review Focus + + + +## Checklist + +* [ ] The change is focused and excludes unrelated code changes. +* [ ] Documentation reflects the current public behavior. +* [ ] Examples are minimal, accurate and safe to copy. +* [ ] Sensitive or private information is not included. +* [ ] I followed `CONTRIBUTING.md` and the engineering principles. diff --git a/.github/PULL_REQUEST_TEMPLATE/feature.md b/.github/PULL_REQUEST_TEMPLATE/feature.md new file mode 100644 index 0000000..f6ce9c6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/feature.md @@ -0,0 +1,106 @@ +## Motivation + +Describe the problem, use case or capability this feature addresses. + + + +## Solution + +Describe the proposed behavior and how consumers are expected to use it. + +## API and Compatibility + +* [ ] No new public API +* [ ] New backward-compatible public API +* [ ] Existing public API or documented behavior changed +* [ ] Backward compatibility may be affected +* [ ] PHP, extension, platform or dependency requirements changed + + + +## Validation + +* [ ] `composer ic:ci` +* [ ] Expected behavior is covered +* [ ] Boundary and edge cases are covered +* [ ] Failure and exception paths are covered +* [ ] Public API usage is covered + + + +
+Focused validation + + + +* [ ] `composer ic:test:syntax` +* [ ] `composer ic:test:code` +* [ ] `composer ic:test:lint` +* [ ] `composer ic:test:sniff` +* [ ] `composer ic:test:duplicates` +* [ ] `composer ic:test:probe` +* [ ] `composer ic:test:comments` +* [ ] `composer ic:test:architecture` +* [ ] `composer ic:test:static` +* [ ] `composer ic:test:security` +* [ ] `composer ic:test:refactor` + +
+ + + +## Performance + + + +* [ ] Relevant benchmarks were added or updated +* [ ] Results were compared against a relevant baseline +* [ ] `composer ic:benchmark` +* [ ] `composer ic:bench:quick` +* [ ] `composer ic:bench:chart` + + + +## Review Focus + + + +## Checklist + +* [ ] The feature is focused and excludes unrelated changes. +* [ ] Tests cover the public contract and failure behavior. +* [ ] Public API and backward-compatibility implications were considered. +* [ ] Documentation, examples and type information were updated. +* [ ] Performance claims are supported by reproducible benchmarks. +* [ ] No credentials, secrets, personal data or sensitive debug output are included. +* [ ] I followed `CONTRIBUTING.md` and the engineering principles. diff --git a/.github/PULL_REQUEST_TEMPLATE/maintenance.md b/.github/PULL_REQUEST_TEMPLATE/maintenance.md new file mode 100644 index 0000000..a4cab0a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/maintenance.md @@ -0,0 +1,91 @@ +## Maintenance Change + +Describe what changed, why it was needed and the expected effect on development, CI or releases. + + + +## Category + +* [ ] Dependency update +* [ ] CI or workflow change +* [ ] Build or release tooling +* [ ] PHPForge configuration +* [ ] Development tooling +* [ ] Repository maintenance +* [ ] Other + +## Impact and Compatibility + +* [ ] Runtime behavior is unaffected +* [ ] Development workflow changed +* [ ] CI or release behavior changed +* [ ] Supported PHP, extension, platform or dependency requirements changed +* [ ] Generated files or configuration changed +* [ ] Backward compatibility may be affected + + + +## Validation + +* [ ] `composer ic:ci` +* [ ] Relevant workflow or job was exercised +* [ ] Supported matrix or dependency mode was considered +* [ ] Generated or published files were verified +* [ ] Failure and rollback behavior was considered + + + +
+Focused validation + + + +* [ ] `composer ic:test:syntax` +* [ ] `composer ic:test:code` +* [ ] `composer ic:test:lint` +* [ ] `composer ic:test:sniff` +* [ ] `composer ic:test:duplicates` +* [ ] `composer ic:test:probe` +* [ ] `composer ic:test:comments` +* [ ] `composer ic:test:architecture` +* [ ] `composer ic:test:static` +* [ ] `composer ic:test:security` +* [ ] `composer ic:test:refactor` + +
+ + + +## Review Focus + + + +## Checklist + +* [ ] The change is focused and excludes unrelated source refactoring. +* [ ] Dependency or workflow changes are minimal and justified. +* [ ] Public API and backward-compatibility implications were considered. +* [ ] Documentation and generated files were updated where required. +* [ ] No credentials, secrets, personal data or sensitive debug output are included. +* [ ] I followed `CONTRIBUTING.md` and the engineering principles. diff --git a/.github/PULL_REQUEST_TEMPLATE/performance.md b/.github/PULL_REQUEST_TEMPLATE/performance.md new file mode 100644 index 0000000..ce2d10d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/performance.md @@ -0,0 +1,99 @@ +## Bottleneck + +Describe the measured performance problem, affected execution path and practical impact. + + + +## Optimization + +Describe the change, why it improves the measured path and any trade-offs introduced. + +## Correctness and Compatibility + +* [ ] Observable behavior remains unchanged +* [ ] Public API remains compatible +* [ ] Error and exception behavior remains compatible +* [ ] Behavior or public API changed intentionally +* [ ] PHP, extension, platform or dependency requirements changed + + + +## Benchmark Evidence + +* [ ] Relevant benchmarks were added or updated +* [ ] Results were compared against a relevant baseline +* [ ] Multiple stable runs were considered +* [ ] Runtime impact was measured +* [ ] Memory or allocation impact was measured where relevant +* [ ] `composer ic:benchmark` +* [ ] `composer ic:bench:quick` +* [ ] `composer ic:bench:chart` + + + +## Validation + +* [ ] `composer ic:ci` +* [ ] Expected behavior remains covered +* [ ] Boundary and failure paths remain covered +* [ ] Performance-sensitive behavior is covered + + + +
+Focused validation + + + +* [ ] `composer ic:test:syntax` +* [ ] `composer ic:test:code` +* [ ] `composer ic:test:lint` +* [ ] `composer ic:test:sniff` +* [ ] `composer ic:test:duplicates` +* [ ] `composer ic:test:probe` +* [ ] `composer ic:test:comments` +* [ ] `composer ic:test:architecture` +* [ ] `composer ic:test:static` +* [ ] `composer ic:test:security` +* [ ] `composer ic:test:refactor` + +
+ + + +## Review Focus + + + +## Checklist + +* [ ] The optimization targets a measured bottleneck. +* [ ] Results are reproducible in comparable environments. +* [ ] Correctness was not traded for an unverified micro-optimization. +* [ ] Public API and backward-compatibility implications were considered. +* [ ] Benchmark and documentation changes are included where required. +* [ ] No credentials, secrets, personal data or sensitive debug output are included. +* [ ] I followed `CONTRIBUTING.md` and the engineering principles. diff --git a/.github/PULL_REQUEST_TEMPLATE/refactor.md b/.github/PULL_REQUEST_TEMPLATE/refactor.md new file mode 100644 index 0000000..588be4f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/refactor.md @@ -0,0 +1,108 @@ +## Intent and Scope + +Describe what was restructured, why it was necessary and what remains intentionally unchanged. + + + +## Behavioral Guarantee + +* [ ] No observable behavior changed +* [ ] Public API remains unchanged +* [ ] Existing behavior was intentionally corrected +* [ ] Public API or documented behavior changed +* [ ] Backward compatibility may be affected + + + +## Design Notes + + + +## Validation + +* [ ] `composer ic:ci` +* [ ] Existing behavior remains covered +* [ ] Relevant regression and edge cases are covered +* [ ] Public API compatibility was verified + + + +
+Focused validation + + + +* [ ] `composer ic:test:syntax` +* [ ] `composer ic:test:code` +* [ ] `composer ic:test:lint` +* [ ] `composer ic:test:sniff` +* [ ] `composer ic:test:duplicates` +* [ ] `composer ic:test:probe` +* [ ] `composer ic:test:comments` +* [ ] `composer ic:test:architecture` +* [ ] `composer ic:test:static` +* [ ] `composer ic:test:security` +* [ ] `composer ic:test:refactor` + +
+ + + +## Performance + + + +* [ ] Results were compared against a relevant baseline +* [ ] `composer ic:benchmark` +* [ ] `composer ic:bench:quick` +* [ ] `composer ic:bench:chart` + + + +## Review Focus + + + +## Checklist + +* [ ] The refactor is focused and excludes unrelated behavior changes. +* [ ] Complexity was reduced without unnecessary abstraction or file growth. +* [ ] Existing contracts and failure behavior remain covered. +* [ ] Public API and backward-compatibility implications were considered. +* [ ] Documentation and type information were updated where required. +* [ ] No credentials, secrets, personal data or sensitive debug output are included. +* [ ] I followed `CONTRIBUTING.md` and the engineering principles. diff --git a/.github/PULL_REQUEST_TEMPLATE/security_reliability.md b/.github/PULL_REQUEST_TEMPLATE/security_reliability.md new file mode 100644 index 0000000..ef2ca28 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/security_reliability.md @@ -0,0 +1,92 @@ + + +## Concern + +Describe the security weakness, reliability failure mode or defensive gap being addressed. + + + +## Mitigation + +Describe how the change reduces the risk and what assumptions or limitations remain. + +## Impact and Compatibility + +* [ ] Security hardening with no observable behavior change +* [ ] Reliability improvement with no public API change +* [ ] Failure or exception behavior changed +* [ ] Public API or documented behavior changed +* [ ] Backward compatibility may be affected +* [ ] PHP, extension, platform or dependency requirements changed + + + +## Validation + +* [ ] `composer ic:ci` +* [ ] Security-sensitive or failure behavior is covered +* [ ] Abuse, malformed-input or failure paths are covered +* [ ] Regression coverage was added or updated +* [ ] `composer ic:test:security` + + + +
+Focused validation + + + +* [ ] `composer ic:test:syntax` +* [ ] `composer ic:test:code` +* [ ] `composer ic:test:lint` +* [ ] `composer ic:test:sniff` +* [ ] `composer ic:test:duplicates` +* [ ] `composer ic:test:probe` +* [ ] `composer ic:test:comments` +* [ ] `composer ic:test:architecture` +* [ ] `composer ic:test:static` +* [ ] `composer ic:test:security` +* [ ] `composer ic:test:refactor` + +
+ + + +## Review Focus + + + +## Checklist + +* [ ] Confidential vulnerability details are not exposed publicly. +* [ ] The change is focused and avoids unrelated refactoring. +* [ ] Security or reliability claims are supported by tests. +* [ ] Failure paths and backward-compatibility implications were considered. +* [ ] Documentation and upgrade guidance were updated where required. +* [ ] No credentials, secrets, personal data or sensitive debug output are included. +* [ ] I followed `SECURITY.md`, `CONTRIBUTING.md` and the engineering principles. diff --git a/.github/workflows/security-standards.yml b/.github/workflows/security-standards.yml index f412cfc..df294b8 100644 --- a/.github/workflows/security-standards.yml +++ b/.github/workflows/security-standards.yml @@ -11,35 +11,15 @@ on: jobs: phpforge: uses: infocyph/phpforge/.github/workflows/security-standards.yml@main - permissions: - security-events: write - actions: read - contents: read with: php_versions: '["8.4","8.5"]' dependency_versions: '["prefer-lowest","prefer-stable"]' php_extensions: "pcntl" - composer_flags: "" - phpstan_memory_limit: "1G" - psalm_threads: "1" - run_analysis: true - run_svg_report: true fail_on_skipped_tests: true run_clean_install: true - benchmark_composer_script: "" - benchmark_result_file: "" - benchmark_baseline_file: "" - benchmark_max_regression_percent: 2 - benchmark_stable_environment: false - enable_redis_service: false - enable_valkey_service: false - enable_memcached_service: false - enable_postgres_service: false - enable_mysql_service: false - enable_scylladb_service: false - enable_elasticsearch_service: false - enable_mongodb_service: false - service_db_name: "infbyte" - service_db_user: "infbyte" - service_db_password: "infbyte" - artifact_retention_days: 61 + integration_services: '[]' + service_topologies: '{}' + permissions: + security-events: write + actions: read + contents: read diff --git a/.gitignore b/.gitignore index af056dd..fafe7d1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,13 +5,13 @@ !/.env.example /.idea/ /.phpunit.cache/ +/.phpforge-report/ /.psalm-cache/ /.route-cache-* /.vscode/ /.windsurf/ -/bootstrap/cache/container.php -/bootstrap/cache/modules.php -/bootstrap/cache/optimize.php +/bootstrap/cache/* +!/bootstrap/cache/.gitignore /database/*.sqlite /vendor/ *~ diff --git a/README.md b/README.md index 06d90de..1a8cc55 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,14 @@ Infbyte is the minimal application skeleton for [Infocyph Foundation](https://github.com/infocyph/Foundation). Foundation owns -the reusable framework runtime; Infbyte provides the application bootstrap, -configuration, routes, storage layout, and starter code. +the reusable framework/runtime layer; Infbyte provides opinionated application +bootstrap, starter configuration, routes, writable layout, and application code. -Infbyte 1.x requires PHP 8.4 or newer. +Infbyte requires PHP 8.4+ and Foundation 2.0: + +```json +"infocyph/foundation": "^2.0" +``` ## Quick start @@ -15,146 +19,269 @@ cd my-app php infbyte serve ``` -The development server listens on `http://127.0.0.1:8000` by default. Select a -different address or port when needed: +The built-in server defaults to `127.0.0.1:8000` and is for local development +only: ```bash php infbyte serve --host=0.0.0.0 --port=8080 ``` -The built-in server is intended for local development only. Composer invokes -Foundation's `app:install` command to create `.env` from `.env.example` and -generate a unique authentication secret without replacing an existing one. +Composer invokes Foundation's `app:install` post-create command. The application +root executable remains `infbyte`; there is no separate Artisan/Console runtime. + +## Foundation runtime boundary + +Infbyte does not implement a framework layer above Foundation. + +The Web bootstrap is deliberately small: + +```php +use Infocyph\Foundation\Foundation; + +return Foundation::web([ + 'base_path' => dirname(__DIR__), +]); +``` + +Foundation's `CommandDispatcher` selects CLI, Worker, and Scheduler runtimes for +the root `infbyte` executable when commands require them. -The starter application exposes: +The four Foundation runtimes are: -- `GET /api/health` -- `GET /json` +- Web +- CLI +- Worker +- Scheduler -## Console +Optional package providers remain lazy until configuration/code selects their +capability. -Run `php infbyte list` to see every available command. Frequently used commands -include: +## CLI + +List the current command surface with: + +```bash +php infbyte list +``` + +Common families include: | Purpose | Commands | | --- | --- | -| Inspect the application | `about`, `env:show`, `config:show`, `route:list` | +| Inspect | `about`, `env:show`, `config:show`, `config:validate`, `route:list` | | Local development | `serve`, `create:*` | -| Optional capabilities | `module:list`, `module:install`, `module:remove` | -| Database work | `db:show`, `db:table`, `db:seed`, `migrate*` | -| Runtime maintenance | `app:install`, `cache:clear`, `storage:link`, `secret:generate` | -| Background work | `schedule:*`, `worker:*`, `queue:consume` | -| Deployment | `optimize`, `optimize:clear`, `app:ready` | +| Modules | `module:list`, `module:show`, `module:install`, `module:remove`, `module:config:publish`, `module:schema:*` | +| Database | `db:*`, `migrate*` | +| Operations | `execution:*`, `maintenance:*`, `runtime:reload`, `log:tail` | +| Background work | `schedule:*`, `worker:*`, `queue:*`, `messaging:list` | +| Storage/session/auth | `storage:*`, `session:prune`, `auth:prune` | +| Environment protection | `env:encrypt`, `env:decrypt` | +| Deployment | `optimize`, `optimize:clear`, `optimize:report`, `app:ready` | + +Global controls include `--json`, `-q|--quiet`, `--silent`, `-v|-vv|-vvv`, +`--profile`, `--env`, and `-n|--no-interaction`. -Some commands become usable only after their corresponding optional module is -installed. Missing capabilities report the exact installation command. +Application commands are registered explicitly in `routes/console.php`, +schedules in `routes/schedule.php`, and non-message maintenance workers in +`routes/workers.php`. -Create application artifacts without adding unused runtime services: +## Application generators + +Foundation generators create application starting points without silently +editing registration/configuration: ```bash php infbyte create:controller Admin/User php infbyte create:command Reports/Daily +php infbyte create:request StoreUser +php infbyte create:rule ValidVatNumber +php infbyte create:resource User +php infbyte create:job GenerateReport +php infbyte create:handler GenerateReport +php infbyte create:job-middleware AuditJob +php infbyte create:mail Welcome +php infbyte create:notification PasswordChanged +php infbyte create:notification-channel Sms php infbyte create:repository User -php infbyte create:worker Queue -php infbyte create:test Http/UserAccess +php infbyte create:migration CreateUsers +php infbyte create:seeder Production +php infbyte create:worker Metrics ``` -Existing files are preserved unless `--force` is supplied. Register application -commands explicitly in `routes/console.php`, schedules in `routes/schedule.php`, -and supervised workers in `routes/workers.php`. - -For an intentionally static controller action, use a first-class callable such -as `Route::get('/reports', ReportsController::show(...))`; route caching converts -that safe form to a plain descriptor. Captured closures and instance handlers -remain supported through the general cached-handler path. +Existing files are preserved unless `--force` is supplied. -## Optional modules +## Purpose-first optional modules -A new project installs only the core runtime. Add or publish capabilities as the -application needs them: +A new application keeps only the Foundation core runtime. Add capabilities as +they are needed: ```bash php infbyte module:list -php infbyte module:install db +php infbyte module:install database php infbyte module:install cache +php infbyte module:install communication php infbyte module:install messaging -php infbyte module:install session +php infbyte module:config:publish operations ``` -Available modules include database, cache, communication, cryptography, -filesystem, logging, messaging, OTP, passkeys, JSON resources, sessions, and -validation. Installation publishes documented configuration without activating -global middleware, providers, connections, workers, or sessions. Optional -services remain lazy until selected by application code, a route, or a command. +Canonical modules are: + +- `auth` +- `cache` +- `communication` +- `database` +- `filesystem` +- `logging` (built in) +- `messaging` +- `operations` (built in) +- `resources` (built in) +- `security` +- `session` (built in) +- `validation` + +Aliases such as `db`, `crypto`, `otp`, `passkeys`, and `queue` remain accepted, +but the application documentation uses purpose names. OTP and WebAuthn are +implementations inside the `auth` module rather than standalone public modules. + +Installing a module does not add global middleware, open connections, or start +workers. Optional config is published only when requested/needed and remains +outside the lean checked-in skeleton by default. + +## Module schema lifecycle + +Capability-owned schemas use one command family: + +```bash +php infbyte module:schema:status auth +php infbyte module:schema:install auth +php infbyte module:schema:status cache +php infbyte module:schema:install session +php infbyte module:schema:sync +``` + +The `database` module owns DB/migration infrastructure; it does not own arbitrary +application tables. Removing a module never drops schemas/application data. ## Application structure -- `app/` contains application controllers, commands, and generated code. -- `bootstrap/` contains the isolated web and console composition roots. -- `config/` contains application-owned configuration. -- `routes/` contains HTTP, command, schedule, and worker mappings. -- `public/` contains the web entry point. -- `storage/` contains writable runtime data. -- `tests/Feature/` and `tests/Unit/` contain starter Pest examples. +The starter intentionally stays small: + +```text +app/ +bootstrap/ + app.php + providers.php + cache/ +config/ + app.php + auth.php + router.php +public/ +routes/ + api.php + console.php + schedule.php + workers.php +storage/ +tests/ +composer.json +infbyte +``` + +Only application-default config is checked in. Cache/database/filesystem/ +messaging/operations/security/session/validation/communication config belongs +to optional module publication rather than being bulk-copied into every new +project. + +## Database + +```bash +php infbyte module:install database +php infbyte migrate +php infbyte migrate --pretend +php infbyte migrate:status +php infbyte db:monitor --section=status +``` + +Application migrations are registered explicitly under +`database.migrations.classes`; Foundation does not scan migration directories. + +## Messaging and workers -Web requests boot only the HTTP path. CLI requests boot only the command -capabilities they require; the ordinary web path does not load console command, -schedule, or worker definitions. +Omnibus-backed messaging is optional: -## Testing +```bash +php infbyte module:install messaging +php infbyte worker:list +php infbyte worker:run reports +php infbyte worker:status reports +php infbyte queue:failed +``` + +Foundation does not expose a parallel messaging manager. Application services +resolve native Omnibus `MessageBus`, `EventDispatcher`, and related APIs through +DI. + +`routes/workers.php` is only for application maintenance workers; queue worker +loops remain Omnibus-owned. + +## Runtime operations -The generated project includes one feature test and one unit test: +Useful production/runtime commands include: ```bash -composer ic:tests +php infbyte config:validate --production +php infbyte maintenance:status +php infbyte runtime:reload +php infbyte worker:restart +php infbyte schedule:interrupt +php infbyte storage:status +php infbyte log:tail --follow ``` -Foundation provides a native Webrick HTTP test client, as demonstrated by -`tests/Feature/ExampleTest.php`. Infbyte's repository-only release tests are -excluded from Composer-created projects. +Runtime generation commands request graceful shutdown. Foundation does not +replace Supervisor/systemd/Docker/Kubernetes process supervision. ## Production -Compile application metadata before serving production traffic: +Build deployment-owned optimized artifacts before serving production traffic: ```bash php infbyte optimize +php infbyte optimize:report php infbyte app:ready ``` -`optimize` compiles configuration, route, command, schedule, and module metadata -plus the eligible HTTP container graph so requests do less work. It publishes a -final optimize manifest only after the complete set is ready. `optimize:clear` -removes every generated artifact and leaves uncached execution available. +Clear generated artifacts with: -`APP_CONFIG_CACHE_TYPE=single` loads one snapshot; `sharded` loads configuration -namespaces on demand. Neither is universally faster: measure the application's -minimal, authenticated, and database-backed routes before selecting one. -Compiled-container activation remains `off` by default because small requests -may not amortize its boot cost; use `always` only for a measured deployment, -commonly a persistent worker with OPcache. +```bash +php infbyte optimize:clear +``` -Webrick automatically selects the response emitter. A known deployment may set -`WEBRICK_EMITTER` to `fpm`, `frankenphp`, `lsapi`, `unit`, `swoole`, -`roadrunner`, or `workerman`; leave it unset when runtime detection is desired. +Compiled config/route/command/schedule/container artifacts belong to deployment +and are ignored by the skeleton repository. -The included deployment helper validates writable runtime paths and builds the -same caches: +Before deployment: -```bash -./deploy.sh -``` +- set production environment/debug policy; +- configure only the modules the application actually uses; +- run application migrations; +- provision applicable module schemas; +- require `config:validate --production` / `app:ready` to pass; +- use a production web server and external process manager. + +## Testing and release checks -Before deployment, set `APP_ENV=production`, disable debug output, configure -installed modules, apply required migrations, and require `app:ready` to pass. +The current skeleton Composer file does not invent generic test/release script +aliases. Run the tools/scripts actually installed by the application/release +candidate. Foundation's full Composer/PHPForge/static/PHPUnit/integration and +performance matrix is performed in its dedicated release-verification phase. ## Documentation -- [Foundation](https://github.com/infocyph/Foundation/tree/main/docs) -- [Console](https://github.com/infocyph/Console/tree/main/docs) -- [Omnibus](https://github.com/infocyph/Omnibus/tree/main/docs) -- [Webrick](https://docs.infocyph.com/projects/webrick/en/latest/) -- [JsonDispatch](https://docs.infocyph.com/projects/json-dispatch/) +- [Foundation documentation](https://github.com/infocyph/Foundation/tree/2.0/docs) +- [Omnibus](https://github.com/infocyph/Omnibus) +- [Webrick](https://github.com/infocyph/Webrick) -Detailed framework and package documentation will expand separately; this -README intentionally remains a concise application quick start. +Infbyte keeps framework details in Foundation documentation rather than copying +them into the application skeleton. diff --git a/app/Console/Commands/.gitkeep b/app/Console/Commands/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/app/Console/Commands/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/bootstrap/app.php b/bootstrap/app.php index f5c5e5f..049da00 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -4,7 +4,6 @@ use Infocyph\Foundation\Foundation; -/** @var array $options */ -$options = require __DIR__ . '/options.php'; - -return Foundation::web($options); +return Foundation::web([ + 'base_path' => dirname(__DIR__), +]); diff --git a/bootstrap/cache/config/.gitignore b/bootstrap/cache/.gitignore similarity index 100% rename from bootstrap/cache/config/.gitignore rename to bootstrap/cache/.gitignore diff --git a/bootstrap/cache/console/.gitignore b/bootstrap/cache/console/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/bootstrap/cache/console/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/bootstrap/cache/routes/.gitignore b/bootstrap/cache/routes/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/bootstrap/cache/routes/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/bootstrap/console.php b/bootstrap/console.php deleted file mode 100644 index 55734cd..0000000 --- a/bootstrap/console.php +++ /dev/null @@ -1,10 +0,0 @@ - $options */ -$options = require __DIR__ . '/options.php'; - -return Foundation::console($options); diff --git a/bootstrap/options.php b/bootstrap/options.php deleted file mode 100644 index e434e74..0000000 --- a/bootstrap/options.php +++ /dev/null @@ -1,9 +0,0 @@ - $basePath, -]; diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 70ceb85..4b0a029 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -15,10 +15,22 @@ * \App\Providers\WebServiceProvider::class, */ ], - 'console' => [ + 'cli' => [ /** * Usage: - * \App\Providers\ConsoleServiceProvider::class, + * \App\Providers\CliServiceProvider::class, + */ + ], + 'worker' => [ + /** + * Usage: + * \App\Providers\WorkerServiceProvider::class, + */ + ], + 'scheduler' => [ + /** + * Usage: + * \App\Providers\SchedulerServiceProvider::class, */ ], ]; diff --git a/captainhook.json b/captainhook.json deleted file mode 100644 index 782a292..0000000 --- a/captainhook.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "commit-msg": { - "enabled": false, - "actions": [] - }, - "pre-push": { - "enabled": false, - "actions": [] - }, - "pre-commit": { - "enabled": true, - "actions": [ - { - "action": "composer validate --strict", - "options": [] - }, - { - "action": "composer normalize --dry-run", - "options": [] - }, - { - "action": "composer ic:release:audit", - "options": [] - }, - { - "action": "composer ic:ci", - "options": [] - } - ] - }, - "prepare-commit-msg": { - "enabled": false, - "actions": [] - }, - "post-commit": { - "enabled": false, - "actions": [] - }, - "post-merge": { - "enabled": false, - "actions": [] - }, - "post-checkout": { - "enabled": false, - "actions": [] - }, - "post-rewrite": { - "enabled": false, - "actions": [] - }, - "post-change": { - "enabled": false, - "actions": [] - } -} diff --git a/composer.json b/composer.json index 08f0a6d..8044bb0 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,8 @@ "license": "MIT", "type": "project", "require": { - "php": ">=8.4", - "infocyph/foundation": "^1.3" + "php": "^8.4", + "infocyph/foundation": "^2.0" }, "require-dev": { "infocyph/phpforge": "dev-main@dev" diff --git a/config/app.php b/config/app.php index 8c5ecc6..ed119fc 100644 --- a/config/app.php +++ b/config/app.php @@ -8,64 +8,50 @@ | Application Identity |-------------------------------------------------------------------------- | - | "name" labels the application in diagnostics and integrations. "env" - | selects environment-sensitive defaults, "debug" enables development - | diagnostics, and "url" is the canonical base URL used to build links. - | Names and environments are free-form strings, for example `Acme API` and - | `staging`; debug accepts `true|false`; URL example: `https://api.acme.test`. + | Infbyte owns application-facing defaults while Foundation owns the runtime + | implementation. "name" labels diagnostics and integrations, "env" selects + | environment-sensitive policy, "debug" enables development diagnostics, + | and "url" is the application's canonical external base URL. | */ - 'name' => env('APP_NAME', 'Infbyte'), - 'env' => env('APP_ENV', 'local'), - 'debug' => env('APP_DEBUG', true), - 'url' => env('APP_URL', 'http://localhost'), + 'name' => env_string('APP_NAME', 'Infbyte'), + 'env' => env_string('APP_ENV', 'local'), + 'debug' => env_bool('APP_DEBUG', true), + 'url' => env_string('APP_URL', 'http://localhost'), /* |-------------------------------------------------------------------------- | Configuration Cache |-------------------------------------------------------------------------- | - | "type" accepts "sharded" or "single". Sharded caches load namespaces - | on demand; single caches load one compiled snapshot. Build either form - | during deployment with `php infbyte config:cache`. Allowed values: - | `sharded|single`. + | Sharded config remains the normal application default so namespaces stay + | lazy. Build the selected artifact explicitly during deployment. + | Allowed values: sharded|single. | */ 'config_cache' => [ - 'type' => env('APP_CONFIG_CACHE_TYPE', 'sharded'), + 'type' => env_string('APP_CONFIG_CACHE_TYPE', 'sharded'), ], /* |-------------------------------------------------------------------------- - | Dependency Injection Container + | InterMix Container |-------------------------------------------------------------------------- | - | "alias" optionally names a configured container profile. "environment" - | controls environment-aware definitions. "lazy_loading" defers supported - | services, while "request_scope" isolates request-lived entries. - | - | "compiled" selects the application-owned resolver artifact. Activation - | remains off unless a measured deployment explicitly selects "always". - | Debug tracing is disabled by default; "level" selects its detail. - | - | Alias/environment examples: `http` and `production`. Boolean switches use - | `true|false`. The compiled artifact defaults to - | `bootstrap/cache/container.php`; APP_CONTAINER_COMPILED may select another - | application-owned relative or absolute path. - | Compiled activation values: `off|always`. - | Trace levels: `off|node|info|warn|warning|error|verbose`. + | Foundation 2.0 is lazy by default and owns execution scopes directly. + | There is no application request_scope switch. Compiled container activation + | remains opt-in until a deployment deliberately builds and enables it. | */ 'container' => [ 'alias' => env('APP_CONTAINER_ALIAS'), - 'environment' => env('APP_ENV', 'local'), - 'lazy_loading' => env('APP_CONTAINER_LAZY_LOADING', false), - 'request_scope' => env('APP_CONTAINER_REQUEST_SCOPE', true), - 'compiled' => env('APP_CONTAINER_COMPILED', 'bootstrap/cache/container.php'), - 'compiled_activation' => env('APP_CONTAINER_COMPILED_ACTIVATION', 'off'), + 'environment' => env_string('APP_ENV', 'local'), + 'lazy_loading' => env_bool('APP_CONTAINER_LAZY_LOADING', true), + 'compiled' => env_string('APP_CONTAINER_COMPILED', 'bootstrap/cache/container.php'), + 'compiled_activation' => env_string('APP_CONTAINER_COMPILED_ACTIVATION', 'off'), 'debug_tracing' => [ - 'enabled' => env('APP_CONTAINER_DEBUG_TRACING', false), - 'level' => env('APP_CONTAINER_DEBUG_TRACE_LEVEL', 'node'), + 'enabled' => env_bool('APP_CONTAINER_DEBUG_TRACING', false), + 'level' => env_string('APP_CONTAINER_DEBUG_TRACE_LEVEL', 'node'), ], ], ]; diff --git a/config/auth.php b/config/auth.php index d8664ce..3d7e002 100644 --- a/config/auth.php +++ b/config/auth.php @@ -8,25 +8,34 @@ | Authentication Drivers |-------------------------------------------------------------------------- | - | Each key selects the implementation for one authentication capability. - | IDs: `random|uid`. Storage: `memory|database`. Cache: - | `array|cache`. Passwords: `native|security`. Tokens: - | `simple|security`. MFA: `simple|otp`. Notifications: - | `collect|talkingbytes`. Passkeys: `disabled|memory|webauthn`. + | Foundation owns identity generation through UID. These switches select + | only replaceable authentication capabilities. The dependency-light + | defaults work without optional modules. | - | The self-contained defaults keep auth adapters optional. Production auth - | must install and explicitly select the durable modules it requires. + | Optional choices map to purpose-level modules: + | storage=database -> module:install database + | cache=cache -> module:install cache + | passwords/tokens=security -> module:install security + | mfa=otp or passkey=webauthn -> module:install auth + | notifications=talkingbytes -> module:install communication + | + | Module installation also synchronizes database schemas required by the + | active configuration. Database-backed authentication uses the auth schema; + | inspect or provision it explicitly with `module:schema:status auth` and + | `module:schema:install auth` when preparing persistence ahead of activation. + | + | OTP and WebAuthn remain independently selectable even though they share + | the same extended-auth module and installation bundle. | */ 'drivers' => [ - 'ids' => env('AUTH_IDS', 'random'), - 'storage' => env('AUTH_STORAGE', 'memory'), - 'cache' => env('AUTH_CACHE', 'array'), - 'passwords' => env('AUTH_PASSWORDS', 'native'), - 'tokens' => env('AUTH_TOKENS', 'simple'), - 'mfa' => env('AUTH_MFA', 'simple'), - 'notifications' => env('AUTH_NOTIFICATIONS', 'collect'), - 'passkey' => env('AUTH_PASSKEY', 'disabled'), + 'storage' => env_string('AUTH_STORAGE', 'memory'), + 'cache' => env_string('AUTH_CACHE', 'array'), + 'passwords' => env_string('AUTH_PASSWORDS', 'native'), + 'tokens' => env_string('AUTH_TOKENS', 'simple'), + 'mfa' => env_string('AUTH_MFA', 'simple'), + 'notifications' => env_string('AUTH_NOTIFICATIONS', 'collect'), + 'passkey' => env_string('AUTH_PASSKEY', 'disabled'), ], /* @@ -34,11 +43,8 @@ | Token Signing Secret |-------------------------------------------------------------------------- | - | This secret protects authentication tokens. It has no config-file - | default. Local runtime may supply its development-only fallback, while - | production rejects missing or short values. Set AUTH_TOKEN_SECRET to a - | unique high-entropy secret of at least 32 bytes and never commit it. - | Example format: a randomly generated 64-character hexadecimal string. + | app:install generates this value for a new application. Production must + | use unique high-entropy secret material and must never commit it. | */ 'token_secret' => env('AUTH_TOKEN_SECRET'), @@ -48,38 +54,29 @@ | One-Time Passwords |-------------------------------------------------------------------------- | - | "issuer" is shown by authenticator applications. "freshness_window" is - | the number of seconds an MFA result remains recent. TOTP "algorithm", - | `digits`, `period`, and `secret_bytes` define provisioning parameters. - | "window" is the accepted number of adjacent time steps. - | - | Recovery-code "count" and "length" control the generated backup set. - | Replay protection records consumed codes when "enabled" and retains each - | marker for "ttl" seconds. Keep replay protection enabled in production. - | - | Issuer example: `Acme`. Algorithm: `sha1|sha256|sha512`. Digits: `4..10`. - | Period: `1..86400` seconds. Window: `0..100` time steps. Secret bytes, - | recovery count/length, freshness, and replay TTL are positive integers. - | Typical values respectively are 64, 10, 10, 900, and 90. Replay accepts - | `true|false`. + | OTP-backed MFA is provided by the auth module. Enable it with AUTH_MFA=otp + | after installing the module. Replay-safe production use also requires the + | configured shared cache/coordination capability. | */ 'otp' => [ - 'issuer' => env('AUTH_OTP_ISSUER', env('APP_NAME', 'Infbyte')), - 'freshness_window' => env_int('AUTH_OTP_FRESHNESS_WINDOW', 900), + 'issuer' => env_string('AUTH_OTP_ISSUER', env_string('APP_NAME', 'Infbyte')), + 'hotp' => [ + 'look_ahead' => env_int('AUTH_OTP_HOTP_LOOK_AHEAD', 5), + ], 'totp' => [ - 'algorithm' => env('AUTH_OTP_ALGORITHM', 'sha1'), + 'algorithm' => env_string('AUTH_OTP_ALGORITHM', 'sha1'), 'digits' => env_int('AUTH_OTP_DIGITS', 6), 'period' => env_int('AUTH_OTP_PERIOD', 30), - 'secret_bytes' => env_int('AUTH_OTP_SECRET_BYTES', 64), + 'secret_bytes' => env_int('AUTH_OTP_SECRET_BYTES', 20), 'window' => env_int('AUTH_OTP_WINDOW', 1), ], 'recovery_codes' => [ 'count' => env_int('AUTH_OTP_RECOVERY_CODES', 10), - 'length' => env_int('AUTH_OTP_RECOVERY_LENGTH', 10), + 'length' => env_int('AUTH_OTP_RECOVERY_LENGTH', 12), ], 'replay' => [ - 'enabled' => env('AUTH_OTP_REPLAY', true), + 'store' => env('AUTH_OTP_REPLAY_STORE'), 'ttl' => env_int('AUTH_OTP_REPLAY_TTL', 90), ], ], @@ -89,25 +86,17 @@ | WebAuthn / Passkeys |-------------------------------------------------------------------------- | - | "rp_id", "rp_name", and "origin" identify the relying party and must - | match the deployed domain. `attestation` controls authenticator evidence. - | "user_verification" and "resident_key" use WebAuthn preference values. - | "algorithms" lists accepted COSE algorithms and "transports" lists the - | authenticator transports advertised during registration. - | - | RP ID example: `example.com`; RP name example: `Acme`; origin example: - | `https://example.com`. Attestation: `none|direct|indirect|enterprise`. - | User verification and resident key: `required|preferred|discouraged`. - | Algorithms: `ES256|RS256`. Transports: `internal|hybrid|usb|nfc|ble`. + | WebAuthn is provided by the auth module. Enable it with + | AUTH_PASSKEY=webauthn after installing that module. | */ 'webauthn' => [ 'rp_id' => env('WEBAUTHN_RP_ID'), - 'rp_name' => env('WEBAUTHN_RP_NAME', 'Infbyte'), + 'rp_name' => env_string('WEBAUTHN_RP_NAME', env_string('APP_NAME', 'Infbyte')), 'origin' => env('WEBAUTHN_ORIGIN'), - 'attestation' => 'none', - 'user_verification' => env('WEBAUTHN_USER_VERIFICATION', 'preferred'), - 'resident_key' => env('WEBAUTHN_RESIDENT_KEY', 'preferred'), + 'attestation' => env_string('WEBAUTHN_ATTESTATION', 'none'), + 'user_verification' => env_string('WEBAUTHN_USER_VERIFICATION', 'preferred'), + 'resident_key' => env_string('WEBAUTHN_RESIDENT_KEY', 'preferred'), 'algorithms' => ['ES256', 'RS256'], 'transports' => ['internal', 'hybrid', 'usb', 'nfc', 'ble'], ], diff --git a/config/ids.php b/config/ids.php deleted file mode 100644 index cc4e190..0000000 --- a/config/ids.php +++ /dev/null @@ -1,208 +0,0 @@ - env('IDS_DEFAULT', 'uuid7'), - - /* - |-------------------------------------------------------------------------- - | Shared Sequence Coordination - |-------------------------------------------------------------------------- - | - | "driver" selects the sequence backend and "directory" stores filesystem - | sequence state. "wait_time" is the lock retry delay in microseconds and - | "max_attempts" bounds acquisition attempts. Distributed deployments must - | use sequence state that is shared by every participating node. - | Drivers: `file|filesystem|memory`. Directory example: `cache/ids`. - | Wait/max-attempt examples: `1000` microseconds and `1000` attempts. - | - */ - 'sequence' => [ - 'driver' => env('IDS_SEQUENCE_DRIVER', 'filesystem'), - 'directory' => env_string('IDS_SEQUENCE_DIRECTORY', 'cache/ids'), - 'wait_time' => env_int('IDS_SEQUENCE_WAIT_TIME', 1000), - 'max_attempts' => env_int('IDS_SEQUENCE_MAX_ATTEMPTS', 1000), - ], - - /* - |-------------------------------------------------------------------------- - | ULID, NanoID And CUID2 - |-------------------------------------------------------------------------- - | - | ULID "mode" controls random or monotonic ordering. NanoID and CUID2 - | "length" values control their textual output size and collision budget. - | ULID modes: `monotonic|random`. NanoID length: `1..1048576`, example `21`. - | CUID2 length: `2..32`, example `24`. - | - */ - 'ulid' => [ - 'mode' => env('IDS_ULID_MODE', 'monotonic'), - ], - 'nanoid' => [ - 'length' => env_int('IDS_NANOID_LENGTH', 21), - ], - 'cuid2' => [ - 'length' => env_int('IDS_CUID2_LENGTH', 24), - ], - - /* - |-------------------------------------------------------------------------- - | Opaque And Deterministic Identifiers - |-------------------------------------------------------------------------- - | - | Opaque IDs use "length" for output size and "salt" for application-level - | separation. Deterministic IDs use "length" and "namespace" to keep equal - | inputs isolated by purpose. Use high-entropy secrets where the selected - | generator treats a salt as confidential. Opaque length: `1..1024`, example - | `12`; salt example: a random application-specific string. Deterministic - | length: `1..43`, example `24`; namespace example: `invoice`. - | - */ - 'opaque' => [ - 'length' => env_int('IDS_OPAQUE_LENGTH', 12), - 'salt' => env_string('IDS_OPAQUE_SALT', ''), - ], - 'deterministic' => [ - 'length' => env_int('IDS_DETERMINISTIC_LENGTH', 24), - 'namespace' => env_string('IDS_DETERMINISTIC_NAMESPACE', 'default'), - ], - - /* - |-------------------------------------------------------------------------- - | Snowflake Identifiers - |-------------------------------------------------------------------------- - | - | "datacenter_id" and "worker_id" must uniquely identify the generator. - | "custom_epoch" sets the timestamp origin, "clock_backward_policy" controls - | rollback handling, and "output" selects the returned representation. - | Sequence "driver" and "directory" override the shared defaults above. - | Datacenter/worker examples: `3` and `17`; epoch examples: Unix milliseconds - | or `2024-01-01T00:00:00Z`. Clock policy: `wait|throw`. Output: - | `string|int|binary`. Sequence drivers: `file|filesystem|memory`. - | - */ - 'snowflake' => [ - 'datacenter_id' => env_int('IDS_SNOWFLAKE_DATACENTER_ID', 0), - 'worker_id' => env_int('IDS_SNOWFLAKE_WORKER_ID', 0), - 'custom_epoch' => env('IDS_SNOWFLAKE_CUSTOM_EPOCH'), - 'clock_backward_policy' => env('IDS_SNOWFLAKE_CLOCK_BACKWARD_POLICY', 'wait'), - 'output' => env('IDS_SNOWFLAKE_OUTPUT', 'string'), - 'sequence' => [ - 'driver' => env('IDS_SNOWFLAKE_SEQUENCE_DRIVER', env('IDS_SEQUENCE_DRIVER', 'filesystem')), - 'directory' => env_string('IDS_SNOWFLAKE_SEQUENCE_DIRECTORY', env_string('IDS_SEQUENCE_DIRECTORY', 'cache/ids')), - ], - ], - - /* - |-------------------------------------------------------------------------- - | Sonyflake Identifiers - |-------------------------------------------------------------------------- - | - | "machine_id" must be unique among active generators. "custom_epoch", - | "clock_backward_policy", and "output" control time origin, rollback - | handling, and representation. Sequence keys override shared coordination. - | Machine example: `42`; epoch example: `2024-01-01T00:00:00Z`; clock policy: - | `wait|throw`; output: `string|int|binary`; sequence driver: - | `file|filesystem|memory`; directory example: `cache/ids`. - | - */ - 'sonyflake' => [ - 'machine_id' => env_int('IDS_SONYFLAKE_MACHINE_ID', 0), - 'custom_epoch' => env('IDS_SONYFLAKE_CUSTOM_EPOCH'), - 'clock_backward_policy' => env('IDS_SONYFLAKE_CLOCK_BACKWARD_POLICY', 'wait'), - 'output' => env('IDS_SONYFLAKE_OUTPUT', 'string'), - 'sequence' => [ - 'driver' => env('IDS_SONYFLAKE_SEQUENCE_DRIVER', env('IDS_SEQUENCE_DRIVER', 'filesystem')), - 'directory' => env_string('IDS_SONYFLAKE_SEQUENCE_DIRECTORY', env_string('IDS_SEQUENCE_DIRECTORY', 'cache/ids')), - ], - ], - - /* - |-------------------------------------------------------------------------- - | TBSL Identifiers - |-------------------------------------------------------------------------- - | - | "machine_id" identifies the node, "sequenced" enables coordinated values, - | "clock_backward_policy" defines rollback behavior, and "output" selects - | representation. Sequence "driver" and "directory" are profile overrides. - | A machine ID may be 42. Sequenced accepts `true|false`; clock policy accepts - | `wait|throw`; output accepts `string|int|binary`; and sequence driver accepts - | `file|filesystem|memory`. A sequence directory may be `cache/ids`. - | - */ - 'tbsl' => [ - 'machine_id' => env_int('IDS_TBSL_MACHINE_ID', 0), - 'sequenced' => env_bool('IDS_TBSL_SEQUENCED', false), - 'clock_backward_policy' => env('IDS_TBSL_CLOCK_BACKWARD_POLICY', 'wait'), - 'output' => env('IDS_TBSL_OUTPUT', 'string'), - 'sequence' => [ - 'driver' => env('IDS_TBSL_SEQUENCE_DRIVER', env('IDS_SEQUENCE_DRIVER', 'filesystem')), - 'directory' => env_string('IDS_TBSL_SEQUENCE_DIRECTORY', env_string('IDS_SEQUENCE_DIRECTORY', 'cache/ids')), - ], - ], - - /* - |-------------------------------------------------------------------------- - | Randflake Identifiers - |-------------------------------------------------------------------------- - | - | "node_id" identifies the generator. "lease_start" and "lease_end" bound - | its allocated range; "secret" protects generator-specific derivation and - | must be replaced in production. "output" selects representation, while - | sequence "driver" and "directory" override shared coordination settings. - | Node example: `7`; lease example: `100000..199999`; secret example: a - | random 32-byte Base64 string; output: `string|int|binary`; sequence driver: - | `file|filesystem|memory`; directory example: `cache/ids`. - | - */ - 'randflake' => [ - 'node_id' => env_int('IDS_RANDFLAKE_NODE_ID', 0), - 'lease_start' => env_int('IDS_RANDFLAKE_LEASE_START', 0), - 'lease_end' => env_int('IDS_RANDFLAKE_LEASE_END', 0), - 'secret' => env_string('IDS_RANDFLAKE_SECRET', 'change-me'), - 'output' => env('IDS_RANDFLAKE_OUTPUT', 'string'), - 'sequence' => [ - 'driver' => env('IDS_RANDFLAKE_SEQUENCE_DRIVER', env('IDS_SEQUENCE_DRIVER', 'filesystem')), - 'directory' => env_string('IDS_RANDFLAKE_SEQUENCE_DIRECTORY', env_string('IDS_SEQUENCE_DIRECTORY', 'cache/ids')), - ], - ], - - /* - |-------------------------------------------------------------------------- - | Authentication Identifier Purposes - |-------------------------------------------------------------------------- - | - | Each key selects the generator for that auth record type: accounts, audit - | events, challenges, correlations, credentials, devices, grants, - | permissions, roles, and sessions. Keep persisted types stable after data - | exists unless a migration explicitly handles the representation change. - | Every value accepts one of the generator names listed under Default - | Identifier. Example: account `uuid7`, correlation `ulid`, session `uuid7`. - | - */ - 'auth' => [ - 'account' => env('IDS_AUTH_ACCOUNT', 'uuid7'), - 'audit_event' => env('IDS_AUTH_AUDIT_EVENT', 'uuid7'), - 'challenge' => env('IDS_AUTH_CHALLENGE', 'uuid7'), - 'correlation' => env('IDS_AUTH_CORRELATION', 'ulid'), - 'credential' => env('IDS_AUTH_CREDENTIAL', 'uuid7'), - 'device' => env('IDS_AUTH_DEVICE', 'uuid7'), - 'grant' => env('IDS_AUTH_GRANT', 'uuid7'), - 'permission' => env('IDS_AUTH_PERMISSION', 'uuid7'), - 'role' => env('IDS_AUTH_ROLE', 'uuid7'), - 'session' => env('IDS_AUTH_SESSION', 'uuid7'), - ], -]; diff --git a/config/router.php b/config/router.php index f682dd6..879bf1f 100644 --- a/config/router.php +++ b/config/router.php @@ -10,7 +10,7 @@ | | These files are loaded from the routes directory when route metadata has | not been cached. Add a filename for each application-owned route surface. - | Example values: `api.php`, `web.php`, and `admin.php`. + | Example values: api.php, web.php, and admin.php. | */ 'files' => [ @@ -23,9 +23,9 @@ |-------------------------------------------------------------------------- | | The matcher controls how Webrick compiles and dispatches routes. Build the - | selected form during deployment with `php infbyte route:cache`. - | Allowed values: `fused|generated|sharded`. + | selected form during deployment with `php infbyte route:cache` or + | `php infbyte optimize`. Allowed values: fused|generated|sharded. | */ - 'matcher' => env('ROUTER_MATCHER', 'fused'), + 'matcher' => env_string('ROUTER_MATCHER', 'fused'), ]; diff --git a/deploy.sh b/deploy.sh index e06223d..5843cbd 100755 --- a/deploy.sh +++ b/deploy.sh @@ -28,7 +28,6 @@ fi runtime_directories=( "bootstrap/cache" "bootstrap/cache/config" - "bootstrap/cache/console" "bootstrap/cache/routes" "storage" "storage/app" @@ -74,4 +73,4 @@ php_path="$(command -v "$php_bin")" "$php_path" infbyte optimize -printf 'Deployment directories and application caches are ready.\n' +printf 'Deployment directories and Foundation 2.0 runtime artifacts are ready.\n' diff --git a/infbyte b/infbyte index 6327240..694f8de 100755 --- a/infbyte +++ b/infbyte @@ -3,49 +3,17 @@ declare(strict_types=1); -use Composer\InstalledVersions; -use Infocyph\Foundation\Application\Application; -use Infocyph\Foundation\Console\FoundationConsole; -use Infocyph\Foundation\Config\EnvironmentLoader; +use Infocyph\Foundation\Command\CommandDispatcher; -require __DIR__ . '/vendor/autoload.php'; - -$basePath = __DIR__; -$commandManifest = $basePath . '/bootstrap/cache/console/commands.php'; -$commands = []; - -new EnvironmentLoader()->load($basePath); -$applicationName = trim(env_string('APP_NAME', 'infbyte')); -if ($applicationName === '') { - $applicationName = 'infbyte'; +$autoload = __DIR__ . '/vendor/autoload.php'; +if (!is_file($autoload)) { + fwrite(STDERR, "Unable to locate vendor/autoload.php; run composer install first.\n"); + exit(1); } -if (!is_file($commandManifest)) { - $commands = require $basePath . '/routes/console.php'; - - if (!is_array($commands)) { - throw new RuntimeException('Console routes must return a command-to-class map.'); - } -} - -$console = FoundationConsole::create( - applicationFactory: static function (?string $profile) use ($basePath): Application { - $application = require $basePath . '/bootstrap/console.php'; - - if ($profile !== null && $application->environment() !== $profile) { - throw new RuntimeException(sprintf( - 'Console profile "%s" does not match the application environment "%s".', - $profile, - $application->environment() ?? '', - )); - } - - return $application; - }, - name: $applicationName, - version: InstalledVersions::getPrettyVersion('infocyph/foundation') ?? 'dev-main', - commands: $commands, - commandManifest: $commandManifest, -); +require $autoload; -exit($console->run()); +exit(CommandDispatcher::project( + ['base_path' => __DIR__], + displayName: 'Infbyte', +)->run($argv)); diff --git a/routes/api.php b/routes/api.php index 9dedb76..0a73ca2 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,7 +3,8 @@ declare(strict_types=1); use App\Http\Controllers\SystemController; -use Infocyph\Webrick\Router\Facade\Router as Route; +use Infocyph\Webrick\Router\Definition\Registrar; -Route::get('/api/health', SystemController::health(...)); -Route::get('/json', SystemController::json(...), 'json'); +/** @var Registrar $router */ +$router->get('/api/health', SystemController::health(...)); +$router->get('/json', SystemController::json(...), 'json'); diff --git a/routes/console.php b/routes/console.php index eb030cb..dedf624 100644 --- a/routes/console.php +++ b/routes/console.php @@ -5,6 +5,6 @@ return [ /** * Usage: - * 'reports:daily' => \App\Console\Commands\Reports\DailyCommand::class, + * 'reports:daily' => \App\Command\Reports\DailyCommand::class, */ ]; diff --git a/routes/schedule.php b/routes/schedule.php index d362161..2c064b8 100644 --- a/routes/schedule.php +++ b/routes/schedule.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use Infocyph\Console\Scheduling\Schedule; +use Infocyph\Foundation\Scheduling\Schedule; return static function (Schedule $schedule): void { /** diff --git a/routes/workers.php b/routes/workers.php index 651de55..3a60206 100644 --- a/routes/workers.php +++ b/routes/workers.php @@ -4,7 +4,11 @@ return [ /** + * Application maintenance workers implement + * \Infocyph\Foundation\Worker\WorkerProvider and normally live in + * app/Worker. Queue/message workers are configured through Omnibus. + * * Usage: - * 'queue' => \App\Console\Workers\QueueWorker::class, + * 'maintenance' => \App\Worker\MaintenanceWorker::class, */ ]; diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index 1e8c52b..ac190ff 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -3,17 +3,15 @@ declare(strict_types=1); use Infocyph\Foundation\Application\Application; +use Infocyph\Webrick\Request\Request; it('serves the application health endpoint', function (): void { /** @var Application $app */ $app = require dirname(__DIR__, 2) . '/bootstrap/app.php'; - $app->boot(); - $response = $app->testing() - ->http() - ->get('/api/health') - ->assertStatus(200) - ->assertJson(['status' => 'ok']); + $response = $app->handle(Request::fake(method: 'GET', uri: 'http://localhost/api/health')); + $payload = json_decode((string) $response->getBody(), true, flags: JSON_THROW_ON_ERROR); - expect($response->json())->toBe(['status' => 'ok']); + expect($response->getStatusCode())->toBe(200) + ->and($payload)->toBe(['status' => 'ok']); }); diff --git a/tests/Feature/FrameworkRuntimeTest.php b/tests/Feature/FrameworkRuntimeTest.php index b45a8d0..04d2bda 100644 --- a/tests/Feature/FrameworkRuntimeTest.php +++ b/tests/Feature/FrameworkRuntimeTest.php @@ -3,27 +3,15 @@ declare(strict_types=1); use Infocyph\Foundation\Application\Application; -use Infocyph\Foundation\Auth\Contract\Notification\AuthNotifierInterface; -use Infocyph\Foundation\Cache\CacheManager; -use Infocyph\Console\Command\CommandContract; -use Infocyph\Foundation\Console\FoundationConsole; -use Infocyph\Foundation\Database\DatabaseManager; -use Infocyph\Foundation\Filesystem\FilesystemManager; +use Infocyph\Foundation\Application\RuntimeMode; +use Infocyph\Foundation\Config\ConfigRepository; +use Infocyph\Foundation\Filesystem\PathManager; use Infocyph\Foundation\Foundation; use Infocyph\Foundation\Http\HttpKernel; -use Infocyph\Foundation\Notifications\NotificationManager; -use Infocyph\Foundation\Routing\RouteFileLoader; -use Infocyph\Foundation\Routing\RouterManager; -use Infocyph\Foundation\Validation\ValidationManager; +use Infocyph\Foundation\Runtime\ExecutionScope; +use Infocyph\Webrick\Request\Request; -function infbyteApp(): Application -{ - return Foundation::web(infbyteTestOptions()); -} - -/** - * @return array - */ +/** @return array */ function infbyteTestOptions(): array { $root = dirname(__DIR__, 2); @@ -31,7 +19,7 @@ function infbyteTestOptions(): array . '/infbyte-runtime-' . getmypid(); - foreach (['app', 'app/public', 'cache', 'logs', 'sessions', 'uploads'] as $directory) { + foreach (['cache', 'logs', 'sessions', 'uploads'] as $directory) { $path = $runtime . DIRECTORY_SEPARATOR . $directory; if (!is_dir($path) && !mkdir($path, 0775, true) && !is_dir($path)) { @@ -42,7 +30,9 @@ function infbyteTestOptions(): array return [ 'base_path' => $root, '_config_cache' => false, - 'env' => 'testing', + 'app' => [ + 'env' => 'testing', + ], 'paths' => [ 'storage' => $runtime, 'cache' => $runtime . '/cache', @@ -50,124 +40,113 @@ function infbyteTestOptions(): array 'sessions' => $runtime . '/sessions', 'uploads' => $runtime . '/uploads', ], - 'auth' => [ - 'drivers' => [ - 'cache' => 'array', - ], - ], 'router' => [ 'cache' => false, ], ]; } -function expectedNotifierClass(Application $app): string -{ - return $app->config()->get('auth.drivers.notifications') === 'talkingbytes' - ? 'Infocyph\\Foundation\\Auth\\Adapter\\TalkingBytes\\TalkingBytesAuthNotifier' - : 'Infocyph\\Foundation\\Auth\\Support\\CollectingAuthNotifier'; -} - -it('boots with the expected application shape', function (): void { - $app = infbyteApp(); +it('constructs exactly the four explicit Foundation runtimes', function (): void { + $options = infbyteTestOptions(); + $cases = [ + [Foundation::web(...), RuntimeMode::Web, 'runningInWeb'], + [Foundation::cli(...), RuntimeMode::Cli, 'runningInCli'], + [Foundation::worker(...), RuntimeMode::Worker, 'runningInWorker'], + [Foundation::scheduler(...), RuntimeMode::Scheduler, 'runningInScheduler'], + ]; - expect($app->environment())->toBeString()->not->toBeEmpty() - ->and($app->runningInWeb())->toBeTrue(); + foreach ($cases as [$factory, $mode, $predicate]) { + /** @var callable(array):Application $factory */ + $app = $factory($options); - $paths = $app->paths()->all(); + expect($app)->toBeInstanceOf(Application::class) + ->and($app->runtimeMode())->toBe($mode) + ->and($app->{$predicate}())->toBeTrue() + ->and($app->booted())->toBeFalse() + ->and($app->environment())->toBe('testing') + ->and($app->basePath())->toBe(dirname(__DIR__, 2)); + } - expect($paths)->toHaveKey('providers'); - expect(is_file($paths['providers']))->toBeTrue(); - expect(is_dir($paths['storage']))->toBeTrue(); + expect(method_exists(Foundation::class, 'console'))->toBeFalse(); }); -it('keeps the console bootstrap outside the web boot graph', function (): void { - $app = Foundation::console(infbyteTestOptions()); - - expect($app)->toBeInstanceOf(Application::class) - ->and($app->runningInConsole())->toBeTrue() - ->and($app->container()->has(RouteFileLoader::class))->toBeFalse() - ->and($app->container()->has(HttpKernel::class))->toBeFalse(); - - $app->boot(); +it('keeps HTTP unavailable outside the web runtime', function (): void { + $app = Foundation::cli(infbyteTestOptions())->boot(); - expect($app->container()->has(RouteFileLoader::class))->toBeFalse() - ->and($app->container()->has(HttpKernel::class))->toBeFalse() + expect($app->runningInCli())->toBeTrue() + ->and($app->booted())->toBeTrue() ->and(fn() => $app->http()) - ->toThrow(LogicException::class, 'HTTP kernel is unavailable'); + ->toThrow(LogicException::class, 'The HTTP kernel is unavailable in the cli runtime.'); }); -it('defines console commands through an explicit command route map', function (): void { - $commands = require dirname(__DIR__, 2) . '/routes/console.php'; +it('resolves only the narrow Foundation application core directly', function (): void { + $app = Foundation::web(infbyteTestOptions())->boot(); + + expect($app->make(Application::class))->toBe($app) + ->and($app->config())->toBeInstanceOf(ConfigRepository::class) + ->and($app->paths())->toBeInstanceOf(PathManager::class) + ->and($app->execution())->toBeInstanceOf(ExecutionScope::class) + ->and($app->http())->toBeInstanceOf(HttpKernel::class) + ->and($app->booted())->toBeTrue(); + + foreach ([ + 'auth', + 'authManager', + 'cache', + 'database', + 'filesystem', + 'ids', + 'notifications', + 'router', + 'testing', + 'validation', + ] as $retiredConvenienceMethod) { + expect(method_exists($app, $retiredConvenienceMethod))->toBeFalse(); + } +}); - expect($commands)->toBeArray() - ->and($commands)->toBe([]); +it('defines application commands through the explicit command route file', function (): void { + $commands = require dirname(__DIR__, 2) . '/routes/console.php'; - foreach ($commands as $command) { - expect(is_string($command) && is_a($command, CommandContract::class, true))->toBeTrue(); - } + expect($commands)->toBeArray()->toBe([]); }); -it('keeps Foundation system commands out of application Composer scripts', function (): void { +it('keeps Foundation system commands out of application Composer script keys', function (): void { $composer = json_decode( - file_get_contents(dirname(__DIR__, 2) . '/composer.json'), + (string) file_get_contents(dirname(__DIR__, 2) . '/composer.json'), true, flags: JSON_THROW_ON_ERROR, ); $scripts = $composer['scripts'] ?? []; - expect($scripts)->toBeArray() - ->and(array_intersect( - array_keys($scripts), - array_keys(FoundationConsole::commands([])), - ))->toBe([]); -}); - -it('registers the core services', function (): void { - $app = infbyteApp()->boot(); - - expect($app->auth())->toBeObject(); - expect($app->authManager())->toBeObject(); - expect($app->authActions())->toBeObject(); - expect($app->http())->toBeObject(); - expect($app->ids())->toBeObject(); - expect($app->has(CacheManager::class))->toBeFalse(); - expect($app->has(DatabaseManager::class))->toBeFalse(); - expect($app->has(FilesystemManager::class))->toBeFalse(); - expect($app->has(NotificationManager::class))->toBeFalse(); - expect($app->has(ValidationManager::class))->toBeFalse(); -}); - -it('serves the health and JSON routes', function (): void { - $app = infbyteApp()->boot(); - $router = $app->make(RouterManager::class); - $registered = []; - - foreach ($router->routes() as $route) { - $registered[$route->getMethod() . ' ' . $route->getPath()] = $route->getHandler(); + expect($scripts)->toBeArray(); + + foreach ([ + 'app:ready', + 'config:cache', + 'db:monitor', + 'module:list', + 'optimize', + 'route:cache', + 'schedule:run', + 'worker:run', + ] as $systemCommand) { + expect(array_key_exists($systemCommand, $scripts))->toBeFalse(); } - - expect(array_keys($registered))->toBe(['GET /api/health', 'GET /json']); - expect($registered['GET /api/health'])->toBeInstanceOf(Closure::class) - ->and($registered['GET /json'])->toBeInstanceOf(Closure::class); - - $http = $app->testing()->http(); - $health = $http->get('/api/health') - ->assertStatus(200) - ->assertHeader('Content-Type') - ->assertJson(['status' => 'ok']); - $json = $http->get('/json') - ->assertStatus(200) - ->assertHeader('Content-Type') - ->json(); - - expect($health->json())->toBe(['status' => 'ok']) - ->and($json)->toHaveKey('memory') - ->and($json['memory'])->toBeInt(); }); -it('uses the self-contained auth notifier without optional modules', function (): void { - $app = infbyteApp()->boot(); +it('serves the skeleton routes through canonical Foundation web handling', function (): void { + $app = Foundation::web(infbyteTestOptions()); + + $health = $app->handle(Request::fake(method: 'GET', uri: 'http://localhost/api/health')); + $json = $app->handle(Request::fake(method: 'GET', uri: 'http://localhost/json')); + $healthPayload = json_decode((string) $health->getBody(), true, flags: JSON_THROW_ON_ERROR); + $jsonPayload = json_decode((string) $json->getBody(), true, flags: JSON_THROW_ON_ERROR); - expect($app->make(AuthNotifierInterface::class)::class)->toBe(expectedNotifierClass($app)); + expect($app->booted())->toBeTrue() + ->and($health->getStatusCode())->toBe(200) + ->and($healthPayload)->toBe(['status' => 'ok']) + ->and($json->getStatusCode())->toBe(200) + ->and($jsonPayload)->toHaveKey('memory') + ->and($jsonPayload['memory'])->toBeInt(); }); diff --git a/tests/Feature/RouteCacheCliTest.php b/tests/Feature/RouteCacheCliTest.php index 8eed028..58717c7 100644 --- a/tests/Feature/RouteCacheCliTest.php +++ b/tests/Feature/RouteCacheCliTest.php @@ -2,130 +2,87 @@ declare(strict_types=1); -use Composer\InstalledVersions; use App\Http\Controllers\SystemController; -use Infocyph\Foundation\Auth\AuthManager; -use Infocyph\Foundation\Database\DatabaseManager; +use Composer\InstalledVersions; use Infocyph\Foundation\Foundation; -use Infocyph\Foundation\Messaging\MessagingManager; use Infocyph\Foundation\Routing\RouteCachePath; -use Infocyph\Foundation\Session\SessionManager; use Infocyph\Webrick\Request\Request; use Infocyph\Webrick\Router\Matching\FusedMatcher; -it('uses the environment application name and reports the Foundation runtime version', function (): void { - $root = dirname(__DIR__, 2); - [$exitCode, $output] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - '--version', - ], ['APP_NAME' => 'Acme Console']); - - expect($exitCode)->toBe(0) - ->and($output)->toBe( - 'Acme Console ' . (InstalledVersions::getPrettyVersion('infocyph/foundation') ?? 'dev-main'), - ); -}); +it('keeps the Infbyte CLI identity independent of the application display name', function (): void { + $fixture = createInfbyteCliFixture(); -it('falls back to infbyte when the environment application name is empty', function (): void { - $root = dirname(__DIR__, 2); - [$exitCode, $output] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - '--version', - ], ['APP_NAME' => '']); - - expect($exitCode)->toBe(0) - ->and($output)->toBe( - 'infbyte ' . (InstalledVersions::getPrettyVersion('infocyph/foundation') ?? 'dev-main'), - ); + try { + [$exitCode, $output] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + '--version', + ], ['APP_NAME' => 'Acme Console']); + + expect($exitCode)->toBe(0) + ->and($output)->toBe( + 'Infbyte ' . (InstalledVersions::getPrettyVersion('infocyph/foundation') ?? 'dev-main'), + ); + } finally { + removeInfbyteTestDirectory($fixture); + } }); -it('builds and clears route cache through the infbyte cli wrapper', function (): void { - $root = dirname(__DIR__, 2); - $cacheFile = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) - . '/infbyte-route-cache-' . bin2hex(random_bytes(5)) . '.php'; - - [$buildExitCode, $buildOutput] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'route:cache', - '--matcher=fused', - '--cache=' . $cacheFile, - ]); +it('builds, consumes, and clears route cache through the infbyte cli wrapper', function (): void { + $fixture = createInfbyteCliFixture(); + $cacheFile = $fixture . '/bootstrap/cache/routes/fused.php'; - expect($buildExitCode)->toBe(0); - expect($buildOutput)->toContain('Route cache ready at:'); - expect(is_file($cacheFile))->toBeTrue(); - expect(filesize($cacheFile))->toBeGreaterThan(0); - - $matcher = FusedMatcher::make()->enableCache($cacheFile); - [$cachedRoute] = $matcher->match('GET', 'localhost', '/json'); - $cachedHandler = $cachedRoute->getHandler(); - $webrickVersion = InstalledVersions::getVersion('infocyph/webrick') ?? '0.0.0'; - $usesNativeHandler = version_compare($webrickVersion, '3.3.0', '>='); - if ($usesNativeHandler) { - expect($cachedHandler)->toBe([SystemController::class, 'json']) - ->and(class_exists(\Opis\Closure\Serializer::class, false))->toBeFalse(); - } else { - expect($cachedHandler)->toBeInstanceOf(Closure::class); - } + try { + [$buildExitCode, $buildOutput] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'route:cache', + ]); - $runtime = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) - . '/infbyte-cached-runtime-' . bin2hex(random_bytes(5)); - $runtimeCache = $runtime . '/bootstrap/cache/routes/fused.php'; - mkdir(dirname($runtimeCache), 0775, true); - mkdir($runtime . '/routes', 0775, true); - copy($cacheFile, $runtimeCache); - file_put_contents( - $runtime . '/routes/missing.php', - "toBe(0) + ->and($buildOutput)->toContain('Routes cached using fused matcher at ') + ->and($cacheFile)->toBeFile() + ->and(filesize($cacheFile))->toBeGreaterThan(0); + + $matcher = FusedMatcher::make()->enableCache($cacheFile); + [$cachedRoute] = $matcher->match('GET', 'localhost', '/json'); + + expect($cachedRoute->getHandler())->toBe([SystemController::class, 'json']); + + file_put_contents( + $fixture . '/routes/api.php', + " $runtime, + 'base_path' => $fixture, '_config_cache' => false, 'router' => [ 'cache' => true, - 'files' => ['missing.php'], + 'files' => ['api.php'], 'matcher' => 'fused', ], ]); - $repository = $app->container()->getRepository(); $response = $app->handle(Request::fake(method: 'GET', uri: 'http://localhost/json')); + $payload = json_decode((string) $response->getBody(), true, flags: JSON_THROW_ON_ERROR); expect($response->getStatusCode())->toBe(200) - ->and((string) $response->getBody())->toContain('memory') - ->and($repository->hasResolvedSingleton(AuthManager::class))->toBeFalse() - ->and($repository->hasResolvedSingleton(SessionManager::class))->toBeFalse() - ->and($repository->hasResolvedSingleton(DatabaseManager::class))->toBeFalse() - ->and($repository->hasResolvedSingleton(MessagingManager::class))->toBeFalse() - ->and(get_included_files())->not->toContain($runtime . '/routes/missing.php'); + ->and($payload)->toHaveKey('memory') + ->and($payload['memory'])->toBeInt() + ->and(get_included_files())->not->toContain($fixture . '/routes/api.php'); + + [$clearExitCode, $clearOutput] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'route:clear', + ]); + + expect($clearExitCode)->toBe(0) + ->and($clearOutput)->toContain('Route cache cleared.') + ->and($cacheFile)->not->toBeFile(); } finally { - if (isset($app)) { - $app->container()->unset(); - } - unlink($runtimeCache); - rmdir(dirname($runtimeCache)); - rmdir(dirname(dirname($runtimeCache))); - rmdir(dirname(dirname(dirname($runtimeCache)))); - unlink($runtime . '/routes/missing.php'); - rmdir($runtime . '/routes'); - rmdir($runtime); + removeInfbyteTestDirectory($fixture); } - - [$clearExitCode, $clearOutput] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'route:clear', - '--matcher=fused', - '--cache=' . $cacheFile, - ]); - - expect($clearExitCode)->toBe(0); - expect($clearOutput)->toContain('Route cache cleared:'); - expect(file_exists($cacheFile))->toBeFalse(); }); it('derives the dedicated routes cache path by default', function (): void { @@ -138,154 +95,173 @@ expect(RouteCachePath::for($app->config()))->toBe($root . '/bootstrap/cache/routes/fused.php'); }); -it('builds and clears the default sharded config cache through the infbyte cli wrapper', function (): void { - $root = dirname(__DIR__, 2); - $cacheDirectory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) - . '/infbyte-config-cache-' . bin2hex(random_bytes(5)); - - [$buildExitCode, $buildOutput] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'config:cache', - '--path=' . $cacheDirectory, - ]); +it('builds and fully clears the default sharded config cache through the infbyte cli wrapper', function (): void { + $fixture = createInfbyteCliFixture(); + $cacheDirectory = $fixture . '/bootstrap/cache/config'; - expect($buildExitCode)->toBe(0); - expect($buildOutput)->toContain('Configuration cached (sharded):'); - expect($cacheDirectory . '/__manifest.php')->toBeFile() - ->and($cacheDirectory . '/app.php')->toBeFile() - ->and($cacheDirectory . '/__flat.php')->not->toBeFile() - ->and($cacheDirectory . '/__compiled.php')->not->toBeFile(); - - [$clearExitCode, $clearOutput] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'config:clear', - '--path=' . $cacheDirectory, - ]); + try { + [$buildExitCode, $buildOutput] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'config:cache', + ]); - expect($clearExitCode)->toBe(0); - expect($clearOutput)->toContain('Configuration cache cleared:'); - expect($cacheDirectory . '/__manifest.php')->not->toBeFile() - ->and($cacheDirectory . '/app.php')->not->toBeFile() - ->and($cacheDirectory . '/__flat.php')->not->toBeFile(); + expect($buildExitCode)->toBe(0) + ->and($buildOutput)->toContain('Configuration cached using sharded.') + ->and($cacheDirectory . '/__manifest.php')->toBeFile() + ->and($cacheDirectory . '/app.php')->toBeFile() + ->and($cacheDirectory . '/__flat.php')->toBeFile(); - rmdir($cacheDirectory); + [$clearExitCode, $clearOutput] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'config:clear', + ]); + + expect($clearExitCode)->toBe(0) + ->and($clearOutput)->toContain('Configuration cache cleared.') + ->and($cacheDirectory)->not->toBeDirectory(); + } finally { + removeInfbyteTestDirectory($fixture); + } }); -it('can explicitly build a single config cache through the infbyte cli wrapper', function (): void { - $root = dirname(__DIR__, 2); - $cacheDirectory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) - . '/infbyte-single-config-cache-' . bin2hex(random_bytes(5)); - - [$exitCode, $output] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'config:cache', - '--type=single', - '--path=' . $cacheDirectory, - ]); +it('can select and fully clear the single config cache through application configuration', function (): void { + $fixture = createInfbyteCliFixture(); + $cacheDirectory = $fixture . '/bootstrap/cache/config'; - expect($exitCode)->toBe(0); - expect($output)->toContain('Configuration cached (single):'); - expect($cacheDirectory . '/__manifest.php')->toBeFile() - ->and($cacheDirectory . '/app.php')->not->toBeFile() - ->and($cacheDirectory . '/__flat.php')->not->toBeFile(); - - [$clearExitCode] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'config:clear', - '--path=' . $cacheDirectory, - ]); + try { + [$exitCode, $output] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'config:cache', + ], ['APP_CONFIG_CACHE_TYPE' => 'single']); + + expect($exitCode)->toBe(0) + ->and($output)->toContain('Configuration cached using single.') + ->and($cacheDirectory . '/__manifest.php')->toBeFile() + ->and($cacheDirectory . '/app.php')->not->toBeFile() + ->and($cacheDirectory . '/__flat.php')->not->toBeFile(); + + [$clearExitCode] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'config:clear', + ]); - expect($clearExitCode)->toBe(0); - rmdir($cacheDirectory); + expect($clearExitCode)->toBe(0) + ->and($cacheDirectory)->not->toBeDirectory(); + } finally { + removeInfbyteTestDirectory($fixture); + } }); it('builds and clears compiled command metadata through the infbyte cli wrapper', function (): void { - $root = dirname(__DIR__, 2); - $cacheDirectory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) - . '/infbyte-command-cache-' . bin2hex(random_bytes(5)); - $manifest = $cacheDirectory . '/commands.php'; - - [$buildExitCode, $buildOutput] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'command:cache', - '--path=' . $manifest, - ]); + $fixture = createInfbyteCliFixture(); + $manifest = $fixture . '/bootstrap/cache/commands.php'; - expect($buildExitCode)->toBe(0); - expect($buildOutput)->toContain('Command manifest ready at:'); - expect($manifest)->toBeFile(); - expect(glob($cacheDirectory . '/commands-*.php') ?: [])->not->toBeEmpty(); - expect($manifest . '.d')->not->toBeDirectory(); - - [$clearExitCode, $clearOutput] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'command:clear', - '--path=' . $manifest, - ]); - - expect($clearExitCode)->toBe(0); - expect($clearOutput)->toContain('Command manifest cleared:'); - expect($manifest)->not->toBeFile(); - expect(glob($cacheDirectory . '/commands-*.php') ?: [])->toBeEmpty(); - expect($manifest . '.d')->not->toBeDirectory(); + try { + [$buildExitCode, $buildOutput] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'command:cache', + ]); - rmdir($cacheDirectory); -}); + expect($buildExitCode)->toBe(0) + ->and($buildOutput)->toContain('Command manifest cached: ') + ->and($manifest)->toBeFile() + ->and(glob($fixture . '/bootstrap/cache/.commands-*') ?: [])->toBeEmpty(); -it('reports readiness and optional module state through the infbyte cli', function (): void { - $root = dirname(__DIR__, 2); + [$clearExitCode, $clearOutput] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'command:clear', + ]); - [$readinessExitCode, $readinessOutput] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'app:ready', - '--json=1', - ]); - [$modulesExitCode, $modulesOutput] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'module:list', - '--json=true', - ]); + expect($clearExitCode)->toBe(0) + ->and($clearOutput)->toContain('Command manifest cleared.') + ->and($manifest)->not->toBeFile(); + } finally { + removeInfbyteTestDirectory($fixture); + } +}); - expect($readinessExitCode)->toBe(2); - expect(json_decode($readinessOutput, true, flags: JSON_THROW_ON_ERROR)['production_ready'])->toBeFalse(); - expect($modulesExitCode)->toBe(0); +it('reports readiness and canonical module state through the infbyte cli', function (): void { + $fixture = createInfbyteCliFixture(); - $modules = array_column( - json_decode($modulesOutput, true, flags: JSON_THROW_ON_ERROR)['modules'], - null, - 'name', - ); + try { + [$readinessExitCode, $readinessOutput] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'app:ready', + '--json=1', + ]); + [$modulesExitCode, $modulesOutput] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'module:list', + '--json=true', + ]); - expect($modules['db']['installed'])->toBeFalse() - ->and($modules['cache']['installed'])->toBeFalse() - ->and($modules['filesystem']['installed'])->toBeFalse() - ->and($modules['logging']['installed'])->toBeTrue() - ->and($modules['messaging']['installed'])->toBeTrue() - ->and($modules['resources']['installed'])->toBeTrue() - ->and($modules['session']['installed'])->toBeTrue(); + $readiness = json_decode($readinessOutput, true, flags: JSON_THROW_ON_ERROR); + $moduleRows = json_decode($modulesOutput, true, flags: JSON_THROW_ON_ERROR); + + expect($readinessExitCode)->toBe(1) + ->and($readiness['ready'])->toBeFalse() + ->and($readiness)->toHaveKey('checks') + ->and($modulesExitCode)->toBe(0) + ->and($moduleRows)->toBeArray(); + + $modules = array_column($moduleRows, null, 'name'); + + expect($modules)->toHaveKeys([ + 'auth', + 'cache', + 'communication', + 'database', + 'filesystem', + 'logging', + 'messaging', + 'operations', + 'resources', + 'security', + 'session', + 'validation', + ])->not->toHaveKey('db') + ->and($modules['database']['packages']['infocyph/dblayer']['installed'])->toBeFalse() + ->and($modules['database']['packages']['infocyph/dblayer']['constraint'])->toBe('^5.0'); + + foreach (['logging', 'operations', 'resources', 'session'] as $builtIn) { + expect($modules[$builtIn]['installed'])->toBeTrue(); + } + } finally { + removeInfbyteTestDirectory($fixture); + } }); -it('explains how to install a service owned by an absent optional module', function (): void { - $root = dirname(__DIR__, 2); +it('reports canonical database installation guidance through module schema metadata', function (): void { + $fixture = createInfbyteCliFixture(); - [$exitCode, $output] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'auth:schema:status', - '--json=1', - ]); + try { + [$exitCode, $output] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'module:show', + 'auth', + '--json=1', + ]); - expect($exitCode)->toBe(2) - ->and($output)->toContain('requires infocyph/dblayer') - ->and($output)->toContain('php infbyte module:install db'); + $module = json_decode($output, true, flags: JSON_THROW_ON_ERROR); + $authSchema = $module['schema_status'][0] ?? null; + + expect($exitCode)->toBe(0) + ->and($module['name'])->toBe('auth') + ->and($authSchema)->toBeArray() + ->and($authSchema['state'])->toBe('unavailable') + ->and($authSchema['detail'])->toContain('php infbyte module:install database'); + } finally { + removeInfbyteTestDirectory($fixture); + } }); /** @@ -316,3 +292,82 @@ function runInfbyteCommand(array $arguments, array $environment = []): array return [$exitCode, implode("\n", $output)]; } + +function createInfbyteCliFixture(): string +{ + $root = dirname(__DIR__, 2); + $fixture = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) + . '/infbyte-cli-' . bin2hex(random_bytes(6)); + + foreach ([ + $fixture, + $fixture . '/bootstrap/cache', + $fixture . '/storage/cache', + $fixture . '/storage/logs', + $fixture . '/storage/sessions', + $fixture . '/storage/uploads', + $fixture . '/vendor', + ] as $directory) { + if (!mkdir($directory, 0775, true) && !is_dir($directory)) { + throw new RuntimeException(sprintf('Unable to create CLI fixture directory "%s".', $directory)); + } + } + + copy($root . '/infbyte', $fixture . '/infbyte'); + copy($root . '/composer.json', $fixture . '/composer.json'); + copyInfbyteTestDirectory($root . '/config', $fixture . '/config'); + copyInfbyteTestDirectory($root . '/routes', $fixture . '/routes'); + copy($root . '/bootstrap/providers.php', $fixture . '/bootstrap/providers.php'); + file_put_contents( + $fixture . '/vendor/autoload.php', + 'isDot()) { + continue; + } + + $target = $destination . DIRECTORY_SEPARATOR . $entry->getFilename(); + if ($entry->isDir()) { + copyInfbyteTestDirectory($entry->getPathname(), $target); + continue; + } + + copy($entry->getPathname(), $target); + } +} + +function removeInfbyteTestDirectory(string $directory): void +{ + if (!is_dir($directory)) { + return; + } + + $entries = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST, + ); + + foreach ($entries as $entry) { + $path = $entry->getPathname(); + + if ($entry->isLink() || $entry->isFile()) { + unlink($path); + continue; + } + + rmdir($path); + } + + rmdir($directory); +} diff --git a/tests/Feature/SkeletonDistributionTest.php b/tests/Feature/SkeletonDistributionTest.php index 5da994b..42c53c6 100644 --- a/tests/Feature/SkeletonDistributionTest.php +++ b/tests/Feature/SkeletonDistributionTest.php @@ -13,7 +13,6 @@ expect($config)->toBe([ 'app.php', 'auth.php', - 'ids.php', 'router.php', ]); }); @@ -26,7 +25,12 @@ ->and($attributes)->not->toContain("/tests export-ignore\n") ->and($attributes)->toContain('/tests/Feature/FrameworkRuntimeTest.php export-ignore') ->and($attributes)->toContain('/tests/Feature/RouteCacheCliTest.php export-ignore') - ->and($attributes)->toContain('/tests/Feature/SkeletonDistributionTest.php export-ignore'); + ->and($attributes)->toContain('/tests/Feature/SkeletonDistributionTest.php export-ignore') + ->and($attributes)->toContain('/bootstrap/cache/*.php export-ignore') + ->and($attributes)->toContain('/bootstrap/cache/config export-ignore') + ->and($attributes)->toContain('/bootstrap/cache/console export-ignore') + ->and($attributes)->toContain('/bootstrap/cache/container export-ignore') + ->and($attributes)->toContain('/bootstrap/cache/routes export-ignore'); }); it('builds a clean create-project archive and provisions its environment', function (): void { @@ -79,9 +83,10 @@ expect($project . '/composer.json')->toBeFile() ->and($project . '/bootstrap/install.php')->not->toBeFile() - ->and($project . '/bootstrap/cache/config/.gitignore')->toBeFile() - ->and($project . '/bootstrap/cache/console/.gitignore')->toBeFile() - ->and($project . '/bootstrap/cache/routes/.gitignore')->toBeFile() + ->and($project . '/bootstrap/cache/.gitignore')->toBeFile() + ->and($project . '/bootstrap/cache/config')->not->toBeDirectory() + ->and($project . '/bootstrap/cache/container')->not->toBeDirectory() + ->and($project . '/bootstrap/cache/routes')->not->toBeDirectory() ->and($project . '/storage/cache/.gitignore')->toBeFile() ->and($project . '/storage/logs/.gitignore')->toBeFile() ->and($project . '/storage/sessions/.gitignore')->toBeFile() @@ -89,6 +94,7 @@ ->and($project . '/.env.example')->toBeFile() ->and($project . '/.gitattributes')->not->toBeFile() ->and($project . '/.github')->not->toBeDirectory() + ->and($project . '/.phpforge-report')->not->toBeDirectory() ->and($project . '/captainhook.json')->not->toBeFile() ->and($project . '/composer.lock')->not->toBeFile() ->and($project . '/CODE_OF_CONDUCT.md')->not->toBeFile() @@ -102,7 +108,6 @@ ->and($project . '/tests/Feature/RouteCacheCliTest.php')->not->toBeFile() ->and($project . '/tests/Feature/SkeletonDistributionTest.php')->not->toBeFile() ->and($project . '/vendor')->not->toBeDirectory() - ->and($project . '/bootstrap/cache/config/__manifest.php')->not->toBeFile() ->and($project . '/database')->not->toBeDirectory() ->and($cachedPhpFiles)->toBe([]) ->and($exportedComposer['require']['infocyph/foundation'] ?? null) @@ -151,8 +156,13 @@ expect($deployExitCode)->toBe(0, implode("\n", $deployOutput)) ->and($project . '/bootstrap/cache/config/__manifest.php')->toBeFile() ->and($project . '/bootstrap/cache/routes/fused.php')->toBeFile() - ->and($project . '/bootstrap/cache/console/commands.php')->toBeFile() - ->and($project . '/bootstrap/cache/modules.php')->toBeFile(); + ->and($project . '/bootstrap/cache/commands.php')->toBeFile() + ->and($project . '/bootstrap/cache/schedule.php')->toBeFile() + ->and($project . '/bootstrap/cache/optimize.php')->toBeFile() + ->and($project . '/bootstrap/cache/container/web.php')->toBeFile() + ->and($project . '/bootstrap/cache/container/cli.php')->toBeFile() + ->and($project . '/bootstrap/cache/container/worker.php')->toBeFile() + ->and($project . '/bootstrap/cache/container/scheduler.php')->toBeFile(); $clearOutput = []; $clearExitCode = 0; @@ -163,10 +173,15 @@ ), $clearOutput, $clearExitCode); expect($clearExitCode)->toBe(0, implode("\n", $clearOutput)) - ->and($project . '/bootstrap/cache/config/__manifest.php')->not->toBeFile() + ->and($project . '/bootstrap/cache/config')->not->toBeDirectory() ->and($project . '/bootstrap/cache/routes/fused.php')->not->toBeFile() - ->and($project . '/bootstrap/cache/console/commands.php')->not->toBeFile() - ->and($project . '/bootstrap/cache/modules.php')->not->toBeFile(); + ->and($project . '/bootstrap/cache/commands.php')->not->toBeFile() + ->and($project . '/bootstrap/cache/schedule.php')->not->toBeFile() + ->and($project . '/bootstrap/cache/optimize.php')->not->toBeFile() + ->and($project . '/bootstrap/cache/container/web.php')->not->toBeFile() + ->and($project . '/bootstrap/cache/container/cli.php')->not->toBeFile() + ->and($project . '/bootstrap/cache/container/worker.php')->not->toBeFile() + ->and($project . '/bootstrap/cache/container/scheduler.php')->not->toBeFile(); } finally { removeInfbyteDistributionFixture($fixture); } @@ -184,7 +199,7 @@ function removeInfbyteDistributionFixture(string $directory): void ); foreach ($files as $file) { - if ($file->isDir()) { + if ($file->isDir() && !$file->isLink()) { rmdir($file->getPathname()); } else { unlink($file->getPathname());