Skip to content

Commit ab50ca8

Browse files
committed
refactor: core architecture hardening and type-safe implementation
1 parent 6db4fa5 commit ab50ca8

91 files changed

Lines changed: 9494 additions & 1227 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 164 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ on:
33
branches:
44
- "main"
55
pull_request:
6+
# schedule:
7+
# - cron: '24 10 * * 4'
68

79
name: "CI"
810

@@ -13,37 +15,43 @@ env:
1315
PHP_VERSION: "8.3"
1416

1517
jobs:
16-
configuration:
17-
name: "Configuration"
18-
runs-on: "ubuntu-latest"
19-
outputs:
20-
php-version: ${{ steps.determine-php-version.outputs.php-version }}
21-
steps:
22-
- name: "Determine PHP version"
23-
id: "determine-php-version"
24-
run: "echo \"php-version=${{ env.PHP_VERSION }}\" >> $GITHUB_OUTPUT"
25-
2618
qa:
2719
name: "QA (lint + static analysis)"
2820
if: "!startsWith(github.event.head_commit.message, 'chore(release)')"
2921
runs-on: "ubuntu-latest"
3022
steps:
3123
- name: "Checkout"
32-
uses: "actions/checkout@v4"
24+
uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2
3325

3426
- name: "Install PHP"
35-
uses: "shivammathur/setup-php@v2"
27+
uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # 2.36.0
3628
with:
3729
coverage: "none"
3830
php-version: "${{ env.PHP_VERSION }}"
3931

4032
- name: "Validate composer.json and composer.lock"
33+
if: "github.actor != 'renovate[bot]' || contains(github.head_ref, 'lock-file-maintenance')"
4134
run: "composer validate --ansi --strict"
4235

43-
- name: "Install dependencies with composer"
44-
run: "composer install --no-interaction --no-progress"
36+
- name: "Determine composer cache directory"
37+
uses: "ergebnis/.github/actions/composer/determine-cache-directory@4103ff7c010d2c18dc84f72d6704227868412482" # 1.10.0
38+
39+
- name: "Cache dependencies installed with composer"
40+
uses: "actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306" # v5.0.3
41+
with:
42+
path: "${{ env.COMPOSER_CACHE_DIR }}"
43+
key: "php-${{ env.PHP_VERSION }}-composer-locked-${{ hashFiles('composer.lock') }}"
44+
restore-keys: |
45+
php-${{ env.PHP_VERSION }}-composer-locked-${{ github.ref_name }}
46+
php-${{ env.PHP_VERSION }}-composer-locked-
47+
php-${{ env.PHP_VERSION }}-composer-main
48+
49+
- name: "Install locked dependencies with composer"
50+
uses: "ergebnis/.github/actions/composer/install@4103ff7c010d2c18dc84f72d6704227868412482" # 1.10.0
51+
with:
52+
dependencies: "${{ (github.actor == 'renovate[bot]' && !contains(github.head_ref, 'lock-file-maintenance')) && 'highest' || 'locked' }}"
4553

46-
- name: "Build Codeception"
54+
- name: "Check coding style"
4755
run: "vendor/bin/codecept build"
4856

4957
- name: "Check coding style"
@@ -52,30 +60,162 @@ jobs:
5260
- name: "Run static analysis"
5361
run: "composer stan"
5462

63+
# codacy:
64+
# name: "Codacy Security Scan"
65+
# if: "!startsWith(github.event.head_commit.message, 'chore(release)')"
66+
# runs-on: "ubuntu-latest"
67+
# permissions:
68+
# contents: read
69+
# security-events: write
70+
# actions: read
71+
# steps:
72+
# - name: Checkout code
73+
# uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
74+
#
75+
# - name: Run Codacy Analysis CLI
76+
# uses: codacy/codacy-analysis-cli-action@30783d03e758713bb5ed7b79292cfb14b9dd9a4a
77+
# with:
78+
# project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
79+
# verbose: false
80+
# output: results.sarif
81+
# format: sarif
82+
# upload: true
83+
# skip-uncommitted-files-check: true
84+
# gh-code-scanning-compat: true
85+
# max-allowed-issues: 2147483647
86+
5587
tests:
5688
name: "Run codeception tests"
57-
needs: [ configuration, qa ]
89+
needs: [ qa ]
5890
runs-on: "ubuntu-latest"
5991
strategy:
6092
matrix:
6193
include:
62-
- { php-version: "8.3", dependencies: locked, with_coverage: false }
63-
- { php-version: "8.4", dependencies: highest, with_coverage: false }
94+
- { php-version: 8.3, dependencies: locked, coverage: pcov, with_coverage: true, allow-fail: false }
95+
96+
- { php-version: 8.4, dependencies: highest, coverage: pcov, with_coverage: false, allow-fail: true }
97+
- { php-version: 8.5, dependencies: highest, coverage: pcov, with_coverage: false, allow-fail: true }
6498
steps:
6599
- name: "Checkout"
66-
uses: "actions/checkout@v4"
100+
uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2
67101

