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());