diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..8a5f881 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,70 @@ +name: Bug report +description: Report a reproducible problem +title: "[Bug]: " +labels: + - bug +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug. Please include enough detail to reproduce it. + - type: textarea + id: summary + attributes: + label: Summary + description: What is wrong? + placeholder: Clear and short description of the bug. + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Steps to reproduce + description: Share exact commands, config, and steps. + placeholder: | + 1. Run `composer ic:tests` + 2. ... + 3. Observe ... + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + placeholder: What did you expect to happen? + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + placeholder: What happened instead? Include full error output if possible. + validations: + required: true + - type: input + id: php_version + attributes: + label: PHP version + placeholder: "e.g. 8.3.8" + validations: + required: true + - type: input + id: composer_version + attributes: + label: Composer version + placeholder: "e.g. 2.9.2" + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment details + description: OS, CI provider, shell, and anything else relevant. + placeholder: Ubuntu 24.04, GitHub Actions, bash... + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional context + description: Links, screenshots, logs, or related issues. diff --git a/.github/ISSUE_TEMPLATE/ci_failure.yml b/.github/ISSUE_TEMPLATE/ci_failure.yml new file mode 100644 index 0000000..3dcbac9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ci_failure.yml @@ -0,0 +1,48 @@ +name: CI failure +description: Report a reproducible CI or workflow failure +title: "[CI]: " +labels: + - ci +body: + - type: markdown + attributes: + value: | + Use this form when CI fails unexpectedly and can be reproduced. + - type: input + id: workflow + attributes: + label: Workflow/job name + placeholder: security-standards / phpforge + validations: + required: true + - type: input + id: run_url + attributes: + label: Failing run URL + placeholder: https://github.com/OWNER/REPOSITORY/actions/runs/... + validations: + required: true + - type: textarea + id: command + attributes: + label: Failing command + description: Exact command or step that failed. + placeholder: composer ic:ci + validations: + required: true + - type: textarea + id: logs + attributes: + label: Error output + description: Paste the relevant error section. + render: shell + validations: + required: true + - type: textarea + id: local_check + attributes: + label: Local reproduction + description: Can you reproduce locally? If yes, include steps. + placeholder: Yes/No + details + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/docs_improvement.yml b/.github/ISSUE_TEMPLATE/docs_improvement.yml new file mode 100644 index 0000000..80b9607 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/docs_improvement.yml @@ -0,0 +1,34 @@ +name: Docs improvement +description: Report missing, unclear, or incorrect documentation +title: "[Docs]: " +labels: + - documentation +body: + - type: textarea + id: location + attributes: + label: Documentation location + description: File path or URL. + placeholder: README.md section "Quick Start" + validations: + required: true + - type: textarea + id: issue + attributes: + label: What is unclear or incorrect? + placeholder: This section says... + validations: + required: true + - type: textarea + id: suggestion + attributes: + label: Suggested improvement + description: Propose revised wording, structure, or examples. + placeholder: It would be clearer if... + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional context + description: Related links, screenshots, or prior discussions. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..cc29614 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,44 @@ +name: Feature request +description: Suggest an improvement or new capability +title: "[Feature]: " +labels: + - enhancement +body: + - type: markdown + attributes: + value: | + Thanks for the idea. Please describe the use case first, then the proposed solution. + - type: textarea + id: problem + attributes: + label: Problem or use case + description: What limitation are you hitting? + placeholder: I need to... + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: What should happen? + placeholder: Add a command/config/workflow option that... + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Any workaround or alternative approach you evaluated. + - type: textarea + id: impact + attributes: + label: Expected impact + description: Who benefits and what changes for users/CI? + placeholder: This would improve... + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional context + description: Related issues, links, examples, or prior art. diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000..2ca776f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,40 @@ +name: Question +description: Ask a usage or integration question +title: "[Question]: " +labels: + - question +body: + - type: markdown + attributes: + value: | + Use this form for usage questions. For confirmed defects, use the bug report form. + - type: textarea + id: context + attributes: + label: What are you trying to do? + description: Describe your goal and expected outcome. + placeholder: I want to... + validations: + required: true + - type: textarea + id: attempted + attributes: + label: What have you tried? + description: Include commands, config snippets, or links you already checked. + placeholder: I tried... + validations: + required: true + - type: textarea + id: output + attributes: + label: Current output or behavior + description: Include relevant command output, logs, or errors. + render: shell + - type: textarea + id: environment + attributes: + label: Environment details + description: PHP version, Composer version, OS, CI provider (if relevant). + placeholder: PHP 8.3, Composer 2.9, Ubuntu 24.04... + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/regression_report.yml b/.github/ISSUE_TEMPLATE/regression_report.yml new file mode 100644 index 0000000..36392bc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/regression_report.yml @@ -0,0 +1,51 @@ +name: Regression report +description: Report behavior that previously worked but now fails +title: "[Regression]: " +labels: + - regression + - bug +body: + - type: textarea + id: summary + attributes: + label: Regression summary + placeholder: This worked before, but now... + validations: + required: true + - type: input + id: last_known_good + attributes: + label: Last known working version/commit + placeholder: v1.2.3 or abc1234 + validations: + required: true + - type: input + id: first_bad + attributes: + label: First broken version/commit + placeholder: v1.2.4 or def5678 + - type: textarea + id: reproduce + attributes: + label: Steps to reproduce + placeholder: | + 1. ... + 2. ... + 3. ... + validations: + required: true + - type: textarea + id: expected_actual + attributes: + label: Expected vs actual behavior + placeholder: Expected ..., but got ... + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment details + description: PHP version, Composer version, OS, CI provider (if relevant). + placeholder: PHP 8.3, Composer 2.9, Ubuntu 24.04... + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..59ae734 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,33 @@ +## Summary + +Describe what changed and why. + +## Related Issues + +Link issues with `Closes #...` or `Relates #...`. + +## Type of Change + +- [ ] Bug fix +- [ ] New feature +- [ ] Refactor +- [ ] Documentation update +- [ ] CI or tooling update +- [ ] Other (describe in summary) + +## Validation + +List the commands you ran and their result. + +```bash +composer ic:tests +``` + +If full suite was not run, explain why and list focused checks. + +## Checklist + +- [ ] I followed `CONTRIBUTING.md`. +- [ ] I added or updated tests for behavior changes. +- [ ] I updated docs/config/examples when needed. +- [ ] I confirmed no security-sensitive data is exposed. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 9edb388..9c2638f 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,65 +1,50 @@ # Code of Conduct -## Our Pledge +## Our Commitment -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to make participation in this project and -our community a harassment-free experience for everyone, regardless of age, -body size, disability, ethnicity, sex characteristics, gender identity and -expression, level of experience, education, socio-economic status, nationality, -personal appearance, race, religion, or sexual identity and orientation. +We are committed to making participation in this project a harassment-free +experience for everyone, regardless of age, body size, disability, ethnicity, +gender identity and expression, level of experience, nationality, personal +appearance, race, religion or sexual identity and orientation. -## Our Standards +## Expected Behavior -Examples of behavior that contributes to a positive environment include: +Examples of behavior that contributes to a positive environment: -- Using welcoming and inclusive language. -- Being respectful of differing viewpoints and experiences. -- Gracefully accepting constructive criticism. -- Focusing on what is best for the community. -- Showing empathy toward other community members. +- Be respectful and constructive. +- Assume good intent and ask clarifying questions. +- Give and receive feedback professionally. +- Focus on what is best for the community and project. -Examples of unacceptable behavior include: +## Unacceptable Behavior -- The use of sexualized language or imagery and unwelcome sexual attention or - advances. -- Trolling, insulting or derogatory comments, and personal or political attacks. -- Public or private harassment. -- Publishing others' private information, such as a physical or electronic - address, without explicit permission. -- Other conduct which could reasonably be considered inappropriate in a - professional setting. +Examples of unacceptable behavior include: -## Our Responsibilities +- Harassment, discrimination or personal attacks. +- Trolling, insulting or derogatory comments. +- Publishing private information without consent. +- Any conduct that is inappropriate in a professional setting. -Project maintainers are responsible for clarifying standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +## Enforcement Responsibilities -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned with this Code of Conduct. +Project maintainers are responsible for clarifying and enforcing this code of +conduct. They may remove, edit or reject comments, commits, code, issues, and +other contributions that violate this policy. ## Scope -This Code of Conduct applies within all project spaces, and it also applies -when an individual is representing the project or its community in public -spaces. Examples of representing a project or community include using an -official project email address, posting via an official social media account, -or acting as an appointed representative at an online or offline event. +This code of conduct applies in all project spaces, including: -## Enforcement +- Issue trackers +- Pull requests +- Discussions and chat related to the project +- Any public or private communication where someone represents the project -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the project maintainers by opening a private security report or by -contacting the maintainer email listed in `composer.json`. +## Reporting -All complaints will be reviewed and investigated and will result in a response -that is deemed necessary and appropriate to the circumstances. Project -maintainers are obligated to maintain confidentiality with regard to the -reporter of an incident. +To report unacceptable behavior, contact project maintainers privately. -## Attribution +## Enforcement -This Code of Conduct is adapted from the Contributor Covenant, version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct/ +Maintainers may take any action they deem appropriate, including warnings, +temporary bans or permanent bans from community participation. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9950065 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,68 @@ +# Contributing + +Thanks for contributing. + +## Before You Start + +- Review the project code of conduct. +- For security issues, use private reporting and avoid opening a public issue. +- Check existing issues and pull requests first to avoid duplicates. + +## Local Setup + +Requirements: + +- See `README.md` for current PHP and Composer requirements. + +Install dependencies: + +```bash +composer install +``` + +## Development Workflow + +Typical contributor workflow: + +1. Create a branch from `main`. +2. Make focused changes. +3. Run quality checks locally. +4. Open a pull request with context and verification notes. + +Recommended checks: + +```bash +composer ic:tests +``` + +Useful targeted commands: + +```bash +composer ic:test:syntax +composer ic:test:code +composer ic:test:lint +composer ic:test:sniff +composer ic:test:static +composer ic:test:security +composer ic:test:architecture +``` + +Auto-fix and processing helpers: + +```bash +composer ic:process +``` + +## Pull Request Guidelines + +- Keep pull requests scoped to one logical change. +- Include why the change is needed and what behavior changed. +- Add or update tests when behavior changes. +- Update docs when command behavior, config, or workflow behavior changes. +- Ensure CI is green before requesting review. + +## Reporting Bugs and Requesting Features + +- Use issue templates for bugs, regressions, CI failures, documentation updates, questions, and feature requests. +- Include reproducible steps, expected behavior, and actual behavior. +- Share environment details (PHP version, OS, Composer version). diff --git a/README.md b/README.md index 037bc08..06bbd66 100644 --- a/README.md +++ b/README.md @@ -117,12 +117,6 @@ You can also set: - `CACHELAYER_PAYLOAD_INTEGRITY_KEY` - `CACHELAYER_MAX_PAYLOAD_BYTES` -See `SECURITY.md` for deployment guidance and threat model notes. - -## Documentation - -https://docs.infocyph.com/projects/CacheLayer - ## Testing ```bash @@ -135,15 +129,18 @@ Or run the full test pipeline: composer test:all ``` -## Contributing - -Contributions are welcome. +## Security -- Open an issue for bug reports or feature discussions -- Open a pull request with focused changes and tests -- Keep coding style and static checks passing before submitting -- Follow the [Code of Conduct](CODE_OF_CONDUCT.md) +Protected by [PHPForge](https://github.com/infocyph/PHPForge) — an automated quality and security gate for PHP projects. -## License +--- -MIT License. See [LICENSE](LICENSE). +
+ Made with ❤️ for the PHP community
+ MIT Licensed
+ Documentation • + Security • + Code of Conduct • + Contributing • + Report | Request | Suggest +
diff --git a/SECURITY.md b/SECURITY.md index e742ad1..37a355e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,101 +1,49 @@ -# Security Guide +# Security Policy -This document captures CacheLayer hardening guidance and rollout options. +## Supported Versions -## Threat Model +The project currently supports security updates for the latest release. -CacheLayer stores serialized payloads in backends that may be writable by local -or network-adjacent actors if infrastructure is misconfigured. Main risks: +## Reporting a Vulnerability -- Deserialization abuse when payloads are tampered. -- Executable cache-file abuse in `phpFiles` adapter. -- Insecure default temp-directory usage in shared environments. +Please report vulnerabilities privately. -## Implemented Hardening +1. Use GitHub private vulnerability reporting for this repository (`Security` -> `Advisories` -> `Report a vulnerability`). +2. If private reporting is unavailable, contact maintainers through a private channel. +3. Do not open a public issue for security vulnerabilities. -### 1) Serialization and Payload Hardening +Please include: -- `CachePayloadCodec` supports signed payloads (HMAC-SHA256). -- Signed payloads are rejected when integrity verification fails. -- When an integrity key is configured, unsigned payloads are rejected. -- Maximum payload size can be enforced at decode time. -- `ValueSerializer` supports strict mode: - - block closure payloads - - block object payloads -- Native scalar/array serialization paths now decode with - `allowed_classes => false`. +- Affected package version(s) +- PHP version and runtime environment +- Reproduction steps or proof of concept +- Impact assessment (confidentiality/integrity/availability) +- Any known workaround -### Runtime API +## Response Process -```php -$cache - ->configurePayloadSecurity( - integrityKey: 'replace-with-strong-secret', - maxPayloadBytes: 8_388_608, - ) - ->configureSerializationSecurity( - allowClosurePayloads: false, - allowObjectPayloads: false, - ); -``` +- Initial acknowledgment: best effort, typically within a few days +- Triage: best effort, based on maintainer availability +- Fix and release timeline depends on severity and exploitability -### Environment Variables +If a report is accepted, a patched release will be prepared and published. Credit will be provided unless you request otherwise. -- `CACHELAYER_PAYLOAD_INTEGRITY_KEY` -- `CACHELAYER_MAX_PAYLOAD_BYTES` +## Protected by PHPForge -### 2) `phpFiles` Adapter Guardrails +This project is protected by [PHPForge](https://github.com/infocyph/PHPForge), an automated quality and security tooling layer for Infocyph PHP projects. -`phpFiles` keeps executable `.php` cache files for performance, so strict -directory controls are required. Runtime checks now reject: +PHPForge helps keep the project reliable by running checks for: -- symlinked cache directories -- world-writable cache directories +- Code style and standards +- Tests and syntax validation +- Static analysis and type safety +- Security and taint analysis +- Dependency vulnerability audit +- Architecture boundary validation +- Duplicate-code detection +- API snapshot and comment-policy checks +- Refactor safety checks +- Benchmark and release-readiness checks +- Git hooks and CI workflow protection -Use `phpFiles` only on trusted hosts and private directories. - -### 3) Temp-Directory Hardening - -Default filesystem locations are now scoped under dedicated cachelayer temp -subdirectories: - -- file adapter default base: `sys_get_temp_dir()/cachelayer/files` -- php-files adapter default base: `sys_get_temp_dir()/cachelayer/phpfiles` -- PDO SQLite default: `sys_get_temp_dir()/cachelayer/pdo/cache_.sqlite` - -These paths are created with restrictive permissions and world-writable checks. - -## Recommended Production Profile - -1. Set `CACHELAYER_PAYLOAD_INTEGRITY_KEY` to a strong random secret. -2. Disable closure/object payloads unless explicitly required. -3. Use explicit, private cache directories outside shared temp space. -4. Prefer non-executable file storage adapters over `phpFiles` where possible. - -## Backend-Specific Notes - -### Redis / Valkey - -- Require authentication and network-level access controls. -- Prefer TLS-enabled connections when crossing host boundaries. -- Avoid exposing Redis/Valkey ports directly to public networks. - -### MongoDB / ScyllaDB / SQL Backends - -- Use least-privilege database credentials scoped to cache tables/collections. -- Enforce transport security (TLS) where supported. -- Keep cache schema/table permissions separate from application primary data. - -### Tiered Cache Deployments (L1/L2/DB) - -For `Cache::tiered()` production setups: - -- keep L1 (APCu) local-process only -- protect L2 (Redis/Valkey) as a private service -- treat DB fallback resolvers as trusted code paths only -- configure bounded TTLs to reduce stale or poisoned cache lifetime - -## Disclosure - -If you discover a security issue, please open a private report to project -maintainers before public disclosure. +These automated gates strengthen code quality, reduce security risk and help prevent regressions before merge or release. diff --git a/docs/security.rst b/docs/security.rst index 9da3fd1..556928b 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -1,13 +1,40 @@ .. _security: -======== -Security -======== +============== +Security Guide +============== -CacheLayer includes optional hardening controls for payload integrity, -serialization policy, and filesystem defaults. +This document captures CacheLayer hardening guidance and rollout options. -Quick hardening setup: +Threat Model +------------ + +CacheLayer stores serialized payloads in backends that may be writable by local +or network-adjacent actors if infrastructure is misconfigured. Main risks: + +* Deserialization abuse when payloads are tampered. +* Executable cache-file abuse in ``phpFiles`` adapter. +* Insecure default temp-directory usage in shared environments. + +Implemented Hardening +--------------------- + +1) Serialization and Payload Hardening +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ``CachePayloadCodec`` supports signed payloads (HMAC-SHA256). +* Signed payloads are rejected when integrity verification fails. +* When an integrity key is configured, unsigned payloads are rejected. +* Maximum payload size can be enforced at decode time. +* ``ValueSerializer`` supports strict mode: + + * block closure payloads + * block object payloads + +* Native scalar/array serialization paths now decode with + ``allowed_classes => false``. + +Runtime API: .. code-block:: php @@ -21,9 +48,73 @@ Quick hardening setup: allowObjectPayloads: false, ); -Environment variables: +Environment Variables: * ``CACHELAYER_PAYLOAD_INTEGRITY_KEY`` * ``CACHELAYER_MAX_PAYLOAD_BYTES`` -For detailed policy and rollout guidance, see project root ``SECURITY.md``. +2) ``phpFiles`` Adapter Guardrails +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``phpFiles`` keeps executable ``.php`` cache files for performance, so strict +directory controls are required. Runtime checks now reject: + +* symlinked cache directories +* world-writable cache directories + +Use ``phpFiles`` only on trusted hosts and private directories. + +3) Temp-Directory Hardening +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Default filesystem locations are now scoped under dedicated cachelayer temp +subdirectories: + +* file adapter default base: ``sys_get_temp_dir()/cachelayer/files`` +* php-files adapter default base: ``sys_get_temp_dir()/cachelayer/phpfiles`` +* PDO SQLite default: ``sys_get_temp_dir()/cachelayer/pdo/cache_.sqlite`` + +These paths are created with restrictive permissions and world-writable checks. + +Recommended Production Profile +------------------------------ + +1. Set ``CACHELAYER_PAYLOAD_INTEGRITY_KEY`` to a strong random secret. +2. Disable closure/object payloads unless explicitly required. +3. Use explicit, private cache directories outside shared temp space. +4. Prefer non-executable file storage adapters over ``phpFiles`` where + possible. + +Backend-Specific Notes +---------------------- + +Redis / Valkey +~~~~~~~~~~~~~~ + +* Require authentication and network-level access controls. +* Prefer TLS-enabled connections when crossing host boundaries. +* Avoid exposing Redis/Valkey ports directly to public networks. + +MongoDB / ScyllaDB / SQL Backends +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* Use least-privilege database credentials scoped to cache + tables/collections. +* Enforce transport security (TLS) where supported. +* Keep cache schema/table permissions separate from application primary data. + +Tiered Cache Deployments (L1/L2/DB) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For ``Cache::tiered()`` production setups: + +* keep L1 (APCu) local-process only +* protect L2 (Redis/Valkey) as a private service +* treat DB fallback resolvers as trusted code paths only +* configure bounded TTLs to reduce stale or poisoned cache lifetime + +Disclosure +---------- + +If you discover a security issue, please open a private report to project +maintainers before public disclosure. diff --git a/src/Cache/Tiering/TieredPoolFactory.php b/src/Cache/Tiering/TieredPoolFactory.php index 35c5c86..7528901 100644 --- a/src/Cache/Tiering/TieredPoolFactory.php +++ b/src/Cache/Tiering/TieredPoolFactory.php @@ -33,18 +33,14 @@ public static function fromArray(array $tiers): array */ private static function bool(array $descriptor, string $key, bool $default): bool { - if (!array_key_exists($key, $descriptor)) { - return $default; - } - - $value = $descriptor[$key]; - if (!is_bool($value)) { - throw new CacheInvalidArgumentException( - sprintf('Tier descriptor key `%s` must be bool, got %s.', $key, get_debug_type($value)), - ); - } - - return $value; + /** @var bool */ + return self::typedValue( + $descriptor, + $key, + $default, + static fn(mixed $value): bool => is_bool($value), + 'bool', + ); } private static function buildScyllaSession(string $keyspace): object @@ -164,18 +160,14 @@ private static function float(array $descriptor, string $key, float $default): f */ private static function int(array $descriptor, string $key, int $default): int { - if (!array_key_exists($key, $descriptor)) { - return $default; - } - - $value = $descriptor[$key]; - if (!is_int($value)) { - throw new CacheInvalidArgumentException( - sprintf('Tier descriptor key `%s` must be int, got %s.', $key, get_debug_type($value)), - ); - } - - return $value; + /** @var int */ + return self::typedValue( + $descriptor, + $key, + $default, + static fn(mixed $value): bool => is_int($value), + 'int', + ); } private static function memcachedClient(mixed $client, int|string $index): ?\Memcached @@ -405,14 +397,35 @@ private static function sqliteDsn(array $descriptor): ?string */ private static function string(array $descriptor, string $key, string $default): string { + /** @var string */ + return self::typedValue( + $descriptor, + $key, + $default, + static fn(mixed $value): bool => is_string($value), + 'string', + ); + } + + /** + * @param array $descriptor + * @param callable(mixed): bool $validator + */ + private static function typedValue( + array $descriptor, + string $key, + mixed $default, + callable $validator, + string $expectedType, + ): mixed { if (!array_key_exists($key, $descriptor)) { return $default; } $value = $descriptor[$key]; - if (!is_string($value)) { + if (!$validator($value)) { throw new CacheInvalidArgumentException( - sprintf('Tier descriptor key `%s` must be string, got %s.', $key, get_debug_type($value)), + sprintf('Tier descriptor key `%s` must be %s, got %s.', $key, $expectedType, get_debug_type($value)), ); }