68102
- name: "Install PHP"
69-
uses: "shivammathur/setup-php@v2"
103+
uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # 2.36.0
70104
with:
71-
coverage: "none"
105+
coverage: "${{ matrix.coverage }}"
72106
ini-values: display_errors=On, display_startup_errors=On, error_reporting=32767
73107
php-version: "${{ matrix.php-version }}"
74108

75-
- name: "Install dependencies with composer"
76-
run: "composer update --no-interaction --no-progress"
109+
- name: "Set up problem matchers for PHP"
110+
run: "echo \"::add-matcher::${{ runner.tool_cache }}/php.json\""
111+
112+
- name: "Set up problem matchers for phpunit/phpunit"
113+
run: "echo \"::add-matcher::${{ runner.tool_cache }}/phpunit.json\""
114+
115+
- name: "Validate composer.json and composer.lock"
116+
if: "github.actor != 'renovate[bot]' || contains(github.head_ref, 'lock-file-maintenance')"
117+
run: "composer validate --ansi --strict"
118+
119+
- name: "Determine composer cache directory"
120+
uses: "ergebnis/.github/actions/composer/determine-cache-directory@4103ff7c010d2c18dc84f72d6704227868412482" # 1.10.0
121+
122+
- name: "Cache dependencies installed with composer"
123+
uses: "actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306" # v5.0.3
124+
with:
125+
path: "${{ env.COMPOSER_CACHE_DIR }}"
126+
key: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }}"
127+
restore-keys: |
128+
php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ github.ref_name }}
129+
php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-
130+
php-${{ matrix.php-version }}-composer-main
131+
132+
- name: "Install ${{ matrix.dependencies }} dependencies with composer"
133+
uses: "ergebnis/.github/actions/composer/install@4103ff7c010d2c18dc84f72d6704227868412482" # 1.10.0
134+
with:
135+
dependencies: "${{ (github.actor == 'renovate[bot]' && !contains(github.head_ref, 'lock-file-maintenance') && matrix.dependencies == 'locked') && 'highest' || matrix.dependencies }}"
136+
137+
- name: "Run Tests (coverage)"
138+
if: matrix.with_coverage == true
139+
run: |
140+
vendor/bin/codecept build
141+
vendor/bin/codecept run --coverage --coverage-xml=coverage.xml --xml --report
77142
78143
- name: "Run Tests"
144+
if: matrix.with_coverage != true
79145
run: |
80146
vendor/bin/codecept build
81-
vendor/bin/codecept run --report
147+
vendor/bin/codecept run --xml --report
148+
149+
- name: "Upload coverage artifact"
150+
if: matrix.with_coverage == true
151+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
152+
with:
153+
name: code-coverage-results
154+
path: tests/_output/
155+
retention-days: 5
156+
157+
# - name: "Upload Coverage coverage"
158+
# if: matrix.with_coverage == true
159+
# run: |
160+
# export CODACY_PROJECT_TOKEN=${{ secrets.CODACY_PROJECT_TOKEN }}
161+
# bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r ./build/logs/coverage.xml
162+
#
163+
# - name: Upload test results to Codecov
164+
# if: ${{ !cancelled() && matrix.with_coverage == true }}
165+
# uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
166+
# with:
167+
# token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
168+
# files: ./build/logs/report.xml
169+
# flags: unittests # optional
170+
# report_type: test_results
171+
# fail_ci_if_error: "${{ matrix.with_coverage }}" # optional (default = false)
172+
# verbose: false # optional (default = false)
173+
# - name: Upload coverage to Codecov
174+
# if: ${{ !cancelled() && matrix.with_coverage == true }}
175+
# uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
176+
# with:
177+
# token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
178+
# files: ./build/logs/coverage.xml
179+
# flags: unittests # optional
180+
# fail_ci_if_error: "${{ matrix.with_coverage }}" # optional (default = false)
181+
# verbose: false # optional (default = false)
182+
183+
release:
184+
name: "Release"
185+
needs:
186+
- tests
187+
# - codacy
188+
if: "github.event_name == 'push' && github.ref == 'refs/heads/main' && !startsWith(github.event.head_commit.message, 'chore(release)')"
189+
runs-on: "ubuntu-latest"
190+
permissions:
191+
actions: read
192+
contents: read
193+
steps:
194+
- name: Generate Token
195+
id: generate_token
196+
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
197+
with:
198+
app-id: ${{ secrets.BOT_APP_ID }}
199+
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
200+
201+
- name: Checkout
202+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
203+
with:
204+
fetch-depth: 0
205+
token: ${{ steps.generate_token.outputs.token }}
206+
207+
- name: Semantic Release
208+
uses: cycjimmy/semantic-release-action@v6.0.0
209+
with:
210+
tag_format: ${version}
211+
branches: |
212+
['main']
213+
extra_plugins: |
214+
@semantic-release/commit-analyzer
215+
@semantic-release/release-notes-generator
216+
@semantic-release/github
217+
@semantic-release/changelog
218+
@semantic-release/git
219+
conventional-changelog-conventionalcommits
220+
env:
221+
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}

.php-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
8.3

