diff --git a/.agents/agents/README.md b/.agents/agents/README.md new file mode 120000 index 0000000..bff20fe --- /dev/null +++ b/.agents/agents/README.md @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/agents/README.md \ No newline at end of file diff --git a/.agents/agents/agents-maintainer.md b/.agents/agents/agents-maintainer.md new file mode 120000 index 0000000..bdd77cd --- /dev/null +++ b/.agents/agents/agents-maintainer.md @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/agents/agents-maintainer.md \ No newline at end of file diff --git a/.agents/agents/changelog-maintainer.md b/.agents/agents/changelog-maintainer.md new file mode 120000 index 0000000..53b2a9b --- /dev/null +++ b/.agents/agents/changelog-maintainer.md @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/agents/changelog-maintainer.md \ No newline at end of file diff --git a/.agents/agents/consumer-sync-auditor.md b/.agents/agents/consumer-sync-auditor.md new file mode 120000 index 0000000..e1250e6 --- /dev/null +++ b/.agents/agents/consumer-sync-auditor.md @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/agents/consumer-sync-auditor.md \ No newline at end of file diff --git a/.agents/agents/docs-writer.md b/.agents/agents/docs-writer.md new file mode 120000 index 0000000..6c39ce3 --- /dev/null +++ b/.agents/agents/docs-writer.md @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/agents/docs-writer.md \ No newline at end of file diff --git a/.agents/agents/issue-editor.md b/.agents/agents/issue-editor.md new file mode 120000 index 0000000..f6970a6 --- /dev/null +++ b/.agents/agents/issue-editor.md @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/agents/issue-editor.md \ No newline at end of file diff --git a/.agents/agents/issue-implementer.md b/.agents/agents/issue-implementer.md new file mode 120000 index 0000000..97b4113 --- /dev/null +++ b/.agents/agents/issue-implementer.md @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/agents/issue-implementer.md \ No newline at end of file diff --git a/.agents/agents/php-style-curator.md b/.agents/agents/php-style-curator.md new file mode 120000 index 0000000..00eb5fa --- /dev/null +++ b/.agents/agents/php-style-curator.md @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/agents/php-style-curator.md \ No newline at end of file diff --git a/.agents/agents/quality-pipeline-auditor.md b/.agents/agents/quality-pipeline-auditor.md new file mode 120000 index 0000000..fe7a715 --- /dev/null +++ b/.agents/agents/quality-pipeline-auditor.md @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/agents/quality-pipeline-auditor.md \ No newline at end of file diff --git a/.agents/agents/readme-maintainer.md b/.agents/agents/readme-maintainer.md new file mode 120000 index 0000000..161d077 --- /dev/null +++ b/.agents/agents/readme-maintainer.md @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/agents/readme-maintainer.md \ No newline at end of file diff --git a/.agents/agents/review-guardian.md b/.agents/agents/review-guardian.md new file mode 120000 index 0000000..7d09397 --- /dev/null +++ b/.agents/agents/review-guardian.md @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/agents/review-guardian.md \ No newline at end of file diff --git a/.agents/agents/test-guardian.md b/.agents/agents/test-guardian.md new file mode 120000 index 0000000..41df2af --- /dev/null +++ b/.agents/agents/test-guardian.md @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/agents/test-guardian.md \ No newline at end of file diff --git a/.agents/skills/changelog-generator b/.agents/skills/changelog-generator new file mode 120000 index 0000000..2de2d6d --- /dev/null +++ b/.agents/skills/changelog-generator @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/skills/changelog-generator \ No newline at end of file diff --git a/.agents/skills/create-agentsmd b/.agents/skills/create-agentsmd new file mode 120000 index 0000000..65b4cd7 --- /dev/null +++ b/.agents/skills/create-agentsmd @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/skills/create-agentsmd \ No newline at end of file diff --git a/.agents/skills/github-issues b/.agents/skills/github-issues new file mode 120000 index 0000000..6f27b8d --- /dev/null +++ b/.agents/skills/github-issues @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/skills/github-issues \ No newline at end of file diff --git a/.agents/skills/github-pull-request b/.agents/skills/github-pull-request new file mode 120000 index 0000000..4e2dd67 --- /dev/null +++ b/.agents/skills/github-pull-request @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/skills/github-pull-request \ No newline at end of file diff --git a/.agents/skills/package-readme b/.agents/skills/package-readme new file mode 120000 index 0000000..7ef2c5d --- /dev/null +++ b/.agents/skills/package-readme @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/skills/package-readme \ No newline at end of file diff --git a/.agents/skills/phpdoc-code-style b/.agents/skills/phpdoc-code-style new file mode 120000 index 0000000..9f8c2bc --- /dev/null +++ b/.agents/skills/phpdoc-code-style @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/skills/phpdoc-code-style \ No newline at end of file diff --git a/.agents/skills/phpunit-tests b/.agents/skills/phpunit-tests new file mode 120000 index 0000000..6f26fdf --- /dev/null +++ b/.agents/skills/phpunit-tests @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/skills/phpunit-tests \ No newline at end of file diff --git a/.agents/skills/pull-request-review b/.agents/skills/pull-request-review new file mode 120000 index 0000000..c6a2098 --- /dev/null +++ b/.agents/skills/pull-request-review @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/skills/pull-request-review \ No newline at end of file diff --git a/.agents/skills/sphinx-docs b/.agents/skills/sphinx-docs new file mode 120000 index 0000000..9f573ff --- /dev/null +++ b/.agents/skills/sphinx-docs @@ -0,0 +1 @@ +../../vendor/fast-forward/dev-tools/.agents/skills/sphinx-docs \ No newline at end of file diff --git a/.docheader b/.docheader new file mode 100644 index 0000000..620be4d --- /dev/null +++ b/.docheader @@ -0,0 +1,13 @@ +/** + * Ergonomic utilities for PHP enums, including names, values, lookups, and option maps. + * + * This file is part of fast-forward/enum project. + * + * @author Felipe Sayão Lobato Abreu + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000..916c8ae --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{yml,yaml,json}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.rst] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4d40b05 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +* text=auto +/.agents/ export-ignore +/.dev-tools/ export-ignore +/.github/ export-ignore +/docs/ export-ignore +/tests/ export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.gitmodules export-ignore +/.phpunit.result.cache export-ignore +/AGENTS.md export-ignore +/phpunit.xml.dist export-ignore +/README.md export-ignore diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..986b8c3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,12 @@ +# Generated by fast-forward/dev-tools +# +# Review the generated owners before committing this file. +# +# When no GitHub owners can be inferred automatically, replace the placeholder +# example below with the usernames, teams, or emails that should review +# changes in the consumer repository. +# +# Example: +# * @your-github-user @your-org/platform-team + +* @php-fast-forward @coisa diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..03439ec --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: php-fast-forward +custom: + - 'https://www.paypal.com/donate/?business=JLDAF45XZ8D84' diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..21b628d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,23 @@ +version: 2 +updates: + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "Composer" + include: "scope" + labels: + - "composer" + - "dependencies" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "GitHub Actions" + include: "scope" + labels: + - "github-actions" + - "continuous-integration" \ No newline at end of file diff --git a/.github/wiki b/.github/wiki new file mode 160000 index 0000000..260749a --- /dev/null +++ b/.github/wiki @@ -0,0 +1 @@ +Subproject commit 260749a37590db3e0f609e3ee72dd6f511c66cff diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml new file mode 100644 index 0000000..cf1d7a4 --- /dev/null +++ b/.github/workflows/auto-assign.yml @@ -0,0 +1,22 @@ +name: Auto assign + +on: + pull_request_target: + types: [opened, reopened, synchronize] + issues: + types: [opened, reopened] + +jobs: + auto-assign: + permissions: + issues: write + repository-projects: write + pull-requests: write + uses: php-fast-forward/dev-tools/.github/workflows/auto-assign.yml@main + with: + # project: 1 + # + # Consumer repositories SHOULD set this to the target GitHub Project V2 + # number. Repositories inside php-fast-forward MAY omit it and let the + # reusable workflow default to the first organization project. + project: ${{ vars.PROJECT || '' }} diff --git a/.github/workflows/auto-resolve-conflicts.yml b/.github/workflows/auto-resolve-conflicts.yml new file mode 100644 index 0000000..0c248f6 --- /dev/null +++ b/.github/workflows/auto-resolve-conflicts.yml @@ -0,0 +1,36 @@ +name: "Fast Forward Predictable Conflict Resolution" + +on: + push: + branches: [ "main" ] + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + workflow_dispatch: + inputs: + base-ref: + description: Base branch inspected for open pull requests. + required: false + type: string + default: main + pull-request-number: + description: Optional pull request number to inspect. Leave empty to scan open pull requests targeting the base branch. + required: false + type: string + default: '' + +permissions: + actions: write + contents: write + pull-requests: write + +jobs: + auto-resolve-conflicts: + uses: php-fast-forward/dev-tools/.github/workflows/auto-resolve-conflicts.yml@main + with: + base-ref: ${{ inputs.base-ref || github.event.pull_request.base.ref || github.event.repository.default_branch || 'main' }} + pull-request-number: ${{ inputs.pull-request-number || github.event.pull_request.number || '' }} + secrets: inherit diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..2490ecb --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,47 @@ +name: "Fast Forward Changelog" + +on: + pull_request: + types: [opened, reopened, synchronize] + pull_request_target: + types: [closed] + workflow_dispatch: + inputs: + changelog-file: + description: Path to the managed changelog file. + required: false + type: string + default: CHANGELOG.md + project: + description: Optional GitHub Project V2 number used for project-board release transitions. + required: false + type: string + default: '' + version: + description: Optional version to promote. Leave empty to infer from Unreleased. + required: false + type: string + default: '' + release-branch-prefix: + description: Prefix used for release-preparation branches. + required: false + type: string + default: release/v + +permissions: + contents: write + pull-requests: write + +jobs: + changelog: + uses: php-fast-forward/dev-tools/.github/workflows/changelog.yml@main + with: + changelog-file: ${{ inputs.changelog-file || 'CHANGELOG.md' }} + # project: 1 + # + # Consumer repositories SHOULD set this when they want changelog-driven + # release transitions to update a specific GitHub Project V2 board. + project: ${{ inputs.project || vars.PROJECT || '' }} + version: ${{ inputs.version || '' }} + release-branch-prefix: ${{ inputs.release-branch-prefix || 'release/v' }} + secrets: inherit diff --git a/.github/workflows/label-sync.yml b/.github/workflows/label-sync.yml new file mode 100644 index 0000000..649aab9 --- /dev/null +++ b/.github/workflows/label-sync.yml @@ -0,0 +1,15 @@ +name: Label sync + +on: + pull_request_target: + types: [opened, reopened, synchronize] + issues: + types: [opened, reopened] + +jobs: + label-sync: + permissions: + contents: read + issues: read + pull-requests: write + uses: php-fast-forward/dev-tools/.github/workflows/label-sync.yml@main diff --git a/.github/workflows/reports.yml b/.github/workflows/reports.yml new file mode 100644 index 0000000..f0f8c32 --- /dev/null +++ b/.github/workflows/reports.yml @@ -0,0 +1,28 @@ +name: "Fast Forward Reports" + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + push: + branches: + - main + schedule: + - cron: '41 3 * * *' + workflow_dispatch: + inputs: + cleanup-previews: + description: Remove stale pull request previews without publishing production reports. + required: false + type: boolean + default: false + +permissions: + contents: write + pull-requests: write + +jobs: + reports: + uses: php-fast-forward/dev-tools/.github/workflows/reports.yml@main + secrets: inherit + with: + cleanup-previews: ${{ inputs.cleanup-previews || false }} diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml new file mode 100644 index 0000000..d8c5f9e --- /dev/null +++ b/.github/workflows/review.yml @@ -0,0 +1,20 @@ +name: Rigorous Pull Request Review + +on: + pull_request_target: + types: [ready_for_review] + workflow_dispatch: + inputs: + pull-request-number: + description: Pull request number to review manually. + required: true + type: string + +jobs: + rigorous-review: + permissions: + contents: read + pull-requests: write + uses: php-fast-forward/dev-tools/.github/workflows/review.yml@main + with: + pull-request-number: ${{ github.event.pull_request.number || inputs.pull-request-number || '' }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..d9b5d53 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,21 @@ +name: "Fast Forward Test Suite" + +on: + push: + workflow_dispatch: + inputs: + max-outdated: + description: Maximum number of outdated packages allowed by the dependencies command. + required: false + type: number + default: -1 + +permissions: + contents: read + +jobs: + tests: + uses: php-fast-forward/dev-tools/.github/workflows/tests.yml@main + with: + max-outdated: ${{ inputs.max-outdated || -1 }} + secrets: inherit diff --git a/.github/workflows/wiki-maintenance.yml b/.github/workflows/wiki-maintenance.yml new file mode 100644 index 0000000..f8f15d8 --- /dev/null +++ b/.github/workflows/wiki-maintenance.yml @@ -0,0 +1,26 @@ +name: "Fast Forward Wiki Maintenance" + +on: + pull_request_target: + types: [closed] + schedule: + - cron: '17 3 * * *' + workflow_dispatch: + +permissions: + contents: write + pull-requests: read + +concurrency: + group: fast-forward-wiki-maintenance-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + maintenance: + permissions: + contents: write + pull-requests: read + # This wrapper handles merged publication to the wiki master branch plus + # preview-branch cleanup. Pull-request preview generation lives in wiki.yml. + uses: php-fast-forward/dev-tools/.github/workflows/wiki-maintenance.yml@main + secrets: inherit diff --git a/.github/workflows/wiki.yml b/.github/workflows/wiki.yml new file mode 100644 index 0000000..d54c27a --- /dev/null +++ b/.github/workflows/wiki.yml @@ -0,0 +1,23 @@ +name: "Fast Forward Wiki Update" + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: write + pull-requests: read + +concurrency: + group: fast-forward-wiki-preview-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + preview: + permissions: + contents: write + pull-requests: read + # Pull-request wiki previews live here. Publication and preview cleanup are + # handled by the separate wiki-maintenance wrapper. + uses: php-fast-forward/dev-tools/.github/workflows/wiki-preview.yml@main + secrets: inherit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc80b4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.dev-tools/ +.idea/ +.vscode/ +backup/ +public/ +tmp/ +vendor/ +*.cache +.DS_Store +composer.lock diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9e1746a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule ".github/wiki"] + path = .github/wiki + url = https://github.com/php-fast-forward/enum.wiki.git diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3a22574 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,158 @@ +# AGENTS.md + +## Overview + +`fast-forward/enum` is a standalone PHP 8.3+ library for enum ergonomics and reusable enum catalogs in the Fast Forward ecosystem. + +The public surface is centered on: + +- `src/Trait/` for reusable enum traits such as `HasValues`, `HasNames`, `HasLabel`, `HasDescription`, and `Comparable` +- `src/Helper/EnumHelper.php` for generic static helpers over `UnitEnum` and `BackedEnum` +- root public interfaces such as `LabeledEnumInterface`, `DescribedEnumInterface`, and `ReversibleInterface` +- focused domain namespaces such as `Calendar/`, `Common/`, `Sort/`, `Logger/`, `Runtime/`, `DateTime/`, `Comparison/`, `Outcome/`, `Http/`, `Process/`, `Event/`, `Container/`, and `Pipeline/` +- `src/StateMachine/` for enum-based workflow transitions + +Keep new public API small, explicit, and framework-agnostic. Avoid vague “bucket” namespaces when a more specific domain namespace fits. + +## Setup + +Install dependencies with: + +```bash +composer install +``` + +This package uses `fast-forward/dev-tools` as a dev dependency and inherits its test and repo-maintenance workflows. + +## Reliable Commands + +Use these commands for minimum local validation: + +```bash +./vendor/bin/dev-tools tests +composer dump-autoload +``` + +The PHPUnit config comes from `vendor/fast-forward/dev-tools/phpunit.xml`, so do not recreate a local `phpunit.xml.dist` unless there is a very strong reason. + +Use broader checks when the change touches those surfaces: + +```bash +./vendor/bin/dev-tools standards +./vendor/bin/dev-tools phpdoc +./vendor/bin/dev-tools docs +./vendor/bin/dev-tools dependencies +./vendor/bin/dev-tools changelog:check +``` + +Use `./vendor/bin/dev-tools wiki` only when wiki output or wiki-related docs need to be regenerated. + +## Current Tooling Caveats + +The current `dev-tools` version in this repo has known issues: + +- `./vendor/bin/dev-tools --fix` completes, but may emit noisy Composer plugin errors related to Symfony Console command discovery. +- `composer agents` is not a reliable entrypoint in this checkout; prefer `./vendor/bin/dev-tools agents` and `./vendor/bin/dev-tools skills` when synchronizing those assets. +- `./vendor/bin/dev-tools dev-tools:sync --overwrite --no-interaction` can fail when copying Git hooks into `.git/hooks` because of permissions. + +If you need to format or refactor code, prefer small, targeted edits and re-run the test commands above. + +## Project Agents + +Packaged project-agent prompts live in `.agents/agents/`. They are synchronized from `fast-forward/dev-tools` as relative symlinks into `vendor/`, so they are expected to resolve after `composer install`. Do not replace those links with absolute paths or local copies unless the task explicitly requires changing the packaging model. + +Available agents: + +- `agents-maintainer` keeps `AGENTS.md` aligned with the current repository workflows, packaged skills, and agent surface. +- `docs-writer` maintains the Sphinx documentation tree under `docs/`. +- `readme-maintainer` keeps `README.md` aligned with the public package surface, install flow, badges, links, and onboarding examples. +- `test-guardian` audits and extends PHPUnit coverage. +- `php-style-curator` normalizes PHP headers, PHPDoc, imports, and style without changing behavior. +- `quality-pipeline-auditor` checks test, style, docs, CI, and generated-output risk across the quality pipeline. +- `review-guardian` performs findings-first review passes for bugs, regressions, missing docs, missing tests, and workflow risk. +- `consumer-sync-auditor` reviews downstream impact for synchronized DevTools assets, workflow wrappers, wiki/bootstrap files, packaged agents, and packaged skills. +- `changelog-maintainer` maintains Keep a Changelog entries and release-note material. +- `issue-editor` turns rough maintenance, bug, or feature requests into implementation-ready GitHub issues. +- `issue-implementer` carries ready issues through implementation, verification, and PR publication. + +When delegating, read the matching prompt in `.agents/agents/.md` and keep the assignment narrow. Agent prompts define role boundaries; procedural steps still come from `.agents/skills/`. + +## Repository Layout + +Important paths: + +- `src/` main library code +- `tests/` PHPUnit suite +- `tests/Support/` enum fixtures used across tests +- `README.md` package onboarding and usage examples +- `docs/` Sphinx documentation for installation, quickstart, usage, API references, and advanced integration notes +- `.github/wiki/` generated wiki content used by wiki workflows +- `.agents/agents/` packaged role prompts for repository-specific delegation +- `.agents/skills/` procedural skills used by the packaged agents +- `.github/workflows/` CI workflows managed by `dev-tools` + +The current test layout is intentionally split by concern: + +- `tests/Common/` +- `tests/Sort/` +- `tests/StateMachine/` +- `tests/Helper/` +- `tests/Trait/` +- `tests/Support/` +- `tests/DomainEnumTest.php` for smaller domain namespaces + +When adding tests, keep them close to the namespace or feature they exercise instead of rebuilding monolithic test files. + +## Code Style + +Follow existing Fast Forward PHP conventions: + +- keep `declare(strict_types=1);` +- preserve the repository file header block +- keep PHPDoc in English +- prefer precise namespace names over catch-all buckets +- keep interfaces in the root namespace when they are part of the public package surface +- do not introduce framework dependencies +- avoid reflection-heavy or attribute-heavy abstractions unless there is already an established pattern + +For enum design: + +- prefer methods that express behavior, not just metadata duplication +- avoid enums that encode framework- or PSR-specific runtime policies unless they stay useful in a raw, non-opinionated consumer +- do not call something a polyfill unless it truly mirrors native behavior and loading semantics + +## Testing Expectations + +For any code change: + +1. run `composer dump-autoload` if files or namespaces changed +2. run `./vendor/bin/dev-tools tests` + +When reorganizing namespaces, verify both autoload and test imports before finalizing. + +## Documentation Expectations + +Any meaningful API or namespace change should be reflected in: + +- `README.md` +- `docs/` +- `CHANGELOG.md` once release tracking is initialized +- tests with real usage examples + +Prefer onboarding-friendly examples for new users. Show practical enum calls instead of only listing cases. + +Use `README.md` as the canonical short-form package guide and expand from there into Sphinx docs rather than duplicating content blindly. For new-user documentation, explain what each helper or enum catalog solves, then show the smallest useful example. + +When wiki output is part of the task, update `.github/wiki/` through the supported `dev-tools` wiki command instead of hand-editing generated pages. + +## Collaboration Notes + +This repository may already contain uncommitted work. Check `git status` before editing and do not revert unrelated changes. + +When changing public API: + +- preserve the package’s framework-agnostic posture +- keep naming coherent across namespaces +- update tests and README in the same pass + +When opening repo-maintenance issues against `dev-tools`, prefer separate issues per bug instead of mixing unrelated failures into one report. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e84f7fa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Bootstrap the initial enum component with helper APIs, reusable traits, packaged enum catalogs, state-machine utilities, documentation, and repository automation. + +### Fixed + +- Restore catalog and domain enum coverage so CI dependency and coverage gates pass. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94b8a8c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Felipe Sayão Lobato Abreu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d54fa78 --- /dev/null +++ b/README.md @@ -0,0 +1,316 @@ +# Fast Forward Enum + +Ergonomic utilities and reusable catalogs for PHP enums, including names, values, lookups, maps, +sorting helpers, and enum-driven workflows. + +[![PHP Version](https://img.shields.io/badge/php-^8.3-777BB4?logo=php&logoColor=white)](https://www.php.net/releases/) +[![Composer Package](https://img.shields.io/badge/composer-fast--forward%2Fenum-F28D1A.svg?logo=composer&logoColor=white)](https://packagist.org/packages/fast-forward/enum) +[![Tests](https://img.shields.io/github/actions/workflow/status/php-fast-forward/enum/tests.yml?logo=githubactions&logoColor=white&label=tests&color=22C55E)](https://github.com/php-fast-forward/enum/actions/workflows/tests.yml) +[![Coverage](https://img.shields.io/badge/coverage-phpunit-4ADE80?logo=php&logoColor=white)](https://php-fast-forward.github.io/enum/coverage/index.html) +[![Docs](https://img.shields.io/github/deployments/php-fast-forward/enum/github-pages?logo=readthedocs&logoColor=white&label=docs&labelColor=1E293B&color=38BDF8&style=flat)](https://php-fast-forward.github.io/enum/index.html) +[![License](https://img.shields.io/github/license/php-fast-forward/enum?color=64748B)](LICENSE) +[![GitHub Sponsors](https://img.shields.io/github/sponsors/php-fast-forward?logo=githubsponsors&logoColor=white&color=EC4899)](https://github.com/sponsors/php-fast-forward) + +![Fast Forward Enum mascot banner](docs/_static/enum-mascot-banner.png) + +## ✨ Features + +- 🧩 Traits for `values()`, `names()`, options, maps, lookups, and enum comparisons +- 🧭 `Helper\EnumHelper` for generic operations over `UnitEnum` and `BackedEnum` +- 🔄 Reversible sort-oriented enums such as `SortDirection`, `NullsPosition`, and `ComparisonResult` +- 🗂 Reusable catalogs grouped by domain, including `Calendar`, `Logger`, `Runtime`, and `DateTime` +- 🚦 Enum-based workflow transitions through `StateMachine\HasTransitions` +- 🏷 Optional `LabeledEnumInterface` and readable descriptions without framework lock-in +- 🧼 Small public API with explicit namespaces and no `Contracts` bucket + +## 📦 Installation + +```bash +composer require fast-forward/enum +``` + +Requirements: + +- PHP `^8.3` + +New to the package? Start with the [Quickstart](docs/getting-started/quickstart.rst), then use the +[Usage guide](docs/usage/index.rst) when you want more complete examples. + +## 🛠️ Usage + +Basic enum ergonomics: + +```php + 'draft', 'Published' => 'published'] +Status::fromName('Draft'); // Status::Draft +Status::Draft->is(Status::Published); // false +Status::Draft->description(); // 'Draft' + +EnumHelper::valueMap(Status::class); // ['draft' => Status::Draft, 'published' => Status::Published] +``` + +Labels and label maps: + +```php + 'Low priority', + self::High => 'High priority', + }; + } +} + +EnumHelper::labels(Priority::class); // ['Low priority', 'High priority'] +EnumHelper::labelMap(Priority::class); // ['Low' => 'Low priority', 'High' => 'High priority'] +``` + +Enum-driven workflows: + +```php +name => [self::Reviewing, self::Archived], + self::Reviewing->name => [self::Published, self::Draft], + self::Published->name => [self::Archived], + self::Archived->name => [], + ]; + } + + protected static function initialStateCases(): array + { + return [self::Draft]; + } +} + +ArticleWorkflow::Draft->canTransitionTo(ArticleWorkflow::Reviewing); // true +ArticleWorkflow::Archived->isTerminal(); // true +ArticleWorkflow::initialStates(); // [ArticleWorkflow::Draft] + +try { + ArticleWorkflow::Reviewing->assertCanTransitionTo(ArticleWorkflow::Archived); +} catch (InvalidTransitionException $exception) { + // Invalid transition +} +``` + +Packaged enum catalogs: + +```php +isProduction(); // true +Priority::Critical->isHigherThan(Priority::Normal); // true +Severity::Error->isAtLeast(Severity::Warning); // true +LogLevel::Critical->isAtLeast(LogLevel::Warning); // true +Result::Partial->isSuccessful(); // true +ComparisonOperator::In->compare('draft', ['draft', 'published']); // true +IntervalUnit::Hour->seconds(2); // 7200 +DispatchMode::Async->isAsync(); // true +ServiceLifetime::Singleton->isReusable(); // true +FailureMode::StopOnFailure->stopsOnFailure(); // true +Scheme::Https->defaultPort(); // 443 +SignalBehavior::Handle->isTerminalControl(); // true +Weekday::Saturday->isWeekend(); // true +Month::December->quarter(); // 4 +Quarter::Q2->months(); // [Month::April, Month::May, Month::June] +Semester::H2->quarters(); // [Quarter::Q3, Quarter::Q4] +SortDirection::Descending->reverse(); // SortDirection::Ascending +NullsPosition::Last->compareNullability(null, 'value'); // 1 +CaseSensitivity::Insensitive->equals('Draft', 'draft'); // true +ComparisonResult::fromComparisonResult(-1); // ComparisonResult::RightGreater +``` + +## 🧰 API Summary + +| API | Description | +| --- | --- | +| `Helper\EnumHelper` | Static helpers for cases, names, values, labels, maps, and lookups | +| `Trait\HasValues` | Adds `values()` to backed enums | +| `Trait\HasNames` | Adds `names()` to any enum | +| `Trait\HasNameLookup` | Adds `fromName()`, `tryFromName()`, and `hasName()` | +| `Trait\HasOptions` | Builds option arrays keyed by case name | +| `Trait\HasNameMap` / `Trait\HasValueMap` | Builds lookup maps for names and backed values | +| `Trait\Comparable` | Adds `is()`, `isNot()`, `in()`, and `notIn()` | +| `Trait\HasDescription` | Generates readable descriptions from case names | +| `Trait\HasLabel` | Provides a technical fallback `label()` implementation | +| `LabeledEnumInterface` | Contract for enums that expose presentation labels | +| `DescribedEnumInterface` | Contract for enums that expose human-readable descriptions | +| `ReversibleInterface` | Common contract for enums exposing `reverse()` | +| `StateMachine\HasTransitions` | Adds transition, terminal, and initial-state behavior to workflow enums | +| `StateMachine\InvalidTransitionException` | Exception thrown by invalid workflow transitions | + +## 🔌 Integration + +`fast-forward/enum` is framework-agnostic and works well in: + +- form and UI option generation +- DTO, request, and serializer layers +- validation and name/value normalization +- internal workflow modeling with enum transitions +- logging, sorting, date/time, and runtime catalogs shared across Fast Forward packages + +It does not require a container, framework bridge, or reflection-heavy metadata system. + +## 📁 Directory Structure Example + +```text +src/ +├── Calendar/ +├── Common/ +├── Comparison/ +├── Container/ +├── DateTime/ +├── Event/ +├── Helper/ +├── Http/ +├── Logger/ +├── Outcome/ +├── Pipeline/ +├── Process/ +├── Runtime/ +├── Sort/ +├── StateMachine/ +└── Trait/ +tests/ +├── Common/ +├── Helper/ +├── Sort/ +├── StateMachine/ +├── Support/ +└── Trait/ +docs/ +├── getting-started/ +├── usage/ +├── api/ +└── advanced/ +``` + +## ⚙️ Advanced & Customization + +- Implement `LabeledEnumInterface` when you need explicit presentation labels. +- Use your own domain enums when semantics are business-specific rather than generic. +- Combine `Comparable`, lookup traits, and `HasTransitions` to build compact workflow models. +- Prefer the packaged catalogs only when the semantics are stable and cross-project. + +## 🛠️ Versioning & Breaking Changes + +The current development line tracks `1.x-dev`. There is no published breaking-change history yet for +this package. + +## ❓ FAQ + +**Q: Why does this package expose traits instead of one giant helper class?** +Traits let enums opt into only the ergonomics they need while keeping the public surface explicit. + +**Q: Why is there no `Contracts` namespace?** +Public interfaces stay in the root namespace so the package does not hide core API behind a generic +bucket. + +**Q: Is `ComparisonResult` a PHP polyfill?** +No. It is a Fast Forward enum for comparator-style semantics, not a promise of native compatibility. + +## 🛡 License + +MIT © 2026 [Felipe Sayão Lobato Abreu](https://github.com/mentordosnerds) + +## 🤝 Contributing + +Issues, pull requests, and documentation improvements are welcome. + +- Read [AGENTS.md](AGENTS.md) for repository-specific guidance +- Run `composer dump-autoload` +- Run `./vendor/bin/dev-tools tests` +- Update the [README](README.md) and relevant docs when changing public API + +## 🔗 Links + +- [Repository](https://github.com/php-fast-forward/enum) +- [Packagist](https://packagist.org/packages/fast-forward/enum) +- [Documentation site](https://php-fast-forward.github.io/enum/index.html) +- [Sphinx docs source](docs/index.rst) +- [Wiki](https://github.com/php-fast-forward/enum/wiki) +- [Issue tracker](https://github.com/php-fast-forward/enum/issues) +- [Coverage report](https://php-fast-forward.github.io/enum/coverage/index.html) +- [Tests workflow](https://github.com/php-fast-forward/enum/actions/workflows/tests.yml) +- [MIT License](LICENSE) +- [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1bec609 --- /dev/null +++ b/composer.json @@ -0,0 +1,82 @@ +{ + "name": "fast-forward/enum", + "description": "Ergonomic utilities for PHP enums, including names, values, lookups, and option maps.", + "license": "MIT", + "type": "library", + "keywords": [ + "php", + "enum", + "backed-enum", + "unit-enum", + "fast-forward" + ], + "readme": "README.md", + "authors": [ + { + "name": "Felipe Sayão Lobato Abreu", + "email": "github@mentordosnerds.com", + "homepage": "https://github.com/coisa", + "role": "Maintainer" + } + ], + "homepage": "https://github.com/php-fast-forward/enum", + "support": { + "issues": "https://github.com/php-fast-forward/enum/issues", + "wiki": "https://github.com/php-fast-forward/enum/wiki", + "source": "https://github.com/php-fast-forward/enum", + "docs": "https://php-fast-forward.github.io/enum/" + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/php-fast-forward" + }, + { + "type": "custom", + "url": "https://www.paypal.com/donate/?business=JLDAF45XZ8D84" + } + ], + "require": { + "php": "^8.3", + "ext-mbstring": "*" + }, + "require-dev": { + "fast-forward/dev-tools": "dev-main" + }, + "minimum-stability": "stable", + "autoload": { + "psr-4": { + "FastForward\\Enum\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "FastForward\\Enum\\Tests\\": "tests/" + } + }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true, + "fast-forward/dev-tools": true, + "phpdocumentor/shim": true, + "phpro/grumphp-shim": true, + "pyrech/composer-changelogs": true + }, + "platform": { + "php": "8.3.0" + }, + "sort-packages": true + }, + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + }, + "grumphp": { + "config-default-path": "vendor/fast-forward/dev-tools/grumphp.yml" + } + }, + "scripts": { + "dev-tools": "dev-tools", + "dev-tools:fix": "@dev-tools --fix" + } +} diff --git a/docs/_static/enum-mascot-banner.png b/docs/_static/enum-mascot-banner.png new file mode 100644 index 0000000..a08197f Binary files /dev/null and b/docs/_static/enum-mascot-banner.png differ diff --git a/docs/advanced/design.rst b/docs/advanced/design.rst new file mode 100644 index 0000000..492b722 --- /dev/null +++ b/docs/advanced/design.rst @@ -0,0 +1,35 @@ +Design Notes +============ + +Principles +---------- + +- Keep helpers small and explicit. +- Avoid framework lock-in. +- Prefer specific namespaces over giant utility buckets. +- Avoid calling something a polyfill unless it behaves like a real polyfill. + +Design goals behind the package +------------------------------- + +This package is not trying to become a framework for enum metadata. Its design +leans toward a smaller, more reusable middle layer: + +- traits for the most common ergonomic gaps in PHP enums +- one static helper surface for external access +- a curated set of packaged enums only where cross-project semantics are stable + +Why the package is split by namespace +------------------------------------- + +``Calendar`` and ``Sort`` are intentionally separate from ``Common`` because they represent coherent domains with room to grow. + +The same logic applies to namespaces such as ``Logger``, ``Runtime``, ``DateTime``, ``Comparison``, and ``Outcome``. + +What the package avoids on purpose +---------------------------------- + +- framework-specific adapters in the runtime API +- reflection-heavy attribute systems +- fake polyfills that do not really mirror native behavior +- giant "everything enum" buckets that hide meaning instead of clarifying it diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst new file mode 100644 index 0000000..85d1453 --- /dev/null +++ b/docs/advanced/index.rst @@ -0,0 +1,9 @@ +Advanced Topics +=============== + +.. toctree:: + :maxdepth: 1 + + design + integration + diff --git a/docs/advanced/integration.rst b/docs/advanced/integration.rst new file mode 100644 index 0000000..72f0631 --- /dev/null +++ b/docs/advanced/integration.rst @@ -0,0 +1,49 @@ +Integration +=========== + +This package is framework-agnostic, so integration is usually just normal PHP usage. + +Typical integration points +-------------------------- + +- DTO validation and serialization +- form/select option generation +- API filtering with ``ComparisonOperator`` +- logging decisions with ``LogLevel`` +- runtime configuration with ``Environment`` +- custom sort implementations using ``SortDirection`` and ``ComparisonResult`` +- date/time calculations with ``Month``, ``Quarter``, and ``IntervalUnit`` + +Typical application patterns +---------------------------- + +Common integration patterns include: + +- building form option lists from ``options()`` or ``EnumHelper::options()`` +- validating case names coming from user input with ``fromName()`` or ``hasName()`` +- converting backed values from storage or APIs into cases +- sharing stable enums such as ``Environment`` or ``LogLevel`` across multiple packages + +Container usage +--------------- + +No service provider is required. Import the enum or helper directly where needed. + +Framework usage +--------------- + +In frameworks, this usually means: + +- enums live in your domain or shared library namespace +- traits are added to enums you own +- ``EnumHelper`` is used in UI adapters, serializers, controllers, and validators + +There is no special registration step. + +Troubleshooting +--------------- + +- If a helper expects a backed enum, prefer ``EnumHelper`` methods that are documented for ``BackedEnum`` rather than plain ``UnitEnum``. +- If you need business-specific labels or transitions, create your own enum instead of forcing packaged catalogs to fit. +- When reorganizing enums across namespaces in your own project, run ``composer dump-autoload`` before debugging missing classes. +- If names and values are getting mixed up in external input, document clearly whether your boundary accepts case names or backed values. diff --git a/docs/api/contracts.rst b/docs/api/contracts.rst new file mode 100644 index 0000000..a8b0e2c --- /dev/null +++ b/docs/api/contracts.rst @@ -0,0 +1,40 @@ +Interfaces +========== + +The public interfaces live in the package root namespace, not in a ``Contracts`` bucket. + +Why root-level interfaces? +-------------------------- + +This package keeps its public contracts shallow on purpose. The goal is to make +discovery easy for package users and avoid namespace chains that add structure +without adding real meaning. + +Interfaces +---------- + +- ``LabeledEnumInterface`` +- ``DescribedEnumInterface`` +- ``ReversibleInterface`` + +This keeps the public API shallow and discoverable. + +When to use them +---------------- + +``LabeledEnumInterface`` +^^^^^^^^^^^^^^^^^^^^^^^^ + +Use this when enum cases need explicit presentation labels and you want +``EnumHelper::labels()`` or ``EnumHelper::labelMap()`` to work. + +``DescribedEnumInterface`` +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use this when enum cases need longer human-readable descriptions. + +``ReversibleInterface`` +^^^^^^^^^^^^^^^^^^^^^^^ + +Use this for enums that have a meaningful inverse, such as sort directions or +case-sensitivity flags. diff --git a/docs/api/helper.rst b/docs/api/helper.rst new file mode 100644 index 0000000..af3ae6e --- /dev/null +++ b/docs/api/helper.rst @@ -0,0 +1,115 @@ +Helper +====== + +``FastForward\\Enum\\Helper\\EnumHelper`` provides static helpers for both class strings and enum cases. + +Why it exists +------------- + +``EnumHelper`` is the external face of the package. It is especially useful +when: + +- the enum is defined in another package +- you do not want to add traits to the enum declaration +- you want one shared helper surface in presenters, forms, serializers, or validators + +Accepted inputs +--------------- + +Most methods accept either: + +- a class string such as ``Status::class`` +- or a concrete enum case such as ``Status::Draft`` + +That means you can call helpers without normalizing the enum manually first. + +Main methods +------------ + +.. list-table:: + :header-rows: 1 + + * - Method + - Returns + - Typical use + * - ``cases()`` + - enum case list + - general iteration + * - ``names()`` + - ``list`` + - dropdown labels, documentation, debugging + * - ``values()`` + - ``list`` + - backed enums, schema generation, allowed values + * - ``options()`` + - ``array`` + - forms, prompts, UI option lists + * - ``nameMap()`` + - ``array`` + - lookups by case name + * - ``valueMap()`` + - ``array`` + - lookups by backed value + * - ``fromName()`` / ``tryFromName()`` + - enum case or ``null`` + - name-based input normalization + * - ``hasName()`` / ``hasValue()`` + - ``bool`` + - validation and guards + * - ``labels()`` / ``labelMap()`` + - strings or name-to-label map + - presentation-friendly enums implementing ``LabeledEnumInterface`` + +Examples +-------- + +.. code-block:: php + + value`` option array for backed enums. + +Lookup traits +------------- + +``HasNameLookup`` +^^^^^^^^^^^^^^^^^ + +Adds ``tryFromName()``, ``fromName()``, and ``hasName()``. + +``HasNameMap`` and ``HasValueMap`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Add fast reusable lookup arrays for case names and backed values. + +Behavior traits +--------------- + +``Comparable`` +^^^^^^^^^^^^^^ + +Adds explicit comparison helpers such as ``is()`` and ``in()``. + +``HasDescription`` +^^^^^^^^^^^^^^^^^^ + +Adds a readable description based on the case name. + +``HasLabel`` +^^^^^^^^^^^^ + +Adds a default ``label()`` implementation. This can be useful as a fallback, +but user-facing enums often benefit from a custom label implementation instead. + +Trait composition example +------------------------- + +.. code-block:: php + + enum Status: string + { + use HasValues; + use HasNames; + use HasOptions; + use HasNameLookup; + use Comparable; + } diff --git a/docs/compatibility.rst b/docs/compatibility.rst new file mode 100644 index 0000000..6c49be2 --- /dev/null +++ b/docs/compatibility.rst @@ -0,0 +1,18 @@ +Compatibility +============= + +Version table +------------- + ++----------------------+------------------+ +| Package line | PHP requirement | ++======================+==================+ +| ``1.x-dev`` | ``^8.3`` | ++----------------------+------------------+ + +Notes +----- + +- The package is built around native PHP enums, so modern PHP support is a hard requirement. +- There are no framework compatibility constraints because the library is framework-agnostic. +- When publishing stable tags later, extend this page with upgrade notes and breaking changes. diff --git a/docs/faq.rst b/docs/faq.rst new file mode 100644 index 0000000..5255613 --- /dev/null +++ b/docs/faq.rst @@ -0,0 +1,61 @@ +FAQ +=== + +This FAQ focuses on the questions new users usually hit after the first install: +which API to choose, how to avoid overusing packaged enums, and how the helper +methods relate to native enum behavior. + +Why are interfaces not in ``Contracts``? +---------------------------------------- + +The package keeps public interfaces in the root namespace to avoid deep namespace chains for a very small API. + +Is this package framework-specific? +----------------------------------- + +No. It is intentionally framework-agnostic. + +Should I use the packaged enums everywhere? +------------------------------------------- + +No. Use them when the semantics are truly generic. Keep domain-specific enums in your own package. + +Why both traits and ``EnumHelper``? +---------------------------------- + +Traits are useful when you own the enum declaration. ``EnumHelper`` is useful when you want the behavior externally or prefer not to mix traits into the enum. + +Does ``StateMachine\\HasTransitions`` replace a workflow engine? +--------------------------------------------------------------- + +No. It is a lightweight helper for explicit transitions inside enum-based workflows. + +Is ``ComparisonResult`` a PHP polyfill? +--------------------------------------- + +No. It is a utility enum for sort/comparison flows, not a real native polyfill. + +Can I combine many traits in one enum? +-------------------------------------- + +Yes, but only combine the traits that actually describe the enum behavior you want to expose. + +Where should I start as a new user? +----------------------------------- + +Read ``getting-started/installation.rst`` and ``getting-started/quickstart.rst`` first, then move to ``usage/traits.rst`` or ``usage/catalogs.rst`` depending on your use case. + +How do I use the package from a container or framework? +------------------------------------------------------- + +Usually you do not need special integration. Import the enum or ``EnumHelper`` directly inside your service class and keep the enum itself framework-agnostic. + +What should I check if autoloading looks broken after adding a new enum? +------------------------------------------------------------------------ + +Run ``composer dump-autoload`` and confirm the namespace matches the file path. This is especially important after moving enums between namespaces such as ``Common``, ``Sort``, or ``Calendar``. + +How do I decide between packaged enums and application-specific enums? +---------------------------------------------------------------------- + +Use packaged enums only when the meaning is stable across projects. If the cases, labels, or transitions are tied to your business language, define your own enum and use the traits or helper APIs on top of it. diff --git a/docs/getting-started/index.rst b/docs/getting-started/index.rst new file mode 100644 index 0000000..2be7e36 --- /dev/null +++ b/docs/getting-started/index.rst @@ -0,0 +1,27 @@ +Getting Started +=============== + +This section is for developers opening the package for the first time. The goal +is not only to show the install command, but also to explain how the package is +intended to be used in day-to-day code. + +You will find: + +- installation requirements +- the shortest path to a working example +- guidance on how the package is organized +- when to choose traits instead of ``EnumHelper`` +- compatibility notes in :doc:`../compatibility` + +Suggested reading order +----------------------- + +1. Read :doc:`installation` to understand package requirements and repository tooling. +2. Read :doc:`quickstart` to see the smallest successful enum using the library. +3. Continue with :doc:`../usage/index` if you want to learn how the helpers fit real applications. + +.. toctree:: + :maxdepth: 1 + + installation + quickstart diff --git a/docs/getting-started/installation.rst b/docs/getting-started/installation.rst new file mode 100644 index 0000000..310667d --- /dev/null +++ b/docs/getting-started/installation.rst @@ -0,0 +1,72 @@ +Installation +============ + +``fast-forward/enum`` has a very small runtime footprint. The package depends on +PHP's native enum support and does not require a framework bridge, container +integration, or extra runtime libraries. + +Requirements +------------ + +- PHP ``^8.3`` +- Composer + +Install the package with: + +.. code-block:: bash + + composer require fast-forward/enum + +What you get after installation +------------------------------- + +Once installed, the package exposes three main layers: + +- traits under ``FastForward\\Enum\\Trait`` for enums you control +- ``FastForward\\Enum\\Helper\\EnumHelper`` for static operations over existing enums +- packaged enum catalogs under namespaces such as ``Calendar``, ``Sort``, and ``Runtime`` + +That separation is intentional. New users often expect one of these layers to replace the others, +but in practice they solve different problems. + +Development dependencies +------------------------ + +This repository uses ``fast-forward/dev-tools`` during development. In the package repository itself, the most reliable validation commands are: + +.. code-block:: bash + + composer dump-autoload + ./vendor/bin/dev-tools tests + +Namespace layout +---------------- + +The package is intentionally grouped by concern: + +- ``FastForward\\Enum\\Trait`` for reusable enum traits +- ``FastForward\\Enum\\Helper`` for static helpers +- domain namespaces such as ``Calendar``, ``Sort``, ``Logger``, ``Runtime``, ``DateTime``, ``Comparison``, ``Outcome``, ``Http``, ``Process``, ``Event``, ``Container``, and ``Pipeline`` + +Choosing the right API surface +------------------------------ + +Use traits when: + +- you own the enum declaration +- you want methods such as ``values()`` or ``fromName()`` directly on the enum +- you want the enum itself to advertise those behaviors + +Use ``EnumHelper`` when: + +- the enum is defined in another package +- you do not want to mix traits into the enum declaration +- you need one-off utility access from presenters, validators, serializers, or forms + +Use packaged enums when: + +- the semantics are already stable and reusable across projects +- names like ``Environment`` or ``SortDirection`` really match the problem you have + +If the semantics are business-specific, prefer defining your own enum and layering Fast Forward +traits on top of it. diff --git a/docs/getting-started/quickstart.rst b/docs/getting-started/quickstart.rst new file mode 100644 index 0000000..5a54454 --- /dev/null +++ b/docs/getting-started/quickstart.rst @@ -0,0 +1,115 @@ +Quickstart +========== + +This walkthrough shows the smallest practical path from installation to a +useful enum. It intentionally covers the three ideas new users need first: + +1. adding methods directly to an enum with traits +2. reading the same data externally with ``EnumHelper`` +3. understanding why both approaches exist + +Build a Backed Enum with Trait Helpers +-------------------------------------- + +Start with a backed enum because it demonstrates the most common helpers in one +place. + +.. code-block:: php + + 'draft', 'Published' => 'published'] + Status::fromName('Draft'); // Status::Draft + +What this gives you +------------------- + +- ``values()`` returns the backed values in declaration order +- ``names()`` returns case names without requiring manual ``cases()`` mapping +- ``options()`` returns a simple ``name => value`` array for forms, prompts, or docs +- ``fromName()`` adds a native-feeling name lookup that PHP does not provide out of the box + +Reading the Same Enum Through ``EnumHelper`` +-------------------------------------------- + +.. code-block:: php + + Status::Draft, 'published' => Status::Published] + +This is useful when: + +- you cannot or do not want to modify the enum declaration +- you want to keep utility logic in an application service +- the enum comes from another package + +Seeing a Packaged Enum +---------------------- + +.. code-block:: php + + isProduction(); // true + Month::December->quarter(); // 4 + LogLevel::Critical->isAtLeast(LogLevel::Warning); // true + SortDirection::Descending->reverse(); // SortDirection::Ascending + +Choosing the Right Surface +-------------------------- + +.. list-table:: + :header-rows: 1 + + * - Need + - Use + - Why + * - Add methods to your own enum + - Traits + - The enum advertises the API directly, such as ``Status::values()``. + * - Inspect an enum you do not own + - ``EnumHelper`` + - Helpers work with class strings and enum cases without changing the enum declaration. + * - Reuse a general concept + - Packaged enums + - Shared enums provide stable semantics for calendars, sorting, runtime environments, logging levels, and similar concerns. + +What to read next +----------------- + +- Continue with :doc:`../usage/helper-methods` if you want a method-by-method guide for traits and ``EnumHelper``. +- Continue with :doc:`../usage/working-with-enums` if you want to see where the package sits inside application code. +- Continue with :doc:`../usage/catalogs` if you want to decide whether a packaged enum fits your use case. diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..7799af8 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,53 @@ +Fast Forward Enum +================= + +.. container:: row align-items-center gy-4 my-4 + + .. container:: col-lg-7 + + ``fast-forward/enum`` is a PHP 8.3+ library for enum ergonomics, reusable + enum catalogs, and small behavioral helpers that stay framework-agnostic. + + The package is designed for developers who want: + + - convenience helpers such as ``values()``, ``names()``, maps, and lookups + - reusable enum catalogs grouped by domain instead of one giant ``Common`` bucket + - a clean, typed way to model small behaviors such as reversible sort directions or state-machine transitions + - documentation and examples that are approachable for first-time users + + If you are new to the package, start with + :doc:`getting-started/index` and :doc:`usage/index`. They explain the + difference between traits, ``EnumHelper``, and the packaged enums, which + is the main conceptual jump for most first-time users. + + If you already know the problem you want to solve, continue with + :doc:`api/index` and :doc:`advanced/index` for helper method details, + namespace design, workflow traits, and integration guidance. + + .. container:: col-lg-5 text-center + + .. image:: _static/enum-mascot-banner.png + :alt: Fast Forward Enum mascot banner + :class: img-fluid w-100 rounded-4 shadow-sm border border-light-subtle bg-body-tertiary p-2 + +Useful links +------------ + +- `Repository `_ +- `Packagist `_ +- `Coverage `_ +- `API Reference `_ +- `Issue Tracker `_ +- `Project Wiki `_ + +.. toctree:: + :maxdepth: 2 + :caption: Contents + + getting-started/index + usage/index + api/index + advanced/index + links/index + faq + compatibility diff --git a/docs/links/coverage.rst b/docs/links/coverage.rst new file mode 100644 index 0000000..d987fa3 --- /dev/null +++ b/docs/links/coverage.rst @@ -0,0 +1,14 @@ +Coverage and Reports +==================== + +Live reports +------------ + +- `Coverage report `_ +- `Documentation site `_ + +Notes +----- + +If additional reports such as metrics or testdox become available for this package, link them from +this section as well so contributors can discover them from one place. diff --git a/docs/links/dependencies.rst b/docs/links/dependencies.rst new file mode 100644 index 0000000..ab3adaa --- /dev/null +++ b/docs/links/dependencies.rst @@ -0,0 +1,35 @@ +Dependencies +============ + +Runtime +------- + +- PHP ``^8.3`` +- No additional runtime package dependencies + +Development +----------- + +- ``fast-forward/dev-tools`` for repository tooling, tests, workflows, and managed assets + +Optional integrations +--------------------- + +There are no optional Composer integrations required by this library. The package is designed to be +consumed directly from plain PHP code, frameworks, CLI tools, or internal libraries. + +Related references +------------------ + +- `PHP Enumerations Manual `_ +- `PSR website `_ +- `Fast Forward organization `_ +- `Fast Forward DevTools `_ + +Why this matters for users +-------------------------- + +Because the package has no runtime dependencies beyond PHP itself, most +questions new users ask are really about behavior and semantics rather than +installation complexity. That is part of the reason the package leans so +heavily on documentation and examples. diff --git a/docs/links/index.rst b/docs/links/index.rst new file mode 100644 index 0000000..e5b8b51 --- /dev/null +++ b/docs/links/index.rst @@ -0,0 +1,11 @@ +Links +===== + +This section collects external references, reports, and ecosystem links so new +contributors do not need to search for them manually. + +.. toctree:: + :maxdepth: 1 + + dependencies + coverage diff --git a/docs/usage/catalogs.rst b/docs/usage/catalogs.rst new file mode 100644 index 0000000..5b8b9f0 --- /dev/null +++ b/docs/usage/catalogs.rst @@ -0,0 +1,242 @@ +Enum Catalogs +============= + +The package ships with ready-to-use enums for recurring concerns. These are not +meant to replace every enum in an application. They exist for cases where the +semantics are already broad, stable, and reusable across projects. + +Examples by namespace +--------------------- + +- ``Calendar``: ``Month``, ``Quarter``, ``Semester``, ``Weekday`` +- ``Sort``: ``SortDirection``, ``NullsPosition``, ``CaseSensitivity``, ``ComparisonResult`` +- ``Logger``: ``LogLevel`` +- ``Runtime``: ``Environment`` +- ``DateTime``: ``IntervalUnit`` +- ``Comparison``: ``ComparisonOperator`` +- ``Outcome``: ``Result`` +- ``Http``: ``Scheme`` +- ``Process``: ``SignalBehavior`` + +What users usually want to know first +------------------------------------- + +Most first-time users ask two questions when they reach the packaged enums: + +1. "When should I use one of these instead of defining my own enum?" +2. "What useful behavior do I get besides the cases themselves?" + +The short answer is: + +- use a packaged enum when the name and semantics already match your problem +- expect each packaged enum to expose a few focused methods, not just raw cases + +Examples with practical behavior +-------------------------------- + +``Runtime\\Environment`` +^^^^^^^^^^^^^^^^^^^^^^^^ + +Use this when your code needs to distinguish between development, testing, +staging, and production. + +Useful methods: + +- ``isProduction()`` +- ``isPreProduction()`` +- ``isDebugFriendly()`` +- inherited helper-style behavior such as ``names()``, ``values()``, and ``fromName()`` + +``Calendar\\Month`` and ``Calendar\\Quarter`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use these when you want typed calendar logic instead of repeating integer math. + +Useful methods: + +- ``Month::quarter()`` +- ``Month::isQuarterEnd()`` +- ``Month::ordered()`` +- ``Quarter::months()`` +- ``Quarter::startMonth()`` +- ``Quarter::endMonth()`` +- ``Quarter::includes()`` +- ``Quarter::fromMonth()`` + +``Calendar\\Semester`` and ``Calendar\\Weekday`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use these when reports, schedules, or recurring jobs need names that are more +explicit than raw integers. + +Useful methods: + +- ``Semester::months()`` +- ``Semester::includes()`` +- ``Semester::fromMonth()`` +- ``Weekday::isWeekend()`` +- ``Weekday::isWeekday()`` +- ``Weekday::ordered()`` + +``DateTime\\IntervalUnit`` +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use this when you need named interval units for cache policies, schedules, +retry logic, or reporting windows. + +Useful methods: + +- ``shortLabel()`` +- ``seconds()`` +- ``isCalendarAware()`` + +``Logger\\LogLevel`` and ``Common\\Severity`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use these when you need reusable severity ordering in logs, alerts, or +operational rules. + +Useful methods: + +- ``weight()`` +- ``isAtLeast()`` +- ``ordered()`` + +``Sort\\SortDirection`` and friends +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use these when sorting behavior would otherwise rely on magic values or loose +booleans. + +Useful methods: + +- ``SortDirection::reverse()`` +- ``SortDirection::applyToComparisonResult()`` +- ``NullsPosition::compareNullability()`` +- ``CaseSensitivity::normalize()`` +- ``CaseSensitivity::equals()`` +- ``ComparisonResult::fromComparisonResult()`` +- ``ComparisonResult::toComparisonResult()`` + +``Comparison\\ComparisonOperator`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use this when building filters, query abstractions, or rule systems that need +typed operators. + +Useful methods: + +- ``symbol()`` +- ``isSetOperator()`` +- ``compare()`` +- ``negate()`` + +``Common\\Priority`` +^^^^^^^^^^^^^^^^^^^^ + +Use this when work queues, support tickets, alerts, or retry policies need a +small reusable priority vocabulary. + +Useful methods: + +- ``weight()`` +- ``isHigherThan()`` +- ``isLowerThan()`` +- ``ordered()`` + +``Http\\Scheme`` +^^^^^^^^^^^^^^^^ + +Use this when URLs or transport policies should distinguish ``http`` and +``https`` without string checks. + +Useful methods: + +- ``isSecure()`` +- inherited enum helper behavior such as ``values()`` and ``fromName()`` + +``Process\\SignalBehavior`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use this when process runners or worker loops need to describe what should +happen after a signal. + +Useful methods: + +- ``isTerminalControl()`` + +``Event\\DispatchMode`` +^^^^^^^^^^^^^^^^^^^^^^^ + +Use this when an event dispatcher, message bridge, or outbox integration needs +to carry whether work is expected to happen now or later. + +Useful methods: + +- ``isSync()`` +- ``isAsync()`` + +``Container\\ServiceLifetime`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use this when a container adapter needs a small non-PSR-specific vocabulary for +singleton and transient services. + +Useful methods: + +- ``isReusable()`` + +``Pipeline\\FailureMode`` +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use this when pipeline or middleware execution needs to state whether a failure +stops the chain or is collected while execution continues. + +Useful methods: + +- ``stopsOnFailure()`` + +``Outcome\\Result`` +^^^^^^^^^^^^^^^^^^^ + +Use this when your code needs a compact success/partial/failure outcome type. + +Useful methods: + +- ``isSuccessful()`` +- ``isCompleteSuccess()`` +- ``isFailure()`` + +How to evaluate a packaged enum +------------------------------- + +A packaged enum is usually a good fit when: + +- the case names already match the language you would naturally use +- the behavior would look strange if you had to rename every case +- the enum would make sense in more than one package or application + +A packaged enum is usually a poor fit when: + +- your business vocabulary differs from the generic names +- labels need to be heavily customized +- the lifecycle or transitions are domain-specific + +New-user guidance +----------------- + +Use the packaged enums when the semantics are already general and stable. + +Prefer defining your own enum when: + +- the cases are domain-specific +- the labels are business-language driven +- the lifecycle or behavior only makes sense inside one package + +Examples of strong fits +----------------------- + +- ``Runtime\\Environment`` for deployment/runtime distinctions +- ``Calendar\\Month`` and ``Calendar\\Quarter`` for date-related calculations +- ``Sort\\SortDirection`` and ``Sort\\NullsPosition`` for ordering behavior +- ``Logger\\LogLevel`` for reusable severity-style decisions diff --git a/docs/usage/helper-methods.rst b/docs/usage/helper-methods.rst new file mode 100644 index 0000000..e7f8421 --- /dev/null +++ b/docs/usage/helper-methods.rst @@ -0,0 +1,187 @@ +Helper Methods +============== + +This page explains the helpers most new users look for first: what they return, +why they exist, and when to use the trait version instead of ``EnumHelper``. + +Trait or ``EnumHelper``? +------------------------ + +The package intentionally exposes most collection-style helpers in two forms: + +- as traits, for enums you own +- as ``EnumHelper`` methods, for external access + +In other words, these pairs usually solve the same problem: + +.. list-table:: + :header-rows: 1 + + * - Problem + - Trait form + - Helper form + * - Get case names + - ``HasNames`` + - ``EnumHelper::names()`` + * - Get backed values + - ``HasValues`` + - ``EnumHelper::values()`` + * - Lookup by case name + - ``HasNameLookup`` + - ``EnumHelper::fromName()`` / ``EnumHelper::tryFromName()`` + * - Build maps + - ``HasNameMap`` / ``HasValueMap`` + - ``EnumHelper::nameMap()`` / ``EnumHelper::valueMap()`` + * - Build option arrays + - ``HasOptions`` + - ``EnumHelper::options()`` + +Collection helpers +------------------ + +``cases()`` +^^^^^^^^^^^ + +Use ``EnumHelper::cases()`` when you want the native case list but prefer to +accept either ``Foo::class`` or a concrete case such as ``Foo::Bar``. + +.. code-block:: php + + EnumHelper::cases(Status::class); + EnumHelper::cases(Status::Draft); + +``names()`` +^^^^^^^^^^^ + +Use this when you need a simple list of case names without mapping over +``cases()`` manually. + +.. code-block:: php + + Status::names(); // ['Draft', 'Published'] + EnumHelper::names(Status::class); + +``values()`` +^^^^^^^^^^^^ + +Use this only with backed enums. It returns the scalar backing values in +declaration order. + +.. code-block:: php + + Status::values(); // ['draft', 'published'] + EnumHelper::values(Status::class); + +Maps and options +---------------- + +``nameMap()`` +^^^^^^^^^^^^^ + +Returns ``case name => enum case``. + +.. code-block:: php + + Status::nameMap()['Draft']; // Status::Draft + +This is useful for validation, routing user input by case name, and cheap +lookups without writing loops. + +``valueMap()`` +^^^^^^^^^^^^^^ + +Returns ``backed value => enum case``. + +.. code-block:: php + + Status::valueMap()['draft']; // Status::Draft + +Use this when your system deals with external values such as API payloads, +database values, or configuration arrays. + +``options()`` +^^^^^^^^^^^^^ + +Returns ``case name => backed value``. + +.. code-block:: php + + Status::options(); // ['Draft' => 'draft', 'Published' => 'published'] + +This is a good fit for: + +- HTML selects +- CLI choice prompts +- schema or documentation generation +- admin UIs + +Name lookups +------------ + +``tryFromName()`` +^^^^^^^^^^^^^^^^^ + +Returns ``null`` when the name does not exist. + +.. code-block:: php + + Status::tryFromName('Draft'); // Status::Draft + Status::tryFromName('Unknown'); // null + +``fromName()`` +^^^^^^^^^^^^^^ + +Throws ``ValueError`` when the name is invalid, matching the style of PHP's +native ``from()`` behavior for backed enums. + +.. code-block:: php + + Status::fromName('Draft'); // Status::Draft + +``hasName()`` and ``hasValue()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use these as small intent-revealing checks when validating external input. + +.. code-block:: php + + EnumHelper::hasName(Status::class, 'Draft'); // true + EnumHelper::hasValue(Status::class, 'draft'); // true + +Labels and descriptions +----------------------- + +``HasDescription`` +^^^^^^^^^^^^^^^^^^ + +This trait turns ``CamelCase`` names into readable strings. It is useful as a +low-cost default when you want something better than the raw case name. + +``LabeledEnumInterface`` and ``HasLabel`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use ``LabeledEnumInterface`` when a case needs an explicit presentation label. +The package also ships with ``HasLabel`` as a simple default, but most +user-facing enums will benefit from a custom implementation. + +Comparison helpers +------------------ + +``Comparable`` +^^^^^^^^^^^^^^ + +Use ``Comparable`` when you want intent-revealing methods instead of ad-hoc +strict comparisons. + +.. code-block:: php + + Status::Draft->is(Status::Published); // false + Status::Draft->isNot(Status::Published); // true + Status::Draft->in([Status::Draft, Status::Published]); // true + +When not to use these helpers +----------------------------- + +- Do not add every trait to every enum just because it is available. +- Do not use packaged enums when your semantics are business-specific. +- Do not use ``values()`` or ``options()`` on plain ``UnitEnum`` types. diff --git a/docs/usage/index.rst b/docs/usage/index.rst new file mode 100644 index 0000000..bc5eede --- /dev/null +++ b/docs/usage/index.rst @@ -0,0 +1,23 @@ +Usage +===== + +This section explains the most common ways to use the package in real code. + +The most important question in this package is usually not "which enum should I +pick?" but "which layer should I use?" + +- :doc:`working-with-enums` explains where the package fits inside application code. +- :doc:`helper-methods` explains the practical value of each helper and trait. +- :doc:`use-cases` shows common scenarios such as form options, validation, and workflows. +- :doc:`catalogs` explains when the packaged enums are a good fit and when they are not. + +.. toctree:: + :maxdepth: 1 + + working-with-enums + helper-methods + use-cases + traits + catalogs + state-machines + sort diff --git a/docs/usage/sort.rst b/docs/usage/sort.rst new file mode 100644 index 0000000..4d42703 --- /dev/null +++ b/docs/usage/sort.rst @@ -0,0 +1,56 @@ +Sort Helpers +============ + +The ``Sort`` namespace groups enums that help with ordering logic without +coupling you to a specific library. + +Why this namespace exists +------------------------- + +Sorting code often accumulates "magic" values such as ``1``, ``-1``, ``ASC``, +``DESC``, ad-hoc null rules, and case-sensitivity flags. These enums make those +choices explicit and typed. + +Examples +-------- + +.. code-block:: php + + applyToComparisonResult(5); // -5 + NullsPosition::Last->compareNullability(null, 'value'); // 1 + CaseSensitivity::Insensitive->equals('Draft', 'draft'); // true + ComparisonResult::fromComparisonResult(-1); // ComparisonResult::RightGreater + +What each enum is for +--------------------- + +``SortDirection`` +^^^^^^^^^^^^^^^^^ + +Represents ascending vs descending order and can invert comparator results +through ``applyToComparisonResult()``. + +``NullsPosition`` +^^^^^^^^^^^^^^^^^ + +Represents whether ``null`` values should sort first or last. + +``CaseSensitivity`` +^^^^^^^^^^^^^^^^^^^ + +Represents whether string comparisons should preserve case or normalize it. + +``ComparisonResult`` +^^^^^^^^^^^^^^^^^^^^ + +Represents comparator-style outcomes such as ``LeftGreater`` and +``RightGreater`` without passing around raw integers. diff --git a/docs/usage/state-machines.rst b/docs/usage/state-machines.rst new file mode 100644 index 0000000..9da0df1 --- /dev/null +++ b/docs/usage/state-machines.rst @@ -0,0 +1,65 @@ +State Machines +============== + +``StateMachine\\HasTransitions`` is useful when an enum represents a workflow with explicit allowed transitions. + +What problem it solves +---------------------- + +Without the trait, transition rules usually end up spread across ``match`` +expressions, guard methods, or service classes. ``HasTransitions`` keeps that +behavior next to the enum cases, which makes the workflow easier to inspect and +test. + +Example +------- + +.. code-block:: php + + name => [self::Reviewing, self::Archived], + self::Reviewing->name => [self::Published, self::Draft], + self::Published->name => [self::Archived], + self::Archived->name => [], + ]; + } + } + + ArticleWorkflow::Draft->canTransitionTo(ArticleWorkflow::Reviewing); // true + +What the trait adds +------------------- + +- ``allowedTransitions()`` for the current case +- ``canTransitionTo()`` and ``assertCanTransitionTo()`` for validation +- ``isTerminal()`` and ``terminalStates()`` for end states +- ``isInitial()`` and ``initialStates()`` for inferred or explicit entry points + +When to use explicit initial states +----------------------------------- + +If your workflow has more than one valid entry state, implement +``initialStateCases()`` directly. Otherwise the trait infers initial states by +finding cases with no incoming transitions. + +When to avoid it +---------------- + +Do not force every state enum into a transition system. Use the trait only when the transitions are part of the public behavior you want to model. diff --git a/docs/usage/traits.rst b/docs/usage/traits.rst new file mode 100644 index 0000000..3c2c502 --- /dev/null +++ b/docs/usage/traits.rst @@ -0,0 +1,144 @@ +Traits +====== + +The reusable traits are the heart of the package for user-defined enums. They +let you opt into behavior without forcing every enum to carry the same API. + +Common combinations +------------------- + +Backed enums often combine: + +- ``HasValues`` +- ``HasNames`` +- ``HasOptions`` +- ``HasNameLookup`` +- ``HasValueMap`` + +Unit enums often combine: + +- ``HasNames`` +- ``HasNameLookup`` +- ``HasNameMap`` + +Behavior-oriented traits: + +- ``Comparable`` for explicit comparisons and membership checks +- ``HasDescription`` for readable strings derived from case names +- ``HasLabel`` when a default label implementation is acceptable + +Example +------- + +.. code-block:: php + + is(Direction::South); // false + +Why traits are useful here +-------------------------- + +Traits are the most natural fit when the enum itself should advertise the +behavior. For example, if other developers are expected to call +``Status::values()`` directly, adding ``HasValues`` to the enum keeps that API +discoverable. + +Trait selection guidance +------------------------ + +Use only the traits that reflect real public behavior: + +- choose ``HasValues`` only for backed enums +- choose ``HasNameLookup`` when name-based input is a real scenario +- choose ``HasOptions`` when the enum regularly feeds forms or prompts +- choose ``Comparable`` when explicit comparison helpers improve readability + +If the trait API would be used in only one place, ``EnumHelper`` is often the +better fit. + +Trait Reference +--------------- + +.. list-table:: + :header-rows: 1 + + * - Trait + - Works with + - Adds + - Typical return + * - ``HasNames`` + - ``UnitEnum`` + - ``names()`` + - ``list`` + * - ``HasValues`` + - ``BackedEnum`` + - ``values()`` + - ``list`` + * - ``HasOptions`` + - ``BackedEnum`` + - ``options()`` + - ``array`` + * - ``HasNameLookup`` + - ``UnitEnum`` + - ``tryFromName()``, ``fromName()``, ``hasName()`` + - enum case, ``null``, or ``bool`` + * - ``HasNameMap`` + - ``UnitEnum`` + - ``nameMap()`` + - ``array`` + * - ``HasValueMap`` + - ``BackedEnum`` + - ``valueMap()`` + - ``array`` + * - ``Comparable`` + - ``UnitEnum`` + - ``is()``, ``isNot()``, ``in()``, ``notIn()`` + - ``bool`` + * - ``HasDescription`` + - ``UnitEnum`` + - ``description()`` + - readable text derived from the case name + * - ``HasLabel`` + - ``UnitEnum`` + - ``label()`` + - ``self::class`` followed by the case name + +The backed-only traits rely on ``$case->value``. PHP exposes that property only +on backed enums, so unit enums should use name-based traits instead. + +Labels and Descriptions +----------------------- + +``HasDescription`` converts the case name into a readable phrase, which is +useful for internal explanations and generated documentation. + +``HasLabel`` intentionally returns a technical fallback label containing the +enum class and case name. That makes it safe for diagnostics because labels from +different enums remain distinguishable. + +.. code-block:: php + + Status::Draft->label(); // 'App\Domain\Status Draft' + +For user-facing UI, implement ``LabeledEnumInterface`` manually so each label +uses product language instead of class names. diff --git a/docs/usage/use-cases.rst b/docs/usage/use-cases.rst new file mode 100644 index 0000000..3cdf60c --- /dev/null +++ b/docs/usage/use-cases.rst @@ -0,0 +1,192 @@ +Use Cases +========= + +The package is intentionally small, but it covers several recurring enum scenarios that show up in +real applications. + +Building select options +----------------------- + +.. code-block:: php + + 'draft', 'Published' => 'published'] + +This is useful for forms, CLI prompts, admin dashboards, and documentation generators. + +Validating incoming values +-------------------------- + +.. code-block:: php + + > + */ + protected static function transitionMap(): array + { + return [ + self::Draft->name => [self::Reviewing, self::Archived], + self::Reviewing->name => [self::Published, self::Draft], + self::Published->name => [self::Archived], + self::Archived->name => [], + ]; + } + } + + ArticleWorkflow::Draft->allowedTransitions(); + ArticleWorkflow::Reviewing->assertCanTransitionTo(ArticleWorkflow::Published); + +This keeps transition rules close to the enum instead of spreading them across conditionals. + +Presenting user-facing labels +----------------------------- + +.. code-block:: php + + 'Low priority', + self::High => 'High priority', + }; + } + } + + EnumHelper::labelMap(Priority::class); // ['Low' => 'Low priority', 'High' => 'High priority'] + +Use explicit labels for public UI text. The default ``HasLabel`` trait is better suited to diagnostics +and fallbacks. + +Filtering with typed operators +------------------------------ + +.. code-block:: php + + compare(10, 5); // true + ComparisonOperator::In->compare('draft', ['draft', 'published']); // true + ComparisonOperator::In->negate(); // ComparisonOperator::NotIn + +Typed operators are useful for query builders, filtering DTOs, rule systems, and admin screens where +plain strings would hide behavior. + +Reusing stable catalogs +----------------------- + +.. code-block:: php + + isAtLeast(LogLevel::Warning); + SortDirection::Descending->applyToComparisonResult(3); + +This works well for cross-project concerns such as logging, sort behavior, runtime environments, or calendar calculations. + +Sorting with explicit behavior +------------------------------ + +.. code-block:: php + + applyToComparisonResult(-1); // -1 + SortDirection::Descending->applyToComparisonResult(-1); // 1 + NullsPosition::Last->compareNullability(null, 'value'); // 1 + CaseSensitivity::Insensitive->equals('Draft', 'draft'); // true + +Troubleshooting ambiguous semantics +----------------------------------- + +If a packaged enum feels almost right but not quite, it is usually a sign that your application +should define its own enum. Reuse the traits and helper APIs instead of stretching a shared catalog +beyond its intended semantics. diff --git a/docs/usage/working-with-enums.rst b/docs/usage/working-with-enums.rst new file mode 100644 index 0000000..f8fba27 --- /dev/null +++ b/docs/usage/working-with-enums.rst @@ -0,0 +1,100 @@ +Working with Enums +================== + +This package does not expose a service container integration layer. In practice, the most common +ways to use it are: + +- importing enum cases directly +- using traits inside enums you control +- calling ``EnumHelper`` from application services, serializers, or validators + +Three common usage styles +------------------------- + +.. list-table:: + :header-rows: 1 + + * - Style + - Best when + - Typical examples + * - Traits on your enum + - You own the enum declaration and want the behavior directly on the type + - ``Status::values()``, ``Direction::names()``, ``Visibility::fromName()`` + * - ``EnumHelper`` + - The enum lives elsewhere or you want to keep utility access external + - presenters, validators, serializers, admin form builders + * - Packaged enums + - The semantics are generic enough to be reused across projects + - ``Environment``, ``LogLevel``, ``Month``, ``SortDirection`` + +Direct enum usage +----------------- + +.. code-block:: php + + isProduction(); + } + } + +Using ``EnumHelper`` from application code +------------------------------------------ + +.. code-block:: php + + $enumClass + */ + public function names(string $enumClass): array + { + return EnumHelper::names($enumClass); + } + } + +Using traits inside your own enum +--------------------------------- + +.. code-block:: php + + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Calendar; + +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum Month: int implements DescribedEnumInterface, LabeledEnumInterface +{ + use Comparable; + use HasLabel; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case January = 1; + case February = 2; + case March = 3; + case April = 4; + case May = 5; + case June = 6; + case July = 7; + case August = 8; + case September = 9; + case October = 10; + case November = 11; + case December = 12; + + /** + * @return string + */ + public function description(): string + { + return match ($this) { + self::January => 'Month 1 of the Gregorian calendar, commonly associated with yearly planning.', + self::February => 'Month 2 of the Gregorian calendar, with 28 days or 29 in leap years.', + self::March => 'Month 3 of the Gregorian calendar, often used as the end of the first quarter.', + self::April => 'Month 4 of the Gregorian calendar, following the close of Q1 in many businesses.', + self::May => 'Month 5 of the Gregorian calendar, typically part of the second quarter.', + self::June => 'Month 6 of the Gregorian calendar and common end of the first half-year.', + self::July => 'Month 7 of the Gregorian calendar and common start of the second half-year.', + self::August => 'Month 8 of the Gregorian calendar, often used in summer scheduling contexts.', + self::September => 'Month 9 of the Gregorian calendar and common start of many annual cycles.', + self::October => 'Month 10 of the Gregorian calendar, typically within fourth-quarter planning.', + self::November => 'Month 11 of the Gregorian calendar, often used for year-end preparation.', + self::December => 'Month 12 of the Gregorian calendar and common close of fiscal or calendar years.', + }; + } + + /** + * @return int + */ + public function quarter(): int + { + return (int) ceil($this->value / 3); + } + + /** + * @return bool + */ + public function isQuarterEnd(): bool + { + return $this->in([self::March, self::June, self::September, self::December]); + } + + /** + * @return list + */ + public static function ordered(): array + { + return [ + self::January, + self::February, + self::March, + self::April, + self::May, + self::June, + self::July, + self::August, + self::September, + self::October, + self::November, + self::December, + ]; + } +} diff --git a/src/Calendar/Quarter.php b/src/Calendar/Quarter.php new file mode 100644 index 0000000..3d7d373 --- /dev/null +++ b/src/Calendar/Quarter.php @@ -0,0 +1,117 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Calendar; + +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum Quarter: int implements DescribedEnumInterface, LabeledEnumInterface +{ + use Comparable; + use HasLabel; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case Q1 = 1; + case Q2 = 2; + case Q3 = 3; + case Q4 = 4; + + /** + * @return string + */ + public function description(): string + { + return match ($this) { + self::Q1 => 'First quarter of the year, covering January through March.', + self::Q2 => 'Second quarter of the year, covering April through June.', + self::Q3 => 'Third quarter of the year, covering July through September.', + self::Q4 => 'Fourth quarter of the year, covering October through December.', + }; + } + + /** + * @return list + */ + public function months(): array + { + return match ($this) { + self::Q1 => [Month::January, Month::February, Month::March], + self::Q2 => [Month::April, Month::May, Month::June], + self::Q3 => [Month::July, Month::August, Month::September], + self::Q4 => [Month::October, Month::November, Month::December], + }; + } + + /** + * @return Month + */ + public function startMonth(): Month + { + return $this->months()[0]; + } + + /** + * @return Month + */ + public function endMonth(): Month + { + return $this->months()[2]; + } + + /** + * @param Month $month + * + * @return bool + */ + public function includes(Month $month): bool + { + return $month->quarter() === $this->value; + } + + /** + * @return list + */ + public static function ordered(): array + { + return [self::Q1, self::Q2, self::Q3, self::Q4]; + } + + /** + * @param Month $month + * + * @return self + */ + public static function fromMonth(Month $month): self + { + return self::from($month->quarter()); + } +} diff --git a/src/Calendar/Semester.php b/src/Calendar/Semester.php new file mode 100644 index 0000000..4e1b2c0 --- /dev/null +++ b/src/Calendar/Semester.php @@ -0,0 +1,131 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Calendar; + +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum Semester: int implements DescribedEnumInterface, LabeledEnumInterface +{ + use Comparable; + use HasLabel; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case H1 = 1; + case H2 = 2; + + /** + * @return string + */ + public function description(): string + { + return match ($this) { + self::H1 => 'First half of the year, covering January through June.', + self::H2 => 'Second half of the year, covering July through December.', + }; + } + + /** + * @return list + */ + public function months(): array + { + return match ($this) { + self::H1 => [Month::January, Month::February, Month::March, Month::April, Month::May, Month::June], + self::H2 => [ + Month::July, + Month::August, + Month::September, + Month::October, + Month::November, + Month::December, + ], + }; + } + + /** + * @return list + */ + public function quarters(): array + { + return match ($this) { + self::H1 => [Quarter::Q1, Quarter::Q2], + self::H2 => [Quarter::Q3, Quarter::Q4], + }; + } + + /** + * @return Month + */ + public function startMonth(): Month + { + return $this->months()[0]; + } + + /** + * @return Month + */ + public function endMonth(): Month + { + return $this->months()[5]; + } + + /** + * @param Month $month + * + * @return bool + */ + public function includes(Month $month): bool + { + return $this->is(self::fromMonth($month)); + } + + /** + * @param Month $month + * + * @return self + */ + public static function fromMonth(Month $month): self + { + return $month->value <= Month::June->value ? self::H1 : self::H2; + } + + /** + * @param Quarter $quarter + * + * @return self + */ + public static function fromQuarter(Quarter $quarter): self + { + return $quarter->value <= Quarter::Q2->value ? self::H1 : self::H2; + } +} diff --git a/src/Calendar/Weekday.php b/src/Calendar/Weekday.php new file mode 100644 index 0000000..4bb7846 --- /dev/null +++ b/src/Calendar/Weekday.php @@ -0,0 +1,98 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Calendar; + +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum Weekday: int implements DescribedEnumInterface, LabeledEnumInterface +{ + use Comparable; + use HasLabel; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case Monday = 1; + case Tuesday = 2; + case Wednesday = 3; + case Thursday = 4; + case Friday = 5; + case Saturday = 6; + case Sunday = 7; + + /** + * @return string + */ + public function description(): string + { + return match ($this) { + self::Monday => 'First business day of the ISO week in most workflows and calendars.', + self::Tuesday => 'Second day of the ISO week, commonly used for regular working schedules.', + self::Wednesday => 'Midweek day often used for routine meetings and delivery checkpoints.', + self::Thursday => 'Late-week working day before typical end-of-week wrap-up.', + self::Friday => 'Final common business day before the weekend in many regions.', + self::Saturday => 'Weekend day typically treated as non-working in standard business calendars.', + self::Sunday => 'Weekend day that closes the ISO week and often precedes planning for Monday.', + }; + } + + /** + * @return bool + */ + public function isWeekend(): bool + { + return $this->in([self::Saturday, self::Sunday]); + } + + /** + * @return bool + */ + public function isWeekday(): bool + { + return ! $this->isWeekend(); + } + + /** + * @return list + */ + public static function ordered(): array + { + return [ + self::Monday, + self::Tuesday, + self::Wednesday, + self::Thursday, + self::Friday, + self::Saturday, + self::Sunday, + ]; + } +} diff --git a/src/Common/Priority.php b/src/Common/Priority.php new file mode 100644 index 0000000..875074d --- /dev/null +++ b/src/Common/Priority.php @@ -0,0 +1,96 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Common; + +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum Priority: int implements DescribedEnumInterface, LabeledEnumInterface +{ + use Comparable; + use HasLabel; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case Low = 10; + case Normal = 20; + case High = 30; + case Critical = 40; + + /** + * @return string + */ + public function description(): string + { + return match ($this) { + self::Low => 'Can be handled later with minimal impact if delayed.', + self::Normal => 'Default priority for routine work and expected processing order.', + self::High => 'Needs prompt attention because delays may affect users or delivery.', + self::Critical => 'Requires immediate attention due to severe business or operational impact.', + }; + } + + /** + * @return int + */ + public function weight(): int + { + return $this->value; + } + + /** + * @param self $other + * + * @return bool + */ + public function isHigherThan(self $other): bool + { + return $this->value > $other->value; + } + + /** + * @param self $other + * + * @return bool + */ + public function isLowerThan(self $other): bool + { + return $this->value < $other->value; + } + + /** + * @return list + */ + public static function ordered(): array + { + return [self::Low, self::Normal, self::High, self::Critical]; + } +} diff --git a/src/Common/Severity.php b/src/Common/Severity.php new file mode 100644 index 0000000..8b378ce --- /dev/null +++ b/src/Common/Severity.php @@ -0,0 +1,97 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Common; + +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum Severity: string implements DescribedEnumInterface, LabeledEnumInterface +{ + use Comparable; + use HasLabel; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case Debug = 'debug'; + case Info = 'info'; + case Notice = 'notice'; + case Warning = 'warning'; + case Error = 'error'; + case Critical = 'critical'; + + /** + * @return string + */ + public function description(): string + { + return match ($this) { + self::Debug => 'Diagnostic information intended for development and troubleshooting.', + self::Info => 'Informational event describing normal application flow or notable activity.', + self::Notice => 'Significant but expected condition that may deserve attention soon.', + self::Warning => 'Potential problem that should be reviewed before it escalates.', + self::Error => 'Failure condition that affected part of the current operation.', + self::Critical => 'Severe failure requiring urgent human or automated intervention.', + }; + } + + /** + * @return int + */ + public function weight(): int + { + return match ($this) { + self::Debug => 10, + self::Info => 20, + self::Notice => 30, + self::Warning => 40, + self::Error => 50, + self::Critical => 60, + }; + } + + /** + * @param self $severity + * + * @return bool + */ + public function isAtLeast(self $severity): bool + { + return $this->weight() >= $severity->weight(); + } + + /** + * @return list + */ + public static function ordered(): array + { + return [self::Debug, self::Info, self::Notice, self::Warning, self::Error, self::Critical]; + } +} diff --git a/src/Comparison/ComparisonOperator.php b/src/Comparison/ComparisonOperator.php new file mode 100644 index 0000000..244c912 --- /dev/null +++ b/src/Comparison/ComparisonOperator.php @@ -0,0 +1,145 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Comparison; + +use ValueError; +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum ComparisonOperator: string implements DescribedEnumInterface, LabeledEnumInterface +{ + use Comparable; + use HasLabel; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case Equal = 'eq'; + case NotEqual = 'neq'; + case GreaterThan = 'gt'; + case GreaterThanOrEqual = 'gte'; + case LessThan = 'lt'; + case LessThanOrEqual = 'lte'; + case In = 'in'; + case NotIn = 'not_in'; + + /** + * @return string + */ + public function description(): string + { + return match ($this) { + self::Equal => 'Matches when both operands are strictly equal.', + self::NotEqual => 'Matches when both operands are not strictly equal.', + self::GreaterThan => 'Matches when the left operand is greater than the right operand.', + self::GreaterThanOrEqual => 'Matches when the left operand is greater than or equal to the right operand.', + self::LessThan => 'Matches when the left operand is less than the right operand.', + self::LessThanOrEqual => 'Matches when the left operand is less than or equal to the right operand.', + self::In => 'Matches when the left operand is contained in the right-hand candidate set.', + self::NotIn => 'Matches when the left operand is not contained in the right-hand candidate set.', + }; + } + + /** + * @return string + */ + public function symbol(): string + { + return match ($this) { + self::Equal => '=', + self::NotEqual => '!=', + self::GreaterThan => '>', + self::GreaterThanOrEqual => '>=', + self::LessThan => '<', + self::LessThanOrEqual => '<=', + self::In => 'IN', + self::NotIn => 'NOT IN', + }; + } + + /** + * @return bool + */ + public function isSetOperator(): bool + { + return $this->in([self::In, self::NotIn]); + } + + /** + * @param mixed $left + * @param mixed $right + * + * @return bool + */ + public function compare(mixed $left, mixed $right): bool + { + return match ($this) { + self::Equal => $left === $right, + self::NotEqual => $left !== $right, + self::GreaterThan => $left > $right, + self::GreaterThanOrEqual => $left >= $right, + self::LessThan => $left < $right, + self::LessThanOrEqual => $left <= $right, + self::In => \in_array($left, $this->candidateSet($right), true), + self::NotIn => ! \in_array($left, $this->candidateSet($right), true), + }; + } + + /** + * @return self + */ + public function negate(): self + { + return match ($this) { + self::Equal => self::NotEqual, + self::NotEqual => self::Equal, + self::GreaterThan => self::LessThanOrEqual, + self::GreaterThanOrEqual => self::LessThan, + self::LessThan => self::GreaterThanOrEqual, + self::LessThanOrEqual => self::GreaterThan, + self::In => self::NotIn, + self::NotIn => self::In, + }; + } + + /** + * @param mixed $right + * + * @return list + */ + private function candidateSet(mixed $right): array + { + if (! \is_array($right)) { + throw new ValueError(\sprintf('Comparison operator %s expects an array candidate set.', $this->name)); + } + + return array_values($right); + } +} diff --git a/src/Container/ServiceLifetime.php b/src/Container/ServiceLifetime.php new file mode 100644 index 0000000..9d3aa04 --- /dev/null +++ b/src/Container/ServiceLifetime.php @@ -0,0 +1,64 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Container; + +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum ServiceLifetime: string implements DescribedEnumInterface, LabeledEnumInterface +{ + use Comparable; + use HasLabel; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case Singleton = 'singleton'; + case Transient = 'transient'; + + /** + * @return string + */ + public function description(): string + { + return match ($this) { + self::Singleton => 'One shared instance is reused for the full container lifetime.', + self::Transient => 'A new instance is created for each resolution.', + }; + } + + /** + * @return bool + */ + public function isReusable(): bool + { + return $this->is(self::Singleton); + } +} diff --git a/src/DateTime/IntervalUnit.php b/src/DateTime/IntervalUnit.php new file mode 100644 index 0000000..65eb7ec --- /dev/null +++ b/src/DateTime/IntervalUnit.php @@ -0,0 +1,108 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\DateTime; + +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum IntervalUnit: string implements DescribedEnumInterface, LabeledEnumInterface +{ + use Comparable; + use HasLabel; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case Second = 'second'; + case Minute = 'minute'; + case Hour = 'hour'; + case Day = 'day'; + case Week = 'week'; + case Month = 'month'; + case Year = 'year'; + + /** + * @return string + */ + public function description(): string + { + return match ($this) { + self::Second => 'Represents one-second intervals for fine-grained timing and retry policies.', + self::Minute => 'Represents one-minute intervals for short-lived scheduling and cache policies.', + self::Hour => 'Represents one-hour intervals for operational windows and batch execution.', + self::Day => 'Represents one-day intervals for daily schedules and retention policies.', + self::Week => 'Represents one-week intervals for weekly planning, reports, and cleanup jobs.', + self::Month => 'Represents one-month intervals for monthly billing, reporting, and rotation schedules.', + self::Year => 'Represents one-year intervals for annual cycles, compliance, and archival horizons.', + }; + } + + /** + * @return string + */ + public function shortLabel(): string + { + return match ($this) { + self::Second => 's', + self::Minute => 'min', + self::Hour => 'h', + self::Day => 'd', + self::Week => 'w', + self::Month => 'mo', + self::Year => 'y', + }; + } + + /** + * @param int $amount + * + * @return int + */ + public function seconds(int $amount = 1): int + { + return $amount * match ($this) { + self::Second => 1, + self::Minute => 60, + self::Hour => 3600, + self::Day => 86400, + self::Week => 604800, + self::Month => 2628000, + self::Year => 31536000, + }; + } + + /** + * @return bool + */ + public function isCalendarAware(): bool + { + return $this->in([self::Month, self::Year]); + } +} diff --git a/src/DescribedEnumInterface.php b/src/DescribedEnumInterface.php new file mode 100644 index 0000000..bcf3227 --- /dev/null +++ b/src/DescribedEnumInterface.php @@ -0,0 +1,27 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum; + +interface DescribedEnumInterface +{ + /** + * @return string + */ + public function description(): string; +} diff --git a/src/Event/DispatchMode.php b/src/Event/DispatchMode.php new file mode 100644 index 0000000..562af04 --- /dev/null +++ b/src/Event/DispatchMode.php @@ -0,0 +1,72 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Event; + +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum DispatchMode: string implements DescribedEnumInterface, LabeledEnumInterface +{ + use Comparable; + use HasLabel; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case Sync = 'sync'; + case Async = 'async'; + + /** + * @return string + */ + public function description(): string + { + return match ($this) { + self::Sync => 'Dispatches listeners immediately within the current execution flow.', + self::Async => 'Dispatches listeners through a deferred or queued execution path.', + }; + } + + /** + * @return bool + */ + public function isSync(): bool + { + return $this->is(self::Sync); + } + + /** + * @return bool + */ + public function isAsync(): bool + { + return $this->is(self::Async); + } +} diff --git a/src/Helper/EnumHelper.php b/src/Helper/EnumHelper.php new file mode 100644 index 0000000..9070544 --- /dev/null +++ b/src/Helper/EnumHelper.php @@ -0,0 +1,269 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Helper; + +use BackedEnum; +use UnitEnum; +use ValueError; +use FastForward\Enum\LabeledEnumInterface; + +final class EnumHelper +{ + /** + * @template T of UnitEnum + * + * @param class-string|T $enum + * + * @return list + */ + public static function cases(string|UnitEnum $enum): array + { + $enumClass = self::unitEnumClass($enum); + + return $enumClass::cases(); + } + + /** + * @template T of UnitEnum + * + * @param class-string|T $enum + * + * @return list + */ + public static function names(string|UnitEnum $enum): array + { + $enumClass = self::unitEnumClass($enum); + + return array_map(static fn(UnitEnum $case): string => $case->name, $enumClass::cases()); + } + + /** + * @template T of BackedEnum + * + * @param class-string|T $enum + * + * @return list + */ + public static function values(string|BackedEnum $enum): array + { + $enumClass = self::backedEnumClass($enum); + + return array_map(static fn(BackedEnum $case): int|string => $case->value, $enumClass::cases()); + } + + /** + * @template T of UnitEnum + * + * @param class-string|T $enum + * + * @return array + */ + public static function nameMap(string|UnitEnum $enum): array + { + $enumClass = self::unitEnumClass($enum); + $map = []; + + foreach ($enumClass::cases() as $case) { + $map[$case->name] = $case; + } + + return $map; + } + + /** + * @template T of BackedEnum + * + * @param class-string|T $enum + * + * @return array + */ + public static function valueMap(string|BackedEnum $enum): array + { + $enumClass = self::backedEnumClass($enum); + $map = []; + + foreach ($enumClass::cases() as $case) { + $map[$case->value] = $case; + } + + return $map; + } + + /** + * @template T of BackedEnum + * + * @param class-string|T $enum + * + * @return array + */ + public static function options(string|BackedEnum $enum): array + { + $enumClass = self::backedEnumClass($enum); + $options = []; + + foreach ($enumClass::cases() as $case) { + $options[$case->name] = $case->value; + } + + return $options; + } + + /** + * @template T of UnitEnum + * + * @param class-string|T $enum + * @param string $name + */ + public static function hasName(string|UnitEnum $enum, string $name): bool + { + return isset(self::nameMap($enum)[$name]); + } + + /** + * @template T of BackedEnum + * + * @param class-string|T $enum + * @param int|string $value + */ + public static function hasValue(string|BackedEnum $enum, int|string $value): bool + { + $enumClass = self::backedEnumClass($enum); + + return null !== $enumClass::tryFrom($value); + } + + /** + * @template T of UnitEnum + * + * @param class-string|T $enum + * @param string $name + * + * @return T|null + */ + public static function tryFromName(string|UnitEnum $enum, string $name): ?UnitEnum + { + return self::nameMap($enum)[$name] ?? null; + } + + /** + * @template T of UnitEnum + * + * @param class-string|T $enum + * @param string $name + * + * @return T + */ + public static function fromName(string|UnitEnum $enum, string $name): UnitEnum + { + $enumClass = self::unitEnumClass($enum); + + return self::tryFromName($enumClass, $name) + ?? throw new ValueError(\sprintf('"%s" is not a valid name for enum %s.', $name, $enumClass)); + } + + /** + * @template T of UnitEnum&LabeledEnumInterface + * + * @param class-string|T $enum + * + * @return list + */ + public static function labels(string|UnitEnum $enum): array + { + $enumClass = self::labeledEnumClass($enum); + + return array_map(static fn(LabeledEnumInterface $case): string => $case->label(), $enumClass::cases()); + } + + /** + * @template T of UnitEnum&LabeledEnumInterface + * + * @param class-string|T $enum + * + * @return array + */ + public static function labelMap(string|UnitEnum $enum): array + { + $enumClass = self::labeledEnumClass($enum); + $map = []; + + foreach ($enumClass::cases() as $case) { + $map[$case->name] = $case->label(); + } + + return $map; + } + + /** + * @template T of UnitEnum + * + * @param class-string|T $enum + * + * @return class-string + */ + private static function unitEnumClass(string|UnitEnum $enum): string + { + /** @var class-string $enumClass */ + $enumClass = \is_string($enum) ? $enum : $enum::class; + + if (! enum_exists($enumClass) || ! is_subclass_of($enumClass, UnitEnum::class, true)) { + throw new ValueError(\sprintf('Enum %s must be a unit enum.', $enumClass)); + } + + return $enumClass; + } + + /** + * @template T of BackedEnum + * + * @param class-string|T $enum + * + * @return class-string + */ + private static function backedEnumClass(string|BackedEnum $enum): string + { + /** @var class-string $enumClass */ + $enumClass = \is_string($enum) ? $enum : $enum::class; + + if (! enum_exists($enumClass) || ! is_subclass_of($enumClass, BackedEnum::class, true)) { + throw new ValueError(\sprintf('Enum %s must be a backed enum.', $enumClass)); + } + + return $enumClass; + } + + /** + * @template T of UnitEnum&LabeledEnumInterface + * + * @param class-string|T $enum + * + * @return class-string + */ + private static function labeledEnumClass(string|UnitEnum $enum): string + { + /** @var class-string $enumClass */ + $enumClass = self::unitEnumClass($enum); + + if (! is_subclass_of($enumClass, LabeledEnumInterface::class, true)) { + throw new ValueError(\sprintf('Enum %s must implement %s.', $enumClass, LabeledEnumInterface::class)); + } + + return $enumClass; + } +} diff --git a/src/Http/Scheme.php b/src/Http/Scheme.php new file mode 100644 index 0000000..e24a66d --- /dev/null +++ b/src/Http/Scheme.php @@ -0,0 +1,75 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Http; + +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum Scheme: string implements DescribedEnumInterface, LabeledEnumInterface +{ + use Comparable; + use HasLabel; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case Http = 'http'; + case Https = 'https'; + + /** + * @return string + */ + public function description(): string + { + return match ($this) { + self::Http => 'Plain HTTP transport without transport-layer encryption.', + self::Https => 'HTTP transport secured with TLS encryption.', + }; + } + + /** + * @return int + */ + public function defaultPort(): int + { + return match ($this) { + self::Http => 80, + self::Https => 443, + }; + } + + /** + * @return bool + */ + public function isSecure(): bool + { + return $this->is(self::Https); + } +} diff --git a/src/LabeledEnumInterface.php b/src/LabeledEnumInterface.php new file mode 100644 index 0000000..045e209 --- /dev/null +++ b/src/LabeledEnumInterface.php @@ -0,0 +1,27 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum; + +interface LabeledEnumInterface +{ + /** + * @return string + */ + public function label(): string; +} diff --git a/src/Logger/LogLevel.php b/src/Logger/LogLevel.php new file mode 100644 index 0000000..013395c --- /dev/null +++ b/src/Logger/LogLevel.php @@ -0,0 +1,112 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Logger; + +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum LogLevel: string implements DescribedEnumInterface, LabeledEnumInterface +{ + use Comparable; + use HasLabel; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case Emergency = 'emergency'; + case Alert = 'alert'; + case Critical = 'critical'; + case Error = 'error'; + case Warning = 'warning'; + case Notice = 'notice'; + case Info = 'info'; + case Debug = 'debug'; + + /** + * @return string + */ + public function description(): string + { + return match ($this) { + self::Emergency => 'System is unusable and requires immediate global attention.', + self::Alert => 'Action must be taken immediately to avoid severe service disruption.', + self::Critical => 'Critical condition indicating serious failure in a core capability.', + self::Error => 'Runtime error indicating part of the current operation failed.', + self::Warning => 'Potential issue that should be reviewed before it becomes an error.', + self::Notice => 'Normal but noteworthy event that may deserve operational awareness.', + self::Info => 'Informational event describing expected application flow.', + self::Debug => 'Verbose diagnostic information intended for debugging and development.', + }; + } + + /** + * @return int + */ + public function weight(): int + { + return match ($this) { + self::Debug => 10, + self::Info => 20, + self::Notice => 30, + self::Warning => 40, + self::Error => 50, + self::Critical => 60, + self::Alert => 70, + self::Emergency => 80, + }; + } + + /** + * @param self $level + * + * @return bool + */ + public function isAtLeast(self $level): bool + { + return $this->weight() >= $level->weight(); + } + + /** + * @return list + */ + public static function ordered(): array + { + return [ + self::Debug, + self::Info, + self::Notice, + self::Warning, + self::Error, + self::Critical, + self::Alert, + self::Emergency, + ]; + } +} diff --git a/src/Outcome/Result.php b/src/Outcome/Result.php new file mode 100644 index 0000000..2f18dbe --- /dev/null +++ b/src/Outcome/Result.php @@ -0,0 +1,82 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Outcome; + +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum Result: string implements DescribedEnumInterface, LabeledEnumInterface +{ + use Comparable; + use HasLabel; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case Success = 'success'; + case Partial = 'partial'; + case Failure = 'failure'; + + /** + * @return string + */ + public function description(): string + { + return match ($this) { + self::Success => 'Operation completed successfully without notable issues.', + self::Partial => 'Operation completed only in part and may require follow-up.', + self::Failure => 'Operation did not complete successfully.', + }; + } + + /** + * @return bool + */ + public function isSuccessful(): bool + { + return $this->in([self::Success, self::Partial]); + } + + /** + * @return bool + */ + public function isCompleteSuccess(): bool + { + return $this->is(self::Success); + } + + /** + * @return bool + */ + public function isFailure(): bool + { + return $this->is(self::Failure); + } +} diff --git a/src/Pipeline/FailureMode.php b/src/Pipeline/FailureMode.php new file mode 100644 index 0000000..4c5507e --- /dev/null +++ b/src/Pipeline/FailureMode.php @@ -0,0 +1,64 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Pipeline; + +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum FailureMode: string implements DescribedEnumInterface, LabeledEnumInterface +{ + use Comparable; + use HasLabel; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case StopOnFailure = 'stop_on_failure'; + case ContinueOnFailure = 'continue_on_failure'; + + /** + * @return string + */ + public function description(): string + { + return match ($this) { + self::StopOnFailure => 'Stops the pipeline immediately when a stage fails.', + self::ContinueOnFailure => 'Continues processing remaining stages even after a failure.', + }; + } + + /** + * @return bool + */ + public function stopsOnFailure(): bool + { + return $this->is(self::StopOnFailure); + } +} diff --git a/src/Process/SignalBehavior.php b/src/Process/SignalBehavior.php new file mode 100644 index 0000000..fe411ee --- /dev/null +++ b/src/Process/SignalBehavior.php @@ -0,0 +1,66 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Process; + +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum SignalBehavior: string implements DescribedEnumInterface, LabeledEnumInterface +{ + use Comparable; + use HasLabel; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case Ignore = 'ignore'; + case Handle = 'handle'; + case Propagate = 'propagate'; + + /** + * @return string + */ + public function description(): string + { + return match ($this) { + self::Ignore => 'Signal is observed but intentionally ignored.', + self::Handle => 'Signal is intercepted and processed locally.', + self::Propagate => 'Signal is forwarded to child processes or outer handlers.', + }; + } + + /** + * @return bool + */ + public function isTerminalControl(): bool + { + return $this->in([self::Handle, self::Propagate]); + } +} diff --git a/src/ReversibleInterface.php b/src/ReversibleInterface.php new file mode 100644 index 0000000..13927cc --- /dev/null +++ b/src/ReversibleInterface.php @@ -0,0 +1,27 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum; + +interface ReversibleInterface +{ + /** + * @return static + */ + public function reverse(): static; +} diff --git a/src/Runtime/Environment.php b/src/Runtime/Environment.php new file mode 100644 index 0000000..28d8406 --- /dev/null +++ b/src/Runtime/Environment.php @@ -0,0 +1,84 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Runtime; + +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum Environment: string implements DescribedEnumInterface, LabeledEnumInterface +{ + use Comparable; + use HasLabel; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case Development = 'development'; + case Testing = 'testing'; + case Staging = 'staging'; + case Production = 'production'; + + /** + * @return string + */ + public function description(): string + { + return match ($this) { + self::Development => 'Local development environment with fast feedback and debugging enabled.', + self::Testing => 'Automated test environment intended for repeatable checks and validation.', + self::Staging => 'Pre-production environment used to verify releases before going live.', + self::Production => 'Live environment serving real users and production workloads.', + }; + } + + /** + * @return bool + */ + public function isProduction(): bool + { + return self::Production === $this; + } + + /** + * @return bool + */ + public function isPreProduction(): bool + { + return $this->in([self::Development, self::Testing, self::Staging]); + } + + /** + * @return bool + */ + public function isDebugFriendly(): bool + { + return $this->in([self::Development, self::Testing]); + } +} diff --git a/src/Sort/CaseSensitivity.php b/src/Sort/CaseSensitivity.php new file mode 100644 index 0000000..8ba74f1 --- /dev/null +++ b/src/Sort/CaseSensitivity.php @@ -0,0 +1,84 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Sort; + +use FastForward\Enum\ReversibleInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; + +enum CaseSensitivity implements ReversibleInterface +{ + use Comparable; + use HasNameLookup; + use HasNameMap; + use HasNames; + + case Sensitive; + case Insensitive; + + /** + * @return static + */ + public function reverse(): static + { + return match ($this) { + self::Sensitive => self::Insensitive, + self::Insensitive => self::Sensitive, + }; + } + + /** + * @return bool + */ + public function isSensitive(): bool + { + return $this->is(self::Sensitive); + } + + /** + * @return bool + */ + public function isInsensitive(): bool + { + return $this->is(self::Insensitive); + } + + /** + * @param string $value + * + * @return string + */ + public function normalize(string $value): string + { + return $this->isInsensitive() ? mb_strtolower($value) : $value; + } + + /** + * @param string $left + * @param string $right + * + * @return bool + */ + public function equals(string $left, string $right): bool + { + return $this->normalize($left) === $this->normalize($right); + } +} diff --git a/src/Sort/ComparisonResult.php b/src/Sort/ComparisonResult.php new file mode 100644 index 0000000..aac5175 --- /dev/null +++ b/src/Sort/ComparisonResult.php @@ -0,0 +1,86 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Sort; + +use FastForward\Enum\ReversibleInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; + +enum ComparisonResult implements ReversibleInterface +{ + use Comparable; + use HasNameLookup; + use HasNameMap; + use HasNames; + + case LeftGreater; + case RightGreater; + case Equal; + case Incomparable; + + /** + * @param int $result + * + * @return self + */ + public static function fromComparisonResult(int $result): self + { + return match (true) { + $result > 0 => self::LeftGreater, + $result < 0 => self::RightGreater, + default => self::Equal, + }; + } + + /** + * @return int + */ + public function toComparisonResult(): int + { + return match ($this) { + self::LeftGreater => 1, + self::RightGreater => -1, + self::Equal => 0, + // Mirrors PHP's legacy comparator fallback for values that are not logically comparable. + self::Incomparable => 1, + }; + } + + /** + * @return static + */ + public function reverse(): static + { + return match ($this) { + self::LeftGreater => self::RightGreater, + self::RightGreater => self::LeftGreater, + default => $this, + }; + } + + /** + * @return bool + */ + public function isComparable(): bool + { + return ! $this->is(self::Incomparable); + } +} diff --git a/src/Sort/NullsPosition.php b/src/Sort/NullsPosition.php new file mode 100644 index 0000000..e03bfea --- /dev/null +++ b/src/Sort/NullsPosition.php @@ -0,0 +1,86 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Sort; + +use FastForward\Enum\ReversibleInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; + +enum NullsPosition implements ReversibleInterface +{ + use Comparable; + use HasNameLookup; + use HasNameMap; + use HasNames; + + case First; + case Last; + + /** + * @return static + */ + public function reverse(): static + { + return match ($this) { + self::First => self::Last, + self::Last => self::First, + }; + } + + /** + * @return bool + */ + public function isFirst(): bool + { + return $this->is(self::First); + } + + /** + * @return bool + */ + public function isLast(): bool + { + return $this->is(self::Last); + } + + /** + * @param mixed $left + * @param mixed $right + * + * @return int + */ + public function compareNullability(mixed $left, mixed $right): int + { + if (null === $left && null === $right) { + return 0; + } + + if (null === $left) { + return $this->isFirst() ? -1 : 1; + } + + if (null === $right) { + return $this->isFirst() ? 1 : -1; + } + + return 0; + } +} diff --git a/src/Sort/SortDirection.php b/src/Sort/SortDirection.php new file mode 100644 index 0000000..ff7bb20 --- /dev/null +++ b/src/Sort/SortDirection.php @@ -0,0 +1,84 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Sort; + +use FastForward\Enum\ReversibleInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; + +enum SortDirection implements ReversibleInterface +{ + use Comparable; + use HasNameLookup; + use HasNameMap; + use HasNames; + + case Ascending; + case Descending; + + /** + * @return static + */ + public function reverse(): static + { + return match ($this) { + self::Ascending => self::Descending, + self::Descending => self::Ascending, + }; + } + + /** + * @return bool + */ + public function isAscending(): bool + { + return $this->is(self::Ascending); + } + + /** + * @return bool + */ + public function isDescending(): bool + { + return $this->is(self::Descending); + } + + /** + * @return int + */ + public function factor(): int + { + return match ($this) { + self::Ascending => 1, + self::Descending => -1, + }; + } + + /** + * @param int $result + * + * @return int + */ + public function applyToComparisonResult(int $result): int + { + return $result * $this->factor(); + } +} diff --git a/src/StateMachine/HasTransitions.php b/src/StateMachine/HasTransitions.php new file mode 100644 index 0000000..bbd6271 --- /dev/null +++ b/src/StateMachine/HasTransitions.php @@ -0,0 +1,114 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\StateMachine; + +trait HasTransitions +{ + /** + * @return array> + */ + abstract protected static function transitionMap(): array; + + /** + * @return list + */ + protected static function initialStateCases(): array + { + return []; + } + + /** + * @return list + */ + public function allowedTransitions(): array + { + return self::transitionMap()[$this->name] ?? []; + } + + /** + * @param self $target + * + * @return bool + */ + public function canTransitionTo(self $target): bool + { + return \in_array($target, $this->allowedTransitions(), true); + } + + /** + * @param self $target + * + * @return void + */ + public function assertCanTransitionTo(self $target): void + { + if (! $this->canTransitionTo($target)) { + throw InvalidTransitionException::between($this, $target); + } + } + + /** + * @return bool + */ + public function isTerminal(): bool + { + return [] === $this->allowedTransitions(); + } + + /** + * @return bool + */ + public function isInitial(): bool + { + return \in_array($this, self::initialStates(), true); + } + + /** + * @return list + */ + public static function initialStates(): array + { + $initialStates = static::initialStateCases(); + + if ([] !== $initialStates) { + return $initialStates; + } + + $incoming = []; + + foreach (self::transitionMap() as $targets) { + foreach ($targets as $target) { + $incoming[$target->name] = true; + } + } + + return array_values(array_filter( + self::cases(), + static fn(self $case): bool => ! isset($incoming[$case->name]), + )); + } + + /** + * @return list + */ + public static function terminalStates(): array + { + return array_values(array_filter(self::cases(), static fn(self $case): bool => $case->isTerminal())); + } +} diff --git a/src/StateMachine/InvalidTransitionException.php b/src/StateMachine/InvalidTransitionException.php new file mode 100644 index 0000000..9aa7046 --- /dev/null +++ b/src/StateMachine/InvalidTransitionException.php @@ -0,0 +1,42 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\StateMachine; + +use DomainException; +use UnitEnum; + +final class InvalidTransitionException extends DomainException +{ + /** + * @param UnitEnum $from + * @param UnitEnum $to + * + * @return self + */ + public static function between(UnitEnum $from, UnitEnum $to): self + { + return new self(\sprintf( + 'Invalid transition from %s::%s to %s::%s.', + $from::class, + $from->name, + $to::class, + $to->name, + )); + } +} diff --git a/src/Trait/Comparable.php b/src/Trait/Comparable.php new file mode 100644 index 0000000..a6746a2 --- /dev/null +++ b/src/Trait/Comparable.php @@ -0,0 +1,58 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Trait; + +trait Comparable +{ + /** + * @param self $other + * + * @return bool + */ + public function is(self $other): bool + { + return $this === $other; + } + + /** + * @param self $other + * + * @return bool + */ + public function isNot(self $other): bool + { + return $this !== $other; + } + + /** + * @param list $cases + */ + public function in(array $cases): bool + { + return \in_array($this, $cases, true); + } + + /** + * @param list $cases + */ + public function notIn(array $cases): bool + { + return ! $this->in($cases); + } +} diff --git a/src/Trait/HasDescription.php b/src/Trait/HasDescription.php new file mode 100644 index 0000000..51d7e8b --- /dev/null +++ b/src/Trait/HasDescription.php @@ -0,0 +1,32 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Trait; + +trait HasDescription +{ + /** + * @return string + */ + public function description(): string + { + $description = preg_replace('/(?name); + + return $description ?? $this->name; + } +} diff --git a/src/Trait/HasLabel.php b/src/Trait/HasLabel.php new file mode 100644 index 0000000..07e8595 --- /dev/null +++ b/src/Trait/HasLabel.php @@ -0,0 +1,30 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Trait; + +trait HasLabel +{ + /** + * @return string + */ + public function label(): string + { + return \sprintf('%s %s', self::class, $this->name); + } +} diff --git a/src/Trait/HasNameLookup.php b/src/Trait/HasNameLookup.php new file mode 100644 index 0000000..ba2bfc6 --- /dev/null +++ b/src/Trait/HasNameLookup.php @@ -0,0 +1,60 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Trait; + +use FastForward\Enum\Helper\EnumHelper; + +trait HasNameLookup +{ + /** + * @param string $name + * + * @return self|null + */ + public static function tryFromName(string $name): ?self + { + /** @var self|null $case */ + $case = EnumHelper::tryFromName(self::class, $name); + + return $case; + } + + /** + * @param string $name + * + * @return self + */ + public static function fromName(string $name): self + { + /** @var self $case */ + $case = EnumHelper::fromName(self::class, $name); + + return $case; + } + + /** + * @param string $name + * + * @return bool + */ + public static function hasName(string $name): bool + { + return EnumHelper::hasName(self::class, $name); + } +} diff --git a/src/Trait/HasNameMap.php b/src/Trait/HasNameMap.php new file mode 100644 index 0000000..454830c --- /dev/null +++ b/src/Trait/HasNameMap.php @@ -0,0 +1,35 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Trait; + +use FastForward\Enum\Helper\EnumHelper; + +trait HasNameMap +{ + /** + * @return array + */ + public static function nameMap(): array + { + /** @var array $map */ + $map = EnumHelper::nameMap(self::class); + + return $map; + } +} diff --git a/src/Trait/HasNames.php b/src/Trait/HasNames.php new file mode 100644 index 0000000..b3a95e6 --- /dev/null +++ b/src/Trait/HasNames.php @@ -0,0 +1,32 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Trait; + +use FastForward\Enum\Helper\EnumHelper; + +trait HasNames +{ + /** + * @return list + */ + public static function names(): array + { + return EnumHelper::names(self::class); + } +} diff --git a/src/Trait/HasOptions.php b/src/Trait/HasOptions.php new file mode 100644 index 0000000..995eb9e --- /dev/null +++ b/src/Trait/HasOptions.php @@ -0,0 +1,32 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Trait; + +use FastForward\Enum\Helper\EnumHelper; + +trait HasOptions +{ + /** + * @return array + */ + public static function options(): array + { + return EnumHelper::options(self::class); + } +} diff --git a/src/Trait/HasValueMap.php b/src/Trait/HasValueMap.php new file mode 100644 index 0000000..4e6e40d --- /dev/null +++ b/src/Trait/HasValueMap.php @@ -0,0 +1,35 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Trait; + +use FastForward\Enum\Helper\EnumHelper; + +trait HasValueMap +{ + /** + * @return array + */ + public static function valueMap(): array + { + /** @var array $map */ + $map = EnumHelper::valueMap(self::class); + + return $map; + } +} diff --git a/src/Trait/HasValues.php b/src/Trait/HasValues.php new file mode 100644 index 0000000..b7908fa --- /dev/null +++ b/src/Trait/HasValues.php @@ -0,0 +1,32 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Trait; + +use FastForward\Enum\Helper\EnumHelper; + +trait HasValues +{ + /** + * @return list + */ + public static function values(): array + { + return EnumHelper::values(self::class); + } +} diff --git a/tests/Calendar/MonthTest.php b/tests/Calendar/MonthTest.php new file mode 100644 index 0000000..79f2d5d --- /dev/null +++ b/tests/Calendar/MonthTest.php @@ -0,0 +1,81 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Calendar; + +use FastForward\Enum\Calendar\Month; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +#[CoversClass(Month::class)] +final class MonthTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itKeepsCalendarDeclarationOrder(): void + { + self::assertSame(Month::cases(), Month::ordered()); + } + + /** + * @return void + */ + #[Test] + public function itIdentifiesTheQuarterForAMonth(): void + { + self::assertSame(1, Month::January->quarter()); + self::assertSame(2, Month::April->quarter()); + self::assertSame(3, Month::September->quarter()); + self::assertSame(4, Month::December->quarter()); + } + + /** + * @return void + */ + #[Test] + public function itIdentifiesQuarterEndMonths(): void + { + self::assertTrue(Month::June->isQuarterEnd()); + self::assertFalse(Month::May->isQuarterEnd()); + } + + /** + * @return void + */ + #[Test] + public function itDescribesMonths(): void + { + self::assertSame([ + 'Month 1 of the Gregorian calendar, commonly associated with yearly planning.', + 'Month 2 of the Gregorian calendar, with 28 days or 29 in leap years.', + 'Month 3 of the Gregorian calendar, often used as the end of the first quarter.', + 'Month 4 of the Gregorian calendar, following the close of Q1 in many businesses.', + 'Month 5 of the Gregorian calendar, typically part of the second quarter.', + 'Month 6 of the Gregorian calendar and common end of the first half-year.', + 'Month 7 of the Gregorian calendar and common start of the second half-year.', + 'Month 8 of the Gregorian calendar, often used in summer scheduling contexts.', + 'Month 9 of the Gregorian calendar and common start of many annual cycles.', + 'Month 10 of the Gregorian calendar, typically within fourth-quarter planning.', + 'Month 11 of the Gregorian calendar, often used for year-end preparation.', + 'Month 12 of the Gregorian calendar and common close of fiscal or calendar years.', + ], array_map(static fn(Month $month): string => $month->description(), Month::cases())); + } +} diff --git a/tests/Calendar/QuarterTest.php b/tests/Calendar/QuarterTest.php new file mode 100644 index 0000000..823d346 --- /dev/null +++ b/tests/Calendar/QuarterTest.php @@ -0,0 +1,84 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Calendar; + +use FastForward\Enum\Calendar\Month; +use FastForward\Enum\Calendar\Quarter; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\TestCase; + +#[CoversClass(Quarter::class)] +#[UsesClass(Month::class)] +final class QuarterTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itKeepsCalendarDeclarationOrder(): void + { + self::assertSame([Quarter::Q1, Quarter::Q2, Quarter::Q3, Quarter::Q4], Quarter::ordered()); + } + + /** + * @return void + */ + #[Test] + public function itExposesTheMonthsInsideAQuarter(): void + { + self::assertSame([Month::April, Month::May, Month::June], Quarter::Q2->months()); + self::assertSame(Month::October, Quarter::Q4->startMonth()); + self::assertSame(Month::December, Quarter::Q4->endMonth()); + } + + /** + * @return void + */ + #[Test] + public function itChecksWhetherAMonthBelongsToAQuarter(): void + { + self::assertTrue(Quarter::Q3->includes(Month::September)); + self::assertFalse(Quarter::Q1->includes(Month::April)); + } + + /** + * @return void + */ + #[Test] + public function itResolvesAQuarterFromAMonth(): void + { + self::assertSame(Quarter::Q2, Quarter::fromMonth(Month::May)); + } + + /** + * @return void + */ + #[Test] + public function itDescribesQuarters(): void + { + self::assertSame([ + 'First quarter of the year, covering January through March.', + 'Second quarter of the year, covering April through June.', + 'Third quarter of the year, covering July through September.', + 'Fourth quarter of the year, covering October through December.', + ], array_map(static fn(Quarter $quarter): string => $quarter->description(), Quarter::cases())); + } +} diff --git a/tests/Calendar/SemesterTest.php b/tests/Calendar/SemesterTest.php new file mode 100644 index 0000000..cc2e9c4 --- /dev/null +++ b/tests/Calendar/SemesterTest.php @@ -0,0 +1,89 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Calendar; + +use FastForward\Enum\Calendar\Month; +use FastForward\Enum\Calendar\Quarter; +use FastForward\Enum\Calendar\Semester; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\TestCase; + +#[CoversClass(Semester::class)] +#[UsesClass(Month::class)] +#[UsesClass(Quarter::class)] +final class SemesterTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itExposesTheQuartersInsideASemester(): void + { + self::assertSame([Quarter::Q1, Quarter::Q2], Semester::H1->quarters()); + } + + /** + * @return void + */ + #[Test] + public function itExposesTheMonthsInsideASemester(): void + { + self::assertSame( + [Month::January, Month::February, Month::March, Month::April, Month::May, Month::June], + Semester::H1->months(), + ); + self::assertSame( + [Month::July, Month::August, Month::September, Month::October, Month::November, Month::December], + Semester::H2->months(), + ); + self::assertSame(Month::July, Semester::H2->startMonth()); + self::assertSame(Month::December, Semester::H2->endMonth()); + } + + /** + * @return void + */ + #[Test] + public function itChecksWhetherAMonthBelongsToASemester(): void + { + self::assertTrue(Semester::H1->includes(Month::March)); + self::assertFalse(Semester::H1->includes(Month::October)); + } + + /** + * @return void + */ + #[Test] + public function itResolvesASemesterFromCalendarUnits(): void + { + self::assertSame(Semester::H1, Semester::fromMonth(Month::May)); + self::assertSame(Semester::H2, Semester::fromQuarter(Quarter::Q4)); + } + + /** + * @return void + */ + #[Test] + public function itDescribesSemesters(): void + { + self::assertSame('First half of the year, covering January through June.', Semester::H1->description()); + } +} diff --git a/tests/Calendar/WeekdayTest.php b/tests/Calendar/WeekdayTest.php new file mode 100644 index 0000000..3f9c2f1 --- /dev/null +++ b/tests/Calendar/WeekdayTest.php @@ -0,0 +1,76 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Calendar; + +use FastForward\Enum\Calendar\Weekday; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +#[CoversClass(Weekday::class)] +final class WeekdayTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itIdentifiesWeekendsAndWeekdays(): void + { + self::assertTrue(Weekday::Saturday->isWeekend()); + self::assertFalse(Weekday::Wednesday->isWeekend()); + self::assertTrue(Weekday::Wednesday->isWeekday()); + } + + /** + * @return void + */ + #[Test] + public function itKeepsIsoWeekOrder(): void + { + self::assertSame( + [ + Weekday::Monday, + Weekday::Tuesday, + Weekday::Wednesday, + Weekday::Thursday, + Weekday::Friday, + Weekday::Saturday, + Weekday::Sunday, + ], + Weekday::ordered(), + ); + } + + /** + * @return void + */ + #[Test] + public function itDescribesWeekdays(): void + { + self::assertSame([ + 'First business day of the ISO week in most workflows and calendars.', + 'Second day of the ISO week, commonly used for regular working schedules.', + 'Midweek day often used for routine meetings and delivery checkpoints.', + 'Late-week working day before typical end-of-week wrap-up.', + 'Final common business day before the weekend in many regions.', + 'Weekend day typically treated as non-working in standard business calendars.', + 'Weekend day that closes the ISO week and often precedes planning for Monday.', + ], array_map(static fn(Weekday $weekday): string => $weekday->description(), Weekday::cases())); + } +} diff --git a/tests/Common/PriorityTest.php b/tests/Common/PriorityTest.php new file mode 100644 index 0000000..0b0ed32 --- /dev/null +++ b/tests/Common/PriorityTest.php @@ -0,0 +1,60 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Common; + +use FastForward\Enum\Common\Priority; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +#[CoversClass(Priority::class)] +final class PriorityTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itOrdersPrioritiesByWeight(): void + { + self::assertSame([Priority::Low, Priority::Normal, Priority::High, Priority::Critical], Priority::ordered()); + } + + /** + * @return void + */ + #[Test] + public function itComparesPriorityWeights(): void + { + self::assertTrue(Priority::Critical->isHigherThan(Priority::Normal)); + self::assertTrue(Priority::Low->isLowerThan(Priority::High)); + self::assertSame(40, Priority::Critical->weight()); + } + + /** + * @return void + */ + #[Test] + public function itDescribesPriorities(): void + { + self::assertSame( + 'Requires immediate attention due to severe business or operational impact.', + Priority::Critical->description(), + ); + } +} diff --git a/tests/Common/SeverityTest.php b/tests/Common/SeverityTest.php new file mode 100644 index 0000000..3ffc22a --- /dev/null +++ b/tests/Common/SeverityTest.php @@ -0,0 +1,63 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Common; + +use FastForward\Enum\Common\Severity; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +#[CoversClass(Severity::class)] +final class SeverityTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itComparesSeverityWeights(): void + { + self::assertTrue(Severity::Error->isAtLeast(Severity::Warning)); + self::assertFalse(Severity::Info->isAtLeast(Severity::Error)); + self::assertSame(60, Severity::Critical->weight()); + } + + /** + * @return void + */ + #[Test] + public function itKeepsSeverityOrder(): void + { + self::assertSame( + [Severity::Debug, Severity::Info, Severity::Notice, Severity::Warning, Severity::Error, Severity::Critical], + Severity::ordered(), + ); + } + + /** + * @return void + */ + #[Test] + public function itDescribesSeverities(): void + { + self::assertSame( + 'Severe failure requiring urgent human or automated intervention.', + Severity::Critical->description(), + ); + } +} diff --git a/tests/Comparison/ComparisonOperatorTest.php b/tests/Comparison/ComparisonOperatorTest.php new file mode 100644 index 0000000..5dd5045 --- /dev/null +++ b/tests/Comparison/ComparisonOperatorTest.php @@ -0,0 +1,128 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Comparison; + +use FastForward\Enum\Comparison\ComparisonOperator; +use FastForward\Enum\Helper\EnumHelper; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\TestCase; +use ValueError; + +#[CoversClass(ComparisonOperator::class)] +#[UsesClass(EnumHelper::class)] +final class ComparisonOperatorTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itExposesCompactBackedValues(): void + { + self::assertSame(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'not_in'], ComparisonOperator::values()); + } + + /** + * @return void + */ + #[Test] + public function itMapsOperatorsToSymbols(): void + { + self::assertSame(['=', '!=', '>', '>=', '<', '<=', 'IN', 'NOT IN'], array_map( + static fn(ComparisonOperator $operator): string => $operator->symbol(), + ComparisonOperator::cases(), + )); + } + + /** + * @return void + */ + #[Test] + public function itComparesScalarValues(): void + { + self::assertTrue(ComparisonOperator::Equal->compare('draft', 'draft')); + self::assertTrue(ComparisonOperator::NotEqual->compare('draft', 'published')); + self::assertTrue(ComparisonOperator::GreaterThan->compare(10, 5)); + self::assertTrue(ComparisonOperator::GreaterThanOrEqual->compare(10, 10)); + self::assertTrue(ComparisonOperator::LessThan->compare(5, 10)); + self::assertTrue(ComparisonOperator::LessThanOrEqual->compare(10, 10)); + } + + /** + * @return void + */ + #[Test] + public function itComparesValuesAgainstCandidateSets(): void + { + self::assertTrue(ComparisonOperator::In->compare('draft', ['draft', 'published'])); + self::assertFalse(ComparisonOperator::NotIn->compare('draft', ['draft', 'published'])); + self::assertTrue(ComparisonOperator::In->isSetOperator()); + self::assertFalse(ComparisonOperator::Equal->isSetOperator()); + } + + /** + * @return void + */ + #[Test] + public function itRejectsInvalidCandidateSets(): void + { + $this->expectException(ValueError::class); + $this->expectExceptionMessage('Comparison operator In expects an array candidate set.'); + + ComparisonOperator::In->compare('draft', 'draft'); + } + + /** + * @return void + */ + #[Test] + public function itNegatesOperators(): void + { + self::assertSame(ComparisonOperator::NotEqual, ComparisonOperator::Equal->negate()); + self::assertSame(ComparisonOperator::Equal, ComparisonOperator::NotEqual->negate()); + self::assertSame(ComparisonOperator::LessThanOrEqual, ComparisonOperator::GreaterThan->negate()); + self::assertSame(ComparisonOperator::LessThan, ComparisonOperator::GreaterThanOrEqual->negate()); + self::assertSame(ComparisonOperator::GreaterThanOrEqual, ComparisonOperator::LessThan->negate()); + self::assertSame(ComparisonOperator::GreaterThan, ComparisonOperator::LessThanOrEqual->negate()); + self::assertSame(ComparisonOperator::NotIn, ComparisonOperator::In->negate()); + self::assertSame(ComparisonOperator::In, ComparisonOperator::NotIn->negate()); + } + + /** + * @return void + */ + #[Test] + public function itDescribesOperators(): void + { + self::assertSame([ + 'Matches when both operands are strictly equal.', + 'Matches when both operands are not strictly equal.', + 'Matches when the left operand is greater than the right operand.', + 'Matches when the left operand is greater than or equal to the right operand.', + 'Matches when the left operand is less than the right operand.', + 'Matches when the left operand is less than or equal to the right operand.', + 'Matches when the left operand is contained in the right-hand candidate set.', + 'Matches when the left operand is not contained in the right-hand candidate set.', + ], array_map( + static fn(ComparisonOperator $operator): string => $operator->description(), + ComparisonOperator::cases(), + )); + } +} diff --git a/tests/Container/ServiceLifetimeTest.php b/tests/Container/ServiceLifetimeTest.php new file mode 100644 index 0000000..554c686 --- /dev/null +++ b/tests/Container/ServiceLifetimeTest.php @@ -0,0 +1,63 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Container; + +use FastForward\Enum\Container\ServiceLifetime; +use FastForward\Enum\Helper\EnumHelper; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\TestCase; + +#[CoversClass(ServiceLifetime::class)] +#[UsesClass(EnumHelper::class)] +final class ServiceLifetimeTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itExposesStableBackedValues(): void + { + self::assertSame(['singleton', 'transient'], ServiceLifetime::values()); + } + + /** + * @return void + */ + #[Test] + public function itIdentifiesReusableLifetimes(): void + { + self::assertTrue(ServiceLifetime::Singleton->isReusable()); + self::assertFalse(ServiceLifetime::Transient->isReusable()); + } + + /** + * @return void + */ + #[Test] + public function itDescribesServiceLifetimes(): void + { + self::assertSame( + 'One shared instance is reused for the full container lifetime.', + ServiceLifetime::Singleton->description(), + ); + self::assertSame('A new instance is created for each resolution.', ServiceLifetime::Transient->description()); + } +} diff --git a/tests/DateTime/IntervalUnitTest.php b/tests/DateTime/IntervalUnitTest.php new file mode 100644 index 0000000..387aca2 --- /dev/null +++ b/tests/DateTime/IntervalUnitTest.php @@ -0,0 +1,82 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\DateTime; + +use FastForward\Enum\DateTime\IntervalUnit; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +#[CoversClass(IntervalUnit::class)] +final class IntervalUnitTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itExposesShortLabels(): void + { + self::assertSame(['s', 'min', 'h', 'd', 'w', 'mo', 'y'], array_map( + static fn(IntervalUnit $unit): string => $unit->shortLabel(), + IntervalUnit::cases(), + )); + } + + /** + * @return void + */ + #[Test] + public function itConvertsUnitsToSeconds(): void + { + self::assertSame(1, IntervalUnit::Second->seconds()); + self::assertSame(5400, IntervalUnit::Minute->seconds(90)); + self::assertSame(7200, IntervalUnit::Hour->seconds(2)); + self::assertSame(86400, IntervalUnit::Day->seconds()); + self::assertSame(604800, IntervalUnit::Week->seconds()); + self::assertSame(2628000, IntervalUnit::Month->seconds()); + self::assertSame(31536000, IntervalUnit::Year->seconds()); + } + + /** + * @return void + */ + #[Test] + public function itIdentifiesCalendarAwareUnits(): void + { + self::assertFalse(IntervalUnit::Week->isCalendarAware()); + self::assertTrue(IntervalUnit::Month->isCalendarAware()); + } + + /** + * @return void + */ + #[Test] + public function itDescribesIntervalUnits(): void + { + self::assertSame([ + 'Represents one-second intervals for fine-grained timing and retry policies.', + 'Represents one-minute intervals for short-lived scheduling and cache policies.', + 'Represents one-hour intervals for operational windows and batch execution.', + 'Represents one-day intervals for daily schedules and retention policies.', + 'Represents one-week intervals for weekly planning, reports, and cleanup jobs.', + 'Represents one-month intervals for monthly billing, reporting, and rotation schedules.', + 'Represents one-year intervals for annual cycles, compliance, and archival horizons.', + ], array_map(static fn(IntervalUnit $unit): string => $unit->description(), IntervalUnit::cases())); + } +} diff --git a/tests/Event/DispatchModeTest.php b/tests/Event/DispatchModeTest.php new file mode 100644 index 0000000..a462efe --- /dev/null +++ b/tests/Event/DispatchModeTest.php @@ -0,0 +1,68 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Event; + +use FastForward\Enum\Event\DispatchMode; +use FastForward\Enum\Helper\EnumHelper; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\TestCase; + +#[CoversClass(DispatchMode::class)] +#[UsesClass(EnumHelper::class)] +final class DispatchModeTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itExposesStableBackedValues(): void + { + self::assertSame(['sync', 'async'], DispatchMode::values()); + } + + /** + * @return void + */ + #[Test] + public function itIdentifiesSyncAndAsyncDispatch(): void + { + self::assertTrue(DispatchMode::Sync->isSync()); + self::assertFalse(DispatchMode::Async->isSync()); + self::assertTrue(DispatchMode::Async->isAsync()); + self::assertFalse(DispatchMode::Sync->isAsync()); + } + + /** + * @return void + */ + #[Test] + public function itDescribesDispatchModes(): void + { + self::assertSame( + 'Dispatches listeners immediately within the current execution flow.', + DispatchMode::Sync->description(), + ); + self::assertSame( + 'Dispatches listeners through a deferred or queued execution path.', + DispatchMode::Async->description(), + ); + } +} diff --git a/tests/Helper/EnumHelperTest.php b/tests/Helper/EnumHelperTest.php new file mode 100644 index 0000000..584dd3d --- /dev/null +++ b/tests/Helper/EnumHelperTest.php @@ -0,0 +1,192 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Helper; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesTrait; +use PHPUnit\Framework\TestCase; +use FastForward\Enum\Helper\EnumHelper; +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Tests\Support\DefaultLabeledStatus; +use FastForward\Enum\Tests\Support\Direction; +use FastForward\Enum\Tests\Support\Priority; +use FastForward\Enum\Tests\Support\Status; +use ValueError; +use stdClass; + +#[CoversClass(EnumHelper::class)] +#[UsesTrait(HasLabel::class)] +final class EnumHelperTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itReturnsCasesForUnitEnums(): void + { + self::assertSame([Direction::North, Direction::South], EnumHelper::cases(Direction::class)); + self::assertSame([Direction::North, Direction::South], EnumHelper::cases(Direction::North)); + } + + /** + * @return void + */ + #[Test] + public function itReturnsNamesForUnitEnums(): void + { + self::assertSame(['North', 'South'], EnumHelper::names(Direction::class)); + self::assertSame(['North', 'South'], EnumHelper::names(Direction::North)); + } + + /** + * @return void + */ + #[Test] + public function itReturnsValuesForBackedEnums(): void + { + self::assertSame(['draft', 'published'], EnumHelper::values(Status::class)); + self::assertSame(['draft', 'published'], EnumHelper::values(Status::Draft)); + } + + /** + * @return void + */ + #[Test] + public function itBuildsMapsForNamesAndValues(): void + { + self::assertSame([ + 'North' => Direction::North, + 'South' => Direction::South, + ], EnumHelper::nameMap(Direction::North)); + + self::assertSame([ + 'draft' => Status::Draft, + 'published' => Status::Published, + ], EnumHelper::valueMap(Status::Draft)); + } + + /** + * @return void + */ + #[Test] + public function itBuildsOptionsForBackedEnums(): void + { + self::assertSame([ + 'Draft' => 'draft', + 'Published' => 'published', + ], EnumHelper::options(Status::class)); + + self::assertSame([ + 'Draft' => 'draft', + 'Published' => 'published', + ], EnumHelper::options(Status::Draft)); + } + + /** + * @return void + */ + #[Test] + public function itCanLookUpCasesByName(): void + { + self::assertSame(Direction::South, EnumHelper::fromName(Direction::class, 'South')); + self::assertSame(Direction::North, EnumHelper::fromName(Direction::South, 'North')); + self::assertTrue(EnumHelper::hasName(Direction::class, 'North')); + self::assertFalse(EnumHelper::hasName(Direction::class, 'East')); + self::assertNull(EnumHelper::tryFromName(Direction::class, 'East')); + } + + /** + * @return void + */ + #[Test] + public function itThrowsForInvalidNames(): void + { + $this->expectException(ValueError::class); + $this->expectExceptionMessage('"Archived" is not a valid name for enum ' . Status::class . '.'); + + EnumHelper::fromName(Status::class, 'Archived'); + } + + /** + * @return void + */ + #[Test] + public function itChecksForBackedValues(): void + { + self::assertTrue(EnumHelper::hasValue(Status::class, 'draft')); + self::assertFalse(EnumHelper::hasValue(Status::class, 'archived')); + self::assertTrue(EnumHelper::hasValue(Status::Draft, 'draft')); + } + + /** + * @return void + */ + #[Test] + public function itRejectsNonBackedEnumClassStringsForBackedHelpers(): void + { + $this->expectException(ValueError::class); + $this->expectExceptionMessage('Enum ' . Direction::class . ' must be a backed enum.'); + + EnumHelper::values(Direction::class); + } + + /** + * @return void + */ + #[Test] + public function itRejectsNonEnumClassStringsForUnitHelpers(): void + { + $this->expectException(ValueError::class); + $this->expectExceptionMessage('Enum ' . stdClass::class . ' must be a unit enum.'); + + EnumHelper::names(stdClass::class); + } + + /** + * @return void + */ + #[Test] + public function itExposesLabelsForLabeledEnums(): void + { + self::assertSame(['Low priority', 'High priority'], EnumHelper::labels(Priority::class)); + self::assertSame(['Low priority', 'High priority'], EnumHelper::labels(Priority::Low)); + self::assertSame([ + 'Low' => 'Low priority', + 'High' => 'High priority', + ], EnumHelper::labelMap(Priority::class)); + self::assertSame([ + DefaultLabeledStatus::class . ' Draft', + DefaultLabeledStatus::class . ' Published', + ], EnumHelper::labels(DefaultLabeledStatus::class)); + } + + /** + * @return void + */ + #[Test] + public function itRejectsNonLabeledEnumsForLabelHelpers(): void + { + $this->expectException(ValueError::class); + $this->expectExceptionMessage('Enum ' . Status::class . ' must implement ' . LabeledEnumInterface::class . '.'); + + EnumHelper::labels(Status::class); + } +} diff --git a/tests/Http/SchemeTest.php b/tests/Http/SchemeTest.php new file mode 100644 index 0000000..e1d5f26 --- /dev/null +++ b/tests/Http/SchemeTest.php @@ -0,0 +1,70 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Http; + +use FastForward\Enum\Helper\EnumHelper; +use FastForward\Enum\Http\Scheme; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\TestCase; + +#[CoversClass(Scheme::class)] +#[UsesClass(EnumHelper::class)] +final class SchemeTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itExposesStableBackedValues(): void + { + self::assertSame(['http', 'https'], Scheme::values()); + } + + /** + * @return void + */ + #[Test] + public function itProvidesDefaultPorts(): void + { + self::assertSame(80, Scheme::Http->defaultPort()); + self::assertSame(443, Scheme::Https->defaultPort()); + } + + /** + * @return void + */ + #[Test] + public function itIdentifiesSecureSchemes(): void + { + self::assertFalse(Scheme::Http->isSecure()); + self::assertTrue(Scheme::Https->isSecure()); + } + + /** + * @return void + */ + #[Test] + public function itDescribesSchemes(): void + { + self::assertSame('Plain HTTP transport without transport-layer encryption.', Scheme::Http->description()); + self::assertSame('HTTP transport secured with TLS encryption.', Scheme::Https->description()); + } +} diff --git a/tests/Logger/LogLevelTest.php b/tests/Logger/LogLevelTest.php new file mode 100644 index 0000000..d077901 --- /dev/null +++ b/tests/Logger/LogLevelTest.php @@ -0,0 +1,78 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Logger; + +use FastForward\Enum\Logger\LogLevel; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +#[CoversClass(LogLevel::class)] +final class LogLevelTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itComparesLogLevelWeights(): void + { + self::assertTrue(LogLevel::Critical->isAtLeast(LogLevel::Warning)); + self::assertFalse(LogLevel::Info->isAtLeast(LogLevel::Error)); + self::assertSame(80, LogLevel::Emergency->weight()); + } + + /** + * @return void + */ + #[Test] + public function itKeepsLogLevelOrder(): void + { + self::assertSame( + [ + LogLevel::Debug, + LogLevel::Info, + LogLevel::Notice, + LogLevel::Warning, + LogLevel::Error, + LogLevel::Critical, + LogLevel::Alert, + LogLevel::Emergency, + ], + LogLevel::ordered(), + ); + } + + /** + * @return void + */ + #[Test] + public function itDescribesLogLevels(): void + { + self::assertSame([ + 'System is unusable and requires immediate global attention.', + 'Action must be taken immediately to avoid severe service disruption.', + 'Critical condition indicating serious failure in a core capability.', + 'Runtime error indicating part of the current operation failed.', + 'Potential issue that should be reviewed before it becomes an error.', + 'Normal but noteworthy event that may deserve operational awareness.', + 'Informational event describing expected application flow.', + 'Verbose diagnostic information intended for debugging and development.', + ], array_map(static fn(LogLevel $level): string => $level->description(), LogLevel::cases())); + } +} diff --git a/tests/Outcome/ResultTest.php b/tests/Outcome/ResultTest.php new file mode 100644 index 0000000..69d89dc --- /dev/null +++ b/tests/Outcome/ResultTest.php @@ -0,0 +1,49 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Outcome; + +use FastForward\Enum\Outcome\Result; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +#[CoversClass(Result::class)] +final class ResultTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itIdentifiesSuccessfulResults(): void + { + self::assertTrue(Result::Success->isSuccessful()); + self::assertTrue(Result::Partial->isSuccessful()); + self::assertTrue(Result::Success->isCompleteSuccess()); + self::assertTrue(Result::Failure->isFailure()); + } + + /** + * @return void + */ + #[Test] + public function itDescribesResults(): void + { + self::assertSame('Operation completed only in part and may require follow-up.', Result::Partial->description()); + } +} diff --git a/tests/Pipeline/FailureModeTest.php b/tests/Pipeline/FailureModeTest.php new file mode 100644 index 0000000..4010453 --- /dev/null +++ b/tests/Pipeline/FailureModeTest.php @@ -0,0 +1,66 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Pipeline; + +use FastForward\Enum\Helper\EnumHelper; +use FastForward\Enum\Pipeline\FailureMode; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\TestCase; + +#[CoversClass(FailureMode::class)] +#[UsesClass(EnumHelper::class)] +final class FailureModeTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itExposesStableBackedValues(): void + { + self::assertSame(['stop_on_failure', 'continue_on_failure'], FailureMode::values()); + } + + /** + * @return void + */ + #[Test] + public function itIdentifiesStopOnFailureModes(): void + { + self::assertTrue(FailureMode::StopOnFailure->stopsOnFailure()); + self::assertFalse(FailureMode::ContinueOnFailure->stopsOnFailure()); + } + + /** + * @return void + */ + #[Test] + public function itDescribesFailureModes(): void + { + self::assertSame( + 'Stops the pipeline immediately when a stage fails.', + FailureMode::StopOnFailure->description() + ); + self::assertSame( + 'Continues processing remaining stages even after a failure.', + FailureMode::ContinueOnFailure->description(), + ); + } +} diff --git a/tests/Process/SignalBehaviorTest.php b/tests/Process/SignalBehaviorTest.php new file mode 100644 index 0000000..44852e7 --- /dev/null +++ b/tests/Process/SignalBehaviorTest.php @@ -0,0 +1,65 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Process; + +use FastForward\Enum\Helper\EnumHelper; +use FastForward\Enum\Process\SignalBehavior; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\TestCase; + +#[CoversClass(SignalBehavior::class)] +#[UsesClass(EnumHelper::class)] +final class SignalBehaviorTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itExposesStableBackedValues(): void + { + self::assertSame(['ignore', 'handle', 'propagate'], SignalBehavior::values()); + } + + /** + * @return void + */ + #[Test] + public function itIdentifiesTerminalControlBehaviors(): void + { + self::assertFalse(SignalBehavior::Ignore->isTerminalControl()); + self::assertTrue(SignalBehavior::Handle->isTerminalControl()); + self::assertTrue(SignalBehavior::Propagate->isTerminalControl()); + } + + /** + * @return void + */ + #[Test] + public function itDescribesSignalBehaviors(): void + { + self::assertSame('Signal is observed but intentionally ignored.', SignalBehavior::Ignore->description()); + self::assertSame('Signal is intercepted and processed locally.', SignalBehavior::Handle->description()); + self::assertSame( + 'Signal is forwarded to child processes or outer handlers.', + SignalBehavior::Propagate->description(), + ); + } +} diff --git a/tests/Runtime/EnvironmentTest.php b/tests/Runtime/EnvironmentTest.php new file mode 100644 index 0000000..6524c96 --- /dev/null +++ b/tests/Runtime/EnvironmentTest.php @@ -0,0 +1,72 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Runtime; + +use FastForward\Enum\Helper\EnumHelper; +use FastForward\Enum\Runtime\Environment; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\TestCase; + +#[CoversClass(Environment::class)] +#[UsesClass(EnumHelper::class)] +final class EnvironmentTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itExposesStableBackedValues(): void + { + self::assertSame(['development', 'testing', 'staging', 'production'], Environment::values()); + } + + /** + * @return void + */ + #[Test] + public function itIdentifiesProductionAndPreProductionEnvironments(): void + { + self::assertTrue(Environment::Production->isProduction()); + self::assertTrue(Environment::Development->isPreProduction()); + } + + /** + * @return void + */ + #[Test] + public function itIdentifiesDebugFriendlyEnvironments(): void + { + self::assertTrue(Environment::Testing->isDebugFriendly()); + self::assertFalse(Environment::Production->isDebugFriendly()); + } + + /** + * @return void + */ + #[Test] + public function itDescribesEnvironments(): void + { + self::assertSame( + 'Live environment serving real users and production workloads.', + Environment::Production->description(), + ); + } +} diff --git a/tests/Sort/ComparisonResultTest.php b/tests/Sort/ComparisonResultTest.php new file mode 100644 index 0000000..9ce984a --- /dev/null +++ b/tests/Sort/ComparisonResultTest.php @@ -0,0 +1,84 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Sort; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\TestCase; +use FastForward\Enum\Helper\EnumHelper; +use FastForward\Enum\ReversibleInterface; +use FastForward\Enum\Sort\ComparisonResult; + +#[CoversClass(ComparisonResult::class)] +#[UsesClass(EnumHelper::class)] +final class ComparisonResultTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itProvidesNamesAndNameLookupHelpers(): void + { + self::assertSame(['LeftGreater', 'RightGreater', 'Equal', 'Incomparable'], ComparisonResult::names()); + self::assertSame(ComparisonResult::Equal, ComparisonResult::fromName('Equal')); + self::assertSame(ComparisonResult::Incomparable, ComparisonResult::tryFromName('Incomparable')); + self::assertTrue(ComparisonResult::hasName('LeftGreater')); + self::assertFalse(ComparisonResult::hasName('Ascending')); + } + + /** + * @return void + */ + #[Test] + public function itConvertsComparatorResultsToComparisonResultCases(): void + { + self::assertSame(ComparisonResult::LeftGreater, ComparisonResult::fromComparisonResult(1)); + self::assertSame(ComparisonResult::LeftGreater, ComparisonResult::fromComparisonResult(42)); + self::assertSame(ComparisonResult::RightGreater, ComparisonResult::fromComparisonResult(-1)); + self::assertSame(ComparisonResult::Equal, ComparisonResult::fromComparisonResult(0)); + } + + /** + * @return void + */ + #[Test] + public function itConvertsComparisonResultCasesBackToComparatorResults(): void + { + self::assertSame(1, ComparisonResult::LeftGreater->toComparisonResult()); + self::assertSame(-1, ComparisonResult::RightGreater->toComparisonResult()); + self::assertSame(0, ComparisonResult::Equal->toComparisonResult()); + self::assertSame(1, ComparisonResult::Incomparable->toComparisonResult()); + } + + /** + * @return void + */ + #[Test] + public function itCanBeReversedAndCheckedForComparability(): void + { + self::assertSame(ComparisonResult::RightGreater, ComparisonResult::LeftGreater->reverse()); + self::assertSame(ComparisonResult::LeftGreater, ComparisonResult::RightGreater->reverse()); + self::assertSame(ComparisonResult::Equal, ComparisonResult::Equal->reverse()); + self::assertSame(ComparisonResult::Incomparable, ComparisonResult::Incomparable->reverse()); + self::assertInstanceOf(ReversibleInterface::class, ComparisonResult::LeftGreater); + self::assertTrue(ComparisonResult::Equal->isComparable()); + self::assertFalse(ComparisonResult::Incomparable->isComparable()); + } +} diff --git a/tests/Sort/SortDirectionTest.php b/tests/Sort/SortDirectionTest.php new file mode 100644 index 0000000..0212487 --- /dev/null +++ b/tests/Sort/SortDirectionTest.php @@ -0,0 +1,99 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Sort; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\TestCase; +use FastForward\Enum\Helper\EnumHelper; +use FastForward\Enum\ReversibleInterface; +use FastForward\Enum\Sort\CaseSensitivity; +use FastForward\Enum\Sort\NullsPosition; +use FastForward\Enum\Sort\SortDirection; + +#[CoversClass(CaseSensitivity::class)] +#[CoversClass(NullsPosition::class)] +#[CoversClass(SortDirection::class)] +#[UsesClass(EnumHelper::class)] +final class SortDirectionTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itProvidesEnumHelpersForSortDirection(): void + { + self::assertSame(['Ascending', 'Descending'], SortDirection::names()); + self::assertSame(SortDirection::Ascending, SortDirection::fromName('Ascending')); + self::assertTrue(SortDirection::hasName('Descending')); + } + + /** + * @return void + */ + #[Test] + public function itProvidesSortSpecificHelpers(): void + { + self::assertSame(SortDirection::Descending, SortDirection::Ascending->reverse()); + self::assertTrue(SortDirection::Ascending->isAscending()); + self::assertTrue(SortDirection::Descending->isDescending()); + self::assertSame(1, SortDirection::Ascending->factor()); + self::assertSame(-1, SortDirection::Descending->factor()); + self::assertSame(5, SortDirection::Ascending->applyToComparisonResult(5)); + self::assertSame(-5, SortDirection::Descending->applyToComparisonResult(5)); + self::assertSame(7, SortDirection::Descending->applyToComparisonResult(-7)); + self::assertInstanceOf(ReversibleInterface::class, SortDirection::Ascending); + } + + /** + * @return void + */ + #[Test] + public function itProvidesNullsPositionHelpers(): void + { + self::assertSame(['First', 'Last'], NullsPosition::names()); + self::assertSame(NullsPosition::Last, NullsPosition::First->reverse()); + self::assertInstanceOf(ReversibleInterface::class, NullsPosition::First); + self::assertTrue(NullsPosition::First->isFirst()); + self::assertTrue(NullsPosition::Last->isLast()); + self::assertSame(-1, NullsPosition::First->compareNullability(null, 'value')); + self::assertSame(1, NullsPosition::Last->compareNullability(null, 'value')); + self::assertSame(0, NullsPosition::First->compareNullability(null, null)); + self::assertSame(1, NullsPosition::First->compareNullability('value', null)); + self::assertSame(-1, NullsPosition::Last->compareNullability('value', null)); + self::assertSame(0, NullsPosition::First->compareNullability('left', 'right')); + } + + /** + * @return void + */ + #[Test] + public function itProvidesCaseSensitivityHelpers(): void + { + self::assertSame(['Sensitive', 'Insensitive'], CaseSensitivity::names()); + self::assertSame(CaseSensitivity::Insensitive, CaseSensitivity::Sensitive->reverse()); + self::assertInstanceOf(ReversibleInterface::class, CaseSensitivity::Sensitive); + self::assertTrue(CaseSensitivity::Sensitive->isSensitive()); + self::assertTrue(CaseSensitivity::Insensitive->isInsensitive()); + self::assertSame('hello', CaseSensitivity::Insensitive->normalize('HeLLo')); + self::assertTrue(CaseSensitivity::Insensitive->equals('Draft', 'draft')); + self::assertFalse(CaseSensitivity::Sensitive->equals('Draft', 'draft')); + } +} diff --git a/tests/StateMachine/HasTransitionsTest.php b/tests/StateMachine/HasTransitionsTest.php new file mode 100644 index 0000000..6123dbc --- /dev/null +++ b/tests/StateMachine/HasTransitionsTest.php @@ -0,0 +1,85 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\StateMachine; + +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\CoversTrait; +use PHPUnit\Framework\TestCase; +use FastForward\Enum\StateMachine\HasTransitions; +use FastForward\Enum\StateMachine\InvalidTransitionException; +use FastForward\Enum\Tests\Support\ArticleWorkflow; +use FastForward\Enum\Tests\Support\InferredWorkflow; + +#[CoversTrait(HasTransitions::class)] +#[CoversClass(InvalidTransitionException::class)] +final class HasTransitionsTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itSupportsStateMachineStyleTransitions(): void + { + self::assertSame([ArticleWorkflow::Draft], ArticleWorkflow::initialStates()); + self::assertSame([ArticleWorkflow::Archived], ArticleWorkflow::terminalStates()); + self::assertTrue(ArticleWorkflow::Draft->isInitial()); + self::assertFalse(ArticleWorkflow::Reviewing->isInitial()); + self::assertTrue(ArticleWorkflow::Archived->isTerminal()); + self::assertFalse(ArticleWorkflow::Reviewing->isTerminal()); + self::assertSame( + [ArticleWorkflow::Reviewing, ArticleWorkflow::Archived], + ArticleWorkflow::Draft->allowedTransitions(), + ); + self::assertTrue(ArticleWorkflow::Draft->canTransitionTo(ArticleWorkflow::Reviewing)); + self::assertFalse(ArticleWorkflow::Draft->canTransitionTo(ArticleWorkflow::Published)); + + ArticleWorkflow::Draft->assertCanTransitionTo(ArticleWorkflow::Reviewing); + } + + /** + * @return void + */ + #[Test] + public function itInfersInitialStatesWhenNoInitialStateCasesAreConfigured(): void + { + self::assertSame([InferredWorkflow::Created], InferredWorkflow::initialStates()); + self::assertSame([InferredWorkflow::Completed], InferredWorkflow::terminalStates()); + self::assertTrue(InferredWorkflow::Created->isInitial()); + self::assertFalse(InferredWorkflow::Running->isInitial()); + } + + /** + * @return void + */ + #[Test] + public function itThrowsForInvalidStateMachineTransitions(): void + { + $this->expectException(InvalidTransitionException::class); + $this->expectExceptionMessage(\sprintf( + 'Invalid transition from %s::%s to %s::%s.', + ArticleWorkflow::Reviewing::class, + ArticleWorkflow::Reviewing->name, + ArticleWorkflow::Archived::class, + ArticleWorkflow::Archived->name, + )); + + ArticleWorkflow::Reviewing->assertCanTransitionTo(ArticleWorkflow::Archived); + } +} diff --git a/tests/Support/ArticleWorkflow.php b/tests/Support/ArticleWorkflow.php new file mode 100644 index 0000000..80da420 --- /dev/null +++ b/tests/Support/ArticleWorkflow.php @@ -0,0 +1,54 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Support; + +use FastForward\Enum\StateMachine\HasTransitions; +use FastForward\Enum\Trait\Comparable; + +enum ArticleWorkflow: string +{ + use Comparable; + use HasTransitions; + + case Draft = 'draft'; + case Reviewing = 'reviewing'; + case Published = 'published'; + case Archived = 'archived'; + + /** + * @return array> + */ + private static function transitionMap(): array + { + return [ + self::Draft->name => [self::Reviewing, self::Archived], + self::Reviewing->name => [self::Published, self::Draft], + self::Published->name => [self::Archived], + self::Archived->name => [], + ]; + } + + /** + * @return list + */ + private static function initialStateCases(): array + { + return [self::Draft]; + } +} diff --git a/tests/Support/DefaultLabeledStatus.php b/tests/Support/DefaultLabeledStatus.php new file mode 100644 index 0000000..2ad564c --- /dev/null +++ b/tests/Support/DefaultLabeledStatus.php @@ -0,0 +1,30 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Support; + +use FastForward\Enum\LabeledEnumInterface; +use FastForward\Enum\Trait\HasLabel; + +enum DefaultLabeledStatus: string implements LabeledEnumInterface +{ + use HasLabel; + + case Draft = 'draft'; + case Published = 'published'; +} diff --git a/tests/Support/Direction.php b/tests/Support/Direction.php new file mode 100644 index 0000000..9e379b8 --- /dev/null +++ b/tests/Support/Direction.php @@ -0,0 +1,33 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Support; + +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; + +enum Direction +{ + use HasNameLookup; + use HasNameMap; + use HasNames; + + case North; + case South; +} diff --git a/tests/Support/InferredWorkflow.php b/tests/Support/InferredWorkflow.php new file mode 100644 index 0000000..ff018fd --- /dev/null +++ b/tests/Support/InferredWorkflow.php @@ -0,0 +1,42 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Support; + +use FastForward\Enum\StateMachine\HasTransitions; + +enum InferredWorkflow +{ + use HasTransitions; + + case Created; + case Running; + case Completed; + + /** + * @return array> + */ + private static function transitionMap(): array + { + return [ + self::Created->name => [self::Running], + self::Running->name => [self::Completed], + self::Completed->name => [], + ]; + } +} diff --git a/tests/Support/Priority.php b/tests/Support/Priority.php new file mode 100644 index 0000000..193e54e --- /dev/null +++ b/tests/Support/Priority.php @@ -0,0 +1,38 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Support; + +use FastForward\Enum\LabeledEnumInterface; + +enum Priority: int implements LabeledEnumInterface +{ + case Low = 1; + case High = 2; + + /** + * @return string + */ + public function label(): string + { + return match ($this) { + self::Low => 'Low priority', + self::High => 'High priority', + }; + } +} diff --git a/tests/Support/Status.php b/tests/Support/Status.php new file mode 100644 index 0000000..9dafb56 --- /dev/null +++ b/tests/Support/Status.php @@ -0,0 +1,44 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Support; + +use FastForward\Enum\DescribedEnumInterface; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasDescription; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; + +enum Status: string implements DescribedEnumInterface +{ + use Comparable; + use HasDescription; + use HasNameLookup; + use HasNameMap; + use HasNames; + use HasOptions; + use HasValueMap; + use HasValues; + + case Draft = 'draft'; + case Published = 'published'; +} diff --git a/tests/Trait/EnumTraitTest.php b/tests/Trait/EnumTraitTest.php new file mode 100644 index 0000000..b169e95 --- /dev/null +++ b/tests/Trait/EnumTraitTest.php @@ -0,0 +1,116 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/enum + * @see https://github.com/php-fast-forward/enum/issues + * @see https://php-fast-forward.github.io/enum/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Enum\Tests\Trait; + +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\CoversTrait; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\TestCase; +use FastForward\Enum\Helper\EnumHelper; +use FastForward\Enum\Trait\Comparable; +use FastForward\Enum\Trait\HasDescription; +use FastForward\Enum\Trait\HasLabel; +use FastForward\Enum\Trait\HasNameLookup; +use FastForward\Enum\Trait\HasNameMap; +use FastForward\Enum\Trait\HasNames; +use FastForward\Enum\Trait\HasOptions; +use FastForward\Enum\Trait\HasValueMap; +use FastForward\Enum\Trait\HasValues; +use FastForward\Enum\Tests\Support\DefaultLabeledStatus; +use FastForward\Enum\Tests\Support\Direction; +use FastForward\Enum\Tests\Support\Status; + +#[CoversTrait(Comparable::class)] +#[CoversTrait(HasDescription::class)] +#[CoversTrait(HasLabel::class)] +#[CoversTrait(HasNameLookup::class)] +#[CoversTrait(HasNameMap::class)] +#[CoversTrait(HasNames::class)] +#[CoversTrait(HasOptions::class)] +#[CoversTrait(HasValueMap::class)] +#[CoversTrait(HasValues::class)] +#[UsesClass(EnumHelper::class)] +final class EnumTraitTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function itProvidesUnitEnumLookupAndMappingTraits(): void + { + self::assertSame(['North', 'South'], Direction::names()); + self::assertSame([ + 'North' => Direction::North, + 'South' => Direction::South, + ], Direction::nameMap()); + } + + /** + * @return void + */ + #[Test] + public function itProvidesBackedEnumValueAndOptionTraits(): void + { + self::assertSame(['draft', 'published'], Status::values()); + self::assertSame([ + 'Draft' => 'draft', + 'Published' => 'published', + ], Status::options()); + self::assertSame([ + 'draft' => Status::Draft, + 'published' => Status::Published, + ], Status::valueMap()); + self::assertTrue(Status::hasName('Draft')); + self::assertFalse(Status::hasName('Archived')); + self::assertSame(Status::Draft, Status::tryFromName('Draft')); + self::assertNull(Status::tryFromName('Archived')); + self::assertSame(Status::Published, Status::fromName('Published')); + } + + /** + * @return void + */ + #[Test] + public function itProvidesADefaultClassBasedLabelTrait(): void + { + self::assertSame(DefaultLabeledStatus::class . ' Draft', DefaultLabeledStatus::Draft->label()); + } + + /** + * @return void + */ + #[Test] + public function itProvidesComparableHelpers(): void + { + self::assertTrue(Status::Draft->is(Status::Draft)); + self::assertTrue(Status::Draft->isNot(Status::Published)); + self::assertTrue(Status::Draft->in([Status::Draft, Status::Published])); + self::assertTrue(Status::Draft->notIn([Status::Published])); + } + + /** + * @return void + */ + #[Test] + public function itProvidesADefaultDescriptionTrait(): void + { + self::assertSame('Draft', Status::Draft->description()); + self::assertSame('Published', Status::Published->description()); + } +}