GEMINI.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Gemini CLI Instructional Context: PHP OpenAPI Mock Server
2+
3+
This document provides foundational context and instructions for AI agents interacting with the `php-openapi-mock-server` codebase.
4+
5+
## Project Overview
6+
**PHP OpenAPI Mock Server** is a high-performance, lightweight, zero-Docker mock server built with the Mezzio framework. It serves mock data based on OpenAPI 3.x specifications (JSON or YAML) and is designed for rapid prototyping and CI environments.
7+
8+
- **Type:** PHP Web Application (PSR-15 Middleware Stack)
9+
- **Framework:** [Mezzio](https://docs.mezzio.dev/)
10+
- **Core Libraries:**
11+
- `league/openapi-psr7-validator`: For request and response validation against OpenAPI specs.
12+
- `devizzent/cebe-php-openapi`: For parsing OpenAPI specifications.
13+
- `laminas/laminas-diactoros`: PSR-7 implementation.
14+
- `webmozart/assert`: For strict runtime type verification.
15+
- **Language:** PHP 8.3+ (Strict Types enabled)
16+
17+
## Architecture & Logic
18+
The application operates primarily through a series of PSR-15 middlewares and a stateless service registry:
19+
1. **`ForceMockActiveMiddleware`**: Ensures the mock server is active by default by setting the `X-OpenApi-Mock-Active` header.
20+
2. **`OpenApiMockMiddleware`**: The core logic that intercepts requests, validates them, and returns faked responses. It handles all documentation routes (`/`, `/openapi.yaml`) by passing them to the standard routing stack.
21+
3. **`FakerRegistry`**: A central, stateless registry for all mock generation services.
22+
4. **Type-Safe Enums**: Extensive use of PHP 8.3 Enums for `FakerType`, `FakerContext`, `HttpMethod`, and `RequestErrorType` to eliminate magic strings and harden type safety.
23+
5. **Factories:** Found in `src/Factory/` and use **PSR-17 interfaces** (`ResponseFactoryInterface`, `StreamFactoryInterface`) for dependency injection.
24+
25+
## Building and Running
26+
27+
### Development Server
28+
Run the built-in PHP server:
29+
```bash
30+
php -S localhost:8080 -t public
31+
```
32+
33+
### Docker (FrankenPHP)
34+
Optimized [FrankenPHP](https://frankenphp.dev/) environment with **Hot-Reload** support:
35+
```bash
36+
docker compose up -d
37+
```
38+
39+
### Environment Variables
40+
- `OPENAPI_SPEC`: Path or URL to the OpenAPI specification file (Default: `data/openapi.yaml`).
41+
42+
### Key Composer Scripts
43+
- `composer test`: Runs the full Codeception test suite (**95 tests passing**).
44+
- `composer test:coverage`: Generates code coverage reports (**~82% coverage**).
45+
- `composer bench`: Runs the PHPBench performance suite.
46+
- `composer stan`: Runs PHPStan static analysis (**Level 8 clean**).
47+
- `composer rector:fix`: Applies automated refactorings via Rector.
48+
- `composer cs:fix`: Fixes coding standards via PHP-CS-Fixer.
49+
50+
## Development Conventions
51+
52+
### Coding Style
53+
- **Standard:** PSR-12 / PER standard via PHP-CS-Fixer.
54+
- **Strict Typing:** All PHP files MUST start with `declare(strict_types=1);`.
55+
- **Modern PHP:** Leverage PHP 8.3 features like Enums, readonly properties, and constructor property promotion.
56+
57+
### PHPStan & Type Safety
58+
- **Stricteness:** Maintain a zero-error baseline at **PHPStan Level 8**.
59+
- **Assertions:** Use `Webmozart\Assert\Assert` for runtime type narrowing. This is foundational for the project's type safety.
60+
61+
### Testing Practices
62+
- **Framework:** [Codeception](https://codeception.com/).
63+
- **Suites:** `Acceptance`, `Unit`, `JsonAcceptance`, `RemoteAcceptance`.
64+
- **Base Class:** Unit tests MUST extend `\Codeception\Test\Unit`.
65+
- **Reproducing Bugs:** ALWAYS add a new regression test (unit or acceptance) before fixing a bug.
66+
67+
## Performance & Benchmarking
68+
- **PSR-6 Caching:** Caches parsed OpenAPI specifications.
69+
- **Schema Memoization:** Static caching in `SchemaFaker` avoids redundant recursive resolution.
70+
- **Metrics:** Middleware creation is **~21μs**, and full mock request processing averages **~4.2ms**.
71+
72+
## Mocking Logic
73+
- **`FakerRegistry`**: Manages stateless instances of `StringFaker`, `NumberFaker`, `ArrayFaker`, `ObjectFaker`, etc.
74+
- **OpenAPI 3.1 Support:** Full support for numeric `exclusiveMinimum` and `exclusiveMaximum`.
75+
- **Composition:** Robust handling of `anyOf`, `oneOf`, and `allOf`.
76+
- **Swagger UI:** Automatically available at the root (`/`) path.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Benjamin Fahl (WebProject.xyz) and contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

0 commit comments

Comments
 (0)