diff --git a/.cs.php b/.cs.php
new file mode 100644
index 000000000..00e0f6f7d
--- /dev/null
+++ b/.cs.php
@@ -0,0 +1,78 @@
+setUsingCache(false)
+ ->setRiskyAllowed(true)
+ ->setRules(
+ [
+ '@PSR1' => true,
+ '@PSR2' => true,
+ '@Symfony' => true,
+ 'psr_autoloading' => true,
+ // custom rules
+ 'align_multiline_comment' => ['comment_type' => 'phpdocs_only'], // psr-5
+ 'phpdoc_to_comment' => false,
+ 'no_superfluous_phpdoc_tags' => false,
+ 'array_indentation' => true,
+ 'array_syntax' => ['syntax' => 'short'],
+ 'cast_spaces' => ['space' => 'none'],
+ 'concat_space' => ['spacing' => 'one'],
+ 'compact_nullable_type_declaration' => true,
+ 'declare_equal_normalize' => ['space' => 'single'],
+ 'general_phpdoc_annotation_remove' => [
+ 'annotations' => [
+ 'author',
+ 'package',
+ ],
+ ],
+ 'increment_style' => ['style' => 'post'],
+ 'list_syntax' => ['syntax' => 'short'],
+ 'echo_tag_syntax' => ['format' => 'long'],
+ 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false],
+ 'phpdoc_align' => false,
+ 'phpdoc_no_empty_return' => false,
+ 'phpdoc_order' => true, // psr-5
+ 'phpdoc_no_useless_inheritdoc' => false,
+ 'protected_to_private' => false,
+ 'yoda_style' => false,
+ 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
+ 'ordered_imports' => [
+ 'sort_algorithm' => 'alpha',
+ 'imports_order' => ['class', 'function', 'const'],
+ ],
+ 'single_line_throw' => false,
+ 'declare_strict_types' => false,
+ 'blank_line_between_import_groups' => true,
+ 'fully_qualified_strict_types' => true,
+ 'no_null_property_initialization' => false,
+ 'nullable_type_declaration_for_default_null_value' => false,
+ 'operator_linebreak' => [
+ 'only_booleans' => true,
+ 'position' => 'beginning',
+ ],
+ 'global_namespace_import' => [
+ 'import_classes' => true,
+ 'import_constants' => null,
+ 'import_functions' => null
+ ],
+ 'class_definition' => [
+ 'space_before_parenthesis' => true,
+ ],
+ 'declare_equal_normalize' => false,
+ 'phpdoc_summary' => false,
+ 'phpdoc_add_missing_param_annotation' => false,
+ 'no_useless_concat_operator' => false,
+ 'fully_qualified_strict_types' => false,
+ 'trailing_comma_in_multiline' => ['elements' => ['arrays']],
+ ]
+ )
+ ->setFinder(
+ PhpCsFixer\Finder::create()
+ ->in(__DIR__ . '/Slim')
+ ->in(__DIR__ . '/tests')
+ ->name('*.php')
+ ->ignoreDotFiles(true)
+ ->ignoreVCS(true)
+ );
diff --git a/.editorconfig b/.editorconfig
index 6537ca467..89ed921e1 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -13,3 +13,11 @@ trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
+
+[*.neon]
+indent_style = tab
+indent_size = 4
+
+[composer.json]
+indent_style = space
+indent_size = 4
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 45dc50948..4c1ef00e0 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,17 +1,22 @@
-name: Tests
+name: tests
-on: [push, pull_request]
+on:
+ push:
+ branches:
+ - '4.x'
+ - '5.x'
+ pull_request:
+ branches:
+ - '*'
jobs:
tests:
name: Tests PHP ${{ matrix.php }}
runs-on: ubuntu-latest
- continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
- php: [7.4, 8.0, 8.1, 8.2, 8.3]
- experimental: [false]
+ php: [ 8.2, 8.3, 8.4 ]
include:
- php: 8.2
analysis: true
@@ -26,24 +31,30 @@ jobs:
php-version: ${{ matrix.php }}
coverage: xdebug
- - name: Install dependencies with Composer
- uses: ramsey/composer-install@v3
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress --no-suggest
- name: Coding standards
- if: matrix.analysis
- run: vendor/bin/phpcs
+ run: composer cs:check
+
+ - name: Code sniffer
+ run: composer sniffer:check
- name: Static analysis
- if: matrix.analysis
- run: vendor/bin/phpstan
+ run: composer stan
- name: Tests
- run: vendor/bin/phpunit --coverage-clover clover.xml
+ if: ${{ !matrix.analysis }}
+ run: composer test
+
+ - name: Tests with coverage
+ if: matrix.analysis
+ run: composer test:coverage
- name: Upload coverage results to Coveralls
if: matrix.analysis
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
- composer require php-coveralls/php-coveralls -n -W
- vendor/bin/php-coveralls --coverage_clover=clover.xml -v
+ composer global require php-coveralls/php-coveralls
+ php-coveralls --coverage_clover=build/coverage/clover.xml -v
diff --git a/.gitignore b/.gitignore
index 668b8eabc..d1e4bf6a0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,18 @@
-.DS_Store
-.idea
-.phpunit.result.cache
+# Composer
composer.lock
-phpunit.xml
+/vendor
+
+# PHPUnit
+/.phpunit.cache
+.phpunit.result.cache
+
+# IDEs
+/.fleet
+/.idea
+/.vscode
+
+# Build artifacts and temporary files
+.DS_Store
clover.xml
-vendor
-coverage
+/build
+/coverage
diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php
new file mode 100644
index 000000000..098222806
--- /dev/null
+++ b/.phpstorm.meta.php
@@ -0,0 +1,5 @@
+ '@']));
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6bcb127dd..b680319be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,237 +1,121 @@
# Changelog
-# 4.11.0 - 2022-11-06
-- [3180: Declare types](https://github.com/slimphp/Slim/pull/3180) thanks to @nbayramberdiyev
-- [3181: Update laminas/laminas-diactoros requirement from ^2.8 to ^2.9](https://github.com/slimphp/Slim/pull/3181) thanks to @dependabot[bot]
-- [3182: Update guzzlehttp/psr7 requirement from ^2.1 to ^2.2](https://github.com/slimphp/Slim/pull/3182) thanks to @dependabot[bot]
-- [3183: Update phpstan/phpstan requirement from ^1.4 to ^1.5](https://github.com/slimphp/Slim/pull/3183) thanks to @dependabot[bot]
-- [3184: Update adriansuter/php-autoload-override requirement from ^1.2 to ^1.3](https://github.com/slimphp/Slim/pull/3184) thanks to @dependabot[bot]
-- [3189: Update phpstan/phpstan requirement from ^1.5 to ^1.6](https://github.com/slimphp/Slim/pull/3189) thanks to @dependabot[bot]
-- [3191: Adding property types to Middleware classes](https://github.com/slimphp/Slim/pull/3191) thanks to @ashleycoles
-- [3193: Handlers types](https://github.com/slimphp/Slim/pull/3193) thanks to @ashleycoles
-- [3194: Adding types to AbstractErrorRenderer](https://github.com/slimphp/Slim/pull/3194) thanks to @ashleycoles
-- [3195: Adding prop types for Exception classes](https://github.com/slimphp/Slim/pull/3195) thanks to @ashleycoles
-- [3196: Adding property type declarations for Factory classes](https://github.com/slimphp/Slim/pull/3196) thanks to @ashleycoles
-- [3197: Remove redundant docblock types](https://github.com/slimphp/Slim/pull/3197) thanks to @theodorejb
-- [3199: Update laminas/laminas-diactoros requirement from ^2.9 to ^2.11](https://github.com/slimphp/Slim/pull/3199) thanks to @dependabot[bot]
-- [3200: Update phpstan/phpstan requirement from ^1.6 to ^1.7](https://github.com/slimphp/Slim/pull/3200) thanks to @dependabot[bot]
-- [3205: Update guzzlehttp/psr7 requirement from ^2.2 to ^2.4](https://github.com/slimphp/Slim/pull/3205) thanks to @dependabot[bot]
-- [3206: Update squizlabs/php_codesniffer requirement from ^3.6 to ^3.7](https://github.com/slimphp/Slim/pull/3206) thanks to @dependabot[bot]
-- [3207: Update phpstan/phpstan requirement from ^1.7 to ^1.8](https://github.com/slimphp/Slim/pull/3207) thanks to @dependabot[bot]
-- [3211: Assign null coalescing to coalesce equal](https://github.com/slimphp/Slim/pull/3211) thanks to @MathiasReker
-- [3213: Void return](https://github.com/slimphp/Slim/pull/3213) thanks to @MathiasReker
-- [3214: Is null](https://github.com/slimphp/Slim/pull/3214) thanks to @MathiasReker
-- [3216: Refactor](https://github.com/slimphp/Slim/pull/3216) thanks to @mehdihasanpour
-- [3218: Refactor some code](https://github.com/slimphp/Slim/pull/3218) thanks to @mehdihasanpour
-- [3221: Cleanup](https://github.com/slimphp/Slim/pull/3221) thanks to @mehdihasanpour
-- [3225: Update laminas/laminas-diactoros requirement from ^2.11 to ^2.14](https://github.com/slimphp/Slim/pull/3225) thanks to @dependabot[bot]
-- [3228: Using assertSame to let assert equal be restricted](https://github.com/slimphp/Slim/pull/3228) thanks to @peter279k
-- [3229: Update laminas/laminas-diactoros requirement from ^2.14 to ^2.17](https://github.com/slimphp/Slim/pull/3229) thanks to @dependabot[bot]
-- [3235: Persist routes indexed by name in RouteCollector for improved performance.](https://github.com/slimphp/Slim/pull/3235) thanks to @BusterNeece
-
-# 4.10.0 - 2022-03-14
-- [3120: Add a new PSR-17 factory to Psr17FactoryProvider](https://github.com/slimphp/Slim/pull/3120) thanks to @solventt
-- [3123: Replace deprecated setMethods() in tests](https://github.com/slimphp/Slim/pull/3123) thanks to @solventt
-- [3126: Update guzzlehttp/psr7 requirement from ^2.0 to ^2.1](https://github.com/slimphp/Slim/pull/3126) thanks to @dependabot[bot]
-- [3127: PHPStan v1.0](https://github.com/slimphp/Slim/pull/3127) thanks to @t0mmy742
-- [3128: Update phpstan/phpstan requirement from ^1.0 to ^1.2](https://github.com/slimphp/Slim/pull/3128) thanks to @dependabot[bot]
-- [3129: Deprecate PHP 7.3](https://github.com/slimphp/Slim/pull/3129) thanks to @l0gicgate
-- [3130: Removed double defined PHP 7.4](https://github.com/slimphp/Slim/pull/3130) thanks to @flangofas
-- [3132: Add new `RequestResponseNamedArgs` route strategy](https://github.com/slimphp/Slim/pull/3132) thanks to @adoy
-- [3133: Improve typehinting for `RouteParserInterface`](https://github.com/slimphp/Slim/pull/3133) thanks to @jerowork
-- [3135: Update phpstan/phpstan requirement from ^1.2 to ^1.3](https://github.com/slimphp/Slim/pull/3135) thanks to @dependabot[bot]
-- [3137: Update phpspec/prophecy requirement from ^1.14 to ^1.15](https://github.com/slimphp/Slim/pull/3137) thanks to @dependabot[bot]
-- [3138: Update license year](https://github.com/slimphp/Slim/pull/3138) thanks to @Awilum
-- [3139: Fixed #1730 (reintroduced in 4.x)](https://github.com/slimphp/Slim/pull/3139) thanks to @adoy
-- [3145: Update phpstan/phpstan requirement from ^1.3 to ^1.4](https://github.com/slimphp/Slim/pull/3145) thanks to @dependabot[bot]
-- [3146: Inherit HttpException from RuntimeException](https://github.com/slimphp/Slim/pull/3146) thanks to @nbayramberdiyev
-- [3148: Upgrade to HTML5](https://github.com/slimphp/Slim/pull/3148) thanks to @nbayramberdiyev
-- [3172: Update nyholm/psr7 requirement from ^1.4 to ^1.5](https://github.com/slimphp/Slim/pull/3172) thanks to @dependabot[bot]
-
-# 4.9.0 - 2021-10-05
-- [3058: Implement exception class for Gone Http error](https://github.com/slimphp/Slim/pull/3058) thanks to @TheKernelPanic
-- [3086: Update slim/psr7 requirement from ^1.3 to ^1.4](https://github.com/slimphp/Slim/pull/3086) thanks to @dependabot[bot]
-- [3087: Update nyholm/psr7-server requirement from ^1.0.1 to ^1.0.2](https://github.com/slimphp/Slim/pull/3087) thanks to @dependabot[bot]
-- [3093: Update phpstan/phpstan requirement from ^0.12.85 to ^0.12.90](https://github.com/slimphp/Slim/pull/3093) thanks to @dependabot[bot]
-- [3099: Allow updated psr log](https://github.com/slimphp/Slim/pull/3099) thanks to @t0mmy742
-- [3104: Drop php7.2](https://github.com/slimphp/Slim/pull/3104) thanks to @t0mmy742
-- [3106: Use PSR-17 factory from Guzzle/psr7 2.0](https://github.com/slimphp/Slim/pull/3106) thanks to @t0mmy742
-- [3108: Update README file](https://github.com/slimphp/Slim/pull/3108) thanks to @t0mmy742
-- [3112: Update laminas/laminas-diactoros requirement from ^2.6 to ^2.8](https://github.com/slimphp/Slim/pull/3112) thanks to @dependabot[bot]
-- [3114: Update slim/psr7 requirement from ^1.4 to ^1.5](https://github.com/slimphp/Slim/pull/3114) thanks to @dependabot[bot]
-- [3115: Update phpstan/phpstan requirement from ^0.12.96 to ^0.12.99](https://github.com/slimphp/Slim/pull/3115) thanks to @dependabot[bot]
-- [3116: Remove Zend Diactoros references](https://github.com/slimphp/Slim/pull/3116) thanks to @l0gicgate
-
-# 4.8.0 - 2021-05-19
-- [3034: Fix phpunit dependency version](https://github.com/slimphp/Slim/pull/3034) thanks to @l0gicgate
-- [3037: Replace Travis by GitHub Actions](https://github.com/slimphp/Slim/pull/3037) thanks to @t0mmy742
-- [3043: Cover App creation from AppFactory with empty Container](https://github.com/slimphp/Slim/pull/3043) thanks to @t0mmy742
-- [3045: Update phpstan/phpstan requirement from ^0.12.58 to ^0.12.64](https://github.com/slimphp/Slim/pull/3045) thanks to @dependabot-preview[bot]
-- [3047: documentation: min php 7.2 required](https://github.com/slimphp/Slim/pull/3047) thanks to @Rotzbua
-- [3054: Update phpstan/phpstan requirement from ^0.12.64 to ^0.12.70](https://github.com/slimphp/Slim/pull/3054) thanks to @dependabot-preview[bot]
-- [3056: Fix docblock in ErrorMiddleware](https://github.com/slimphp/Slim/pull/3056) thanks to @piotr-cz
-- [3060: Update phpstan/phpstan requirement from ^0.12.70 to ^0.12.80](https://github.com/slimphp/Slim/pull/3060) thanks to @dependabot-preview[bot]
-- [3061: Update nyholm/psr7 requirement from ^1.3 to ^1.4](https://github.com/slimphp/Slim/pull/3061) thanks to @dependabot-preview[bot]
-- [3063: Allow ^1.0 || ^2.0 in psr/container](https://github.com/slimphp/Slim/pull/3063) thanks to @Ayesh
-- [3069: Classname/Method Callable Arrays](https://github.com/slimphp/Slim/pull/3069) thanks to @ddrv
-- [3078: Update squizlabs/php_codesniffer requirement from ^3.5 to ^3.6](https://github.com/slimphp/Slim/pull/3078) thanks to @dependabot[bot]
-- [3079: Update phpspec/prophecy requirement from ^1.12 to ^1.13](https://github.com/slimphp/Slim/pull/3079) thanks to @dependabot[bot]
-- [3080: Update guzzlehttp/psr7 requirement from ^1.7 to ^1.8](https://github.com/slimphp/Slim/pull/3080) thanks to @dependabot[bot]
-- [3082: Update phpstan/phpstan requirement from ^0.12.80 to ^0.12.85](https://github.com/slimphp/Slim/pull/3082) thanks to @dependabot[bot]
-
-# 4.7.0 - 2020-11-30
+All notable changes to this project will be documented in this file.
-### Fixed
-- [3027: Fix: FastRoute dispatcher and data generator should match](https://github.com/slimphp/Slim/pull/3027) thanks to @edudobay
-
-### Added
-- [3015: PHP 8 support](https://github.com/slimphp/Slim/pull/3015) thanks to @edudobay
+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).
-### Optimizations
-- [3024: Randomize tests](https://github.com/slimphp/Slim/pull/3024) thanks to @pawel-slowik
-
-## 4.6.0 - 2020-11-15
-
-### Fixed
-- [2942: Fix PHPdoc for error handlers in ErrorMiddleware ](https://github.com/slimphp/Slim/pull/2942) thanks to @TiMESPLiNTER
-- [2944: Remove unused function in ErrorHandler](https://github.com/slimphp/Slim/pull/2944) thanks to @l0gicgate
-- [2960: Fix phpstan 0.12 errors](https://github.com/slimphp/Slim/pull/2960) thanks to @adriansuter
-- [2982: Removing cloning statements in tests](https://github.com/slimphp/Slim/pull/2982) thanks to @l0gicgate
-- [3017: Fix request creator factory test](https://github.com/slimphp/Slim/pull/3017) thanks to @pawel-slowik
-- [3022: Ensure RouteParser Always Present After Routing](https://github.com/slimphp/Slim/pull/3022) thanks to @l0gicgate
+## [Unreleased]
### Added
-- [2949: Add the support in composer.json](https://github.com/slimphp/Slim/pull/2949) thanks to @ddrv
-- [2958: Strict empty string content type checking in BodyParsingMiddleware::getMediaType](https://github.com/slimphp/Slim/pull/2958) thanks to @Ayesh
-- [2997: Add hints to methods](https://github.com/slimphp/Slim/pull/2997) thanks to @evgsavosin - [3000: Fix route controller test](https://github.com/slimphp/Slim/pull/3000) thanks to @pawel-slowik
-- [3001: Add missing `$strategy` parameter in a Route test](https://github.com/slimphp/Slim/pull/3001) thanks to @pawel-slowik
-
-### Optimizations
-- [2951: Minor optimizations in if() blocks](https://github.com/slimphp/Slim/pull/2951) thanks to @Ayesh
-- [2959: Micro optimization: Declare closures in BodyParsingMiddleware as static](https://github.com/slimphp/Slim/pull/2959) thanks to @Ayesh
-- [2978: Split the routing results to its own function.](https://github.com/slimphp/Slim/pull/2978) thanks to @dlundgren
-
-### Dependencies Updated
-- [2953: Update nyholm/psr7-server requirement from ^0.4.1](https://github.com/slimphp/Slim/pull/2953) thanks to @dependabot-preview[bot]
-- [2954: Update laminas/laminas-diactoros requirement from ^2.1 to ^2.3](https://github.com/slimphp/Slim/pull/2954) thanks to @dependabot-preview[bot]
-- [2955: Update guzzlehttp/psr7 requirement from ^1.5 to ^1.6](https://github.com/slimphp/Slim/pull/2955) thanks to @dependabot-preview[bot]
-- [2956: Update slim/psr7 requirement from ^1.0 to ^1.1](https://github.com/slimphp/Slim/pull/2956) thanks to @dependabot-preview[bot]
-- [2957: Update nyholm/psr7 requirement from ^1.1 to ^1.2](https://github.com/slimphp/Slim/pull/2957) thanks to @dependabot-preview[bot]
-- [2963: Update phpstan/phpstan requirement from ^0.12.23 to ^0.12.25](https://github.com/slimphp/Slim/pull/2963) thanks to @dependabot-preview[bot]
-- [2965: Update adriansuter/php-autoload-override requirement from ^1.0 to ^1.1](https://github.com/slimphp/Slim/pull/2965) thanks to @dependabot-preview[bot]
-- [2967: Update nyholm/psr7 requirement from ^1.2 to ^1.3](https://github.com/slimphp/Slim/pull/2967) thanks to @dependabot-preview[bot]
-- [2969: Update nyholm/psr7-server requirement from ^0.4.1 to ^1.0.0](https://github.com/slimphp/Slim/pull/2969) thanks to @dependabot-preview[bot]
-- [2970: Update phpstan/phpstan requirement from ^0.12.25 to ^0.12.26](https://github.com/slimphp/Slim/pull/2970) thanks to @dependabot-preview[bot]
-- [2971: Update phpstan/phpstan requirement from ^0.12.26 to ^0.12.27](https://github.com/slimphp/Slim/pull/2971) thanks to @dependabot-preview[bot]
-- [2972: Update phpstan/phpstan requirement from ^0.12.27 to ^0.12.28](https://github.com/slimphp/Slim/pull/2972) thanks to @dependabot-preview[bot]
-- [2973: Update phpstan/phpstan requirement from ^0.12.28 to ^0.12.29](https://github.com/slimphp/Slim/pull/2973) thanks to @dependabot-preview[bot]
-- [2975: Update phpstan/phpstan requirement from ^0.12.29 to ^0.12.30](https://github.com/slimphp/Slim/pull/2975) thanks to @dependabot-preview[bot]
-- [2976: Update phpstan/phpstan requirement from ^0.12.30 to ^0.12.31](https://github.com/slimphp/Slim/pull/2976) thanks to @dependabot-preview[bot]
-- [2980: Update phpstan/phpstan requirement from ^0.12.31 to ^0.12.32](https://github.com/slimphp/Slim/pull/2980) thanks to @dependabot-preview[bot]
-- [2981: Update phpspec/prophecy requirement from ^1.10 to ^1.11](https://github.com/slimphp/Slim/pull/2981) thanks to @dependabot-preview[bot]
-- [2986: Update phpstan/phpstan requirement from ^0.12.32 to ^0.12.33](https://github.com/slimphp/Slim/pull/2986) thanks to @dependabot-preview[bot]
-- [2990: Update phpstan/phpstan requirement from ^0.12.33 to ^0.12.34](https://github.com/slimphp/Slim/pull/2990) thanks to @dependabot-preview[bot]
-- [2991: Update phpstan/phpstan requirement from ^0.12.34 to ^0.12.35](https://github.com/slimphp/Slim/pull/2991) thanks to @dependabot-preview[bot]
-- [2993: Update phpstan/phpstan requirement from ^0.12.35 to ^0.12.36](https://github.com/slimphp/Slim/pull/2993) thanks to @dependabot-preview[bot]
-- [2995: Update phpstan/phpstan requirement from ^0.12.36 to ^0.12.37](https://github.com/slimphp/Slim/pull/2995) thanks to @dependabot-preview[bot]
-- [3010: Update guzzlehttp/psr7 requirement from ^1.6 to ^1.7](https://github.com/slimphp/Slim/pull/3010) thanks to @dependabot-preview[bot]
-- [3011: Update phpspec/prophecy requirement from ^1.11 to ^1.12](https://github.com/slimphp/Slim/pull/3011) thanks to @dependabot-preview[bot]
-- [3012: Update slim/http requirement from ^1.0 to ^1.1](https://github.com/slimphp/Slim/pull/3012) thanks to @dependabot-preview[bot]
-- [3013: Update slim/psr7 requirement from ^1.1 to ^1.2](https://github.com/slimphp/Slim/pull/3013) thanks to @dependabot-preview[bot]
-- [3014: Update laminas/laminas-diactoros requirement from ^2.3 to ^2.4](https://github.com/slimphp/Slim/pull/3014) thanks to @dependabot-preview[bot]
-- [3018: Update phpstan/phpstan requirement from ^0.12.37 to ^0.12.54](https://github.com/slimphp/Slim/pull/3018) thanks to @dependabot-preview[bot]
-
-## 4.5.0 - 2020-04-14
-### Added
-- [2928](https://github.com/slimphp/Slim/pull/2928) Test against PHP 7.4
-- [2937](https://github.com/slimphp/Slim/pull/2937) Add support for PSR-3
-
-### Fixed
-- [2916](https://github.com/slimphp/Slim/pull/2916) Rename phpcs.xml to phpcs.xml.dist
-- [2917](https://github.com/slimphp/Slim/pull/2917) Update .editorconfig
-- [2925](https://github.com/slimphp/Slim/pull/2925) ResponseEmitter: Don't remove Content-Type and Content-Length when body is empt
-- [2932](https://github.com/slimphp/Slim/pull/2932) Update the Tidelift enterprise language
-- [2938](https://github.com/slimphp/Slim/pull/2938) Modify usage of deprecated expectExceptionMessageRegExp() method
-
-## 4.4.0 - 2020-01-04
-
-### Added
-- [2862](https://github.com/slimphp/Slim/pull/2862) Optionally handle subclasses of exceptions in custom error handler
-- [2869](https://github.com/slimphp/Slim/pull/2869) php-di/php-di added in composer suggestion
-- [2874](https://github.com/slimphp/Slim/pull/2874) Add `null` to param type-hints
-- [2889](https://github.com/slimphp/Slim/pull/2889) Make `RouteContext` attributes customizable and change default to use private names
-- [2907](https://github.com/slimphp/Slim/pull/2907) Migrate to PSR-12 convention
-- [2910](https://github.com/slimphp/Slim/pull/2910) Migrate Zend to Laminas
-- [2912](https://github.com/slimphp/Slim/pull/2912) Add Laminas PSR17 Factory
-- [2913](https://github.com/slimphp/Slim/pull/2913) Update php-autoload-override version
-- [2914](https://github.com/slimphp/Slim/pull/2914) Added ability to add handled exceptions as an array
-
-### Fixed
-- [2864](https://github.com/slimphp/Slim/pull/2864) Optimize error message in error handling if displayErrorDetails was not set
-- [2876](https://github.com/slimphp/Slim/pull/2876) Update links from http to https
-- [2877](https://github.com/slimphp/Slim/pull/2877) Fix docblock for `Slim\Routing\RouteCollector::cacheFile`
-- [2878](https://github.com/slimphp/Slim/pull/2878) check body is writable only on ouput buffering append
-- [2896](https://github.com/slimphp/Slim/pull/2896) Render errors uniformly
-- [2902](https://github.com/slimphp/Slim/pull/2902) Fix prophecies
-- [2908](https://github.com/slimphp/Slim/pull/2908) Use autoload-dev for `Slim\Tests` namespace
+#### New Features
+
+- New `AppBuilder` to create a Slim App instance for different scenarios. Replaces the `AppFactory`.
+- Unified DI container resolution. All the factory logic has been removed and moved to the DI container. This reduces the internal complexity by delegating the building logic into the DI container.
+- Provide FIFO (first in, first out) middleware order support. LIFO is not supported anymore.
+- Optimized internal routing concept for better separation of concern and flexibility.
+ - `RoutingMiddleware` handles the routing process.
+ - `EndpointMiddleware` processes the routing results and invokes the controller/action handler.
+- Simplified Error handling concept. Relates to #3287.
+ - Separation of Exceptions handling, PHP Error handling and Exception logging into different middleware.
+ - `ExceptionLoggingMiddleware` for custom error logging.
+ - `ExceptionHandlingMiddleware` delegates exceptions to a custom error handler.
+ - `ErrorHandlingMiddleware` converts errors into `ErrorException` instances that can then be handled by the `ExceptionHandlingMiddleware` and `ExceptionLoggingMiddleware`.
+ - New custom error handlers using a new `ExceptionHandlerInterface`. See new `ExceptionHandlingMiddleware`.
+ - New `JsonExceptionRenderer` generates a JSON problem details (rfc7807) response
+ - New `XmlExceptionRenderer` generates a XML problem details (rfc7807) response
+- New `BasePathMiddleware` for dealing with Apache subdirectories.
+- New `HeadMethodMiddleware` ensures that the response body is empty for HEAD requests.
+- New `JsonRenderer` utility class for rendering JSON responses.
+- New `RequestResponseTypedArgs` invocation strategy for route parameters with type declarations.
+- New `UrlGeneratorMiddleware` injects the `UrlGenerator` into the request attributes.
+- New `CorsMiddleware` for handling CORS requests.
+- Support to build a custom middleware pipeline without the Slim App class. See new `ResponseFactoryMiddleware`
+- New media type detector
+- New Config class and ConfigInterface
+
+### Changed
+
+* Require PHP 8.2 or 8.3. News versions will be supported after a review and test process.
+* Migrated all tests to PHPUnit 11
+* Update GitHub action and build settings
+* Improve DI container integration. Make the DI container a first-class citizen. Require a PSR-11 package.
+* Ensure that route attributes are always in the Request. Related to #3280. See new `RoutingArgumentsMiddleware`.
+* Unify `CallableResolver` and `AdvancedCallableResolver`. Resolved with the new CallableResolver. Relates to #3073.
+- PSR-7 and PSR-15 compliance: Require at least psr/http-message 2.0.
+- PSR-11 compliance: Require at least psr/container 2.0.
+- PSR-3 compliance: Require at least psr/log 3.0
+- The `App` class is not a request handler that implements the `RequestHandlerInterface` because the request handler is now used internally and must be "unique" within the DI container.
### Removed
-- [2871](https://github.com/slimphp/Slim/pull/2871) Remove explicit type-hint
-- [2872](https://github.com/slimphp/Slim/pull/2872) Remove type-hint
-## 4.3.0 - 2019-10-05
-
-### Added
-- [2819](https://github.com/slimphp/Slim/pull/2819) Added description to addRoutingMiddleware()
-- [2820](https://github.com/slimphp/Slim/pull/2820) Update link to homepage in composer.json
-- [2828](https://github.com/slimphp/Slim/pull/2828) Allow URIs with leading multi-slashes
-- [2832](https://github.com/slimphp/Slim/pull/2832) Refactor `FastRouteDispatcher`
-- [2835](https://github.com/slimphp/Slim/pull/2835) Rename `pathFor` to `urlFor` in docblock
-- [2846](https://github.com/slimphp/Slim/pull/2846) Correcting the branch name as per issue-2843
-- [2849](https://github.com/slimphp/Slim/pull/2849) Create class alias for FastRoute\RouteCollector
-- [2855](https://github.com/slimphp/Slim/pull/2855) Add list of allowed methods to HttpMethodNotAllowedException
-- [2860](https://github.com/slimphp/Slim/pull/2860) Add base path to `$request` and use `RouteContext` to read
+* Remove LIFO middleware order support. Use FIFO instead.
+* Router cache file support (File IO was never sufficient. PHP OpCache is much faster)
+* The `$app->redirect()` method because it was not aware of the basePath. Use the `UrlGenerator` instead.
+* The route `setArguments` and `setArgument` methods. Use a middleware for custom route arguments now.
+* The `RouteContext::ROUTE` const. Use `$route = $request->getAttribute(RouteContext::ROUTING_RESULTS)->getRoute();` instead.
+* Old tests for PHP 7
+* Psalm
+* phpspec/prophecy
+* phpspec/prophecy-phpunit
### Fixed
-- [2839](https://github.com/slimphp/Slim/pull/2839) Fix description for handler signature in phpdocs
-- [2844](https://github.com/slimphp/Slim/pull/2844) Handle base path by routeCollector instead of RouteCollectorProxy
-- [2845](https://github.com/slimphp/Slim/pull/2845) Fix composer scripts
-- [2851](https://github.com/slimphp/Slim/pull/2851) Fix example of 'Hello World' app
-- [2854](https://github.com/slimphp/Slim/pull/2854) Fix undefined property in tests
-### Removed
-- [2853](https://github.com/slimphp/Slim/pull/2853) Remove unused classes
+- Resolving middleware breaks if resolver throws unexpected exception type #3071. Resolved with the new CallableResolver.
+- Forward logger to own `ErrorHandlingMiddleware` #2943. See new `ExceptionLoggingMiddleware`.
+- Code styles (PSR-12)
-## 4.2.0 - 2019-08-20
+## Files
### Added
-- [2787](https://github.com/slimphp/Slim/pull/2787) Add an advanced callable resolver
-- [2791](https://github.com/slimphp/Slim/pull/2791) Add `inferPrivatePropertyTypeFromConstructor` to phpstan
-- [2793](https://github.com/slimphp/Slim/pull/2793) Add ability to configure application via a container in `AppFactory`
-- [2798](https://github.com/slimphp/Slim/pull/2798) Add PSR-7 Agnostic Body Parsing Middleware
-- [2801](https://github.com/slimphp/Slim/pull/2801) Add `setLogErrorRenderer()` method to `ErrorHandler`
-- [2807](https://github.com/slimphp/Slim/pull/2807) Add check for Slim callable notation if no resolver given
-- [2803](https://github.com/slimphp/Slim/pull/2803) Add ability to emit non seekable streams in `ResponseEmitter`
-- [2817](https://github.com/slimphp/Slim/pull/2817) Add the ability to pass in a custom `MiddlewareDispatcherInterface` to the `App`
-### Fixed
-- [2789](https://github.com/slimphp/Slim/pull/2789) Fix Cookie header detection in `ResponseEmitter`
-- [2796](https://github.com/slimphp/Slim/pull/2796) Fix http message format
-- [2800](https://github.com/slimphp/Slim/pull/2800) Fix null comparisons more clear in `ErrorHandler`
-- [2802](https://github.com/slimphp/Slim/pull/2802) Fix incorrect search of a header in stack
-- [2806](https://github.com/slimphp/Slim/pull/2806) Simplify `Route::prepare()` method argument preparation
-- [2809](https://github.com/slimphp/Slim/pull/2809) Eliminate a duplicate code via HOF in `MiddlewareDispatcher`
-- [2816](https://github.com/slimphp/Slim/pull/2816) Fix RouteCollectorProxy::redirect() bug
+- `Slim/Builder/AppBuilder.php`: Introduced to replace `Slim/Factory/AppFactory.php`.
+- `Slim/Container/CallableResolver.php`: New implementation of the Callable Resolver.
+- `Slim/Container/DefaultDefinitions.php`: Default container definitions.
+- `Slim/Handlers/ExceptionHandler.php`: New Exception Handler for better error handling.
+- `Slim/Handlers/ExceptionRendererTrait.php`: Common functionality for exception renderers.
+- `Slim/Handlers/HtmlExceptionRenderer.php`: HTML-based exception renderer.
+- `Slim/Handlers/JsonExceptionRenderer.php`: JSON-based exception renderer.
+- `Slim/Handlers/XmlExceptionRenderer.php`: XML-based exception renderer.
+- `Slim/Interfaces/ExceptionRendererInterface.php`: New interface for exception renderers.
+- `Slim/Logging/StdErrorLogger.php`: Logger that outputs to stderr.
+- `Slim/Logging/StdOutLogger.php`: Logger that outputs to stdout.
+- `Slim/Middleware/ErrorHandlingMiddleware.php`: Middleware for handling errors.
+- `Slim/Middleware/ExceptionHandlingMiddleware.php`: Middleware for handling exceptions.
+- `Slim/Middleware/ExceptionLoggingMiddleware.php`: Middleware for logging exceptions.
+- `Slim/Middleware/ResponseFactoryMiddleware.php`: Middleware for response creation.
+- `Slim/Middleware/UrlGeneratorMiddleware.php`: Middleware for URL generation.
+- `Slim/Renderers/JsonRenderer.php`: Renderer for JSON responses.
+- `Slim/RequestHandler/MiddlewareRequestHandler.php`: Handles requests through middleware.
+- `Slim/RequestHandler/MiddlewareResolver.php`: Resolves middleware for handling requests.
+- `Slim/RequestHandler/Runner.php`: Handles the execution flow of requests.
+- `Slim/Strategies/RequestResponseNamedArgs.php`: New strategy for named arguments in RequestResponse.
+- `Slim/Strategies/RequestResponseTypedArgs.php`: New strategy for typed arguments in RequestResponse. Requires `php-di/invoker`.
+
+New files for routing, middleware, and factories, including:
+
+- `Slim/Interfaces/EmitterInterface.php`
+- `Slim/Middleware/BasePathMiddleware.php`
+- `Slim/Routing/Router.php`, `RouteGroup.php`, `UrlGenerator.php`
+
+### Changed
+
+- `Slim/Interfaces/ErrorHandlerInterface.php` renamed to `Slim/Interfaces/ExceptionHandlerInterface.php`.
+- `Slim/Interfaces/RouteParserInterface.php` renamed to `Slim/Interfaces/UrlGeneratorInterface.php`.
+- `Slim/Handlers/Strategies/RequestResponse.php` renamed to `Slim/Strategies/RequestResponse.php`.
+- `Slim/Handlers/Strategies/RequestResponseArgs.php` renamed to `Slim/Strategies/RequestResponseArgs.php`.
+- `Slim/Error/Renderers/PlainTextErrorRenderer.php` renamed to `Slim/Handlers/PlainTextExceptionRenderer.php`.
+- `Slim/Routing/RouteContext.php`
### Removed
-- [2811](https://github.com/slimphp/Slim/pull/2811) Remove `DeferredCallable`
-## 4.1.0 - 2019-08-06
+- `Slim/CallableResolver.php`
+- `Slim/Handlers/ErrorHandler.php`
+- `Slim/Factory/AppFactory.php` and related `Psr17` factories.
+- `Slim/Interfaces/AdvancedCallableResolverInterface.php`
+- `Slim/Interfaces/RouteCollectorInterface.php`,
+- `RouteCollectorProxyInterface.php`,
+- `RouteGroupInterface.php`, and other route-related interfaces.
+- `Slim/Routing/Dispatcher.php`, `FastRouteDispatcher.php` and related routing classes.
-### Added
-- [#2779](https://github.com/slimphp/Slim/pull/2774) Add support for Slim callables `Class:method` resolution & Container Closure auto-binding in `MiddlewareDispatcher`
-- [#2774](https://github.com/slimphp/Slim/pull/2774) Add possibility for custom `RequestHandler` invocation strategies
-
-### Fixed
-- [#2776](https://github.com/slimphp/Slim/pull/2774) Fix group middleware on multiple nested groups
diff --git a/LICENSE.md b/LICENSE.md
index d6fd559c7..afe24ed8d 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,4 +1,4 @@
-Copyright (c) 2011-2022 Josh Lockhart
+Copyright (c) 2011-2024 Josh Lockhart
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
index 2b8ad0496..ebdee103a 100644
--- a/MAINTAINERS.md
+++ b/MAINTAINERS.md
@@ -4,7 +4,7 @@ There aren't many rules for maintainers of Slim to remember; what we have is lis
## We don't merge our own PRs
-Our code is better if more than one set of eyes looks at it. Therefore we do not merge our own pull requests unless there is an exceptional circumstance. This helps to spot errors in the patch and also enables us to share information about the project around the maintainer team.
+Our code is better if more than one set of eyes looks at it. Therefore, we do not merge our own pull requests unless there is an exceptional circumstance. This helps to spot errors in the patch and also enables us to share information about the project around the maintainer team.
## PRs tagged `WIP` are not ready to be merged
diff --git a/README.md b/README.md
index 7ffc5575e..ca8c8ab82 100644
--- a/README.md
+++ b/README.md
@@ -7,106 +7,100 @@
Slim is a PHP micro-framework that helps you quickly write simple yet powerful web applications and APIs.
-## Installation
+## Requirements
-It's recommended that you use [Composer](https://getcomposer.org/) to install Slim.
+* PHP 8.2
-```bash
-$ composer require slim/slim
-```
+## Installation
-This will install Slim and all required dependencies. Slim requires PHP 7.4 or newer.
+It's recommended that you use [Composer](https://getcomposer.org/) to install Slim.
-## Choose a PSR-7 Implementation & ServerRequest Creator
+To install the Slim 5 microframework package, run:
-Before you can get up and running with Slim you will need to choose a PSR-7 implementation that best fits your application. A few notable ones:
-- [Slim-Psr7](https://github.com/slimphp/Slim-Psr7) - This is the Slim Framework PSR-7 implementation
-- [httpsoft/http-message](https://github.com/httpsoft/http-message) & [httpsoft/http-server-request](https://github.com/httpsoft/http-server-request) - This is the fastest, strictest and most lightweight implementation available
-- [Nyholm/psr7](https://github.com/Nyholm/psr7) & [Nyholm/psr7-server](https://github.com/Nyholm/psr7-server) - Performance is almost the same as the HttpSoft implementation
-- [Guzzle/psr7](https://github.com/guzzle/psr7) - This is the implementation used by the Guzzle Client, featuring extra functionality for stream and file handling
-- [laminas-diactoros](https://github.com/laminas/laminas-diactoros) - This is the Laminas (Zend) PSR-7 implementation
+```
+composer require slim/slim 5.x-dev
+```
+Then install a DI container (PSR-11) package for dependency injection, e.g. PHP-DI:
-## Slim-Http Decorators
+```
+composer require php-di/php-di
+```
-[Slim-Http](https://github.com/slimphp/Slim-Http) is a set of decorators for any PSR-7 implementation that we recommend is used with Slim Framework.
-To install the Slim-Http library simply run the following command:
+PHP-DI is the recommended DI container implementation.
+Of course, you can install any other [PSR-11](https://packagist.org/search/?tags=PSR-11) compatible package.
-```bash
-composer require slim/http
-```
+## Choose a PSR-7 HTTP Implementation
-The `ServerRequest` and `Response` object decorators are automatically detected and applied by the internal factories. If you have installed Slim-Http and wish to turn off automatic object decoration then you can use the following statements:
-```php
-build();
-// Add error middleware
-$app->addErrorMiddleware(true, true, true);
+// Add middleware
+$app->add(ExceptionHandlingMiddleware::class);
+$app->add(RoutingMiddleware::class);
+$app->add(EndpointMiddleware::class);
// Add routes
$app->get('/', function (Request $request, Response $response) {
- $response->getBody()->write('Try /hello/world');
- return $response;
-});
-
-$app->get('/hello/{name}', function (Request $request, Response $response, $args) {
- $name = $args['name'];
- $response->getBody()->write("Hello, $name");
+ $response->getBody()->write('Hello, World!');
+
return $response;
});
+// Run the request handler
$app->run();
```
You may quickly test this using the built-in PHP server:
```bash
-$ php -S localhost:8000 -t public
+php -S localhost:8000 -t public
```
-Going to http://localhost:8000/hello/world will now display "Hello, world".
+Going to http://localhost:8000/ will now display "Hello, World".
-For more information on how to configure your web server, see the [Documentation](https://www.slimframework.com/docs/v4/start/web-servers.html).
+For more information on how to configure your web server,
+see the [Documentation](https://www.slimframework.com/docs/v5/start/web-servers.html).
## Tests
+
To execute the test suite, you'll need to install all development dependencies.
```bash
-$ git clone https://github.com/slimphp/Slim
-$ composer install
-$ composer test
+git clone https://github.com/slimphp/Slim
+composer install
+composer test
```
## Contributing
@@ -118,21 +112,26 @@ Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
Learn more at these links:
- [Website](https://www.slimframework.com)
-- [Documentation](https://www.slimframework.com/docs/v4/start/installation.html)
+- [Documentation](https://www.slimframework.com/docs/v5/start/installation.html)
- [Slack](https://slimphp.slack.com)
- [Support Forum](https://discourse.slimframework.com)
- [Twitter](https://twitter.com/slimphp)
-- [Resources](https://github.com/xssc/awesome-slim)
## Security
-If you discover security related issues, please email security@slimframework.com instead of using the issue tracker.
+If you discover security related issues, please email security@slimframework.com instead
+of using the issue tracker.
## For enterprise
Available as part of the Tidelift Subscription.
-The maintainers of `Slim` and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/packagist-slim-slim?utm_source=packagist-slim-slim&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
+The maintainers of `Slim` and thousands of other packages are working with Tidelift
+to deliver commercial support and maintenance for the open source dependencies
+you use to build your applications. Save time, reduce risk, and improve code health,
+while paying the maintainers of the exact dependencies you use.
+
+[Learn more.](https://tidelift.com/subscription/pkg/packagist-slim-slim?utm_source=packagist-slim-slim&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
## Contributors
diff --git a/SECURITY.md b/SECURITY.md
index a5b6df0b4..524d997c0 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -4,10 +4,10 @@
| Version | Supported |
-| ------- | ------------------ |
+|---------| ------------------ |
| 3.x.x | :white_check_mark: |
| 4.x.x | :white_check_mark: |
-
+| 5.x.x | :white_check_mark: |
### Reporting a Vulnerability
diff --git a/Slim/App.php b/Slim/App.php
index 68b900023..6d6cc20c7 100644
--- a/Slim/App.php
+++ b/Slim/App.php
@@ -1,9 +1,9 @@
+ *
+ * @api
*/
-class App extends RouteCollectorProxy implements RequestHandlerInterface
+class App implements RouteCollectionInterface
{
+ use RouteCollectionTrait;
+
/**
- * Current version
+ * Current Slim Framework version.
*
* @var string
*/
- public const VERSION = '4.12.0';
+ public const VERSION = '5.0.0-alpha';
- protected RouteResolverInterface $routeResolver;
+ /**
+ * The dependency injection container instance.
+ */
+ private ContainerInterface $container;
- protected MiddlewareDispatcherInterface $middlewareDispatcher;
+ /**
+ * The server request creator instance.
+ */
+ private ServerRequestCreatorInterface $serverRequestCreator;
/**
- * @param TContainerInterface $container
+ * The request handler responsible for processing the request through middleware and routing.
*/
- public function __construct(
- ResponseFactoryInterface $responseFactory,
- ?ContainerInterface $container = null,
- ?CallableResolverInterface $callableResolver = null,
- ?RouteCollectorInterface $routeCollector = null,
- ?RouteResolverInterface $routeResolver = null,
- ?MiddlewareDispatcherInterface $middlewareDispatcher = null
- ) {
- parent::__construct(
- $responseFactory,
- $callableResolver ?? new CallableResolver($container),
- $container,
- $routeCollector
- );
+ private RequestHandlerInterface $requestHandler;
- $this->routeResolver = $routeResolver ?? new RouteResolver($this->routeCollector);
- $routeRunner = new RouteRunner($this->routeResolver, $this->routeCollector->getRouteParser(), $this);
+ /**
+ * The router instance for handling route definitions and matching.
+ */
+ private Router $router;
- if (!$middlewareDispatcher) {
- $middlewareDispatcher = new MiddlewareDispatcher($routeRunner, $this->callableResolver, $container);
- } else {
- $middlewareDispatcher->seedMiddlewareStack($routeRunner);
- }
+ /**
+ * The emitter instance for sending the HTTP response to the client.
+ */
+ private EmitterInterface $emitter;
- $this->middlewareDispatcher = $middlewareDispatcher;
+ /**
+ * The constructor.
+ *
+ * Initializes the Slim application with the provided container, request creator,
+ * request handler, router, and emitter.
+ *
+ * @param ContainerInterface $container The dependency injection container
+ */
+ public function __construct(ContainerInterface $container)
+ {
+ $this->container = $container;
+ $this->serverRequestCreator = $container->get(ServerRequestCreatorInterface::class);
+ $this->requestHandler = $container->get(RequestHandlerInterface::class);
+ $this->router = $container->get(Router::class);
+ $this->emitter = $container->get(EmitterInterface::class);
}
/**
- * @return RouteResolverInterface
+ * Get the dependency injection container.
+ *
+ * @return ContainerInterface The DI container instance
*/
- public function getRouteResolver(): RouteResolverInterface
+ public function getContainer(): ContainerInterface
{
- return $this->routeResolver;
+ return $this->container;
}
/**
- * @return MiddlewareDispatcherInterface
+ * Define a new route with the specified HTTP methods and URI pattern.
+ *
+ * @param array $methods The HTTP methods the route should respond to
+ * @param string $path The URI pattern for the route
+ * @param callable|string $handler The route handler callable or controller method
+ *
+ * @return Route The newly created route instance
*/
- public function getMiddlewareDispatcher(): MiddlewareDispatcherInterface
+ public function map(array $methods, string $path, callable|string $handler): Route
{
- return $this->middlewareDispatcher;
+ return $this->router->map($methods, $path, $handler);
}
/**
- * @param MiddlewareInterface|string|callable $middleware
- * @return App
+ * Define a route group with a common URI prefix and a set of routes or middleware.
+ *
+ * @param string $path The URI pattern prefix for the group
+ * @param callable $handler The group handler which defines routes or middleware
+ *
+ * @return RouteGroup The newly created route group instance
*/
- public function add($middleware): self
+ public function group(string $path, callable $handler): RouteGroup
{
- $this->middlewareDispatcher->add($middleware);
- return $this;
+ return $this->router->group($path, $handler);
}
/**
- * @param MiddlewareInterface $middleware
- * @return App
+ * Get the base path used for routing.
+ *
+ * @return string The base path used for routing
*/
- public function addMiddleware(MiddlewareInterface $middleware): self
+ public function getBasePath(): string
{
- $this->middlewareDispatcher->addMiddleware($middleware);
- return $this;
+ return $this->router->getBasePath();
}
/**
- * Add the Slim built-in routing middleware to the app middleware stack
+ * Set the base path used for routing.
*
- * This method can be used to control middleware order and is not required for default routing operation.
+ * @param string $basePath The base path to use for routing
*
- * @return RoutingMiddleware
+ * @return self The current App instance for method chaining
*/
- public function addRoutingMiddleware(): RoutingMiddleware
+ public function setBasePath(string $basePath): self
{
- $routingMiddleware = new RoutingMiddleware(
- $this->getRouteResolver(),
- $this->getRouteCollector()->getRouteParser()
- );
- $this->add($routingMiddleware);
- return $routingMiddleware;
+ $this->router->setBasePath($basePath);
+
+ return $this;
}
/**
- * Add the Slim built-in error middleware to the app middleware stack
- *
- * @param bool $displayErrorDetails
- * @param bool $logErrors
- * @param bool $logErrorDetails
- * @param LoggerInterface|null $logger
- *
- * @return ErrorMiddleware
- */
- public function addErrorMiddleware(
- bool $displayErrorDetails,
- bool $logErrors,
- bool $logErrorDetails,
- ?LoggerInterface $logger = null
- ): ErrorMiddleware {
- $errorMiddleware = new ErrorMiddleware(
- $this->getCallableResolver(),
- $this->getResponseFactory(),
- $displayErrorDetails,
- $logErrors,
- $logErrorDetails,
- $logger
- );
- $this->add($errorMiddleware);
- return $errorMiddleware;
+ * Add a new middleware to the stack.
+ */
+ public function add(MiddlewareInterface|callable|string $middleware): self
+ {
+ $this->router->add($middleware);
+
+ return $this;
}
/**
- * Add the Slim body parsing middleware to the app middleware stack
+ * Add a new middleware to the application's middleware stack.
*
- * @param callable[] $bodyParsers
+ * @param MiddlewareInterface $middleware The middleware to add
*
- * @return BodyParsingMiddleware
+ * @return self The current App instance for method chaining
*/
- public function addBodyParsingMiddleware(array $bodyParsers = []): BodyParsingMiddleware
+ public function addMiddleware(MiddlewareInterface $middleware): self
{
- $bodyParsingMiddleware = new BodyParsingMiddleware($bodyParsers);
- $this->add($bodyParsingMiddleware);
- return $bodyParsingMiddleware;
+ $this->router->addMiddleware($middleware);
+
+ return $this;
}
/**
- * Run application
+ * Run the Slim application.
+ *
+ * This method traverses the application's middleware stack, processes the incoming HTTP request,
+ * and emits the resultant HTTP response to the client.
*
- * This method traverses the application middleware stack and then sends the
- * resultant Response object to the HTTP client.
+ * @param ServerRequestInterface|null $request The HTTP request to handle.
+ * If null, it creates a request from globals.
*
- * @param ServerRequestInterface|null $request
* @return void
*/
public function run(?ServerRequestInterface $request = null): void
{
if (!$request) {
- $serverRequestCreator = ServerRequestCreatorFactory::create();
- $request = $serverRequestCreator->createServerRequestFromGlobals();
+ $request = $this->serverRequestCreator->createServerRequestFromGlobals();
}
$response = $this->handle($request);
- $responseEmitter = new ResponseEmitter();
- $responseEmitter->emit($response);
+
+ $this->emitter->emit($response);
}
/**
- * Handle a request
+ * Handle an incoming HTTP request.
*
- * This method traverses the application middleware stack and then returns the
- * resultant Response object.
+ * This method processes the request through the application's middleware stack and router,
+ * returning the resulting HTTP response.
*
- * @param ServerRequestInterface $request
- * @return ResponseInterface
+ * @param ServerRequestInterface $request The HTTP request to handle
+ *
+ * @return ResponseInterface The HTTP response
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
- $response = $this->middlewareDispatcher->handle($request);
-
- /**
- * This is to be in compliance with RFC 2616, Section 9.
- * If the incoming request method is HEAD, we need to ensure that the response body
- * is empty as the request may fall back on a GET route handler due to FastRoute's
- * routing logic which could potentially append content to the response body
- * https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
- */
- $method = strtoupper($request->getMethod());
- if ($method === 'HEAD') {
- $emptyBody = $this->responseFactory->createResponse()->getBody();
- return $response->withBody($emptyBody);
- }
+ $request = $request->withAttribute(MiddlewareRequestHandler::MIDDLEWARE, $this->router->getMiddlewareStack());
- return $response;
+ return $this->requestHandler->handle($request);
}
}
diff --git a/Slim/Builder/AppBuilder.php b/Slim/Builder/AppBuilder.php
new file mode 100644
index 000000000..5b64646b1
--- /dev/null
+++ b/Slim/Builder/AppBuilder.php
@@ -0,0 +1,143 @@
+addDefinitions(DefaultDefinitions::class);
+ $this->addDefinitions(HttpDefinitions::class);
+ }
+
+ /**
+ * Builds the Slim application instance using the configured DI container.
+ *
+ * @return App The fully built Slim application instance
+ */
+ public function build(): App
+ {
+ return $this->buildContainer()->get(App::class);
+ }
+
+ /**
+ * Creates and configures the DI container.
+ *
+ * If a custom container factory is set, it will be used to create the container;
+ * otherwise, a default container with the provided definitions will be created.
+ *
+ * @return ContainerInterface The configured DI container
+ */
+ private function buildContainer(): ContainerInterface
+ {
+ return $this->containerFactory
+ ? call_user_func($this->containerFactory, $this->definitions)
+ : new Container($this->definitions);
+ }
+
+ /**
+ * Sets the service definitions for the DI container.
+ *
+ * The method accepts either an array of definitions or the name of a class that provides definitions.
+ * If a class name is provided, its definitions are added to the existing ones.
+ *
+ * @param array|string $definitions An array of service definitions or a class name providing them
+ *
+ * @throws RuntimeException
+ *
+ * @return self The current AppBuilder instance for method chaining
+ */
+ public function addDefinitions(array|string $definitions): self
+ {
+ if (is_string($definitions)) {
+ if (class_exists($definitions)) {
+ $definitions = (array)call_user_func(new $definitions());
+ } else {
+ $definitions = require $definitions;
+
+ if (!is_array($definitions)) {
+ throw new RuntimeException('Definition file should return an array of definitions');
+ }
+ }
+ }
+
+ $this->definitions = array_merge($this->definitions, $definitions);
+
+ return $this;
+ }
+
+ /**
+ * Sets a custom factory for creating the DI container.
+ *
+ * @param callable $factory A callable that returns a configured DI container
+ *
+ * @return self The current AppBuilder instance for method chaining
+ */
+ public function setContainerFactory(callable $factory): self
+ {
+ $this->containerFactory = $factory;
+
+ return $this;
+ }
+
+ /**
+ * Sets application-wide settings in the DI container.
+ *
+ * This method allows the user to configure various settings for the Slim application,
+ * by passing an associative array of settings.
+ *
+ * @param array $settings An associative array of application settings
+ *
+ * @return self The current AppBuilder instance for method chaining
+ */
+ public function setSettings(array $settings): self
+ {
+ $this->addDefinitions(
+ [
+ 'settings' => $settings,
+ ]
+ );
+
+ return $this;
+ }
+}
diff --git a/Slim/CallableResolver.php b/Slim/CallableResolver.php
deleted file mode 100644
index dab46c460..000000000
--- a/Slim/CallableResolver.php
+++ /dev/null
@@ -1,199 +0,0 @@
-container = $container;
- }
-
- /**
- * {@inheritdoc}
- */
- public function resolve($toResolve): callable
- {
- $toResolve = $this->prepareToResolve($toResolve);
- if (is_callable($toResolve)) {
- return $this->bindToContainer($toResolve);
- }
- $resolved = $toResolve;
- if (is_string($toResolve)) {
- $resolved = $this->resolveSlimNotation($toResolve);
- $resolved[1] ??= '__invoke';
- }
- $callable = $this->assertCallable($resolved, $toResolve);
- return $this->bindToContainer($callable);
- }
-
- /**
- * {@inheritdoc}
- */
- public function resolveRoute($toResolve): callable
- {
- return $this->resolveByPredicate($toResolve, [$this, 'isRoute'], 'handle');
- }
-
- /**
- * {@inheritdoc}
- */
- public function resolveMiddleware($toResolve): callable
- {
- return $this->resolveByPredicate($toResolve, [$this, 'isMiddleware'], 'process');
- }
-
- /**
- * @param string|callable $toResolve
- *
- * @throws RuntimeException
- */
- private function resolveByPredicate($toResolve, callable $predicate, string $defaultMethod): callable
- {
- $toResolve = $this->prepareToResolve($toResolve);
- if (is_callable($toResolve)) {
- return $this->bindToContainer($toResolve);
- }
- $resolved = $toResolve;
- if ($predicate($toResolve)) {
- $resolved = [$toResolve, $defaultMethod];
- }
- if (is_string($toResolve)) {
- [$instance, $method] = $this->resolveSlimNotation($toResolve);
- if ($method === null && $predicate($instance)) {
- $method = $defaultMethod;
- }
- $resolved = [$instance, $method ?? '__invoke'];
- }
- $callable = $this->assertCallable($resolved, $toResolve);
- return $this->bindToContainer($callable);
- }
-
- /**
- * @param mixed $toResolve
- */
- private function isRoute($toResolve): bool
- {
- return $toResolve instanceof RequestHandlerInterface;
- }
-
- /**
- * @param mixed $toResolve
- */
- private function isMiddleware($toResolve): bool
- {
- return $toResolve instanceof MiddlewareInterface;
- }
-
- /**
- * @throws RuntimeException
- *
- * @return array{object, string|null} [Instance, Method Name]
- */
- private function resolveSlimNotation(string $toResolve): array
- {
- /** @psalm-suppress ArgumentTypeCoercion */
- preg_match(CallableResolver::$callablePattern, $toResolve, $matches);
- [$class, $method] = $matches ? [$matches[1], $matches[2]] : [$toResolve, null];
-
- if ($this->container && $this->container->has($class)) {
- $instance = $this->container->get($class);
- if (!is_object($instance)) {
- throw new RuntimeException(sprintf('%s container entry is not an object', $class));
- }
- } else {
- if (!class_exists($class)) {
- if ($method) {
- $class .= '::' . $method . '()';
- }
- throw new RuntimeException(sprintf('Callable %s does not exist', $class));
- }
- $instance = new $class($this->container);
- }
- return [$instance, $method];
- }
-
- /**
- * @param mixed $resolved
- * @param mixed $toResolve
- *
- * @throws RuntimeException
- */
- private function assertCallable($resolved, $toResolve): callable
- {
- if (!is_callable($resolved)) {
- if (is_callable($toResolve) || is_object($toResolve) || is_array($toResolve)) {
- $formatedToResolve = ($toResolveJson = json_encode($toResolve)) !== false ? $toResolveJson : '';
- } else {
- $formatedToResolve = is_string($toResolve) ? $toResolve : '';
- }
- throw new RuntimeException(sprintf('%s is not resolvable', $formatedToResolve));
- }
- return $resolved;
- }
-
- private function bindToContainer(callable $callable): callable
- {
- if (is_array($callable) && $callable[0] instanceof Closure) {
- $callable = $callable[0];
- }
- if ($this->container && $callable instanceof Closure) {
- /** @var Closure $callable */
- $callable = $callable->bindTo($this->container);
- }
- return $callable;
- }
-
- /**
- * @param string|callable $toResolve
- * @return string|callable
- */
- private function prepareToResolve($toResolve)
- {
- if (!is_array($toResolve)) {
- return $toResolve;
- }
- $candidate = $toResolve;
- $class = array_shift($candidate);
- $method = array_shift($candidate);
- if (is_string($class) && is_string($method)) {
- return $class . ':' . $method;
- }
- return $toResolve;
- }
-}
diff --git a/Slim/Configuration/Config.php b/Slim/Configuration/Config.php
new file mode 100644
index 000000000..2b1a4169b
--- /dev/null
+++ b/Slim/Configuration/Config.php
@@ -0,0 +1,41 @@
+data = $data;
+ }
+
+ public function get(string $key, mixed $default = null): mixed
+ {
+ if (array_key_exists($key, $this->data)) {
+ return $this->data[$key] ?? $default;
+ }
+
+ $result = $this->data;
+
+ foreach (explode('.', $key) as $offset) {
+ if (!isset($result[$offset])) {
+ return $default;
+ }
+ $result = $result[$offset];
+ }
+
+ return $result;
+ }
+}
diff --git a/Slim/Container/ContainerResolver.php b/Slim/Container/ContainerResolver.php
new file mode 100644
index 000000000..33054f496
--- /dev/null
+++ b/Slim/Container/ContainerResolver.php
@@ -0,0 +1,124 @@
+container = $container;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resolve(callable|object|array|string $identifier): mixed
+ {
+ if (is_object($identifier) || is_callable($identifier)) {
+ return $identifier;
+ }
+
+ // The callable is a container entry name
+ if (is_string($identifier)) {
+ $identifier = $this->processStringNotation($identifier);
+ }
+
+ if (is_string($identifier)) {
+ return $this->container->get($identifier);
+ }
+
+ // The callable is an array whose first item is a container entry name
+ // e.g. ['some-container-entry', 'methodToCall']
+ if (is_string($identifier[0] ?? null)) {
+ // Replace the container entry name by the actual object
+ $identifier[0] = $this->container->get($identifier[0]);
+
+ if (!method_exists($identifier[0], (string)$identifier[1])) {
+ throw new RuntimeException(sprintf('The method "%s" does not exists', $identifier[1]));
+ }
+ }
+
+ return $identifier;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resolveCallable(callable|array|string $identifier): callable
+ {
+ $callable = $this->resolve($identifier);
+
+ if (is_callable($callable)) {
+ return $callable;
+ }
+
+ // Unrecognized stuff, we let it fail
+ throw new RuntimeException(
+ sprintf('The definition "%s" is not a callable.', implode(':', (array)$identifier))
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resolveRoute(callable|array|string $identifier): callable
+ {
+ $callable = $this->resolveCallable($identifier);
+
+ return $this->bindToContainer($callable);
+ }
+
+ private function processStringNotation(string $toResolve): string|array
+ {
+ // Resolve Slim notation
+ $matches = null;
+ if (preg_match($this->callablePattern, $toResolve, $matches)) {
+ return $matches ? [$matches[1], $matches[2]] : [$toResolve, null];
+ }
+
+ return $toResolve;
+ }
+
+ private function bindToContainer(callable $callable): callable
+ {
+ if (is_array($callable) && $callable[0] instanceof Closure) {
+ $callable = $callable[0];
+ }
+
+ if ($callable instanceof Closure) {
+ $callable = $callable->bindTo($this->container) ?? throw new RuntimeException(
+ 'Unable to bind callable to DI container.'
+ );
+ }
+
+ return $callable;
+ }
+}
diff --git a/Slim/Container/DefaultDefinitions.php b/Slim/Container/DefaultDefinitions.php
new file mode 100644
index 000000000..f72893fc4
--- /dev/null
+++ b/Slim/Container/DefaultDefinitions.php
@@ -0,0 +1,137 @@
+ function (ContainerInterface $container) {
+ $mediaTypeDetector = $container->get(MediaTypeDetector::class);
+ $middleware = new BodyParsingMiddleware($mediaTypeDetector);
+
+ return $middleware
+ ->withDefaultMediaType('text/html')
+ ->withDefaultBodyParsers();
+ },
+
+ Config::class => function (ContainerInterface $container) {
+ return new Config($container->has('settings') ? (array)$container->get('settings') : []);
+ },
+
+ ConfigurationInterface::class => function (ContainerInterface $container) {
+ return $container->get(Config::class);
+ },
+
+ ContainerResolverInterface::class => function (ContainerInterface $container) {
+ return $container->get(ContainerResolver::class);
+ },
+
+ EmitterInterface::class => function () {
+ return new ResponseEmitter();
+ },
+
+ ExceptionHandlingMiddleware::class => function (ContainerInterface $container) {
+ $handler = $container->get(ExceptionHandlerInterface::class);
+
+ return (new ExceptionHandlingMiddleware())->withExceptionHandler($handler);
+ },
+
+ ExceptionHandlerInterface::class => function (ContainerInterface $container) {
+ // Default exception handler
+ $exceptionHandler = $container->get(ExceptionHandler::class);
+
+ // Settings
+ $displayErrorDetails = (bool)$container->get(ConfigurationInterface::class)
+ ->get('display_error_details', false);
+
+ $exceptionHandler = $exceptionHandler
+ ->withDisplayErrorDetails($displayErrorDetails)
+ ->withDefaultMediaType(MediaType::TEXT_HTML);
+
+ return $exceptionHandler
+ ->withoutHandlers()
+ ->withHandler(MediaType::APPLICATION_JSON, JsonExceptionRenderer::class)
+ ->withHandler(MediaType::TEXT_HTML, HtmlExceptionRenderer::class)
+ ->withHandler(MediaType::APPLICATION_XHTML_XML, HtmlExceptionRenderer::class)
+ ->withHandler(MediaType::APPLICATION_XML, XmlExceptionRenderer::class)
+ ->withHandler(MediaType::TEXT_XML, XmlExceptionRenderer::class)
+ ->withHandler(MediaType::TEXT_PLAIN, PlainTextExceptionRenderer::class);
+ },
+
+ ExceptionLoggingMiddleware::class => function (ContainerInterface $container) {
+ // Default logger
+ $logger = $container->get(LoggerInterface::class);
+ $middleware = new ExceptionLoggingMiddleware($logger);
+
+ // Read settings
+ $logErrorDetails = (bool)$container->get(ConfigurationInterface::class)
+ ->get('log_error_details', false);
+
+ return $middleware->withLogErrorDetails($logErrorDetails);
+ },
+
+ LoggerInterface::class => function () {
+ return new NullLogger();
+ },
+
+ RequestHandlerInterface::class => function (ContainerInterface $container) {
+ return $container->get(MiddlewareRequestHandler::class);
+ },
+
+ RequestHandlerInvocationStrategyInterface::class => function (ContainerInterface $container) {
+ return $container->get(RequestResponse::class);
+ },
+
+ Router::class => function () {
+ return new Router(new RouteCollector(new Std(), new GroupCountBased()));
+ },
+ ];
+ }
+}
diff --git a/Slim/Container/GuzzleDefinitions.php b/Slim/Container/GuzzleDefinitions.php
new file mode 100644
index 000000000..4466a9381
--- /dev/null
+++ b/Slim/Container/GuzzleDefinitions.php
@@ -0,0 +1,54 @@
+ function (ContainerInterface $container) {
+ return $container->get(HttpFactory::class);
+ },
+ ServerRequestCreatorInterface::class => function () {
+ return new class implements ServerRequestCreatorInterface {
+ public function createServerRequestFromGlobals(): ServerRequestInterface
+ {
+ return ServerRequest::fromGlobals();
+ }
+ };
+ },
+ ResponseFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(HttpFactory::class);
+ },
+ StreamFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(HttpFactory::class);
+ },
+ UriFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(HttpFactory::class);
+ },
+ UploadedFileFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(HttpFactory::class);
+ },
+ ];
+ }
+}
diff --git a/Slim/Container/HttpDefinitions.php b/Slim/Container/HttpDefinitions.php
new file mode 100644
index 000000000..ba29b92be
--- /dev/null
+++ b/Slim/Container/HttpDefinitions.php
@@ -0,0 +1,52 @@
+ SlimHttpDefinitions::class,
+ ServerRequestFactory::class => SlimPsr7Definitions::class,
+ Psr17Factory::class => NyholmDefinitions::class,
+ ServerRequest::class => GuzzleDefinitions::class,
+ RequestFactory::class => HttpSoftDefinitions::class,
+ ];
+
+ public function __invoke(): array
+ {
+ foreach ($this->classes as $factory => $definitionClass) {
+ if (call_user_func($this->classExists, $factory)) {
+ return call_user_func(new $definitionClass());
+ }
+ }
+
+ throw new RuntimeException(
+ 'Could not detect any PSR-17 ResponseFactory implementations. ' .
+ 'Please install a supported implementation. ' .
+ 'See https://github.com/slimphp/Slim/blob/5.x/README.md for a list of supported implementations.'
+ );
+ }
+}
diff --git a/Slim/Container/HttpSoftDefinitions.php b/Slim/Container/HttpSoftDefinitions.php
new file mode 100644
index 000000000..b2e333a4e
--- /dev/null
+++ b/Slim/Container/HttpSoftDefinitions.php
@@ -0,0 +1,58 @@
+ function (ContainerInterface $container) {
+ return $container->get(ServerRequestFactory::class);
+ },
+ ServerRequestCreatorInterface::class => function () {
+ return new class implements ServerRequestCreatorInterface {
+ public function createServerRequestFromGlobals(): ServerRequestInterface
+ {
+ return ServerRequestCreator::createFromGlobals();
+ }
+ };
+ },
+ ResponseFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(ResponseFactory::class);
+ },
+ StreamFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(StreamFactory::class);
+ },
+ UriFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(UriFactory::class);
+ },
+ UploadedFileFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(UploadedFileFactory::class);
+ },
+ ];
+ }
+}
diff --git a/Slim/Container/LaminasDiactorosDefinitions.php b/Slim/Container/LaminasDiactorosDefinitions.php
new file mode 100644
index 000000000..373b89d68
--- /dev/null
+++ b/Slim/Container/LaminasDiactorosDefinitions.php
@@ -0,0 +1,57 @@
+ function (ContainerInterface $container) {
+ return $container->get(ServerRequestFactory::class);
+ },
+ ServerRequestCreatorInterface::class => function () {
+ return new class implements ServerRequestCreatorInterface {
+ public function createServerRequestFromGlobals(): ServerRequestInterface
+ {
+ return ServerRequestFactory::fromGlobals();
+ }
+ };
+ },
+ ResponseFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(ResponseFactory::class);
+ },
+ StreamFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(StreamFactory::class);
+ },
+ UriFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(UriFactory::class);
+ },
+ UploadedFileFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(UploadedFileFactory::class);
+ },
+ ];
+ }
+}
diff --git a/Slim/Container/MiddlewareResolver.php b/Slim/Container/MiddlewareResolver.php
new file mode 100644
index 000000000..6321ee919
--- /dev/null
+++ b/Slim/Container/MiddlewareResolver.php
@@ -0,0 +1,105 @@
+container = $container;
+ $this->containerResolver = $containerResolver;
+ }
+
+ /**
+ * Resolve the middleware stack.
+ *
+ * @param array $queue
+ *
+ * @return array
+ */
+ public function resolveStack(array $queue): array
+ {
+ foreach ($queue as $key => $value) {
+ $queue[$key] = $this->resolveMiddleware($value);
+ }
+
+ return $queue;
+ }
+
+ /**
+ * Add a new middleware to the stack.
+ *
+ * @throws RuntimeException
+ */
+ private function resolveMiddleware(MiddlewareInterface|callable|string|array $middleware): MiddlewareInterface
+ {
+ $middleware = $this->containerResolver->resolve($middleware);
+
+ if ($middleware instanceof MiddlewareInterface) {
+ return $middleware;
+ }
+
+ if (is_callable($middleware)) {
+ return $this->addCallable($middleware);
+ }
+
+ throw new RuntimeException('A middleware must be an object or callable that implements "MiddlewareInterface".');
+ }
+
+ /**
+ * Add a (non-standard) callable middleware to the stack
+ *
+ * @throws RuntimeException
+ */
+ private function addCallable(callable $middleware): MiddlewareInterface
+ {
+ if ($middleware instanceof Closure) {
+ /** @var Closure $middleware */
+ $middleware = $middleware->bindTo($this->container) ?? throw new RuntimeException(
+ 'Unable to bind middleware to DI container.'
+ );
+ }
+
+ return new class ($middleware) implements MiddlewareInterface {
+ /**
+ * @var callable
+ */
+ private $middleware;
+
+ public function __construct(callable $middleware)
+ {
+ $this->middleware = $middleware;
+ }
+
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler,
+ ): ResponseInterface {
+ return ($this->middleware)($request, $handler);
+ }
+ };
+ }
+}
diff --git a/Slim/Container/NyholmDefinitions.php b/Slim/Container/NyholmDefinitions.php
new file mode 100644
index 000000000..463f17682
--- /dev/null
+++ b/Slim/Container/NyholmDefinitions.php
@@ -0,0 +1,63 @@
+ function (ContainerInterface $container) {
+ return $container->get(Psr17Factory::class);
+ },
+ ServerRequestCreatorInterface::class => function (ContainerInterface $container) {
+ $serverRequestCreator = $container->get(ServerRequestCreator::class);
+
+ return new class ($serverRequestCreator) implements ServerRequestCreatorInterface {
+ private ServerRequestCreator $serverRequestCreator;
+
+ public function __construct(ServerRequestCreator $serverRequestCreator)
+ {
+ $this->serverRequestCreator = $serverRequestCreator;
+ }
+
+ public function createServerRequestFromGlobals(): ServerRequestInterface
+ {
+ return $this->serverRequestCreator->fromGlobals();
+ }
+ };
+ },
+ ResponseFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(Psr17Factory::class);
+ },
+ StreamFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(Psr17Factory::class);
+ },
+ UriFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(Psr17Factory::class);
+ },
+ UploadedFileFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(Psr17Factory::class);
+ },
+ ];
+ }
+}
diff --git a/Slim/Container/SlimHttpDefinitions.php b/Slim/Container/SlimHttpDefinitions.php
new file mode 100644
index 000000000..f7287c56d
--- /dev/null
+++ b/Slim/Container/SlimHttpDefinitions.php
@@ -0,0 +1,163 @@
+ function (ContainerInterface $container) {
+ $serverRequestFactory = $container->get(ServerRequestFactory::class);
+
+ return new class ($serverRequestFactory) implements ServerRequestFactoryInterface {
+ private ServerRequestFactory $serverRequestFactory;
+
+ public function __construct(ServerRequestFactory $serverRequestFactory)
+ {
+ $this->serverRequestFactory = $serverRequestFactory;
+ }
+
+ public function createServerRequest(
+ string $method,
+ $uri,
+ array $serverParams = []
+ ): ServerRequestInterface {
+ return new ServerRequest(
+ $this->serverRequestFactory->createServerRequest($method, $uri, $serverParams)
+ );
+ }
+ };
+ },
+ ServerRequestCreatorInterface::class => function () {
+ return new class implements ServerRequestCreatorInterface {
+ public function createServerRequestFromGlobals(): ServerRequestInterface
+ {
+ return new ServerRequest(ServerRequestFactory::createFromGlobals());
+ }
+ };
+ },
+ ResponseFactoryInterface::class => function (ContainerInterface $container) use ($that) {
+ $responseFactory = null;
+
+ $responseFactoryClasses = [
+ \Slim\Psr7\Factory\ResponseFactory::class,
+ \Nyholm\Psr7\Factory\Psr17Factory::class,
+ \Laminas\Diactoros\ResponseFactory::class,
+ \GuzzleHttp\Psr7\HttpFactory::class,
+ \HttpSoft\Message\ResponseFactory::class,
+ ];
+
+ foreach ($responseFactoryClasses as $responseFactoryClass) {
+ if (call_user_func($that->classExists, $responseFactoryClass)) {
+ $responseFactory = $container->get($responseFactoryClass);
+ break;
+ }
+ }
+
+ if ($responseFactory instanceof ResponseFactoryInterface) {
+ /* @var StreamFactoryInterface $streamFactory */
+ $streamFactory = $container->get(StreamFactoryInterface::class);
+ $responseFactory = new DecoratedResponseFactory($responseFactory, $streamFactory);
+ }
+
+ return $responseFactory ?? throw new RuntimeException(
+ 'Could not detect any PSR-17 ResponseFactory implementations. ' .
+ 'Please install a supported implementation. ' .
+ 'See https://github.com/slimphp/Slim/blob/5.x/README.md for a list of supported implementations.'
+ );
+ },
+ StreamFactoryInterface::class => function (ContainerInterface $container) use ($that) {
+ $factoryClasses = [
+ \Slim\Psr7\Factory\StreamFactory::class,
+ \Nyholm\Psr7\Factory\Psr17Factory::class,
+ \Laminas\Diactoros\StreamFactory::class,
+ \GuzzleHttp\Psr7\HttpFactory::class,
+ \HttpSoft\Message\StreamFactory::class,
+ ];
+
+ foreach ($factoryClasses as $factoryClass) {
+ if (call_user_func($that->classExists, $factoryClass)) {
+ return $container->get($factoryClass);
+ }
+ }
+
+ throw new RuntimeException('Could not instantiate a StreamFactory.');
+ },
+ UriFactoryInterface::class => function (ContainerInterface $container) use ($that) {
+ $uriFactory = null;
+
+ $uriFactoryClasses = [
+ \Slim\Psr7\Factory\UriFactory::class,
+ \Nyholm\Psr7\Factory\Psr17Factory::class,
+ \Laminas\Diactoros\UriFactory::class,
+ \GuzzleHttp\Psr7\HttpFactory::class,
+ \HttpSoft\Message\UriFactory::class,
+ ];
+
+ foreach ($uriFactoryClasses as $uriFactoryClass) {
+ if (call_user_func($that->classExists, $uriFactoryClass)) {
+ $uriFactory = $container->get($uriFactoryClass);
+ break;
+ }
+ }
+
+ if ($uriFactory instanceof UriFactoryInterface) {
+ $uriFactory = new DecoratedUriFactory($uriFactory);
+ }
+
+ if ($uriFactory) {
+ return $uriFactory;
+ }
+
+ throw new RuntimeException('Could not instantiate a UriFactory.');
+ },
+ UploadedFileFactoryInterface::class => function (ContainerInterface $container) use ($that) {
+ $factoryClasses = [
+ \Slim\Psr7\Factory\UploadedFileFactory::class,
+ \Nyholm\Psr7\Factory\Psr17Factory::class,
+ \Laminas\Diactoros\UploadedFileFactory::class,
+ \GuzzleHttp\Psr7\HttpFactory::class,
+ \HttpSoft\Message\UploadedFileFactory::class,
+ ];
+
+ foreach ($factoryClasses as $factoryClass) {
+ if (call_user_func($that->classExists, $factoryClass)) {
+ return $container->get($factoryClass);
+ }
+ }
+
+ throw new RuntimeException('Could not instantiate a UploadedFileFactory.');
+ },
+ ];
+ }
+}
diff --git a/Slim/Container/SlimPsr7Definitions.php b/Slim/Container/SlimPsr7Definitions.php
new file mode 100644
index 000000000..926f093e4
--- /dev/null
+++ b/Slim/Container/SlimPsr7Definitions.php
@@ -0,0 +1,57 @@
+ function (ContainerInterface $container) {
+ return $container->get(ServerRequestFactory::class);
+ },
+ ServerRequestCreatorInterface::class => function () {
+ return new class implements ServerRequestCreatorInterface {
+ public function createServerRequestFromGlobals(): ServerRequestInterface
+ {
+ return ServerRequestFactory::createFromGlobals();
+ }
+ };
+ },
+ ResponseFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(ResponseFactory::class);
+ },
+ StreamFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(StreamFactory::class);
+ },
+ UriFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(UriFactory::class);
+ },
+ UploadedFileFactoryInterface::class => function (ContainerInterface $container) {
+ return $container->get(UploadedFileFactory::class);
+ },
+ ];
+ }
+}
diff --git a/Slim/ResponseEmitter.php b/Slim/Emitter/ResponseEmitter.php
similarity index 89%
rename from Slim/ResponseEmitter.php
rename to Slim/Emitter/ResponseEmitter.php
index fac36e9e7..4941ff074 100644
--- a/Slim/ResponseEmitter.php
+++ b/Slim/Emitter/ResponseEmitter.php
@@ -3,14 +3,15 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
-namespace Slim;
+namespace Slim\Emitter;
use Psr\Http\Message\ResponseInterface;
+use Slim\Interfaces\EmitterInterface;
use function connection_status;
use function header;
@@ -23,7 +24,7 @@
use const CONNECTION_NORMAL;
-class ResponseEmitter
+final class ResponseEmitter implements EmitterInterface
{
private int $responseChunkSize;
@@ -33,7 +34,7 @@ public function __construct(int $responseChunkSize = 4096)
}
/**
- * Send the response the client
+ * Send the response the client.
*/
public function emit(ResponseInterface $response): void
{
@@ -54,6 +55,8 @@ public function emit(ResponseInterface $response): void
/**
* Emit Response Headers
+ *
+ * @param ResponseInterface $response
*/
private function emitHeaders(ResponseInterface $response): void
{
@@ -69,6 +72,8 @@ private function emitHeaders(ResponseInterface $response): void
/**
* Emit Status Line
+ *
+ * @param ResponseInterface $response
*/
private function emitStatusLine(ResponseInterface $response): void
{
@@ -91,7 +96,7 @@ private function emitBody(ResponseInterface $response): void
$body->rewind();
}
- $amountToRead = (int) $response->getHeaderLine('Content-Length');
+ $amountToRead = (int)$response->getHeaderLine('Content-Length');
if (!$amountToRead) {
$amountToRead = $body->getSize();
}
@@ -119,7 +124,7 @@ private function emitBody(ResponseInterface $response): void
}
/**
- * Asserts response body is empty or status code is 204, 205 or 304
+ * Asserts response body is empty or status code is 204, 205 or 304.
*/
public function isResponseEmpty(ResponseInterface $response): bool
{
@@ -131,6 +136,7 @@ public function isResponseEmpty(ResponseInterface $response): bool
if ($seekable) {
$stream->rewind();
}
+
return $seekable ? $stream->read(1) === '' : $stream->eof();
}
}
diff --git a/Slim/Error/AbstractErrorRenderer.php b/Slim/Error/AbstractErrorRenderer.php
deleted file mode 100644
index 90b290d41..000000000
--- a/Slim/Error/AbstractErrorRenderer.php
+++ /dev/null
@@ -1,46 +0,0 @@
-getTitle();
- }
-
- return $this->defaultErrorTitle;
- }
-
- protected function getErrorDescription(Throwable $exception): string
- {
- if ($exception instanceof HttpException) {
- return $exception->getDescription();
- }
-
- return $this->defaultErrorDescription;
- }
-}
diff --git a/Slim/Error/Handlers/ExceptionHandler.php b/Slim/Error/Handlers/ExceptionHandler.php
new file mode 100644
index 000000000..32fffa122
--- /dev/null
+++ b/Slim/Error/Handlers/ExceptionHandler.php
@@ -0,0 +1,154 @@
+resolver = $resolver;
+ $this->responseFactory = $responseFactory;
+ $this->mediaTypeDetector = $mediaTypeDetector;
+ }
+
+ public function __invoke(ServerRequestInterface $request, Throwable $exception): ResponseInterface
+ {
+ $statusCode = $this->determineStatusCode($request, $exception);
+ $mediaType = $this->negotiateMediaType($request);
+ $response = $this->createResponse($statusCode, $mediaType, $exception);
+ $handler = $this->negotiateHandler($mediaType);
+
+ // Invoke the formatter handler
+ return call_user_func(
+ $handler,
+ $request,
+ $response,
+ $exception,
+ $this->displayErrorDetails
+ );
+ }
+
+ public function withDisplayErrorDetails(bool $displayErrorDetails): self
+ {
+ $clone = clone $this;
+ $clone->displayErrorDetails = $displayErrorDetails;
+
+ return $clone;
+ }
+
+ public function withDefaultMediaType(string $mediaType): self
+ {
+ $clone = clone $this;
+ $clone->defaultMediaType = $mediaType;
+
+ return $clone;
+ }
+
+ public function withHandler(string $mediaType, ExceptionRendererInterface|callable|string $handler): self
+ {
+ $clone = clone $this;
+ $clone->handlers[$mediaType] = $handler;
+
+ return $clone;
+ }
+
+ public function withoutHandlers(): self
+ {
+ $clone = clone $this;
+ $clone->handlers = [];
+
+ return $clone;
+ }
+
+ private function negotiateMediaType(ServerRequestInterface $request): string
+ {
+ $mediaTypes = $this->mediaTypeDetector->detect($request);
+
+ return $mediaTypes[0] ?? $this->defaultMediaType;
+ }
+
+ /**
+ * Determine which handler to use based on media type.
+ */
+ private function negotiateHandler(string $mediaType): callable
+ {
+ $handler = $this->handlers[$mediaType] ?? reset($this->handlers);
+
+ if (!$handler) {
+ throw new RuntimeException(sprintf('Exception handler for "%s" not found', $mediaType));
+ }
+
+ return $this->resolver->resolveCallable($handler);
+ }
+
+ private function determineStatusCode(ServerRequestInterface $request, Throwable $exception): int
+ {
+ if ($exception instanceof HttpException) {
+ return $exception->getCode();
+ }
+
+ if ($request->getMethod() === 'OPTIONS') {
+ return 200;
+ }
+
+ return 500;
+ }
+
+ private function createResponse(
+ int $statusCode,
+ string $contentType,
+ Throwable $exception,
+ ): ResponseInterface {
+ $response = $this->responseFactory
+ ->createResponse($statusCode)
+ ->withHeader('Content-Type', $contentType);
+
+ if ($exception instanceof HttpMethodNotAllowedException) {
+ $allowedMethods = implode(', ', $exception->getAllowedMethods());
+ $response = $response->withHeader('Allow', $allowedMethods);
+ }
+
+ return $response;
+ }
+}
diff --git a/Slim/Error/Renderers/ExceptionRendererTrait.php b/Slim/Error/Renderers/ExceptionRendererTrait.php
new file mode 100644
index 000000000..3eccba694
--- /dev/null
+++ b/Slim/Error/Renderers/ExceptionRendererTrait.php
@@ -0,0 +1,39 @@
+getTitle();
+ }
+
+ return $this->defaultErrorTitle;
+ }
+
+ private function getErrorDescription(Throwable $exception): string
+ {
+ if ($exception instanceof HttpException) {
+ return $exception->getDescription();
+ }
+
+ return $this->defaultErrorDescription;
+ }
+}
diff --git a/Slim/Error/Renderers/HtmlErrorRenderer.php b/Slim/Error/Renderers/HtmlErrorRenderer.php
deleted file mode 100644
index 8fd838bcb..000000000
--- a/Slim/Error/Renderers/HtmlErrorRenderer.php
+++ /dev/null
@@ -1,83 +0,0 @@
-The application could not run because of the following error:
';
- $html .= 'Details
';
- $html .= $this->renderExceptionFragment($exception);
- } else {
- $html = "{$this->getErrorDescription($exception)}
";
- }
-
- return $this->renderHtmlBody($this->getErrorTitle($exception), $html);
- }
-
- private function renderExceptionFragment(Throwable $exception): string
- {
- $html = sprintf('Type: %s
', get_class($exception));
-
- $code = $exception->getCode();
- $html .= sprintf('Code: %s
', $code);
-
- $html .= sprintf('Message: %s
', htmlentities($exception->getMessage()));
-
- $html .= sprintf('File: %s
', $exception->getFile());
-
- $html .= sprintf('Line: %s
', $exception->getLine());
-
- $html .= 'Trace
';
- $html .= sprintf('%s
', htmlentities($exception->getTraceAsString()));
-
- return $html;
- }
-
- public function renderHtmlBody(string $title = '', string $html = ''): string
- {
- return sprintf(
- '' .
- '' .
- ' ' .
- ' ' .
- ' ' .
- ' %s' .
- ' ' .
- ' ' .
- ' ' .
- ' %s
' .
- ' %s
' .
- ' Go Back' .
- ' ' .
- '',
- $title,
- $title,
- $html
- );
- }
-}
diff --git a/Slim/Error/Renderers/HtmlExceptionRenderer.php b/Slim/Error/Renderers/HtmlExceptionRenderer.php
new file mode 100644
index 000000000..d9ca168ce
--- /dev/null
+++ b/Slim/Error/Renderers/HtmlExceptionRenderer.php
@@ -0,0 +1,124 @@
+streamFactory = $streamFactory;
+ }
+
+ public function __invoke(
+ ServerRequestInterface $request,
+ ResponseInterface $response,
+ ?Throwable $exception = null,
+ bool $displayErrorDetails = false
+ ): ResponseInterface {
+ if ($displayErrorDetails) {
+ $html = 'The application could not run because of the following error:
';
+ $html .= 'Details
';
+ $html .= $this->renderExceptionFragment($exception);
+ } else {
+ $html = sprintf('%s
', $this->getErrorDescription($exception));
+ }
+
+ $html = $this->renderHtmlBody($this->getErrorTitle($exception), $html);
+
+ $body = $this->streamFactory->createStream($html);
+ $response = $response->withBody($body);
+
+ return $response->withHeader('Content-Type', MediaType::TEXT_HTML);
+ }
+
+ private function renderExceptionFragment(Throwable $exception): string
+ {
+ $html = sprintf(
+ 'Type: %s
',
+ $this->escapeHtml(get_class($exception))
+ );
+
+ $code = $exception instanceof ErrorException ? $exception->getSeverity() : $exception->getCode();
+ $html .= sprintf('Code: %s
', $this->escapeHtml((string)$code));
+
+ $html .= sprintf(
+ 'Message: %s
',
+ $this->escapeHtml($exception->getMessage())
+ );
+
+ $html .= sprintf(
+ 'File: %s
',
+ $this->escapeHtml($exception->getFile())
+ );
+
+ $html .= sprintf(
+ 'Line: %s
',
+ $this->escapeHtml((string)$exception->getLine())
+ );
+
+ $html .= 'Trace
';
+ $html .= sprintf('%s
', $this->escapeHtml($exception->getTraceAsString()));
+
+ return $html;
+ }
+
+ public function renderHtmlBody(string $title = '', string $html = ''): string
+ {
+ return sprintf(
+ '' .
+ '' .
+ ' ' .
+ ' ' .
+ ' ' .
+ ' %s' .
+ ' ' .
+ ' ' .
+ ' ' .
+ ' %s
' .
+ ' %s
' .
+ ' Go Back' .
+ ' ' .
+ '',
+ $this->escapeHtml($title),
+ $this->escapeHtml($title),
+ $html
+ );
+ }
+
+ private function escapeHtml(?string $input = null): string
+ {
+ return htmlspecialchars($input ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+ }
+}
diff --git a/Slim/Error/Renderers/JsonErrorRenderer.php b/Slim/Error/Renderers/JsonErrorRenderer.php
deleted file mode 100644
index 06085d2e5..000000000
--- a/Slim/Error/Renderers/JsonErrorRenderer.php
+++ /dev/null
@@ -1,55 +0,0 @@
- $this->getErrorTitle($exception)];
-
- if ($displayErrorDetails) {
- $error['exception'] = [];
- do {
- $error['exception'][] = $this->formatExceptionFragment($exception);
- } while ($exception = $exception->getPrevious());
- }
-
- return (string) json_encode($error, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
- }
-
- /**
- * @return array
- */
- private function formatExceptionFragment(Throwable $exception): array
- {
- $code = $exception->getCode();
- return [
- 'type' => get_class($exception),
- 'code' => $code,
- 'message' => $exception->getMessage(),
- 'file' => $exception->getFile(),
- 'line' => $exception->getLine(),
- ];
- }
-}
diff --git a/Slim/Error/Renderers/JsonExceptionRenderer.php b/Slim/Error/Renderers/JsonExceptionRenderer.php
new file mode 100644
index 000000000..c06346f78
--- /dev/null
+++ b/Slim/Error/Renderers/JsonExceptionRenderer.php
@@ -0,0 +1,66 @@
+jsonRenderer = $jsonRenderer;
+ }
+
+ public function __invoke(
+ ServerRequestInterface $request,
+ ResponseInterface $response,
+ ?Throwable $exception = null,
+ bool $displayErrorDetails = false
+ ): ResponseInterface {
+ $error = ['message' => $this->getErrorTitle($exception)];
+
+ if ($displayErrorDetails) {
+ $error['exception'] = [];
+ do {
+ $error['exception'][] = $this->formatExceptionFragment($exception);
+ } while ($exception = $exception->getPrevious());
+ }
+
+ return $this->jsonRenderer->json($response, $error);
+ }
+
+ private function formatExceptionFragment(Throwable $exception): array
+ {
+ $code = $exception instanceof ErrorException ? $exception->getSeverity() : $exception->getCode();
+
+ return [
+ 'type' => get_class($exception),
+ 'code' => $code,
+ 'message' => $exception->getMessage(),
+ 'file' => $exception->getFile(),
+ 'line' => $exception->getLine(),
+ ];
+ }
+}
diff --git a/Slim/Error/Renderers/PlainTextErrorRenderer.php b/Slim/Error/Renderers/PlainTextErrorRenderer.php
deleted file mode 100644
index fcb6e8a15..000000000
--- a/Slim/Error/Renderers/PlainTextErrorRenderer.php
+++ /dev/null
@@ -1,59 +0,0 @@
-getErrorTitle($exception)}\n";
-
- if ($displayErrorDetails) {
- $text .= $this->formatExceptionFragment($exception);
-
- while ($exception = $exception->getPrevious()) {
- $text .= "\nPrevious Error:\n";
- $text .= $this->formatExceptionFragment($exception);
- }
- }
-
- return $text;
- }
-
- private function formatExceptionFragment(Throwable $exception): string
- {
- $text = sprintf("Type: %s\n", get_class($exception));
-
- $code = $exception->getCode();
-
- $text .= sprintf("Code: %s\n", $code);
-
- $text .= sprintf("Message: %s\n", $exception->getMessage());
-
- $text .= sprintf("File: %s\n", $exception->getFile());
-
- $text .= sprintf("Line: %s\n", $exception->getLine());
-
- $text .= sprintf('Trace: %s', $exception->getTraceAsString());
-
- return $text;
- }
-}
diff --git a/Slim/Error/Renderers/PlainTextExceptionRenderer.php b/Slim/Error/Renderers/PlainTextExceptionRenderer.php
new file mode 100644
index 000000000..2cc625fe1
--- /dev/null
+++ b/Slim/Error/Renderers/PlainTextExceptionRenderer.php
@@ -0,0 +1,75 @@
+streamFactory = $streamFactory;
+ }
+
+ public function __invoke(
+ ServerRequestInterface $request,
+ ResponseInterface $response,
+ ?Throwable $exception = null,
+ bool $displayErrorDetails = false
+ ): ResponseInterface {
+ $text = sprintf("%s\n", $this->getErrorTitle($exception));
+
+ if ($displayErrorDetails) {
+ $text .= $this->formatExceptionFragment($exception);
+
+ while ($exception = $exception->getPrevious()) {
+ $text .= "\nPrevious Exception:\n";
+ $text .= $this->formatExceptionFragment($exception);
+ }
+ }
+
+ $body = $this->streamFactory->createStream($text);
+ $response = $response->withBody($body);
+
+ return $response->withHeader('Content-Type', MediaType::TEXT_PLAIN);
+ }
+
+ private function formatExceptionFragment(Throwable $exception): string
+ {
+ $text = sprintf("Type: %s\n", get_class($exception));
+
+ $code = $exception instanceof ErrorException ? $exception->getSeverity() : $exception->getCode();
+
+ $text .= sprintf("Code: %s\n", $code);
+ $text .= sprintf("Message: %s\n", $exception->getMessage());
+ $text .= sprintf("File: %s\n", $exception->getFile());
+ $text .= sprintf("Line: %s\n", $exception->getLine());
+ $text .= sprintf('Trace: %s', $exception->getTraceAsString());
+
+ return $text;
+ }
+}
diff --git a/Slim/Error/Renderers/XmlErrorRenderer.php b/Slim/Error/Renderers/XmlErrorRenderer.php
deleted file mode 100644
index 1171b79b2..000000000
--- a/Slim/Error/Renderers/XmlErrorRenderer.php
+++ /dev/null
@@ -1,54 +0,0 @@
-\n";
- $xml .= "\n " . $this->createCdataSection($this->getErrorTitle($exception)) . "\n";
-
- if ($displayErrorDetails) {
- do {
- $xml .= " \n";
- $xml .= ' ' . get_class($exception) . "\n";
- $xml .= ' ' . $exception->getCode() . "
\n";
- $xml .= ' ' . $this->createCdataSection($exception->getMessage()) . "\n";
- $xml .= ' ' . $exception->getFile() . "\n";
- $xml .= ' ' . $exception->getLine() . "\n";
- $xml .= " \n";
- } while ($exception = $exception->getPrevious());
- }
-
- $xml .= '';
-
- return $xml;
- }
-
- /**
- * Returns a CDATA section with the given content.
- */
- private function createCdataSection(string $content): string
- {
- return sprintf('', str_replace(']]>', ']]]]>', $content));
- }
-}
diff --git a/Slim/Error/Renderers/XmlExceptionRenderer.php b/Slim/Error/Renderers/XmlExceptionRenderer.php
new file mode 100644
index 000000000..9127405b9
--- /dev/null
+++ b/Slim/Error/Renderers/XmlExceptionRenderer.php
@@ -0,0 +1,83 @@
+streamFactory = $streamFactory;
+ }
+
+ public function __invoke(
+ ServerRequestInterface $request,
+ ResponseInterface $response,
+ ?Throwable $exception = null,
+ bool $displayErrorDetails = false,
+ ): ResponseInterface {
+ $dom = new DOMDocument('1.0', 'UTF-8');
+ $dom->formatOutput = true;
+
+ $errorElement = $dom->createElement('error');
+ $dom->appendChild($errorElement);
+
+ $messageElement = $dom->createElement('message', $this->getErrorTitle($exception));
+ $errorElement->appendChild($messageElement);
+
+ // If error details should be displayed
+ if ($displayErrorDetails) {
+ do {
+ $exceptionElement = $dom->createElement('exception');
+
+ $typeElement = $dom->createElement('type', get_class($exception));
+ $exceptionElement->appendChild($typeElement);
+
+ $code = $exception instanceof ErrorException ? $exception->getSeverity() : $exception->getCode();
+ $codeElement = $dom->createElement('code', (string)$code);
+ $exceptionElement->appendChild($codeElement);
+
+ $messageElement = $dom->createElement('message', $exception->getMessage());
+ $exceptionElement->appendChild($messageElement);
+
+ $fileElement = $dom->createElement('file', $exception->getFile());
+ $exceptionElement->appendChild($fileElement);
+
+ $lineElement = $dom->createElement('line', (string)$exception->getLine());
+ $exceptionElement->appendChild($lineElement);
+
+ $errorElement->appendChild($exceptionElement);
+ } while ($exception = $exception->getPrevious());
+ }
+
+ $body = $this->streamFactory->createStream((string)$dom->saveXML());
+ $response = $response->withBody($body);
+
+ return $response->withHeader('Content-Type', MediaType::APPLICATION_XML);
+ }
+}
diff --git a/Slim/Exception/HttpBadRequestException.php b/Slim/Exception/HttpBadRequestException.php
index 000c2480a..2ae277381 100644
--- a/Slim/Exception/HttpBadRequestException.php
+++ b/Slim/Exception/HttpBadRequestException.php
@@ -3,15 +3,14 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Exception;
-/** @api */
-class HttpBadRequestException extends HttpSpecializedException
+final class HttpBadRequestException extends HttpSpecializedException
{
/**
* @var int
diff --git a/Slim/Exception/HttpException.php b/Slim/Exception/HttpException.php
index c714dbbe5..87473bcc5 100644
--- a/Slim/Exception/HttpException.php
+++ b/Slim/Exception/HttpException.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
@@ -14,10 +14,6 @@
use RuntimeException;
use Throwable;
-/**
- * @api
- * @method int getCode()
- */
class HttpException extends RuntimeException
{
protected ServerRequestInterface $request;
@@ -30,7 +26,7 @@ public function __construct(
ServerRequestInterface $request,
string $message = '',
int $code = 0,
- ?Throwable $previous = null
+ ?Throwable $previous = null,
) {
parent::__construct($message, $code, $previous);
$this->request = $request;
@@ -49,6 +45,7 @@ public function getTitle(): string
public function setTitle(string $title): self
{
$this->title = $title;
+
return $this;
}
@@ -60,6 +57,7 @@ public function getDescription(): string
public function setDescription(string $description): self
{
$this->description = $description;
+
return $this;
}
}
diff --git a/Slim/Exception/HttpForbiddenException.php b/Slim/Exception/HttpForbiddenException.php
index d262a63f6..abeaba2ff 100644
--- a/Slim/Exception/HttpForbiddenException.php
+++ b/Slim/Exception/HttpForbiddenException.php
@@ -3,15 +3,14 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Exception;
-/** @api */
-class HttpForbiddenException extends HttpSpecializedException
+final class HttpForbiddenException extends HttpSpecializedException
{
/**
* @var int
diff --git a/Slim/Exception/HttpGoneException.php b/Slim/Exception/HttpGoneException.php
index b1005d83e..834dc2d71 100644
--- a/Slim/Exception/HttpGoneException.php
+++ b/Slim/Exception/HttpGoneException.php
@@ -3,15 +3,14 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Exception;
-/** @api */
-class HttpGoneException extends HttpSpecializedException
+final class HttpGoneException extends HttpSpecializedException
{
/**
* @var int
diff --git a/Slim/Exception/HttpInternalServerErrorException.php b/Slim/Exception/HttpInternalServerErrorException.php
index f9cb60f7b..416dfc139 100644
--- a/Slim/Exception/HttpInternalServerErrorException.php
+++ b/Slim/Exception/HttpInternalServerErrorException.php
@@ -3,15 +3,14 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Exception;
-/** @api */
-class HttpInternalServerErrorException extends HttpSpecializedException
+final class HttpInternalServerErrorException extends HttpSpecializedException
{
/**
* @var int
diff --git a/Slim/Exception/HttpMethodNotAllowedException.php b/Slim/Exception/HttpMethodNotAllowedException.php
index 30aaaa743..a75d9c4e5 100644
--- a/Slim/Exception/HttpMethodNotAllowedException.php
+++ b/Slim/Exception/HttpMethodNotAllowedException.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
@@ -12,7 +12,7 @@
use function implode;
-class HttpMethodNotAllowedException extends HttpSpecializedException
+final class HttpMethodNotAllowedException extends HttpSpecializedException
{
/**
* @var string[]
@@ -47,6 +47,7 @@ public function setAllowedMethods(array $methods): self
{
$this->allowedMethods = $methods;
$this->message = 'Method not allowed. Must be one of: ' . implode(', ', $methods);
+
return $this;
}
}
diff --git a/Slim/Exception/HttpNotFoundException.php b/Slim/Exception/HttpNotFoundException.php
index 865146d68..30c9c4050 100644
--- a/Slim/Exception/HttpNotFoundException.php
+++ b/Slim/Exception/HttpNotFoundException.php
@@ -3,14 +3,14 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Exception;
-class HttpNotFoundException extends HttpSpecializedException
+final class HttpNotFoundException extends HttpSpecializedException
{
/**
* @var int
diff --git a/Slim/Exception/HttpNotImplementedException.php b/Slim/Exception/HttpNotImplementedException.php
index af4fe253d..b0ea3e07d 100644
--- a/Slim/Exception/HttpNotImplementedException.php
+++ b/Slim/Exception/HttpNotImplementedException.php
@@ -3,15 +3,14 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Exception;
-/** @api */
-class HttpNotImplementedException extends HttpSpecializedException
+final class HttpNotImplementedException extends HttpSpecializedException
{
/**
* @var int
diff --git a/Slim/Exception/HttpSpecializedException.php b/Slim/Exception/HttpSpecializedException.php
index 945e1ad44..5ec5fb320 100644
--- a/Slim/Exception/HttpSpecializedException.php
+++ b/Slim/Exception/HttpSpecializedException.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
@@ -17,8 +17,8 @@ abstract class HttpSpecializedException extends HttpException
{
/**
* @param ServerRequestInterface $request
- * @param string|null $message
- * @param Throwable|null $previous
+ * @param string|null $message
+ * @param Throwable|null $previous
*/
public function __construct(ServerRequestInterface $request, ?string $message = null, ?Throwable $previous = null)
{
diff --git a/Slim/Exception/HttpTooManyRequestsException.php b/Slim/Exception/HttpTooManyRequestsException.php
index af438bad4..d30d543dc 100644
--- a/Slim/Exception/HttpTooManyRequestsException.php
+++ b/Slim/Exception/HttpTooManyRequestsException.php
@@ -3,15 +3,14 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Exception;
-/** @api */
-class HttpTooManyRequestsException extends HttpSpecializedException
+final class HttpTooManyRequestsException extends HttpSpecializedException
{
/**
* @var int
@@ -25,5 +24,5 @@ class HttpTooManyRequestsException extends HttpSpecializedException
protected string $title = '429 Too Many Requests';
protected string $description = 'The client application has surpassed its rate limit, ' .
- 'or number of requests they can send in a given period of time.';
+ 'or number of requests they can send in a given period of time.';
}
diff --git a/Slim/Exception/HttpUnauthorizedException.php b/Slim/Exception/HttpUnauthorizedException.php
index afb0e5407..04c058e29 100644
--- a/Slim/Exception/HttpUnauthorizedException.php
+++ b/Slim/Exception/HttpUnauthorizedException.php
@@ -3,15 +3,14 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Exception;
-/** @api */
-class HttpUnauthorizedException extends HttpSpecializedException
+final class HttpUnauthorizedException extends HttpSpecializedException
{
/**
* @var int
diff --git a/Slim/Factory/AppFactory.php b/Slim/Factory/AppFactory.php
deleted file mode 100644
index be91e88e9..000000000
--- a/Slim/Factory/AppFactory.php
+++ /dev/null
@@ -1,217 +0,0 @@
- : App)
- */
- public static function create(
- ?ResponseFactoryInterface $responseFactory = null,
- ?ContainerInterface $container = null,
- ?CallableResolverInterface $callableResolver = null,
- ?RouteCollectorInterface $routeCollector = null,
- ?RouteResolverInterface $routeResolver = null,
- ?MiddlewareDispatcherInterface $middlewareDispatcher = null
- ): App {
- static::$responseFactory = $responseFactory ?? static::$responseFactory;
- return new App(
- self::determineResponseFactory(),
- $container ?? static::$container,
- $callableResolver ?? static::$callableResolver,
- $routeCollector ?? static::$routeCollector,
- $routeResolver ?? static::$routeResolver,
- $middlewareDispatcher ?? static::$middlewareDispatcher
- );
- }
-
- /**
- * @template TContainerInterface of (ContainerInterface)
- * @param TContainerInterface $container
- * @return App
- */
- public static function createFromContainer(ContainerInterface $container): App
- {
- $responseFactory = $container->has(ResponseFactoryInterface::class)
- && (
- $responseFactoryFromContainer = $container->get(ResponseFactoryInterface::class)
- ) instanceof ResponseFactoryInterface
- ? $responseFactoryFromContainer
- : self::determineResponseFactory();
-
- $callableResolver = $container->has(CallableResolverInterface::class)
- && (
- $callableResolverFromContainer = $container->get(CallableResolverInterface::class)
- ) instanceof CallableResolverInterface
- ? $callableResolverFromContainer
- : null;
-
- $routeCollector = $container->has(RouteCollectorInterface::class)
- && (
- $routeCollectorFromContainer = $container->get(RouteCollectorInterface::class)
- ) instanceof RouteCollectorInterface
- ? $routeCollectorFromContainer
- : null;
-
- $routeResolver = $container->has(RouteResolverInterface::class)
- && (
- $routeResolverFromContainer = $container->get(RouteResolverInterface::class)
- ) instanceof RouteResolverInterface
- ? $routeResolverFromContainer
- : null;
-
- $middlewareDispatcher = $container->has(MiddlewareDispatcherInterface::class)
- && (
- $middlewareDispatcherFromContainer = $container->get(MiddlewareDispatcherInterface::class)
- ) instanceof MiddlewareDispatcherInterface
- ? $middlewareDispatcherFromContainer
- : null;
-
- return new App(
- $responseFactory,
- $container,
- $callableResolver,
- $routeCollector,
- $routeResolver,
- $middlewareDispatcher
- );
- }
-
- /**
- * @throws RuntimeException
- */
- public static function determineResponseFactory(): ResponseFactoryInterface
- {
- if (static::$responseFactory) {
- if (static::$streamFactory) {
- return static::attemptResponseFactoryDecoration(static::$responseFactory, static::$streamFactory);
- }
- return static::$responseFactory;
- }
-
- $psr17FactoryProvider = static::$psr17FactoryProvider ?? new Psr17FactoryProvider();
-
- /** @var Psr17Factory $psr17factory */
- foreach ($psr17FactoryProvider->getFactories() as $psr17factory) {
- if ($psr17factory::isResponseFactoryAvailable()) {
- $responseFactory = $psr17factory::getResponseFactory();
-
- if (static::$streamFactory || $psr17factory::isStreamFactoryAvailable()) {
- $streamFactory = static::$streamFactory ?? $psr17factory::getStreamFactory();
- return static::attemptResponseFactoryDecoration($responseFactory, $streamFactory);
- }
-
- return $responseFactory;
- }
- }
-
- throw new RuntimeException(
- "Could not detect any PSR-17 ResponseFactory implementations. " .
- "Please install a supported implementation in order to use `AppFactory::create()`. " .
- "See https://github.com/slimphp/Slim/blob/4.x/README.md for a list of supported implementations."
- );
- }
-
- protected static function attemptResponseFactoryDecoration(
- ResponseFactoryInterface $responseFactory,
- StreamFactoryInterface $streamFactory
- ): ResponseFactoryInterface {
- if (
- static::$slimHttpDecoratorsAutomaticDetectionEnabled
- && SlimHttpPsr17Factory::isResponseFactoryAvailable()
- ) {
- return SlimHttpPsr17Factory::createDecoratedResponseFactory($responseFactory, $streamFactory);
- }
-
- return $responseFactory;
- }
-
- public static function setPsr17FactoryProvider(Psr17FactoryProviderInterface $psr17FactoryProvider): void
- {
- static::$psr17FactoryProvider = $psr17FactoryProvider;
- }
-
- public static function setResponseFactory(ResponseFactoryInterface $responseFactory): void
- {
- static::$responseFactory = $responseFactory;
- }
-
- public static function setStreamFactory(StreamFactoryInterface $streamFactory): void
- {
- static::$streamFactory = $streamFactory;
- }
-
- public static function setContainer(ContainerInterface $container): void
- {
- static::$container = $container;
- }
-
- public static function setCallableResolver(CallableResolverInterface $callableResolver): void
- {
- static::$callableResolver = $callableResolver;
- }
-
- public static function setRouteCollector(RouteCollectorInterface $routeCollector): void
- {
- static::$routeCollector = $routeCollector;
- }
-
- public static function setRouteResolver(RouteResolverInterface $routeResolver): void
- {
- static::$routeResolver = $routeResolver;
- }
-
- public static function setMiddlewareDispatcher(MiddlewareDispatcherInterface $middlewareDispatcher): void
- {
- static::$middlewareDispatcher = $middlewareDispatcher;
- }
-
- public static function setSlimHttpDecoratorsAutomaticDetection(bool $enabled): void
- {
- static::$slimHttpDecoratorsAutomaticDetectionEnabled = $enabled;
- }
-}
diff --git a/Slim/Factory/Psr17/GuzzlePsr17Factory.php b/Slim/Factory/Psr17/GuzzlePsr17Factory.php
deleted file mode 100644
index 32a548a67..000000000
--- a/Slim/Factory/Psr17/GuzzlePsr17Factory.php
+++ /dev/null
@@ -1,19 +0,0 @@
-serverRequestCreator = $serverRequestCreator;
- $this->serverRequestCreatorMethod = $serverRequestCreatorMethod;
- }
-
- /**
- * {@inheritdoc}
- */
- public function createServerRequestFromGlobals(): ServerRequestInterface
- {
- /** @var callable $callable */
- $callable = [$this->serverRequestCreator, $this->serverRequestCreatorMethod];
- return (Closure::fromCallable($callable))();
- }
-}
diff --git a/Slim/Factory/Psr17/SlimHttpPsr17Factory.php b/Slim/Factory/Psr17/SlimHttpPsr17Factory.php
deleted file mode 100644
index 5d636318b..000000000
--- a/Slim/Factory/Psr17/SlimHttpPsr17Factory.php
+++ /dev/null
@@ -1,39 +0,0 @@
-serverRequestCreator = $serverRequestCreator;
- }
-
- /**
- * {@inheritdoc}
- */
- public function createServerRequestFromGlobals(): ServerRequestInterface
- {
- if (!static::isServerRequestDecoratorAvailable()) {
- throw new RuntimeException('The Slim-Http ServerRequest decorator is not available.');
- }
-
- $request = $this->serverRequestCreator->createServerRequestFromGlobals();
-
- if (
- !((
- $decoratedServerRequest = new static::$serverRequestDecoratorClass($request)
- ) instanceof ServerRequestInterface)
- ) {
- throw new RuntimeException(get_called_class() . ' could not instantiate a decorated server request.');
- }
-
- return $decoratedServerRequest;
- }
-
- public static function isServerRequestDecoratorAvailable(): bool
- {
- return class_exists(static::$serverRequestDecoratorClass);
- }
-}
diff --git a/Slim/Factory/Psr17/SlimPsr17Factory.php b/Slim/Factory/Psr17/SlimPsr17Factory.php
deleted file mode 100644
index 46c46f9ce..000000000
--- a/Slim/Factory/Psr17/SlimPsr17Factory.php
+++ /dev/null
@@ -1,19 +0,0 @@
-getFactories() as $psr17Factory) {
- if ($psr17Factory::isServerRequestCreatorAvailable()) {
- $serverRequestCreator = $psr17Factory::getServerRequestCreator();
- return static::attemptServerRequestCreatorDecoration($serverRequestCreator);
- }
- }
-
- throw new RuntimeException(
- "Could not detect any ServerRequest creator implementations. " .
- "Please install a supported implementation in order to use `App::run()` " .
- "without having to pass in a `ServerRequest` object. " .
- "See https://github.com/slimphp/Slim/blob/4.x/README.md for a list of supported implementations."
- );
- }
-
- protected static function attemptServerRequestCreatorDecoration(
- ServerRequestCreatorInterface $serverRequestCreator
- ): ServerRequestCreatorInterface {
- if (
- static::$slimHttpDecoratorsAutomaticDetectionEnabled
- && SlimHttpServerRequestCreator::isServerRequestDecoratorAvailable()
- ) {
- return new SlimHttpServerRequestCreator($serverRequestCreator);
- }
-
- return $serverRequestCreator;
- }
-
- public static function setPsr17FactoryProvider(Psr17FactoryProviderInterface $psr17FactoryProvider): void
- {
- static::$psr17FactoryProvider = $psr17FactoryProvider;
- }
-
- public static function setServerRequestCreator(ServerRequestCreatorInterface $serverRequestCreator): void
- {
- self::$serverRequestCreator = $serverRequestCreator;
- }
-
- public static function setSlimHttpDecoratorsAutomaticDetection(bool $enabled): void
- {
- static::$slimHttpDecoratorsAutomaticDetectionEnabled = $enabled;
- }
-}
diff --git a/Slim/Handlers/ErrorHandler.php b/Slim/Handlers/ErrorHandler.php
deleted file mode 100644
index 689b4630b..000000000
--- a/Slim/Handlers/ErrorHandler.php
+++ /dev/null
@@ -1,309 +0,0 @@
-
- */
- protected array $errorRenderers = [
- 'application/json' => JsonErrorRenderer::class,
- 'application/xml' => XmlErrorRenderer::class,
- 'text/xml' => XmlErrorRenderer::class,
- 'text/html' => HtmlErrorRenderer::class,
- 'text/plain' => PlainTextErrorRenderer::class,
- ];
-
- protected bool $displayErrorDetails = false;
-
- protected bool $logErrors;
-
- protected bool $logErrorDetails = false;
-
- protected ?string $contentType = null;
-
- protected ?string $method = null;
-
- protected ServerRequestInterface $request;
-
- protected Throwable $exception;
-
- protected int $statusCode;
-
- protected CallableResolverInterface $callableResolver;
-
- protected ResponseFactoryInterface $responseFactory;
-
- protected LoggerInterface $logger;
-
- public function __construct(
- CallableResolverInterface $callableResolver,
- ResponseFactoryInterface $responseFactory,
- ?LoggerInterface $logger = null
- ) {
- $this->callableResolver = $callableResolver;
- $this->responseFactory = $responseFactory;
- $this->logger = $logger ?: $this->getDefaultLogger();
- }
-
- /**
- * Invoke error handler
- *
- * @param ServerRequestInterface $request The most recent Request object
- * @param Throwable $exception The caught Exception object
- * @param bool $displayErrorDetails Whether or not to display the error details
- * @param bool $logErrors Whether or not to log errors
- * @param bool $logErrorDetails Whether or not to log error details
- */
- public function __invoke(
- ServerRequestInterface $request,
- Throwable $exception,
- bool $displayErrorDetails,
- bool $logErrors,
- bool $logErrorDetails
- ): ResponseInterface {
- $this->displayErrorDetails = $displayErrorDetails;
- $this->logErrors = $logErrors;
- $this->logErrorDetails = $logErrorDetails;
- $this->request = $request;
- $this->exception = $exception;
- $this->method = $request->getMethod();
- $this->statusCode = $this->determineStatusCode();
- if ($this->contentType === null) {
- $this->contentType = $this->determineContentType($request);
- }
-
- if ($logErrors) {
- $this->writeToErrorLog();
- }
-
- return $this->respond();
- }
-
- /**
- * Force the content type for all error handler responses.
- *
- * @param string|null $contentType The content type
- */
- public function forceContentType(?string $contentType): void
- {
- $this->contentType = $contentType;
- }
-
- protected function determineStatusCode(): int
- {
- if ($this->method === 'OPTIONS') {
- return 200;
- }
-
- if ($this->exception instanceof HttpException) {
- return $this->exception->getCode();
- }
-
- return 500;
- }
-
- /**
- * Determine which content type we know about is wanted using Accept header
- *
- * Note: This method is a bare-bones implementation designed specifically for
- * Slim's error handling requirements. Consider a fully-feature solution such
- * as willdurand/negotiation for any other situation.
- */
- protected function determineContentType(ServerRequestInterface $request): ?string
- {
- $acceptHeader = $request->getHeaderLine('Accept');
- $selectedContentTypes = array_intersect(
- explode(',', $acceptHeader),
- array_keys($this->errorRenderers)
- );
- $count = count($selectedContentTypes);
-
- if ($count) {
- $current = current($selectedContentTypes);
-
- /**
- * Ensure other supported content types take precedence over text/plain
- * when multiple content types are provided via Accept header.
- */
- if ($current === 'text/plain' && $count > 1) {
- $next = next($selectedContentTypes);
- if (is_string($next)) {
- return $next;
- }
- }
-
- if (is_string($current)) {
- return $current;
- }
- }
-
- if (preg_match('/\+(json|xml)/', $acceptHeader, $matches)) {
- $mediaType = 'application/' . $matches[1];
- if (array_key_exists($mediaType, $this->errorRenderers)) {
- return $mediaType;
- }
- }
-
- return null;
- }
-
- /**
- * Determine which renderer to use based on content type
- *
- * @throws RuntimeException
- */
- protected function determineRenderer(): callable
- {
- if ($this->contentType !== null && array_key_exists($this->contentType, $this->errorRenderers)) {
- $renderer = $this->errorRenderers[$this->contentType];
- } else {
- $renderer = $this->defaultErrorRenderer;
- }
-
- return $this->callableResolver->resolve($renderer);
- }
-
- /**
- * Register an error renderer for a specific content-type
- *
- * @param string $contentType The content-type this renderer should be registered to
- * @param ErrorRendererInterface|string|callable $errorRenderer The error renderer
- */
- public function registerErrorRenderer(string $contentType, $errorRenderer): void
- {
- $this->errorRenderers[$contentType] = $errorRenderer;
- }
-
- /**
- * Set the default error renderer
- *
- * @param string $contentType The content type of the default error renderer
- * @param ErrorRendererInterface|string|callable $errorRenderer The default error renderer
- */
- public function setDefaultErrorRenderer(string $contentType, $errorRenderer): void
- {
- $this->defaultErrorRendererContentType = $contentType;
- $this->defaultErrorRenderer = $errorRenderer;
- }
-
- /**
- * Set the renderer for the error logger
- *
- * @param ErrorRendererInterface|string|callable $logErrorRenderer
- */
- public function setLogErrorRenderer($logErrorRenderer): void
- {
- $this->logErrorRenderer = $logErrorRenderer;
- }
-
- /**
- * Write to the error log if $logErrors has been set to true
- */
- protected function writeToErrorLog(): void
- {
- $renderer = $this->callableResolver->resolve($this->logErrorRenderer);
- $error = $renderer($this->exception, $this->logErrorDetails);
- if ($this->logErrorRenderer === PlainTextErrorRenderer::class && !$this->displayErrorDetails) {
- $error .= "\nTips: To display error details in HTTP response ";
- $error .= 'set "displayErrorDetails" to true in the ErrorHandler constructor.';
- }
- $this->logError($error);
- }
-
- /**
- * Wraps the error_log function so that this can be easily tested
- */
- protected function logError(string $error): void
- {
- $this->logger->error($error);
- }
-
- /**
- * Returns a default logger implementation.
- */
- protected function getDefaultLogger(): LoggerInterface
- {
- return new Logger();
- }
-
- protected function respond(): ResponseInterface
- {
- $response = $this->responseFactory->createResponse($this->statusCode);
- if ($this->contentType !== null && array_key_exists($this->contentType, $this->errorRenderers)) {
- $response = $response->withHeader('Content-type', $this->contentType);
- } else {
- $response = $response->withHeader('Content-type', $this->defaultErrorRendererContentType);
- }
-
- if ($this->exception instanceof HttpMethodNotAllowedException) {
- $allowedMethods = implode(', ', $this->exception->getAllowedMethods());
- $response = $response->withHeader('Allow', $allowedMethods);
- }
-
- $renderer = $this->determineRenderer();
- $body = call_user_func($renderer, $this->exception, $this->displayErrorDetails);
- if ($body !== false) {
- /** @var string $body */
- $response->getBody()->write($body);
- }
-
- return $response;
- }
-}
diff --git a/Slim/Handlers/Strategies/RequestHandler.php b/Slim/Handlers/Strategies/RequestHandler.php
deleted file mode 100644
index ea88a5f12..000000000
--- a/Slim/Handlers/Strategies/RequestHandler.php
+++ /dev/null
@@ -1,48 +0,0 @@
-appendRouteArgumentsToRequestAttributes = $appendRouteArgumentsToRequestAttributes;
- }
-
- /**
- * Invoke a route callable that implements RequestHandlerInterface
- *
- * @param array $routeArguments
- */
- public function __invoke(
- callable $callable,
- ServerRequestInterface $request,
- ResponseInterface $response,
- array $routeArguments
- ): ResponseInterface {
- if ($this->appendRouteArgumentsToRequestAttributes) {
- foreach ($routeArguments as $k => $v) {
- $request = $request->withAttribute($k, $v);
- }
- }
-
- return $callable($request);
- }
-}
diff --git a/Slim/Handlers/Strategies/RequestResponse.php b/Slim/Handlers/Strategies/RequestResponse.php
deleted file mode 100644
index 45b2c05a4..000000000
--- a/Slim/Handlers/Strategies/RequestResponse.php
+++ /dev/null
@@ -1,40 +0,0 @@
- $routeArguments
- */
- public function __invoke(
- callable $callable,
- ServerRequestInterface $request,
- ResponseInterface $response,
- array $routeArguments
- ): ResponseInterface {
- foreach ($routeArguments as $k => $v) {
- $request = $request->withAttribute($k, $v);
- }
-
- return $callable($request, $response, $routeArguments);
- }
-}
diff --git a/Slim/Handlers/Strategies/RequestResponseNamedArgs.php b/Slim/Handlers/Strategies/RequestResponseNamedArgs.php
deleted file mode 100644
index f36059646..000000000
--- a/Slim/Handlers/Strategies/RequestResponseNamedArgs.php
+++ /dev/null
@@ -1,45 +0,0 @@
-= 8.0.0');
- }
- }
-
- /**
- * Invoke a route callable with request, response and all route parameters
- * as individual arguments.
- *
- * @param array $routeArguments
- */
- public function __invoke(
- callable $callable,
- ServerRequestInterface $request,
- ResponseInterface $response,
- array $routeArguments
- ): ResponseInterface {
- return $callable($request, $response, ...$routeArguments);
- }
-}
diff --git a/Slim/Interfaces/AdvancedCallableResolverInterface.php b/Slim/Interfaces/AdvancedCallableResolverInterface.php
deleted file mode 100644
index aa1d897de..000000000
--- a/Slim/Interfaces/AdvancedCallableResolverInterface.php
+++ /dev/null
@@ -1,28 +0,0 @@
- $routeArguments The route's placeholder arguments
- *
- * @return ResponseInterface The response from the callable.
- */
- public function __invoke(
- callable $callable,
- ServerRequestInterface $request,
- ResponseInterface $response,
- array $routeArguments
- ): ResponseInterface;
-}
diff --git a/Slim/Interfaces/MiddlewareCollectionInterface.php b/Slim/Interfaces/MiddlewareCollectionInterface.php
new file mode 100644
index 000000000..3c2750843
--- /dev/null
+++ b/Slim/Interfaces/MiddlewareCollectionInterface.php
@@ -0,0 +1,14 @@
+ $routeArguments The route's placeholder arguments
+ *
+ * @return ResponseInterface The response from the callable
+ */
+ public function __invoke(
+ callable $callable,
+ ServerRequestInterface $request,
+ ResponseInterface $response,
+ array $routeArguments,
+ ): ResponseInterface;
}
diff --git a/Slim/Interfaces/RouteCollectionInterface.php b/Slim/Interfaces/RouteCollectionInterface.php
new file mode 100644
index 000000000..5c3126a59
--- /dev/null
+++ b/Slim/Interfaces/RouteCollectionInterface.php
@@ -0,0 +1,29 @@
+
- */
- public function setBasePath(string $basePath): RouteCollectorProxyInterface;
-
- /**
- * Add GET route
- *
- * @param string $pattern The route URI pattern
- * @param callable|string $callable The route callback routine
- */
- public function get(string $pattern, $callable): RouteInterface;
-
- /**
- * Add POST route
- *
- * @param string $pattern The route URI pattern
- * @param callable|string $callable The route callback routine
- */
- public function post(string $pattern, $callable): RouteInterface;
-
- /**
- * Add PUT route
- *
- * @param string $pattern The route URI pattern
- * @param callable|string $callable The route callback routine
- */
- public function put(string $pattern, $callable): RouteInterface;
-
- /**
- * Add PATCH route
- *
- * @param string $pattern The route URI pattern
- * @param callable|string $callable The route callback routine
- */
- public function patch(string $pattern, $callable): RouteInterface;
-
- /**
- * Add DELETE route
- *
- * @param string $pattern The route URI pattern
- * @param callable|string $callable The route callback routine
- */
- public function delete(string $pattern, $callable): RouteInterface;
-
- /**
- * Add OPTIONS route
- *
- * @param string $pattern The route URI pattern
- * @param callable|string $callable The route callback routine
- */
- public function options(string $pattern, $callable): RouteInterface;
-
- /**
- * Add route for any HTTP method
- *
- * @param string $pattern The route URI pattern
- * @param callable|string $callable The route callback routine
- */
- public function any(string $pattern, $callable): RouteInterface;
-
- /**
- * Add route with multiple methods
- *
- * @param string[] $methods Numeric array of HTTP method names
- * @param string $pattern The route URI pattern
- * @param callable|string $callable The route callback routine
- */
- public function map(array $methods, string $pattern, $callable): RouteInterface;
-
- /**
- * Route Groups
- *
- * This method accepts a route pattern and a callback. All route
- * declarations in the callback will be prepended by the group(s)
- * that it is in.
- * @param string|callable $callable
- */
- public function group(string $pattern, $callable): RouteGroupInterface;
-
- /**
- * Add a route that sends an HTTP redirect
- *
- * @param string|UriInterface $to
- */
- public function redirect(string $from, $to, int $status = 302): RouteInterface;
-}
diff --git a/Slim/Interfaces/RouteGroupInterface.php b/Slim/Interfaces/RouteGroupInterface.php
deleted file mode 100644
index efc1c5a3b..000000000
--- a/Slim/Interfaces/RouteGroupInterface.php
+++ /dev/null
@@ -1,43 +0,0 @@
- $dispatcher
- */
- public function appendMiddlewareToDispatcher(MiddlewareDispatcher $dispatcher): RouteGroupInterface;
-
- /**
- * Get the RouteGroup's pattern
- */
- public function getPattern(): string;
-}
diff --git a/Slim/Interfaces/RouteInterface.php b/Slim/Interfaces/RouteInterface.php
deleted file mode 100644
index 8ed817ef4..000000000
--- a/Slim/Interfaces/RouteInterface.php
+++ /dev/null
@@ -1,124 +0,0 @@
-
- */
- public function getArguments(): array;
-
- /**
- * Set a route argument
- */
- public function setArgument(string $name, string $value): RouteInterface;
-
- /**
- * Replace route arguments
- *
- * @param array $arguments
- */
- public function setArguments(array $arguments): self;
-
- /**
- * @param MiddlewareInterface|string|callable $middleware
- */
- public function add($middleware): self;
-
- public function addMiddleware(MiddlewareInterface $middleware): self;
-
- /**
- * Prepare the route for use
- *
- * @param array $arguments
- */
- public function prepare(array $arguments): self;
-
- /**
- * Run route
- *
- * This method traverses the middleware stack, including the route's callable
- * and captures the resultant HTTP response object. It then sends the response
- * back to the Application.
- */
- public function run(ServerRequestInterface $request): ResponseInterface;
-}
diff --git a/Slim/Interfaces/RouteResolverInterface.php b/Slim/Interfaces/RouteResolverInterface.php
deleted file mode 100644
index 256a35997..000000000
--- a/Slim/Interfaces/RouteResolverInterface.php
+++ /dev/null
@@ -1,17 +0,0 @@
-getPath()
- */
- public function computeRoutingResults(string $uri, string $method): RoutingResults;
-
- public function resolveRoute(string $identifier): RouteInterface;
-}
diff --git a/Slim/Interfaces/ServerRequestCreatorInterface.php b/Slim/Interfaces/ServerRequestCreatorInterface.php
index 54d231edd..aed6b18f9 100644
--- a/Slim/Interfaces/ServerRequestCreatorInterface.php
+++ b/Slim/Interfaces/ServerRequestCreatorInterface.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
diff --git a/Slim/Interfaces/RouteParserInterface.php b/Slim/Interfaces/UrlGeneratorInterface.php
similarity index 65%
rename from Slim/Interfaces/RouteParserInterface.php
rename to Slim/Interfaces/UrlGeneratorInterface.php
index 8ccea3bc3..7142287b8 100644
--- a/Slim/Interfaces/RouteParserInterface.php
+++ b/Slim/Interfaces/UrlGeneratorInterface.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
@@ -14,14 +14,13 @@
use Psr\Http\Message\UriInterface;
use RuntimeException;
-/** @api */
-interface RouteParserInterface
+interface UrlGeneratorInterface
{
/**
* Build the path for a named route excluding the base path
*
- * @param string $routeName Route name
- * @param array $data Named argument replacement data
+ * @param string $routeName Route name
+ * @param array $data Named argument replacement data
* @param array $queryParams Optional query string parameters
*
* @throws RuntimeException If named route does not exist
@@ -32,8 +31,8 @@ public function relativeUrlFor(string $routeName, array $data = [], array $query
/**
* Build the path for a named route including the base path
*
- * @param string $routeName Route name
- * @param array $data Named argument replacement data
+ * @param string $routeName Route name
+ * @param array $data Named argument replacement data
* @param array $queryParams Optional query string parameters
*
* @throws RuntimeException If named route does not exist
@@ -44,10 +43,10 @@ public function urlFor(string $routeName, array $data = [], array $queryParams =
/**
* Get fully qualified URL for named route
*
- * @param UriInterface $uri
- * @param string $routeName Route name
- * @param array $data Named argument replacement data
- * @param array $queryParams Optional query string parameters
+ * @param UriInterface $uri
+ * @param string $routeName Route name
+ * @param array $data Named argument replacement data
+ * @param array $queryParams Optional query string parameters
*/
public function fullUrlFor(UriInterface $uri, string $routeName, array $data = [], array $queryParams = []): string;
}
diff --git a/Slim/Logger.php b/Slim/Logger.php
deleted file mode 100644
index 31282175c..000000000
--- a/Slim/Logger.php
+++ /dev/null
@@ -1,32 +0,0 @@
- $context
- *
- * @throws InvalidArgumentException
- */
- public function log($level, $message, array $context = []): void
- {
- error_log((string) $message);
- }
-}
diff --git a/Slim/Media/MediaType.php b/Slim/Media/MediaType.php
new file mode 100644
index 000000000..4caa71bdd
--- /dev/null
+++ b/Slim/Media/MediaType.php
@@ -0,0 +1,23 @@
+parseAcceptHeader($request->getHeaderLine('Accept'));
+
+ if (!$mediaTypes) {
+ $mediaTypes = $this->parseContentType($request->getHeaderLine('Content-Type'));
+ }
+
+ return $mediaTypes;
+ }
+
+ /**
+ * Parses the 'Accept' header to extract media types.
+ *
+ * This method splits the 'Accept' header value into its components and normalizes
+ * the media types by trimming whitespace and converting them to lowercase.
+ *
+ * This method doesn't consider the quality values (q-values) that can be present in the Accept header.
+ * If prioritization is important for your use case, you might want to consider implementing
+ * q-value parsing and sorting.
+ *
+ * @param string|null $accept the value of the 'Accept' header
+ *
+ * @return array an array of normalized media types from the 'Accept' header
+ */
+ private function parseAcceptHeader(?string $accept): array
+ {
+ $acceptTypes = $accept ? explode(',', $accept) : [];
+
+ // Normalize types
+ $cleanTypes = [];
+ foreach ($acceptTypes as $type) {
+ $tokens = explode(';', $type);
+ $name = trim(strtolower(reset($tokens)));
+ $cleanTypes[] = $name;
+ }
+
+ return $cleanTypes;
+ }
+
+ /**
+ * Parses the 'Content-Type' header to extract the media type.
+ *
+ * This method splits the 'Content-Type' header value to separate the media type
+ * from any additional parameters, normalizes it, and returns it in an array.
+ *
+ * @param string|null $contentType the value of the 'Content-Type' header
+ *
+ * @return array an array containing the normalized media type from the 'Content-Type' header
+ */
+ private function parseContentType(?string $contentType): array
+ {
+ if ($contentType === null) {
+ return [];
+ }
+
+ $parts = explode(';', $contentType);
+ $name = strtolower(trim($parts[0]));
+
+ return $name ? [$name] : [];
+ }
+}
diff --git a/Slim/Middleware/BasePathMiddleware.php b/Slim/Middleware/BasePathMiddleware.php
new file mode 100644
index 000000000..77699b8c9
--- /dev/null
+++ b/Slim/Middleware/BasePathMiddleware.php
@@ -0,0 +1,74 @@
+phpSapi = $phpSapi;
+ $this->app = $app;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ $basePath = '';
+
+ if ($this->phpSapi === 'apache2handler') {
+ $basePath = $this->getBasePathByRequestUri($request);
+ }
+
+ $request = $request->withAttribute(RouteContext::BASE_PATH, $basePath);
+
+ $this->app->setBasePath($basePath);
+
+ return $handler->handle($request);
+ }
+
+ /**
+ * Return basePath for most common webservers, such as Apache.
+ */
+ private function getBasePathByRequestUri(ServerRequestInterface $request): string
+ {
+ $basePath = $request->getUri()->getPath();
+ $scriptName = $request->getServerParams()['SCRIPT_NAME'] ?? '';
+ $scriptName = str_replace('\\', '/', dirname($scriptName, 2));
+
+ if ($scriptName === '/') {
+ return '';
+ }
+
+ $length = strlen($scriptName);
+ $basePath = $length > 0 ? substr($basePath, 0, $length) : $basePath;
+
+ return strlen($basePath) > 1 ? $basePath : '';
+ }
+}
diff --git a/Slim/Middleware/BodyParsingMiddleware.php b/Slim/Middleware/BodyParsingMiddleware.php
index 94bba3713..381825135 100644
--- a/Slim/Middleware/BodyParsingMiddleware.php
+++ b/Slim/Middleware/BodyParsingMiddleware.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
@@ -15,42 +15,28 @@
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use RuntimeException;
+use Slim\Media\MediaType;
+use Slim\Media\MediaTypeDetector;
-use function count;
-use function explode;
use function is_array;
-use function is_null;
use function is_object;
-use function is_string;
use function json_decode;
use function libxml_clear_errors;
-use function libxml_disable_entity_loader;
use function libxml_use_internal_errors;
use function parse_str;
use function simplexml_load_string;
-use function strtolower;
-use function trim;
-use const LIBXML_VERSION;
-
-/** @api */
-class BodyParsingMiddleware implements MiddlewareInterface
+final class BodyParsingMiddleware implements MiddlewareInterface
{
- /**
- * @var callable[]
- */
- protected array $bodyParsers;
+ private MediaTypeDetector $mediaTypeDetector;
- /**
- * @param callable[] $bodyParsers list of body parsers as an associative array of mediaType => callable
- */
- public function __construct(array $bodyParsers = [])
- {
- $this->registerDefaultBodyParsers();
+ private array $handlers = [];
- foreach ($bodyParsers as $mediaType => $parser) {
- $this->registerBodyParser($mediaType, $parser);
- }
+ private string $defaultMediaType = 'text/html';
+
+ public function __construct(MediaTypeDetector $mediaTypeDetector)
+ {
+ $this->mediaTypeDetector = $mediaTypeDetector;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
@@ -66,38 +52,29 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
}
/**
- * @param string $mediaType A HTTP media type (excluding content-type params).
- * @param callable $callable A callable that returns parsed contents for media type.
+ * @param string $mediaType The HTTP media type (excluding content-type params)
+ * @param callable $handler The callable that returns parsed contents for media type
*/
- public function registerBodyParser(string $mediaType, callable $callable): self
+ public function withBodyParser(string $mediaType, callable $handler): self
{
- $this->bodyParsers[$mediaType] = $callable;
- return $this;
- }
+ $clone = clone $this;
+ $clone->handlers[$mediaType] = $handler;
- /**
- * @param string $mediaType A HTTP media type (excluding content-type params).
- */
- public function hasBodyParser(string $mediaType): bool
- {
- return isset($this->bodyParsers[$mediaType]);
+ return $clone;
}
- /**
- * @param string $mediaType A HTTP media type (excluding content-type params).
- * @throws RuntimeException
- */
- public function getBodyParser(string $mediaType): callable
+ public function withDefaultMediaType(string $mediaType): self
{
- if (!isset($this->bodyParsers[$mediaType])) {
- throw new RuntimeException('No parser for type ' . $mediaType);
- }
- return $this->bodyParsers[$mediaType];
+ $clone = clone $this;
+ $clone->defaultMediaType = $mediaType;
+
+ return $clone;
}
- protected function registerDefaultBodyParsers(): void
+ public function withDefaultBodyParsers(): self
{
- $this->registerBodyParser('application/json', static function ($input) {
+ $clone = clone $this;
+ $clone = $clone->withBodyParser(MediaType::APPLICATION_JSON, function ($input) {
$result = json_decode($input, true);
if (!is_array($result)) {
@@ -107,17 +84,16 @@ protected function registerDefaultBodyParsers(): void
return $result;
});
- $this->registerBodyParser('application/x-www-form-urlencoded', static function ($input) {
+ $clone = $clone->withBodyParser(MediaType::APPLICATION_FORM_URLENCODED, function ($input) {
parse_str($input, $data);
+
return $data;
});
- $xmlCallable = static function ($input) {
- $backup = self::disableXmlEntityLoader(true);
+ $xmlCallable = function ($input) {
$backup_errors = libxml_use_internal_errors(true);
$result = simplexml_load_string($input);
- self::disableXmlEntityLoader($backup);
libxml_clear_errors();
libxml_use_internal_errors($backup_errors);
@@ -128,70 +104,37 @@ protected function registerDefaultBodyParsers(): void
return $result;
};
- $this->registerBodyParser('application/xml', $xmlCallable);
- $this->registerBodyParser('text/xml', $xmlCallable);
+ return $clone
+ ->withBodyParser(MediaType::APPLICATION_XML, $xmlCallable)
+ ->withBodyParser(MediaType::TEXT_XML, $xmlCallable);
}
/**
- * @return null|array|object
+ * Parse request body.
+ *
+ * @throws RuntimeException
*/
- protected function parseBody(ServerRequestInterface $request)
+ private function parseBody(ServerRequestInterface $request): array|object|null
{
- $mediaType = $this->getMediaType($request);
- if ($mediaType === null) {
- return null;
- }
-
- // Check if this specific media type has a parser registered first
- if (!isset($this->bodyParsers[$mediaType])) {
- // If not, look for a media type with a structured syntax suffix (RFC 6839)
- $parts = explode('+', $mediaType);
- if (count($parts) >= 2) {
- $mediaType = 'application/' . $parts[count($parts) - 1];
- }
- }
+ // Negotiate content type
+ $contentTypes = $this->mediaTypeDetector->detect($request);
+ $contentType = $contentTypes[0] ?? $this->defaultMediaType;
- if (isset($this->bodyParsers[$mediaType])) {
- $body = (string)$request->getBody();
- $parsed = $this->bodyParsers[$mediaType]($body);
+ // Determine which handler to use based on media type
+ $handler = $this->handlers[$contentType] ?? reset($this->handlers);
- if ($parsed !== null && !is_object($parsed) && !is_array($parsed)) {
- throw new RuntimeException(
- 'Request body media type parser return value must be an array, an object, or null'
- );
- }
+ // Invoke the parser
+ $parsed = call_user_func(
+ $handler,
+ (string)$request->getBody()
+ );
+ if ($parsed === null || is_object($parsed) || is_array($parsed)) {
return $parsed;
}
- return null;
- }
-
- /**
- * @return string|null The serverRequest media type, minus content-type params
- */
- protected function getMediaType(ServerRequestInterface $request): ?string
- {
- $contentType = $request->getHeader('Content-Type')[0] ?? null;
-
- if (is_string($contentType) && trim($contentType) !== '') {
- $contentTypeParts = explode(';', $contentType);
- return strtolower(trim($contentTypeParts[0]));
- }
-
- return null;
- }
-
- protected static function disableXmlEntityLoader(bool $disable): bool
- {
- if (LIBXML_VERSION >= 20900) {
- // libxml >= 2.9.0 disables entity loading by default, so it is
- // safe to skip the real call (deprecated in PHP 8).
- return true;
- }
-
- // @codeCoverageIgnoreStart
- return libxml_disable_entity_loader($disable);
- // @codeCoverageIgnoreEnd
+ throw new RuntimeException(
+ 'Request body media type parser return value must be an array, an object, or null.'
+ );
}
}
diff --git a/Slim/Middleware/ContentLengthMiddleware.php b/Slim/Middleware/ContentLengthMiddleware.php
index e289fa365..cba47caa6 100644
--- a/Slim/Middleware/ContentLengthMiddleware.php
+++ b/Slim/Middleware/ContentLengthMiddleware.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
@@ -15,8 +15,7 @@
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
-/** @api */
-class ContentLengthMiddleware implements MiddlewareInterface
+final class ContentLengthMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
@@ -25,7 +24,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
// Add Content-Length header if not already added
$size = $response->getBody()->getSize();
if ($size !== null && !$response->hasHeader('Content-Length')) {
- $response = $response->withHeader('Content-Length', (string) $size);
+ $response = $response->withHeader('Content-Length', (string)$size);
}
return $response;
diff --git a/Slim/Middleware/CorsMiddleware.php b/Slim/Middleware/CorsMiddleware.php
new file mode 100644
index 000000000..f00e6a8aa
--- /dev/null
+++ b/Slim/Middleware/CorsMiddleware.php
@@ -0,0 +1,189 @@
+responseFactory = $responseFactory;
+ }
+
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler
+ ): ResponseInterface {
+ $origin = $request->getHeaderLine('Origin');
+
+ if ($request->getMethod() === 'OPTIONS') {
+ $response = $this->responseFactory->createResponse();
+ } else {
+ $response = $handler->handle($request);
+ }
+
+ // Handle origin header
+ if ($origin && $this->isOriginAllowed($origin)) {
+ $response = $response->withHeader('Access-Control-Allow-Origin', $origin);
+
+ // Allow credentials only with specific origin
+ if ($this->allowCredentials) {
+ $response = $response->withHeader('Access-Control-Allow-Credentials', 'true');
+ }
+ } elseif ($this->allowedOrigins === null) {
+ // If no specific origins are set, use wildcard
+ $response = $response->withHeader('Access-Control-Allow-Origin', '*');
+ }
+
+ // Add allowed methods
+ if (!empty($this->allowedMethods)) {
+ $response = $response->withHeader(
+ 'Access-Control-Allow-Methods',
+ implode(', ', $this->allowedMethods)
+ );
+ }
+
+ // Add allowed headers
+ if (!empty($this->allowedHeaders)) {
+ $response = $response->withHeader(
+ 'Access-Control-Allow-Headers',
+ implode(', ', $this->allowedHeaders)
+ );
+ }
+
+ // Add exposed headers
+ if (!empty($this->exposedHeaders)) {
+ $response = $response->withHeader(
+ 'Access-Control-Expose-Headers',
+ implode(', ', $this->exposedHeaders)
+ );
+ }
+
+ // Add max age header if configured
+ if ($this->maxAge !== null) {
+ $response = $response->withHeader('Access-Control-Max-Age', (string)$this->maxAge);
+ }
+
+ // Add cache control headers if enabled
+ if ($this->useCache) {
+ $response = $response
+ ->withHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
+ ->withHeader('Pragma', 'no-cache');
+ }
+
+ return $response;
+ }
+
+ /**
+ * Set the Access-Control-Max-Age header value in seconds.
+ * Set to null to disable the header.
+ */
+ public function withMaxAge(?int $maxAge): self
+ {
+ $clone = clone $this;
+ $clone->maxAge = $maxAge;
+
+ return $clone;
+ }
+
+ /**
+ * Set allowed origins. Null means allow all (*).
+ * Pass an array of strings to specify allowed origins.
+ */
+ public function withAllowedOrigins(?array $origins): self
+ {
+ $clone = clone $this;
+ $clone->allowedOrigins = $origins;
+
+ return $clone;
+ }
+
+ /**
+ * Set whether to allow credentials.
+ */
+ public function withAllowCredentials(bool $allow): self
+ {
+ $clone = clone $this;
+ $clone->allowCredentials = $allow;
+
+ return $clone;
+ }
+
+ /**
+ * Set allowed methods.
+ */
+ public function withAllowedMethods(array $methods): self
+ {
+ $clone = clone $this;
+ $clone->allowedMethods = array_map('strtoupper', $methods);
+
+ return $clone;
+ }
+
+ /**
+ * Set allowed headers.
+ */
+ public function withAllowedHeaders(array $headers): self
+ {
+ $clone = clone $this;
+ $clone->allowedHeaders = $headers;
+
+ return $clone;
+ }
+
+ /**
+ * Set exposed headers.
+ */
+ public function withExposedHeaders(array $headers): self
+ {
+ $clone = clone $this;
+ $clone->exposedHeaders = $headers;
+
+ return $clone;
+ }
+
+ /**
+ * Set whether to use cache control headers.
+ */
+ public function withCache(bool $useCache): self
+ {
+ $clone = clone $this;
+ $clone->useCache = $useCache;
+
+ return $clone;
+ }
+
+ /**
+ * Check if origin is allowed.
+ */
+ private function isOriginAllowed(string $origin): bool
+ {
+ if ($this->allowedOrigins === null) {
+ return true;
+ }
+
+ return in_array($origin, $this->allowedOrigins, true);
+ }
+}
diff --git a/Slim/Middleware/EndpointMiddleware.php b/Slim/Middleware/EndpointMiddleware.php
new file mode 100644
index 000000000..c02b703a9
--- /dev/null
+++ b/Slim/Middleware/EndpointMiddleware.php
@@ -0,0 +1,160 @@
+containerResolver = $callableResolver;
+ $this->responseFactory = $responseFactory;
+ $this->requestHandler = $requestHandler;
+ $this->invocationStrategy = $invocationStrategy;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ /* @var RoutingResults $routingResults */
+ $routingResults = $request->getAttribute(RouteContext::ROUTING_RESULTS);
+
+ if (!$routingResults instanceof RoutingResults) {
+ throw new RuntimeException(
+ 'An unexpected error occurred while handling routing results. Routing results are not available.'
+ );
+ }
+
+ $routeStatus = $routingResults->getRouteStatus();
+ if ($routeStatus === RoutingResults::FOUND) {
+ return $this->handleFound($request, $routingResults);
+ }
+
+ if ($routeStatus === RoutingResults::NOT_FOUND) {
+ // 404 Not Found
+ throw new HttpNotFoundException($request);
+ }
+
+ if ($routeStatus === RoutingResults::METHOD_NOT_ALLOWED) {
+ // 405 Method Not Allowed
+ $exception = new HttpMethodNotAllowedException($request);
+ $exception->setAllowedMethods($routingResults->getAllowedMethods());
+
+ throw $exception;
+ }
+
+ throw new RuntimeException('An unexpected error occurred while endpoint handling.');
+ }
+
+ private function handleFound(
+ ServerRequestInterface $request,
+ RoutingResults $routingResults,
+ ): ResponseInterface {
+ $route = $routingResults->getRoute() ?? throw new RuntimeException('Route not found.');
+ $response = $this->responseFactory->createResponse();
+
+ $middlewares = $this->getRouteMiddleware($route);
+
+ // Add route handler middleware
+ $containerResolver = $this->containerResolver;
+ $invocationStrategy = $this->invocationStrategy;
+
+ $middlewares[] = function () use (
+ $request,
+ $response,
+ $routingResults,
+ $containerResolver,
+ $invocationStrategy
+ ) {
+ // Get handler
+ $actionHandler = $routingResults->getRoute()->getHandler();
+ $vars = $routingResults->getRouteArguments();
+ $actionHandler = $containerResolver->resolveRoute($actionHandler);
+
+ // Invoke action handler
+ return call_user_func($invocationStrategy, $actionHandler, $request, $response, $vars);
+ };
+
+ return $this->invokeMiddlewareStack($request, $response, $middlewares);
+ }
+
+ private function invokeMiddlewareStack(
+ ServerRequestInterface $request,
+ ResponseInterface $response,
+ array $middlewares,
+ ): ResponseInterface {
+ // Tunnel the response object through the route/group specific middleware stack
+ $middlewares[] =
+ new class ($response) implements MiddlewareInterface {
+ private ResponseInterface $response;
+
+ public function __construct(ResponseInterface $response)
+ {
+ $this->response = $response;
+ }
+
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler,
+ ): ResponseInterface {
+ return $this->response;
+ }
+ };
+
+ $request = $request->withAttribute(MiddlewareRequestHandler::MIDDLEWARE, $middlewares);
+
+ return $this->requestHandler->handle($request);
+ }
+
+ private function getRouteMiddleware(Route $route): array
+ {
+ $middlewares = [];
+
+ // Append group specific middleware from all parent route groups
+ $group = $route->getRouteGroup();
+ while ($group) {
+ $middlewareStack = $group->getMiddlewareStack();
+ foreach ($middlewareStack as $middleware) {
+ $middlewares[] = $middleware;
+ }
+ $group = $group->getRouteGroup();
+ }
+ $middlewares = array_reverse($middlewares);
+
+ // Append endpoint specific middleware
+ $routeMiddlewares = $route->getMiddlewareStack();
+ foreach ($routeMiddlewares as $routeMiddleware) {
+ $middlewares[] = $routeMiddleware;
+ }
+
+ return $middlewares;
+ }
+}
diff --git a/Slim/Middleware/ErrorHandlingMiddleware.php b/Slim/Middleware/ErrorHandlingMiddleware.php
new file mode 100644
index 000000000..cdeb6c650
--- /dev/null
+++ b/Slim/Middleware/ErrorHandlingMiddleware.php
@@ -0,0 +1,43 @@
+handle($request);
+ } finally {
+ if ($errorHandler) {
+ restore_error_handler();
+ }
+ }
+
+ return $response;
+ }
+}
diff --git a/Slim/Middleware/ErrorMiddleware.php b/Slim/Middleware/ErrorMiddleware.php
deleted file mode 100644
index 39b1e585d..000000000
--- a/Slim/Middleware/ErrorMiddleware.php
+++ /dev/null
@@ -1,213 +0,0 @@
-callableResolver = $callableResolver;
- $this->responseFactory = $responseFactory;
- $this->displayErrorDetails = $displayErrorDetails;
- $this->logErrors = $logErrors;
- $this->logErrorDetails = $logErrorDetails;
- $this->logger = $logger;
- }
-
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
- {
- try {
- return $handler->handle($request);
- } catch (Throwable $e) {
- return $this->handleException($request, $e);
- }
- }
-
- public function handleException(ServerRequestInterface $request, Throwable $exception): ResponseInterface
- {
- if ($exception instanceof HttpException) {
- $request = $exception->getRequest();
- }
-
- $exceptionType = get_class($exception);
- $handler = $this->getErrorHandler($exceptionType);
-
- return $handler($request, $exception, $this->displayErrorDetails, $this->logErrors, $this->logErrorDetails);
- }
-
- /**
- * Get callable to handle scenarios where an error
- * occurs when processing the current request.
- *
- * @param string $type Exception/Throwable name. ie: RuntimeException::class
- * @return callable|ErrorHandler
- */
- public function getErrorHandler(string $type)
- {
- if (isset($this->handlers[$type])) {
- return $this->callableResolver->resolve($this->handlers[$type]);
- }
-
- if (isset($this->subClassHandlers[$type])) {
- return $this->callableResolver->resolve($this->subClassHandlers[$type]);
- }
-
- foreach ($this->subClassHandlers as $class => $handler) {
- if (is_subclass_of($type, $class)) {
- return $this->callableResolver->resolve($handler);
- }
- }
-
- return $this->getDefaultErrorHandler();
- }
-
- /**
- * Get default error handler
- *
- * @return ErrorHandler|callable
- */
- public function getDefaultErrorHandler()
- {
- if ($this->defaultErrorHandler === null) {
- $this->defaultErrorHandler = new ErrorHandler(
- $this->callableResolver,
- $this->responseFactory,
- $this->logger
- );
- }
-
- return $this->callableResolver->resolve($this->defaultErrorHandler);
- }
-
- /**
- * Set callable as the default Slim application error handler.
- *
- * The callable signature MUST match the ErrorHandlerInterface
- *
- * @see \Slim\Interfaces\ErrorHandlerInterface
- *
- * 1. Instance of \Psr\Http\Message\ServerRequestInterface
- * 2. Instance of \Throwable
- * 3. Boolean $displayErrorDetails
- * 4. Boolean $logErrors
- * 5. Boolean $logErrorDetails
- *
- * The callable MUST return an instance of
- * \Psr\Http\Message\ResponseInterface.
- *
- * @param string|callable|ErrorHandler $handler
- */
- public function setDefaultErrorHandler($handler): self
- {
- $this->defaultErrorHandler = $handler;
- return $this;
- }
-
- /**
- * Set callable to handle scenarios where an error
- * occurs when processing the current request.
- *
- * The callable signature MUST match the ErrorHandlerInterface
- *
- * Pass true to $handleSubclasses to make the handler handle all subclasses of
- * the type as well. Pass an array of classes to make the same function handle multiple exceptions.
- *
- * @see \Slim\Interfaces\ErrorHandlerInterface
- *
- * 1. Instance of \Psr\Http\Message\ServerRequestInterface
- * 2. Instance of \Throwable
- * 3. Boolean $displayErrorDetails
- * 4. Boolean $logErrors
- * 5. Boolean $logErrorDetails
- *
- * The callable MUST return an instance of
- * \Psr\Http\Message\ResponseInterface.
- *
- * @param string|string[] $typeOrTypes Exception/Throwable name.
- * ie: RuntimeException::class or an array of classes
- * ie: [HttpNotFoundException::class, HttpMethodNotAllowedException::class]
- * @param string|callable|ErrorHandlerInterface $handler
- */
- public function setErrorHandler($typeOrTypes, $handler, bool $handleSubclasses = false): self
- {
- if (is_array($typeOrTypes)) {
- foreach ($typeOrTypes as $type) {
- $this->addErrorHandler($type, $handler, $handleSubclasses);
- }
- } else {
- $this->addErrorHandler($typeOrTypes, $handler, $handleSubclasses);
- }
-
- return $this;
- }
-
- /**
- * Used internally to avoid code repetition when passing multiple exceptions to setErrorHandler().
- * @param string|callable|ErrorHandlerInterface $handler
- */
- private function addErrorHandler(string $type, $handler, bool $handleSubclasses): void
- {
- if ($handleSubclasses) {
- $this->subClassHandlers[$type] = $handler;
- } else {
- $this->handlers[$type] = $handler;
- }
- }
-}
diff --git a/Slim/Middleware/ExceptionHandlingMiddleware.php b/Slim/Middleware/ExceptionHandlingMiddleware.php
new file mode 100644
index 000000000..0e7a90a05
--- /dev/null
+++ b/Slim/Middleware/ExceptionHandlingMiddleware.php
@@ -0,0 +1,52 @@
+handle($request);
+ } catch (Throwable $exception) {
+ if ($this->exceptionHandler) {
+ return ($this->exceptionHandler)($request, $exception);
+ }
+
+ throw $exception;
+ }
+ }
+
+ public function withExceptionHandler(ExceptionHandlerInterface $exceptionHandler): self
+ {
+ $clone = clone $this;
+ $clone->exceptionHandler = $exceptionHandler;
+
+ return $clone;
+ }
+}
diff --git a/Slim/Middleware/ExceptionLoggingMiddleware.php b/Slim/Middleware/ExceptionLoggingMiddleware.php
new file mode 100644
index 000000000..f4010ec5b
--- /dev/null
+++ b/Slim/Middleware/ExceptionLoggingMiddleware.php
@@ -0,0 +1,78 @@
+logger = $logger;
+ }
+
+ /**
+ * @throws Throwable
+ * @throws ErrorException
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ try {
+ return $handler->handle($request);
+ } catch (ErrorException $exception) {
+ $errorLevels = [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR];
+ $level = in_array($exception->getSeverity(), $errorLevels) ? LogLevel::ERROR : LogLevel::WARNING;
+
+ $context = $this->getContext($exception, $request);
+ $this->logger->log($level, $exception->getMessage(), $context);
+
+ throw $exception;
+ } catch (Throwable $exception) {
+ $context = $this->getContext($exception, $request);
+ $this->logger->error($exception->getMessage(), $context);
+
+ throw $exception;
+ }
+ }
+
+ public function withLogErrorDetails(bool $logErrorDetails): self
+ {
+ $clone = clone $this;
+ $clone->logErrorDetails = $logErrorDetails;
+
+ return $clone;
+ }
+
+ private function getContext(Throwable $exception, ServerRequestInterface $request): array
+ {
+ $context = [];
+
+ if ($this->logErrorDetails) {
+ $context = [
+ 'exception' => $exception,
+ 'request' => $request,
+ ];
+ }
+
+ return $context;
+ }
+}
diff --git a/Slim/Middleware/HeadMethodMiddleware.php b/Slim/Middleware/HeadMethodMiddleware.php
new file mode 100644
index 000000000..faf15b11e
--- /dev/null
+++ b/Slim/Middleware/HeadMethodMiddleware.php
@@ -0,0 +1,55 @@
+responseFactory = $responseFactory;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ $response = $handler->handle($request);
+
+ /**
+ * This is to be in compliance with RFC 2616, Section 9.
+ * If the incoming request method is HEAD, we need to ensure that the response body
+ * is empty as the request may fall back on a GET route handler due to FastRoute's
+ * routing logic which could potentially append content to the response body
+ * https://www.rfc-editor.org/rfc/rfc9110.html#name-head.
+ */
+ $method = strtoupper($request->getMethod());
+ if ($method === 'HEAD') {
+ $emptyBody = $this->responseFactory->createResponse()->getBody();
+
+ return $response->withBody($emptyBody);
+ }
+
+ return $response;
+ }
+}
diff --git a/Slim/Middleware/MethodOverrideMiddleware.php b/Slim/Middleware/MethodOverrideMiddleware.php
index 553463e2d..4b9d72db6 100644
--- a/Slim/Middleware/MethodOverrideMiddleware.php
+++ b/Slim/Middleware/MethodOverrideMiddleware.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
@@ -18,8 +18,7 @@
use function is_array;
use function strtoupper;
-/** @api */
-class MethodOverrideMiddleware implements MiddlewareInterface
+final class MethodOverrideMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
diff --git a/Slim/Middleware/OutputBufferingMiddleware.php b/Slim/Middleware/OutputBufferingMiddleware.php
index 8bfbbf64e..bde8c00a5 100644
--- a/Slim/Middleware/OutputBufferingMiddleware.php
+++ b/Slim/Middleware/OutputBufferingMiddleware.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
@@ -23,15 +23,15 @@
use function ob_get_clean;
use function ob_start;
-/** @api */
-class OutputBufferingMiddleware implements MiddlewareInterface
+final class OutputBufferingMiddleware implements MiddlewareInterface
{
public const APPEND = 'append';
+
public const PREPEND = 'prepend';
- protected StreamFactoryInterface $streamFactory;
+ private StreamFactoryInterface $streamFactory;
- protected string $style;
+ private string $style;
/**
* @param string $style Either "append" or "prepend"
@@ -42,7 +42,7 @@ public function __construct(StreamFactoryInterface $streamFactory, string $style
$this->style = $style;
if (!in_array($style, [static::APPEND, static::PREPEND], true)) {
- throw new InvalidArgumentException("Invalid style `{$style}`. Must be `append` or `prepend`");
+ throw new InvalidArgumentException(sprintf('Invalid style `%s`. Must be `append` or `prepend`', $style));
}
}
diff --git a/Slim/Middleware/ResponseFactoryMiddleware.php b/Slim/Middleware/ResponseFactoryMiddleware.php
new file mode 100644
index 000000000..1e4c5b17a
--- /dev/null
+++ b/Slim/Middleware/ResponseFactoryMiddleware.php
@@ -0,0 +1,24 @@
+responseFactory = $responseFactory;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ return $this->responseFactory->createResponse();
+ }
+}
diff --git a/Slim/Middleware/RoutingArgumentsMiddleware.php b/Slim/Middleware/RoutingArgumentsMiddleware.php
new file mode 100644
index 000000000..e515a2bdd
--- /dev/null
+++ b/Slim/Middleware/RoutingArgumentsMiddleware.php
@@ -0,0 +1,38 @@
+getAttribute(RouteContext::ROUTING_RESULTS);
+
+ if ($routingResults) {
+ foreach ($routingResults->getRouteArguments() as $key => $value) {
+ $request = $request->withAttribute($key, $value);
+ }
+ }
+
+ return $handler->handle($request);
+ }
+}
diff --git a/Slim/Middleware/RoutingMiddleware.php b/Slim/Middleware/RoutingMiddleware.php
index a3d3085bd..2ad1b11b2 100644
--- a/Slim/Middleware/RoutingMiddleware.php
+++ b/Slim/Middleware/RoutingMiddleware.php
@@ -3,96 +3,111 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Middleware;
+use FastRoute\Dispatcher\GroupCountBased;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
-use RuntimeException;
-use Slim\Exception\HttpMethodNotAllowedException;
-use Slim\Exception\HttpNotFoundException;
-use Slim\Interfaces\RouteParserInterface;
-use Slim\Interfaces\RouteResolverInterface;
use Slim\Routing\RouteContext;
+use Slim\Routing\Router;
use Slim\Routing\RoutingResults;
+use Slim\Routing\UrlGenerator;
-class RoutingMiddleware implements MiddlewareInterface
+/**
+ * Middleware for resolving routes.
+ *
+ * This middleware handles the routing process by dispatching the request to the appropriate route
+ * based on the HTTP method and URI. It then stores the routing results in the request attributes.
+ */
+final class RoutingMiddleware implements MiddlewareInterface
{
- protected RouteResolverInterface $routeResolver;
+ private Router $router;
- protected RouteParserInterface $routeParser;
+ private UrlGenerator $urlGenerator;
- public function __construct(RouteResolverInterface $routeResolver, RouteParserInterface $routeParser)
+ public function __construct(Router $router, UrlGenerator $urlGenerator)
{
- $this->routeResolver = $routeResolver;
- $this->routeParser = $routeParser;
+ $this->router = $router;
+ $this->urlGenerator = $urlGenerator;
}
- /**
- * @throws HttpNotFoundException
- * @throws HttpMethodNotAllowedException
- * @throws RuntimeException
- */
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
- $request = $this->performRouting($request);
- return $handler->handle($request);
- }
+ // Dispatch
+ $dispatcher = new GroupCountBased($this->router->getRouteCollector()->getData());
- /**
- * Perform routing
- *
- * @param ServerRequestInterface $request PSR7 Server Request
- *
- * @throws HttpNotFoundException
- * @throws HttpMethodNotAllowedException
- * @throws RuntimeException
- */
- public function performRouting(ServerRequestInterface $request): ServerRequestInterface
- {
- $request = $request->withAttribute(RouteContext::ROUTE_PARSER, $this->routeParser);
+ $httpMethod = $request->getMethod();
+ $uri = $request->getUri()->getPath();
- $routingResults = $this->resolveRoutingResultsFromRequest($request);
- $routeStatus = $routingResults->getRouteStatus();
+ // Determine base path
+ $basePath = $request->getAttribute(RouteContext::BASE_PATH) ?? $this->router->getBasePath();
- $request = $request->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults);
+ $dispatcherUri = $uri;
+ if ($basePath) {
+ // Remove base path for the dispatcher
+ $dispatcherUri = substr($dispatcherUri, strlen($basePath));
+ $dispatcherUri = $this->normalizePath($dispatcherUri);
+ }
- switch ($routeStatus) {
- case RoutingResults::FOUND:
- $routeArguments = $routingResults->getRouteArguments();
- $routeIdentifier = $routingResults->getRouteIdentifier() ?? '';
- $route = $this->routeResolver
- ->resolveRoute($routeIdentifier)
- ->prepare($routeArguments);
- return $request->withAttribute(RouteContext::ROUTE, $route);
+ $dispatcherUri = rawurldecode($dispatcherUri);
+ $routeInfo = $dispatcher->dispatch($httpMethod, $dispatcherUri);
+ $routeStatus = (int)$routeInfo[0];
+ $routingResults = null;
+
+ if ($routeStatus === RoutingResults::FOUND) {
+ $routingResults = new RoutingResults(
+ $routeStatus,
+ $routeInfo[1],
+ $request->getMethod(),
+ $uri,
+ $routeInfo[2]
+ );
+ }
- case RoutingResults::NOT_FOUND:
- throw new HttpNotFoundException($request);
+ if ($routeStatus === RoutingResults::METHOD_NOT_ALLOWED) {
+ $routingResults = new RoutingResults(
+ $routeStatus,
+ null,
+ $request->getMethod(),
+ $uri,
+ $routeInfo[1],
+ );
+ }
- case RoutingResults::METHOD_NOT_ALLOWED:
- $exception = new HttpMethodNotAllowedException($request);
- $exception->setAllowedMethods($routingResults->getAllowedMethods());
- throw $exception;
+ if ($routeStatus === RoutingResults::NOT_FOUND) {
+ $routingResults = new RoutingResults($routeStatus, null, $request->getMethod(), $uri);
+ }
- default:
- throw new RuntimeException('An unexpected error occurred while performing routing.');
+ if ($routingResults) {
+ $request = $request
+ ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults)
+ ->withAttribute(RouteContext::URL_GENERATOR, $this->urlGenerator);
}
+
+ return $handler->handle($request);
}
- /**
- * Resolves the route from the given request
- */
- protected function resolveRoutingResultsFromRequest(ServerRequestInterface $request): RoutingResults
+ private function normalizePath(string $path): string
{
- return $this->routeResolver->computeRoutingResults(
- $request->getUri()->getPath(),
- $request->getMethod()
- );
+ // If path is empty or just a slash, return single slash
+ if ($path === '' || $path === '/') {
+ return '/';
+ }
+
+ // Ensure path starts with a slash
+ $path = '/' . ltrim($path, '/');
+
+ // Remove trailing slash unless it's the root path
+ $path = rtrim($path, '/');
+
+ // Replace multiple consecutive slashes with a single slash
+ return preg_replace('#/+#', '/', $path);
}
}
diff --git a/Slim/MiddlewareDispatcher.php b/Slim/MiddlewareDispatcher.php
deleted file mode 100644
index 1d4c668ff..000000000
--- a/Slim/MiddlewareDispatcher.php
+++ /dev/null
@@ -1,286 +0,0 @@
-seedMiddlewareStack($kernel);
- $this->callableResolver = $callableResolver;
- $this->container = $container;
- }
-
- /**
- * {@inheritdoc}
- */
- public function seedMiddlewareStack(RequestHandlerInterface $kernel): void
- {
- $this->tip = $kernel;
- }
-
- /**
- * Invoke the middleware stack
- */
- public function handle(ServerRequestInterface $request): ResponseInterface
- {
- return $this->tip->handle($request);
- }
-
- /**
- * Add a new middleware to the stack
- *
- * Middleware are organized as a stack. That means middleware
- * that have been added before will be executed after the newly
- * added one (last in, first out).
- *
- * @param MiddlewareInterface|string|callable $middleware
- */
- public function add($middleware): MiddlewareDispatcherInterface
- {
- if ($middleware instanceof MiddlewareInterface) {
- return $this->addMiddleware($middleware);
- }
-
- if (is_string($middleware)) {
- return $this->addDeferred($middleware);
- }
-
- if (is_callable($middleware)) {
- return $this->addCallable($middleware);
- }
-
- /** @phpstan-ignore-next-line */
- throw new RuntimeException(
- 'A middleware must be an object/class name referencing an implementation of ' .
- 'MiddlewareInterface or a callable with a matching signature.'
- );
- }
-
- /**
- * Add a new middleware to the stack
- *
- * Middleware are organized as a stack. That means middleware
- * that have been added before will be executed after the newly
- * added one (last in, first out).
- */
- public function addMiddleware(MiddlewareInterface $middleware): MiddlewareDispatcherInterface
- {
- $next = $this->tip;
- $this->tip = new class ($middleware, $next) implements RequestHandlerInterface {
- private MiddlewareInterface $middleware;
-
- private RequestHandlerInterface $next;
-
- public function __construct(MiddlewareInterface $middleware, RequestHandlerInterface $next)
- {
- $this->middleware = $middleware;
- $this->next = $next;
- }
-
- public function handle(ServerRequestInterface $request): ResponseInterface
- {
- return $this->middleware->process($request, $this->next);
- }
- };
-
- return $this;
- }
-
- /**
- * Add a new middleware by class name
- *
- * Middleware are organized as a stack. That means middleware
- * that have been added before will be executed after the newly
- * added one (last in, first out).
- * @return MiddlewareDispatcher
- */
- public function addDeferred(string $middleware): self
- {
- $next = $this->tip;
- $this->tip = new class (
- $middleware,
- $next,
- $this->container,
- $this->callableResolver
- ) implements RequestHandlerInterface {
- private string $middleware;
-
- private RequestHandlerInterface $next;
-
- private ?ContainerInterface $container;
-
- private ?CallableResolverInterface $callableResolver;
-
- public function __construct(
- string $middleware,
- RequestHandlerInterface $next,
- ?ContainerInterface $container = null,
- ?CallableResolverInterface $callableResolver = null
- ) {
- $this->middleware = $middleware;
- $this->next = $next;
- $this->container = $container;
- $this->callableResolver = $callableResolver;
- }
-
- public function handle(ServerRequestInterface $request): ResponseInterface
- {
- if ($this->callableResolver instanceof AdvancedCallableResolverInterface) {
- $callable = $this->callableResolver->resolveMiddleware($this->middleware);
- return $callable($request, $this->next);
- }
-
- $callable = null;
-
- if ($this->callableResolver instanceof CallableResolverInterface) {
- try {
- $callable = $this->callableResolver->resolve($this->middleware);
- } catch (RuntimeException $e) {
- // Do Nothing
- }
- }
-
- if (!$callable) {
- $resolved = $this->middleware;
- $instance = null;
- $method = null;
-
- /** @psalm-suppress ArgumentTypeCoercion */
- // Check for Slim callable as `class:method`
- if (preg_match(CallableResolver::$callablePattern, $resolved, $matches)) {
- $resolved = $matches[1];
- $method = $matches[2];
- }
-
- if ($this->container && $this->container->has($resolved)) {
- $instance = $this->container->get($resolved);
- if ($instance instanceof MiddlewareInterface) {
- return $instance->process($request, $this->next);
- }
- } elseif (!function_exists($resolved)) {
- if (!class_exists($resolved)) {
- throw new RuntimeException(sprintf('Middleware %s does not exist', $resolved));
- }
- $instance = new $resolved($this->container);
- }
-
- if ($instance && $instance instanceof MiddlewareInterface) {
- return $instance->process($request, $this->next);
- }
-
- $callable = $instance ?? $resolved;
- if ($instance && $method) {
- $callable = [$instance, $method];
- }
-
- if ($this->container && $callable instanceof Closure) {
- $callable = $callable->bindTo($this->container);
- }
- }
-
- if (!is_callable($callable)) {
- throw new RuntimeException(
- sprintf(
- 'Middleware %s is not resolvable',
- $this->middleware
- )
- );
- }
-
- return $callable($request, $this->next);
- }
- };
-
- return $this;
- }
-
- /**
- * Add a (non-standard) callable middleware to the stack
- *
- * Middleware are organized as a stack. That means middleware
- * that have been added before will be executed after the newly
- * added one (last in, first out).
- * @return MiddlewareDispatcher
- */
- public function addCallable(callable $middleware): self
- {
- $next = $this->tip;
-
- if ($this->container && $middleware instanceof Closure) {
- /** @var Closure $middleware */
- $middleware = $middleware->bindTo($this->container);
- }
-
- $this->tip = new class ($middleware, $next) implements RequestHandlerInterface {
- /**
- * @var callable
- */
- private $middleware;
-
- /**
- * @var RequestHandlerInterface
- */
- private $next;
-
- public function __construct(callable $middleware, RequestHandlerInterface $next)
- {
- $this->middleware = $middleware;
- $this->next = $next;
- }
-
- public function handle(ServerRequestInterface $request): ResponseInterface
- {
- return ($this->middleware)($request, $this->next);
- }
- };
-
- return $this;
- }
-}
diff --git a/Slim/Renderers/JsonRenderer.php b/Slim/Renderers/JsonRenderer.php
new file mode 100644
index 000000000..4ee14a2a2
--- /dev/null
+++ b/Slim/Renderers/JsonRenderer.php
@@ -0,0 +1,65 @@
+json($response, ['key' => 'value']);
+ * ```
+ */
+final class JsonRenderer
+{
+ private StreamFactoryInterface $streamFactory;
+
+ private int $jsonOptions = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR;
+
+ private string $contentType = MediaType::APPLICATION_JSON;
+
+ public function __construct(StreamFactoryInterface $streamFactory)
+ {
+ $this->streamFactory = $streamFactory;
+ }
+
+ public function json(ResponseInterface $response, mixed $data = null): ResponseInterface
+ {
+ $response = $response->withHeader('Content-Type', $this->contentType);
+ $json = (string)json_encode($data, $this->jsonOptions);
+
+ return $response->withBody($this->streamFactory->createStream($json));
+ }
+
+ /**
+ * Change the content type of the response.
+ */
+ public function withContentType(string $type): self
+ {
+ $clone = clone $this;
+ $clone->contentType = $type;
+
+ return $clone;
+ }
+
+ /**
+ * Set options for JSON encoding.
+ *
+ * @see https://php.net/manual/function.json-encode.php
+ * @see https://php.net/manual/json.constants.php
+ */
+ public function withJsonOptions(int $options): self
+ {
+ $clone = clone $this;
+ $clone->jsonOptions = $options;
+
+ return $clone;
+ }
+}
diff --git a/Slim/RequestHandler/MiddlewareRequestHandler.php b/Slim/RequestHandler/MiddlewareRequestHandler.php
new file mode 100644
index 000000000..c8efb0c70
--- /dev/null
+++ b/Slim/RequestHandler/MiddlewareRequestHandler.php
@@ -0,0 +1,48 @@
+resolver = $resolver;
+ }
+
+ /**
+ * Handles the current entry in the middleware queue and advances.
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ /** @var array $middlewares */
+ $middlewares = $request->getAttribute(self::MIDDLEWARE) ?: [];
+ $queue = $this->resolver->resolveStack($middlewares);
+
+ reset($queue);
+ $runner = new Runner($queue);
+
+ $request = $request->withoutAttribute(self::MIDDLEWARE);
+
+ return $runner->handle($request);
+ }
+}
diff --git a/Slim/RequestHandler/Runner.php b/Slim/RequestHandler/Runner.php
new file mode 100644
index 000000000..ba2408c7a
--- /dev/null
+++ b/Slim/RequestHandler/Runner.php
@@ -0,0 +1,66 @@
+queue = $queue;
+ }
+
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $middleware = current($this->queue);
+
+ if (!$middleware) {
+ throw new RuntimeException('No middleware found. Add a response factory middleware.');
+ }
+
+ next($this->queue);
+
+ if ($middleware instanceof MiddlewareInterface) {
+ return $middleware->process($request, $this);
+ }
+
+ if ($middleware instanceof RequestHandlerInterface) {
+ return $middleware->handle($request);
+ }
+
+ if (is_callable($middleware)) {
+ return $middleware($request, $this);
+ }
+
+ throw new RuntimeException(
+ sprintf(
+ 'Invalid middleware queue entry "%s". Middleware must either be callable or implement %s.',
+ is_scalar($middleware) ? (string)$middleware : gettype($middleware),
+ MiddlewareInterface::class
+ )
+ );
+ }
+}
diff --git a/Slim/Routing/Dispatcher.php b/Slim/Routing/Dispatcher.php
deleted file mode 100644
index e33eac396..000000000
--- a/Slim/Routing/Dispatcher.php
+++ /dev/null
@@ -1,78 +0,0 @@
-routeCollector = $routeCollector;
- }
-
- protected function createDispatcher(): FastRouteDispatcher
- {
- if ($this->dispatcher) {
- return $this->dispatcher;
- }
-
- $routeDefinitionCallback = function (FastRouteCollector $r): void {
- $basePath = $this->routeCollector->getBasePath();
-
- foreach ($this->routeCollector->getRoutes() as $route) {
- $r->addRoute($route->getMethods(), $basePath . $route->getPattern(), $route->getIdentifier());
- }
- };
-
- $cacheFile = $this->routeCollector->getCacheFile();
- if ($cacheFile) {
- /** @var FastRouteDispatcher $dispatcher */
- $dispatcher = \FastRoute\cachedDispatcher($routeDefinitionCallback, [
- 'dataGenerator' => GroupCountBased::class,
- 'dispatcher' => FastRouteDispatcher::class,
- 'routeParser' => new Std(),
- 'cacheFile' => $cacheFile,
- ]);
- } else {
- /** @var FastRouteDispatcher $dispatcher */
- $dispatcher = \FastRoute\simpleDispatcher($routeDefinitionCallback, [
- 'dataGenerator' => GroupCountBased::class,
- 'dispatcher' => FastRouteDispatcher::class,
- 'routeParser' => new Std(),
- ]);
- }
-
- $this->dispatcher = $dispatcher;
- return $this->dispatcher;
- }
-
- /**
- * {@inheritdoc}
- */
- public function dispatch(string $method, string $uri): RoutingResults
- {
- $dispatcher = $this->createDispatcher();
- $results = $dispatcher->dispatch($method, $uri);
- return new RoutingResults($this, $method, $uri, $results[0], $results[1], $results[2]);
- }
-
- /**
- * {@inheritdoc}
- */
- public function getAllowedMethods(string $uri): array
- {
- $dispatcher = $this->createDispatcher();
- return $dispatcher->getAllowedMethods($uri);
- }
-}
diff --git a/Slim/Routing/FastRouteDispatcher.php b/Slim/Routing/FastRouteDispatcher.php
deleted file mode 100644
index 797746bbb..000000000
--- a/Slim/Routing/FastRouteDispatcher.php
+++ /dev/null
@@ -1,109 +0,0 @@
-}
- */
- public function dispatch($httpMethod, $uri): array
- {
- $routingResults = $this->routingResults($httpMethod, $uri);
- if ($routingResults[0] === self::FOUND) {
- return $routingResults;
- }
-
- // For HEAD requests, attempt fallback to GET
- if ($httpMethod === 'HEAD') {
- $routingResults = $this->routingResults('GET', $uri);
- if ($routingResults[0] === self::FOUND) {
- return $routingResults;
- }
- }
-
- // If nothing else matches, try fallback routes
- $routingResults = $this->routingResults('*', $uri);
- if ($routingResults[0] === self::FOUND) {
- return $routingResults;
- }
-
- if (!empty($this->getAllowedMethods($uri))) {
- return [self::METHOD_NOT_ALLOWED, null, []];
- }
-
- return [self::NOT_FOUND, null, []];
- }
-
- /**
- * @param string $httpMethod
- * @param string $uri
- *
- * @return array{int, string|null, array}
- */
- private function routingResults(string $httpMethod, string $uri): array
- {
- if (isset($this->staticRouteMap[$httpMethod][$uri])) {
- /** @var string $routeIdentifier */
- $routeIdentifier = $this->staticRouteMap[$httpMethod][$uri];
- return [self::FOUND, $routeIdentifier, []];
- }
-
- if (isset($this->variableRouteData[$httpMethod])) {
- /** @var array{0: int, 1?: string, 2?: array} $result */
- $result = $this->dispatchVariableRoute($this->variableRouteData[$httpMethod], $uri);
- if ($result[0] === self::FOUND) {
- /** @var array{int, string, array} $result */
- return [self::FOUND, $result[1], $result[2]];
- }
- }
-
- return [self::NOT_FOUND, null, []];
- }
-
- /**
- * @param string $uri
- *
- * @return string[]
- */
- public function getAllowedMethods(string $uri): array
- {
- if (isset($this->allowedMethods[$uri])) {
- return $this->allowedMethods[$uri];
- }
-
- $allowedMethods = [];
- foreach ($this->staticRouteMap as $method => $uriMap) {
- if (isset($uriMap[$uri])) {
- $allowedMethods[$method] = true;
- }
- }
-
- foreach ($this->variableRouteData as $method => $routeData) {
- $result = $this->dispatchVariableRoute($routeData, $uri);
- if ($result[0] === self::FOUND) {
- $allowedMethods[$method] = true;
- }
- }
-
- return $this->allowedMethods[$uri] = array_keys($allowedMethods);
- }
-}
diff --git a/Slim/Routing/MiddlewareAwareTrait.php b/Slim/Routing/MiddlewareAwareTrait.php
new file mode 100644
index 000000000..b07f5f8d3
--- /dev/null
+++ b/Slim/Routing/MiddlewareAwareTrait.php
@@ -0,0 +1,37 @@
+
+ */
+ private array $middleware = [];
+
+ /**
+ * @return array
+ */
+ public function getMiddlewareStack(): array
+ {
+ return $this->middleware;
+ }
+
+ public function add(MiddlewareInterface|callable|string $middleware): self
+ {
+ $this->middleware[] = $middleware;
+
+ return $this;
+ }
+
+ public function addMiddleware(MiddlewareInterface $middleware): self
+ {
+ $this->middleware[] = $middleware;
+
+ return $this;
+ }
+}
diff --git a/Slim/Routing/Route.php b/Slim/Routing/Route.php
index a2d9d04a2..865c7edf2 100644
--- a/Slim/Routing/Route.php
+++ b/Slim/Routing/Route.php
@@ -1,365 +1,69 @@
- */
- protected array $arguments = [];
+ use MiddlewareAwareTrait;
/**
- * Route arguments parameters
- *
- * @var string[]
+ * @var array
*/
- protected array $savedArguments = [];
+ private array $methods;
- /**
- * Container
- * @var TContainerInterface $container
- */
- protected ?ContainerInterface $container = null;
-
- /** @var MiddlewareDispatcher $middlewareDispatcher */
- protected MiddlewareDispatcher $middlewareDispatcher;
+ private string $pattern;
/**
- * Route callable
- *
* @var callable|string
*/
- protected $callable;
+ private $handler;
- protected CallableResolverInterface $callableResolver;
+ private ?string $name = null;
- protected ResponseFactoryInterface $responseFactory;
+ private ?RouteGroup $group;
/**
- * Route pattern
+ * @param array $methods
*/
- protected string $pattern;
-
- protected bool $groupMiddlewareAppended = false;
-
- /**
- * @param string[] $methods The route HTTP methods
- * @param string $pattern The route pattern
- * @param callable|string $callable The route callable
- * @param ResponseFactoryInterface $responseFactory
- * @param CallableResolverInterface $callableResolver
- * @param TContainerInterface $container
- * @param InvocationStrategyInterface|null $invocationStrategy
- * @param RouteGroupInterface[] $groups The parent route groups
- * @param int $identifier The route identifier
- */
- public function __construct(
- array $methods,
- string $pattern,
- $callable,
- ResponseFactoryInterface $responseFactory,
- CallableResolverInterface $callableResolver,
- ?ContainerInterface $container = null,
- ?InvocationStrategyInterface $invocationStrategy = null,
- array $groups = [],
- int $identifier = 0
- ) {
+ public function __construct(array $methods, string $pattern, callable|string $handler, ?RouteGroup $group = null)
+ {
$this->methods = $methods;
$this->pattern = $pattern;
- $this->callable = $callable;
- $this->responseFactory = $responseFactory;
- $this->callableResolver = $callableResolver;
- $this->container = $container;
- $this->invocationStrategy = $invocationStrategy ?? new RequestResponse();
- $this->groups = $groups;
- $this->identifier = 'route' . $identifier;
- $this->middlewareDispatcher = new MiddlewareDispatcher($this, $callableResolver, $container);
- }
-
- public function getCallableResolver(): CallableResolverInterface
- {
- return $this->callableResolver;
+ $this->handler = $handler;
+ $this->group = $group;
}
- /**
- * {@inheritdoc}
- */
- public function getInvocationStrategy(): InvocationStrategyInterface
+ public function getHandler(): callable|string
{
- return $this->invocationStrategy;
+ return $this->handler;
}
- /**
- * {@inheritdoc}
- */
- public function setInvocationStrategy(InvocationStrategyInterface $invocationStrategy): RouteInterface
+ public function setName(string $name): self
{
- $this->invocationStrategy = $invocationStrategy;
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getMethods(): array
- {
- return $this->methods;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getPattern(): string
- {
- return $this->pattern;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setPattern(string $pattern): RouteInterface
- {
- $this->pattern = $pattern;
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getCallable()
- {
- return $this->callable;
- }
+ $this->name = $name;
- /**
- * {@inheritdoc}
- */
- public function setCallable($callable): RouteInterface
- {
- $this->callable = $callable;
return $this;
}
- /**
- * {@inheritdoc}
- */
public function getName(): ?string
{
return $this->name;
}
- /**
- * {@inheritdoc}
- */
- public function setName(string $name): RouteInterface
- {
- $this->name = $name;
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getIdentifier(): string
- {
- return $this->identifier;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getArgument(string $name, ?string $default = null): ?string
- {
- if (array_key_exists($name, $this->arguments)) {
- return $this->arguments[$name];
- }
- return $default;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getArguments(): array
- {
- return $this->arguments;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setArguments(array $arguments, bool $includeInSavedArguments = true): RouteInterface
- {
- if ($includeInSavedArguments) {
- $this->savedArguments = $arguments;
- }
-
- $this->arguments = $arguments;
- return $this;
- }
-
- /**
- * @return RouteGroupInterface[]
- */
- public function getGroups(): array
- {
- return $this->groups;
- }
-
- /**
- * {@inheritdoc}
- */
- public function add($middleware): RouteInterface
- {
- $this->middlewareDispatcher->add($middleware);
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function addMiddleware(MiddlewareInterface $middleware): RouteInterface
- {
- $this->middlewareDispatcher->addMiddleware($middleware);
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function prepare(array $arguments): RouteInterface
- {
- $this->arguments = array_replace($this->savedArguments, $arguments);
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setArgument(string $name, string $value, bool $includeInSavedArguments = true): RouteInterface
- {
- if ($includeInSavedArguments) {
- $this->savedArguments[$name] = $value;
- }
-
- $this->arguments[$name] = $value;
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function run(ServerRequestInterface $request): ResponseInterface
+ public function getPattern(): string
{
- if (!$this->groupMiddlewareAppended) {
- $this->appendGroupMiddlewareToRoute();
- }
-
- return $this->middlewareDispatcher->handle($request);
+ return $this->pattern;
}
- /**
- * @return void
- */
- protected function appendGroupMiddlewareToRoute(): void
+ public function getMethods(): array
{
- $inner = $this->middlewareDispatcher;
- $this->middlewareDispatcher = new MiddlewareDispatcher($inner, $this->callableResolver, $this->container);
-
- foreach (array_reverse($this->groups) as $group) {
- $group->appendMiddlewareToDispatcher($this->middlewareDispatcher);
- }
-
- $this->groupMiddlewareAppended = true;
+ return $this->methods;
}
- /**
- * {@inheritdoc}
- */
- public function handle(ServerRequestInterface $request): ResponseInterface
+ public function getRouteGroup(): ?RouteGroup
{
- if ($this->callableResolver instanceof AdvancedCallableResolverInterface) {
- $callable = $this->callableResolver->resolveRoute($this->callable);
- } else {
- $callable = $this->callableResolver->resolve($this->callable);
- }
- $strategy = $this->invocationStrategy;
-
- /** @var string[] $strategyImplements */
- $strategyImplements = class_implements($strategy);
-
- if (
- is_array($callable)
- && $callable[0] instanceof RequestHandlerInterface
- && !in_array(RequestHandlerInvocationStrategyInterface::class, $strategyImplements)
- ) {
- $strategy = new RequestHandler();
- }
-
- $response = $this->responseFactory->createResponse();
- return $strategy($callable, $request, $response, $this->arguments);
+ return $this->group;
}
}
diff --git a/Slim/Routing/RouteCollectionTrait.php b/Slim/Routing/RouteCollectionTrait.php
new file mode 100644
index 000000000..ff170c2c2
--- /dev/null
+++ b/Slim/Routing/RouteCollectionTrait.php
@@ -0,0 +1,49 @@
+map(['GET'], $path, $handler);
+ }
+
+ public function post(string $path, callable|string $handler): Route
+ {
+ return $this->map(['POST'], $path, $handler);
+ }
+
+ public function put(string $path, callable|string $handler): Route
+ {
+ return $this->map(['PUT'], $path, $handler);
+ }
+
+ public function patch(string $path, callable|string $handler): Route
+ {
+ return $this->map(['PATCH'], $path, $handler);
+ }
+
+ public function delete(string $path, callable|string $handler): Route
+ {
+ return $this->map(['DELETE'], $path, $handler);
+ }
+
+ public function options(string $path, callable|string $handler): Route
+ {
+ return $this->map(['OPTIONS'], $path, $handler);
+ }
+
+ public function any(string $pattern, callable|string $handler): Route
+ {
+ return $this->map(['*'], $pattern, $handler);
+ }
+}
diff --git a/Slim/Routing/RouteCollector.php b/Slim/Routing/RouteCollector.php
deleted file mode 100644
index 6f9f0f66b..000000000
--- a/Slim/Routing/RouteCollector.php
+++ /dev/null
@@ -1,302 +0,0 @@
-responseFactory = $responseFactory;
- $this->callableResolver = $callableResolver;
- $this->container = $container;
- $this->defaultInvocationStrategy = $defaultInvocationStrategy ?? new RequestResponse();
- $this->routeParser = $routeParser ?? new RouteParser($this);
-
- if ($cacheFile) {
- $this->setCacheFile($cacheFile);
- }
- }
-
- public function getRouteParser(): RouteParserInterface
- {
- return $this->routeParser;
- }
-
- /**
- * Get default route invocation strategy
- */
- public function getDefaultInvocationStrategy(): InvocationStrategyInterface
- {
- return $this->defaultInvocationStrategy;
- }
-
- public function setDefaultInvocationStrategy(InvocationStrategyInterface $strategy): RouteCollectorInterface
- {
- $this->defaultInvocationStrategy = $strategy;
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getCacheFile(): ?string
- {
- return $this->cacheFile;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setCacheFile(string $cacheFile): RouteCollectorInterface
- {
- if (file_exists($cacheFile) && !is_readable($cacheFile)) {
- throw new RuntimeException(
- sprintf('Route collector cache file `%s` is not readable', $cacheFile)
- );
- }
-
- if (!file_exists($cacheFile) && !is_writable(dirname($cacheFile))) {
- throw new RuntimeException(
- sprintf('Route collector cache file directory `%s` is not writable', dirname($cacheFile))
- );
- }
-
- $this->cacheFile = $cacheFile;
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getBasePath(): string
- {
- return $this->basePath;
- }
-
- /**
- * Set the base path used in urlFor()
- */
- public function setBasePath(string $basePath): RouteCollectorInterface
- {
- $this->basePath = $basePath;
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getRoutes(): array
- {
- return $this->routes;
- }
-
- /**
- * {@inheritdoc}
- */
- public function removeNamedRoute(string $name): RouteCollectorInterface
- {
- $route = $this->getNamedRoute($name);
-
- /** @psalm-suppress PossiblyNullArrayOffset */
- unset($this->routesByName[$route->getName()], $this->routes[$route->getIdentifier()]);
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getNamedRoute(string $name): RouteInterface
- {
- if (isset($this->routesByName[$name])) {
- $route = $this->routesByName[$name];
- if ($route->getName() === $name) {
- return $route;
- }
-
- unset($this->routesByName[$name]);
- }
-
- foreach ($this->routes as $route) {
- if ($name === $route->getName()) {
- $this->routesByName[$name] = $route;
- return $route;
- }
- }
-
- throw new RuntimeException('Named route does not exist for name: ' . $name);
- }
-
- /**
- * {@inheritdoc}
- */
- public function lookupRoute(string $identifier): RouteInterface
- {
- if (!isset($this->routes[$identifier])) {
- throw new RuntimeException('Route not found, looks like your route cache is stale.');
- }
- return $this->routes[$identifier];
- }
-
- /**
- * {@inheritdoc}
- */
- public function group(string $pattern, $callable): RouteGroupInterface
- {
- $routeGroup = $this->createGroup($pattern, $callable);
- $this->routeGroups[] = $routeGroup;
-
- $routeGroup->collectRoutes();
- array_pop($this->routeGroups);
-
- return $routeGroup;
- }
-
- /**
- * @param string|callable $callable
- */
- protected function createGroup(string $pattern, $callable): RouteGroupInterface
- {
- $routeCollectorProxy = $this->createProxy($pattern);
- return new RouteGroup($pattern, $callable, $this->callableResolver, $routeCollectorProxy);
- }
-
- /**
- * @return RouteCollectorProxyInterface
- */
- protected function createProxy(string $pattern): RouteCollectorProxyInterface
- {
- /** @var RouteCollectorProxyInterface */
- return new RouteCollectorProxy(
- $this->responseFactory,
- $this->callableResolver,
- $this->container,
- $this,
- $pattern
- );
- }
-
- /**
- * {@inheritdoc}
- */
- public function map(array $methods, string $pattern, $handler): RouteInterface
- {
- $route = $this->createRoute($methods, $pattern, $handler);
- $this->routes[$route->getIdentifier()] = $route;
-
- $routeName = $route->getName();
- if ($routeName !== null && !isset($this->routesByName[$routeName])) {
- $this->routesByName[$routeName] = $route;
- }
-
- $this->routeCounter++;
-
- return $route;
- }
-
- /**
- * @param string[] $methods
- * @param callable|string $callable
- */
- protected function createRoute(array $methods, string $pattern, $callable): RouteInterface
- {
- return new Route(
- $methods,
- $pattern,
- $callable,
- $this->responseFactory,
- $this->callableResolver,
- $this->container,
- $this->defaultInvocationStrategy,
- $this->routeGroups,
- $this->routeCounter
- );
- }
-}
diff --git a/Slim/Routing/RouteCollectorProxy.php b/Slim/Routing/RouteCollectorProxy.php
deleted file mode 100644
index a946d148f..000000000
--- a/Slim/Routing/RouteCollectorProxy.php
+++ /dev/null
@@ -1,196 +0,0 @@
-
- */
-class RouteCollectorProxy implements RouteCollectorProxyInterface
-{
- protected ResponseFactoryInterface $responseFactory;
-
- protected CallableResolverInterface $callableResolver;
-
- /** @var TContainerInterface */
- protected ?ContainerInterface $container = null;
-
- protected RouteCollectorInterface $routeCollector;
-
- protected string $groupPattern;
-
- /**
- * @param TContainerInterface $container
- */
- public function __construct(
- ResponseFactoryInterface $responseFactory,
- CallableResolverInterface $callableResolver,
- ?ContainerInterface $container = null,
- ?RouteCollectorInterface $routeCollector = null,
- string $groupPattern = ''
- ) {
- $this->responseFactory = $responseFactory;
- $this->callableResolver = $callableResolver;
- $this->container = $container;
- $this->routeCollector = $routeCollector ?? new RouteCollector($responseFactory, $callableResolver, $container);
- $this->groupPattern = $groupPattern;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getResponseFactory(): ResponseFactoryInterface
- {
- return $this->responseFactory;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getCallableResolver(): CallableResolverInterface
- {
- return $this->callableResolver;
- }
-
- /**
- * {@inheritdoc}
- * @return TContainerInterface
- */
- public function getContainer(): ?ContainerInterface
- {
- return $this->container;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getRouteCollector(): RouteCollectorInterface
- {
- return $this->routeCollector;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getBasePath(): string
- {
- return $this->routeCollector->getBasePath();
- }
-
- /**
- * {@inheritdoc}
- */
- public function setBasePath(string $basePath): RouteCollectorProxyInterface
- {
- $this->routeCollector->setBasePath($basePath);
-
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function get(string $pattern, $callable): RouteInterface
- {
- return $this->map(['GET'], $pattern, $callable);
- }
-
- /**
- * {@inheritdoc}
- */
- public function post(string $pattern, $callable): RouteInterface
- {
- return $this->map(['POST'], $pattern, $callable);
- }
-
- /**
- * {@inheritdoc}
- */
- public function put(string $pattern, $callable): RouteInterface
- {
- return $this->map(['PUT'], $pattern, $callable);
- }
-
- /**
- * {@inheritdoc}
- */
- public function patch(string $pattern, $callable): RouteInterface
- {
- return $this->map(['PATCH'], $pattern, $callable);
- }
-
- /**
- * {@inheritdoc}
- */
- public function delete(string $pattern, $callable): RouteInterface
- {
- return $this->map(['DELETE'], $pattern, $callable);
- }
-
- /**
- * {@inheritdoc}
- */
- public function options(string $pattern, $callable): RouteInterface
- {
- return $this->map(['OPTIONS'], $pattern, $callable);
- }
-
- /**
- * {@inheritdoc}
- */
- public function any(string $pattern, $callable): RouteInterface
- {
- return $this->map(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], $pattern, $callable);
- }
-
- /**
- * {@inheritdoc}
- */
- public function map(array $methods, string $pattern, $callable): RouteInterface
- {
- $pattern = $this->groupPattern . $pattern;
-
- return $this->routeCollector->map($methods, $pattern, $callable);
- }
-
- /**
- * {@inheritdoc}
- */
- public function group(string $pattern, $callable): RouteGroupInterface
- {
- $pattern = $this->groupPattern . $pattern;
-
- return $this->routeCollector->group($pattern, $callable);
- }
-
- /**
- * {@inheritdoc}
- */
- public function redirect(string $from, $to, int $status = 302): RouteInterface
- {
- $responseFactory = $this->responseFactory;
-
- $handler = function () use ($to, $status, $responseFactory) {
- $response = $responseFactory->createResponse($status);
- return $response->withHeader('Location', (string) $to);
- };
-
- return $this->get($from, $handler);
- }
-}
diff --git a/Slim/Routing/RouteContext.php b/Slim/Routing/RouteContext.php
index 853b0df3e..73b180020 100644
--- a/Slim/Routing/RouteContext.php
+++ b/Slim/Routing/RouteContext.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
@@ -12,66 +12,60 @@
use Psr\Http\Message\ServerRequestInterface;
use RuntimeException;
-use Slim\Interfaces\RouteInterface;
-use Slim\Interfaces\RouteParserInterface;
-/** @api */
final class RouteContext
{
- public const ROUTE = '__route__';
-
- public const ROUTE_PARSER = '__routeParser__';
+ public const URL_GENERATOR = '__urlGenerator__';
public const ROUTING_RESULTS = '__routingResults__';
public const BASE_PATH = '__basePath__';
- public static function fromRequest(ServerRequestInterface $serverRequest): self
- {
- $route = $serverRequest->getAttribute(self::ROUTE);
- $routeParser = $serverRequest->getAttribute(self::ROUTE_PARSER);
- $routingResults = $serverRequest->getAttribute(self::ROUTING_RESULTS);
- $basePath = $serverRequest->getAttribute(self::BASE_PATH);
-
- if ($routeParser === null || $routingResults === null) {
- throw new RuntimeException('Cannot create RouteContext before routing has been completed');
- }
-
- /** @var RouteInterface|null $route */
- /** @var RouteParserInterface $routeParser */
- /** @var RoutingResults $routingResults */
- /** @var string|null $basePath */
- return new self($route, $routeParser, $routingResults, $basePath);
- }
-
- private ?RouteInterface $route;
-
- private RouteParserInterface $routeParser;
-
private RoutingResults $routingResults;
+ private UrlGenerator $urlGenerator;
+
private ?string $basePath;
private function __construct(
- ?RouteInterface $route,
- RouteParserInterface $routeParser,
RoutingResults $routingResults,
+ UrlGenerator $urlGenerator,
?string $basePath = null
) {
- $this->route = $route;
- $this->routeParser = $routeParser;
+ $this->urlGenerator = $urlGenerator;
$this->routingResults = $routingResults;
$this->basePath = $basePath;
}
- public function getRoute(): ?RouteInterface
+ public static function fromRequest(ServerRequestInterface $request): self
{
- return $this->route;
+ /* @var UrlGenerator|null $urlGenerator */
+ $urlGenerator = $request->getAttribute(self::URL_GENERATOR);
+
+ /* @var RoutingResults|null $routingResults */
+ $routingResults = $request->getAttribute(self::ROUTING_RESULTS);
+
+ /* @var string|null $basePath */
+ $basePath = $request->getAttribute(self::BASE_PATH);
+
+ if ($urlGenerator === null) {
+ throw new RuntimeException(
+ 'Cannot create RouteContext before routing has been completed. Add UrlGeneratorMiddleware to fix this.'
+ );
+ }
+
+ if ($routingResults === null) {
+ throw new RuntimeException(
+ 'Cannot create RouteContext before routing has been completed. Add RoutingMiddleware to fix this.'
+ );
+ }
+
+ return new self($routingResults, $urlGenerator, $basePath);
}
- public function getRouteParser(): RouteParserInterface
+ public function getUrlGenerator(): UrlGenerator
{
- return $this->routeParser;
+ return $this->urlGenerator;
}
public function getRoutingResults(): RoutingResults
@@ -79,11 +73,23 @@ public function getRoutingResults(): RoutingResults
return $this->routingResults;
}
- public function getBasePath(): string
+ public function getBasePath(): ?string
{
- if ($this->basePath === null) {
- throw new RuntimeException('No base path defined.');
- }
return $this->basePath;
}
+
+ public function getRoute(): ?Route
+ {
+ return $this->routingResults->getRoute();
+ }
+
+ public function getArguments(): array
+ {
+ return $this->routingResults->getRouteArguments();
+ }
+
+ public function getArgument(string $key): mixed
+ {
+ return $this->routingResults->getRouteArgument($key);
+ }
}
diff --git a/Slim/Routing/RouteGroup.php b/Slim/Routing/RouteGroup.php
index 60d3df3d8..ee18ab848 100644
--- a/Slim/Routing/RouteGroup.php
+++ b/Slim/Routing/RouteGroup.php
@@ -1,109 +1,79 @@
+ * @var callable
*/
- protected RouteCollectorProxyInterface $routeCollectorProxy;
+ private $callback;
- /**
- * @var MiddlewareInterface[]|string[]|callable[]
- */
- protected array $middleware = [];
+ private RouteCollector $routeCollector;
- protected string $pattern;
+ private string $prefix;
- /**
- * @param callable|string $callable
- * @param RouteCollectorProxyInterface<\Psr\Container\ContainerInterface|null> $routeCollectorProxy
- */
- public function __construct(
- string $pattern,
- $callable,
- CallableResolverInterface $callableResolver,
- RouteCollectorProxyInterface $routeCollectorProxy
- ) {
- $this->pattern = $pattern;
- $this->callable = $callable;
- $this->callableResolver = $callableResolver;
- $this->routeCollectorProxy = $routeCollectorProxy;
+ private Router $router;
+
+ private ?RouteGroup $group;
+
+ public function __construct(string $prefix, callable $callback, Router $router, ?RouteGroup $group = null)
+ {
+ $this->prefix = sprintf('/%s', ltrim($prefix, '/'));
+ $this->callback = $callback;
+ $this->router = $router;
+ $this->routeCollector = $router->getRouteCollector();
+ $this->group = $group;
}
- /**
- * {@inheritdoc}
- */
- public function collectRoutes(): RouteGroupInterface
+ public function __invoke(): void
{
- if ($this->callableResolver instanceof AdvancedCallableResolverInterface) {
- $callable = $this->callableResolver->resolveRoute($this->callable);
- } else {
- $callable = $this->callableResolver->resolve($this->callable);
- }
- $callable($this->routeCollectorProxy);
- return $this;
+ // This will be invoked by FastRoute to collect the route groups
+ ($this->callback)($this);
}
- /**
- * {@inheritdoc}
- */
- public function add($middleware): RouteGroupInterface
+ public function getPrefix(): string
{
- $this->middleware[] = $middleware;
- return $this;
+ return $this->prefix;
}
/**
- * {@inheritdoc}
+ * Get parent route group.
*/
- public function addMiddleware(MiddlewareInterface $middleware): RouteGroupInterface
+ public function getRouteGroup(): ?RouteGroup
{
- $this->middleware[] = $middleware;
- return $this;
+ return $this->group;
}
/**
- * {@inheritdoc}
- * @param MiddlewareDispatcher<\Psr\Container\ContainerInterface|null> $dispatcher
+ * @param array $methods
*/
- public function appendMiddlewareToDispatcher(MiddlewareDispatcher $dispatcher): RouteGroupInterface
+ public function map(array $methods, string $path, callable|string $handler): Route
{
- foreach ($this->middleware as $middleware) {
- $dispatcher->add($middleware);
- }
+ $routePath = ($path === '/') ? $this->prefix : $this->prefix . sprintf('/%s', ltrim($path, '/'));
+ $route = new Route($methods, $routePath, $handler, $this);
+ $this->routeCollector->addRoute($methods, $path, $route);
- return $this;
+ return $route;
}
- /**
- * {@inheritdoc}
- */
- public function getPattern(): string
+ public function group(string $path, callable $handler): RouteGroup
{
- return $this->pattern;
+ $routePath = ($path === '/') ? $this->prefix : $this->prefix . sprintf('/%s', ltrim($path, '/'));
+ $routeGroup = new RouteGroup($routePath, $handler, $this->router, $this);
+
+ $this->routeCollector->addGroup($path, $routeGroup);
+
+ return $routeGroup;
}
}
diff --git a/Slim/Routing/RouteResolver.php b/Slim/Routing/RouteResolver.php
deleted file mode 100644
index d4f4eafa3..000000000
--- a/Slim/Routing/RouteResolver.php
+++ /dev/null
@@ -1,56 +0,0 @@
-routeCollector = $routeCollector;
- $this->dispatcher = $dispatcher ?? new Dispatcher($routeCollector);
- }
-
- /**
- * @param string $uri Should be $request->getUri()->getPath()
- */
- public function computeRoutingResults(string $uri, string $method): RoutingResults
- {
- $uri = rawurldecode($uri);
- if ($uri === '' || $uri[0] !== '/') {
- $uri = '/' . $uri;
- }
- return $this->dispatcher->dispatch($method, $uri);
- }
-
- /**
- * @throws RuntimeException
- */
- public function resolveRoute(string $identifier): RouteInterface
- {
- return $this->routeCollector->lookupRoute($identifier);
- }
-}
diff --git a/Slim/Routing/RouteRunner.php b/Slim/Routing/RouteRunner.php
deleted file mode 100644
index 3fb541352..000000000
--- a/Slim/Routing/RouteRunner.php
+++ /dev/null
@@ -1,76 +0,0 @@
-
- */
- private ?RouteCollectorProxyInterface $routeCollectorProxy;
-
- /**
- * @param RouteCollectorProxyInterface<\Psr\Container\ContainerInterface|null> $routeCollectorProxy
- */
- public function __construct(
- RouteResolverInterface $routeResolver,
- RouteParserInterface $routeParser,
- ?RouteCollectorProxyInterface $routeCollectorProxy = null
- ) {
- $this->routeResolver = $routeResolver;
- $this->routeParser = $routeParser;
- $this->routeCollectorProxy = $routeCollectorProxy;
- }
-
- /**
- * This request handler is instantiated automatically in App::__construct()
- * It is at the very tip of the middleware queue meaning it will be executed
- * last and it detects whether or not routing has been performed in the user
- * defined middleware stack. In the event that the user did not perform routing
- * it is done here
- *
- * @throws HttpNotFoundException
- * @throws HttpMethodNotAllowedException
- */
- public function handle(ServerRequestInterface $request): ResponseInterface
- {
- // If routing hasn't been done, then do it now so we can dispatch
- if ($request->getAttribute(RouteContext::ROUTING_RESULTS) === null) {
- $routingMiddleware = new RoutingMiddleware($this->routeResolver, $this->routeParser);
- $request = $routingMiddleware->performRouting($request);
- }
-
- if ($this->routeCollectorProxy !== null) {
- $request = $request->withAttribute(
- RouteContext::BASE_PATH,
- $this->routeCollectorProxy->getBasePath()
- );
- }
-
- /** @var Route<\Psr\Container\ContainerInterface|null> $route */
- $route = $request->getAttribute(RouteContext::ROUTE);
- return $route->run($request);
- }
-}
diff --git a/Slim/Routing/Router.php b/Slim/Routing/Router.php
new file mode 100644
index 000000000..ba42ebe3e
--- /dev/null
+++ b/Slim/Routing/Router.php
@@ -0,0 +1,88 @@
+collector = $collector;
+ }
+
+ /**
+ * @param array $methods
+ *
+ * @throws InvalidArgumentException
+ */
+ public function map(array $methods, string $path, callable|string $handler): Route
+ {
+ if (!$methods) {
+ throw new InvalidArgumentException('HTTP methods array cannot be empty');
+ }
+
+ $routePattern = $this->normalizePath($path);
+ $route = new Route($methods, $routePattern, $handler, null);
+
+ $this->collector->addRoute($methods, $routePattern, $route);
+
+ return $route;
+ }
+
+ public function group(string $path, callable $handler): RouteGroup
+ {
+ $routePattern = $this->normalizePath($path);
+ $routeGroup = new RouteGroup($routePattern, $handler, $this);
+ $this->collector->addGroup($routePattern, $routeGroup);
+
+ return $routeGroup;
+ }
+
+ public function getRouteCollector(): RouteCollector
+ {
+ return $this->collector;
+ }
+
+ public function setBasePath(string $basePath): void
+ {
+ $this->basePath = $basePath;
+ }
+
+ public function getBasePath(): string
+ {
+ return $this->basePath;
+ }
+
+ /**
+ * Normalizes a path by ensuring:
+ * - Starts with a forward slash
+ * - No trailing slash (unless root path)
+ * - No double slashes
+ */
+ private function normalizePath(string $path): string
+ {
+ // If path is empty or just a slash, return single slash
+ if ($path === '' || $path === '/') {
+ return '/';
+ }
+
+ // Ensure path starts with a slash
+ $path = '/' . ltrim($path, '/');
+
+ // Remove trailing slash unless it's the root path
+ $path = rtrim($path, '/');
+
+ // Replace multiple consecutive slashes with a single slash
+ return preg_replace('#/+#', '/', $path);
+ }
+}
diff --git a/Slim/Routing/RoutingResults.php b/Slim/Routing/RoutingResults.php
index 4fd8973f8..962551bd0 100644
--- a/Slim/Routing/RoutingResults.php
+++ b/Slim/Routing/RoutingResults.php
@@ -1,69 +1,57 @@
+ */
+ private array $routeArguments;
/**
- * @var array
+ * @var array
*/
- protected array $routeArguments;
+ private array $allowedMethods;
/**
* @param array $routeArguments
*/
public function __construct(
- DispatcherInterface $dispatcher,
+ int $routeStatus,
+ ?Route $route,
string $method,
string $uri,
- int $routeStatus,
- ?string $routeIdentifier = null,
- array $routeArguments = []
+ array $routeArguments = [],
+ array $allowedMethods = [],
) {
- $this->dispatcher = $dispatcher;
+ $this->route = $route;
$this->method = $method;
$this->uri = $uri;
$this->routeStatus = $routeStatus;
- $this->routeIdentifier = $routeIdentifier;
$this->routeArguments = $routeArguments;
+ $this->allowedMethods = $allowedMethods;
}
- public function getDispatcher(): DispatcherInterface
+ public function getRoute(): ?Route
{
- return $this->dispatcher;
+ return $this->route;
}
public function getMethod(): string
@@ -81,26 +69,17 @@ public function getRouteStatus(): int
return $this->routeStatus;
}
- public function getRouteIdentifier(): ?string
- {
- return $this->routeIdentifier;
- }
-
/**
- * @return array
+ * @return array
*/
- public function getRouteArguments(bool $urlDecode = true): array
+ public function getRouteArguments(): array
{
- if (!$urlDecode) {
- return $this->routeArguments;
- }
-
- $routeArguments = [];
- foreach ($this->routeArguments as $key => $value) {
- $routeArguments[$key] = rawurldecode($value);
- }
+ return $this->routeArguments;
+ }
- return $routeArguments;
+ public function getRouteArgument(string $key): mixed
+ {
+ return $this->routeArguments[$key] ?? null;
}
/**
@@ -108,6 +87,6 @@ public function getRouteArguments(bool $urlDecode = true): array
*/
public function getAllowedMethods(): array
{
- return $this->dispatcher->getAllowedMethods($this->uri);
+ return $this->allowedMethods;
}
}
diff --git a/tests/Mocks/MockCustomRequestHandlerInvocationStrategy.php b/Slim/Routing/Strategies/RequestHandler.php
similarity index 65%
rename from tests/Mocks/MockCustomRequestHandlerInvocationStrategy.php
rename to Slim/Routing/Strategies/RequestHandler.php
index 5761a9adf..b4942c171 100644
--- a/tests/Mocks/MockCustomRequestHandlerInvocationStrategy.php
+++ b/Slim/Routing/Strategies/RequestHandler.php
@@ -3,28 +3,28 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
-namespace Slim\Tests\Mocks;
+namespace Slim\Routing\Strategies;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Interfaces\RequestHandlerInvocationStrategyInterface;
-class MockCustomRequestHandlerInvocationStrategy implements RequestHandlerInvocationStrategyInterface
+/**
+ * Invoke a route callable that implements RequestHandlerInterface.
+ */
+final class RequestHandler implements RequestHandlerInvocationStrategyInterface
{
- public static $CalledCount = 0;
-
public function __invoke(
callable $callable,
ServerRequestInterface $request,
ResponseInterface $response,
array $routeArguments
): ResponseInterface {
- self::$CalledCount += 1;
return $callable($request);
}
}
diff --git a/Slim/Routing/Strategies/RequestResponse.php b/Slim/Routing/Strategies/RequestResponse.php
new file mode 100644
index 000000000..e1e4dbebb
--- /dev/null
+++ b/Slim/Routing/Strategies/RequestResponse.php
@@ -0,0 +1,30 @@
+ $routeArguments
- */
public function __invoke(
callable $callable,
ServerRequestInterface $request,
ResponseInterface $response,
- array $routeArguments
+ array $routeArguments,
): ResponseInterface {
return $callable($request, $response, ...array_values($routeArguments));
}
diff --git a/Slim/Routing/Strategies/RequestResponseNamedArgs.php b/Slim/Routing/Strategies/RequestResponseNamedArgs.php
new file mode 100644
index 000000000..c64a4a90a
--- /dev/null
+++ b/Slim/Routing/Strategies/RequestResponseNamedArgs.php
@@ -0,0 +1,30 @@
+invoker = $invoker;
+ }
+
+ public function __invoke(
+ callable $callable,
+ ServerRequestInterface $request,
+ ResponseInterface $response,
+ array $routeArguments,
+ ): ResponseInterface {
+ $routeArguments['request'] = $request;
+ $routeArguments['response'] = $response;
+
+ return $this->invoker->call($callable, $routeArguments);
+ }
+}
diff --git a/Slim/Routing/RouteParser.php b/Slim/Routing/UrlGenerator.php
similarity index 71%
rename from Slim/Routing/RouteParser.php
rename to Slim/Routing/UrlGenerator.php
index afb533cc5..5d695ea39 100644
--- a/Slim/Routing/RouteParser.php
+++ b/Slim/Routing/UrlGenerator.php
@@ -1,20 +1,14 @@
routeCollector = $routeCollector;
+ $this->router = $router;
$this->routeParser = new Std();
}
@@ -39,9 +33,63 @@ public function __construct(RouteCollectorInterface $routeCollector)
*/
public function relativeUrlFor(string $routeName, array $data = [], array $queryParams = []): string
{
- $route = $this->routeCollector->getNamedRoute($routeName);
+ $route = $this->getNamedRoute($routeName);
$pattern = $route->getPattern();
+ $segments = $this->getSegments($pattern, $data);
+
+ $url = implode('', $segments);
+ if ($queryParams) {
+ $url .= '?' . http_build_query($queryParams);
+ }
+
+ $basePath = $this->router->getBasePath();
+ if ($basePath) {
+ $url = $basePath . $url;
+ }
+
+ return $url;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function urlFor(string $routeName, array $data = [], array $queryParams = []): string
+ {
+ return $this->relativeUrlFor($routeName, $data, $queryParams);
+ }
+ /**
+ * {@inheritdoc}
+ */
+ public function fullUrlFor(UriInterface $uri, string $routeName, array $data = [], array $queryParams = []): string
+ {
+ $path = $this->urlFor($routeName, $data, $queryParams);
+ $scheme = $uri->getScheme();
+ $authority = $uri->getAuthority();
+ $protocol = ($scheme ? $scheme . ':' : '') . ($authority ? '//' . $authority : '');
+
+ return $protocol . $path;
+ }
+
+ private function getNamedRoute(string $name): Route
+ {
+ $routes = $this->router->getRouteCollector()->getData();
+
+ $iterator = new RecursiveIteratorIterator(
+ new RecursiveArrayIterator($routes, RecursiveArrayIterator::CHILD_ARRAYS_ONLY)
+ );
+
+ foreach ($iterator as $route) {
+ if ($route instanceof Route && $name === $route->getName()) {
+ return $route;
+ }
+ }
+
+ throw new UnexpectedValueException('Named route does not exist for name: ' . $name);
+ }
+
+ private function getSegments(string $pattern, array $data): array
+ {
$segments = [];
$segmentName = '';
@@ -51,6 +99,7 @@ public function relativeUrlFor(string $routeName, array $data = [], array $query
* The most specific is last, hence why we reverse the array before iterating over it
*/
$expressions = array_reverse($this->routeParser->parse($pattern));
+
foreach ($expressions as $expression) {
foreach ($expression as $segment) {
/*
@@ -81,47 +130,15 @@ public function relativeUrlFor(string $routeName, array $data = [], array $query
* for the provided $data which means we don't need to continue testing
* less specific expressions
*/
- if (!empty($segments)) {
+ if (!$segments) {
break;
}
}
- if (empty($segments)) {
+ if (!$segments) {
throw new InvalidArgumentException('Missing data for URL segment: ' . $segmentName);
}
- $url = implode('', $segments);
- if ($queryParams) {
- $url .= '?' . http_build_query($queryParams);
- }
-
- return $url;
- }
-
- /**
- * {@inheritdoc}
- */
- public function urlFor(string $routeName, array $data = [], array $queryParams = []): string
- {
- $basePath = $this->routeCollector->getBasePath();
- $url = $this->relativeUrlFor($routeName, $data, $queryParams);
-
- if ($basePath) {
- $url = $basePath . $url;
- }
-
- return $url;
- }
-
- /**
- * {@inheritdoc}
- */
- public function fullUrlFor(UriInterface $uri, string $routeName, array $data = [], array $queryParams = []): string
- {
- $path = $this->urlFor($routeName, $data, $queryParams);
- $scheme = $uri->getScheme();
- $authority = $uri->getAuthority();
- $protocol = ($scheme ? $scheme . ':' : '') . ($authority ? '//' . $authority : '');
- return $protocol . $path;
+ return $segments;
}
}
diff --git a/UPGRADING.md b/UPGRADING.md
index 0b10fda0e..00a5f1a79 100644
--- a/UPGRADING.md
+++ b/UPGRADING.md
@@ -5,11 +5,11 @@
* [2622] - `Router` has been removed. It is now split into `RouteCollector`, `RouteRunner` and `RouteParser`
* [2555] - PSR-15 Middleware support was implemented at the cost of Double-Pass middleware being deprecated.
* [2529] - Slim no longer ships with its own PSR-7 implementation you will need to provide your own before you can create/run an app.
-* [2507] - Method names are now case sensitive when using `App::map()`.
+* [2507] - Method names are now case-sensitive when using `App::map()`.
* [2404] - Slim 4 requires PHP 7.1 or higher
* [2398] - Error handling was extracted into its own middleware. Add `RoutingMiddleware` to your middleware pipeline to handle errors by default. See PR for more information.
* [2329] - If you were overriding the HTTP method using either the custom header or the body param, you need to add the `Middleware\MethodOverrideMiddleware` middleware to be able to override the method like before.
-* [2290] - Slim no longer ships with `Pimple` as container dependency so you need to supply your own. `App::__call()` has been deprecated.
+* [2290] - Slim no longer ships with `Pimple` as container dependency, so you need to supply your own. `App::__call()` has been deprecated.
* [2288] - If you were using `determineRouteBeforeAppMiddleware`, you need to add the `Middleware\RoutingMiddleware` middleware to your application just before your call `run()` to maintain the previous behaviour.
* [2254] - You need to add the `Middleware\ContentLengthMiddleware` middleware if you want Slim to add the Content-Length header this automatically.
* [2166] - You need to add the `Middleware\OutputBuffering` middleware to capture echo'd or var_dump'd output from your code.
diff --git a/composer.json b/composer.json
index fdbc786b2..e6e752eae 100644
--- a/composer.json
+++ b/composer.json
@@ -1,10 +1,14 @@
{
"name": "slim/slim",
- "type": "library",
"description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs",
- "keywords": ["framework","micro","api","router"],
- "homepage": "https://www.slimframework.com",
"license": "MIT",
+ "type": "library",
+ "keywords": [
+ "framework",
+ "micro",
+ "api",
+ "router"
+ ],
"authors": [
{
"name": "Josh Lockhart",
@@ -14,62 +18,74 @@
{
"name": "Andrew Smith",
"email": "a.smith@silentworks.co.uk",
- "homepage": "http://silentworks.co.uk"
+ "homepage": "https://silentworks.co.uk"
},
{
"name": "Rob Allen",
"email": "rob@akrabat.com",
- "homepage": "http://akrabat.com"
+ "homepage": "https://akrabat.com"
},
{
"name": "Pierre Berube",
"email": "pierre@lgse.com",
- "homepage": "http://www.lgse.com"
+ "homepage": "https://www.lgse.com"
},
{
"name": "Gabriel Manricks",
"email": "gmanricks@me.com",
- "homepage": "http://gabrielmanricks.com"
+ "homepage": "https://gabrielmanricks.com"
+ },
+ {
+ "name": "Daniel Opitz",
+ "homepage": "https://odan.github.io/"
}
],
+ "homepage": "https://www.slimframework.com",
"support": {
- "docs": "https://www.slimframework.com/docs/v4/",
+ "issues": "https://github.com/slimphp/Slim/issues",
"forum": "https://discourse.slimframework.com/",
+ "wiki": "https://github.com/slimphp/Slim/wiki",
"irc": "irc://irc.freenode.net:6667/slimphp",
- "issues": "https://github.com/slimphp/Slim/issues",
- "rss": "https://www.slimframework.com/blog/feed.rss",
- "slack": "https://slimphp.slack.com/",
"source": "https://github.com/slimphp/Slim",
- "wiki": "https://github.com/slimphp/Slim/wiki"
+ "docs": "https://www.slimframework.com/docs/v5/",
+ "rss": "https://www.slimframework.com/blog/feed.rss",
+ "slack": "https://slimphp.slack.com/"
},
"require": {
- "php": "^7.4 || ^8.0",
+ "php": "8.2.* || 8.3.* || 8.4.*",
"ext-json": "*",
"nikic/fast-route": "^1.3",
- "psr/container": "^1.0 || ^2.0",
+ "psr/container": "^2.0",
"psr/http-factory": "^1.1",
- "psr/http-message": "^1.1 || ^2.0",
+ "psr/http-message": "^2.0",
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
- "psr/log": "^1.1 || ^2.0 || ^3.0"
+ "psr/log": "^3.0"
},
"require-dev": {
"ext-simplexml": "*",
- "adriansuter/php-autoload-override": "^1.4",
+ "ext-xml": "*",
+ "bnf/phpstan-psr-container": "^1.0",
+ "friendsofphp/php-cs-fixer": "^3.60",
"guzzlehttp/psr7": "^2.6",
"httpsoft/http-message": "^1.1",
"httpsoft/http-server-request": "^1.1",
- "laminas/laminas-diactoros": "^2.17 || ^3",
+ "laminas/laminas-diactoros": "^3",
"nyholm/psr7": "^1.8",
"nyholm/psr7-server": "^1.1",
- "phpspec/prophecy": "^1.19",
- "phpspec/prophecy-phpunit": "^2.1",
- "phpstan/phpstan": "^1.11",
- "phpunit/phpunit": "^9.6",
+ "php-di/php-di": "^7.0",
+ "phpstan/phpstan": "^2",
+ "phpunit/phpunit": "^11",
"slim/http": "^1.3",
"slim/psr7": "^1.6",
- "squizlabs/php_codesniffer": "^3.10",
- "vimeo/psalm": "^5.24"
+ "squizlabs/php_codesniffer": "^3.10"
+ },
+ "suggest": {
+ "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware",
+ "ext-libxml": "Needed to support XML format in BodyParsingMiddleware",
+ "ext-dom": "Needed to support XML format in XmlErrorFormatter",
+ "php-di/php-di": "PHP-DI is the recommended container library to be used with Slim",
+ "slim/psr7": "Slim PSR-7 implementation. See https://www.slimframework.com/docs/v5/start/installation.html for more information."
},
"autoload": {
"psr-4": {
@@ -81,25 +97,31 @@
"Slim\\Tests\\": "tests"
}
},
- "scripts": {
- "test": [
- "@phpunit",
- "@phpcs",
- "@phpstan",
- "@psalm"
- ],
- "phpunit": "phpunit",
- "phpcs": "phpcs",
- "phpstan": "phpstan --memory-limit=-1",
- "psalm": "psalm --no-cache"
- },
- "suggest": {
- "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware",
- "ext-xml": "Needed to support XML format in BodyParsingMiddleware",
- "slim/psr7": "Slim PSR-7 implementation. See https://www.slimframework.com/docs/v4/start/installation.html for more information.",
- "php-di/php-di": "PHP-DI is the recommended container library to be used with Slim"
- },
"config": {
"sort-packages": true
+ },
+ "scripts": {
+ "cs:check": [
+ "@putenv PHP_CS_FIXER_IGNORE_ENV=1",
+ "php-cs-fixer fix --dry-run --format=txt --verbose --config=.cs.php --ansi"
+ ],
+ "cs:fix": [
+ "@putenv PHP_CS_FIXER_IGNORE_ENV=1",
+ "php-cs-fixer fix --config=.cs.php --ansi --verbose"
+ ],
+ "sniffer:check": "phpcs --standard=phpcs.xml",
+ "sniffer:fix": "phpcbf --standard=phpcs.xml",
+ "stan": "phpstan analyse -c phpstan.neon --no-progress --ansi",
+ "test": "phpunit --configuration phpunit.xml --do-not-cache-result --colors=always --display-warnings --display-deprecations --no-coverage",
+ "test:all": [
+ "@cs:check",
+ "@sniffer:check",
+ "@stan",
+ "@test"
+ ],
+ "test:coverage": [
+ "@putenv XDEBUG_MODE=coverage",
+ "phpunit --configuration phpunit.xml --do-not-cache-result --colors=always --display-warnings --display-deprecations --coverage-clover build/coverage/clover.xml --coverage-html build/coverage --coverage-text"
+ ]
}
}
diff --git a/phpcs.xml.dist b/phpcs.xml
similarity index 100%
rename from phpcs.xml.dist
rename to phpcs.xml
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 000000000..97e042c04
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,6 @@
+parameters:
+ level: 5
+ paths:
+ - Slim
+includes:
+ - vendor/bnf/phpstan-psr-container/extension.neon
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
deleted file mode 100644
index d81898e60..000000000
--- a/phpstan.neon.dist
+++ /dev/null
@@ -1,4 +0,0 @@
-parameters:
- level: max
- paths:
- - Slim
diff --git a/phpunit.xml.dist b/phpunit.xml
similarity index 66%
rename from phpunit.xml.dist
rename to phpunit.xml
index 2bbfaf422..04f26762a 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml
@@ -1,24 +1,21 @@
+ xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.2/phpunit.xsd"
+ beStrictAboutChangesToGlobalState="true" beStrictAboutOutputDuringTests="true" colors="true"
+ bootstrap="tests/bootstrap.php" executionOrder="random" cacheDirectory=".phpunit.cache">
tests
-
-
-
- Slim
-
+
+
+
+ Slim
+
+
diff --git a/psalm.xml b/psalm.xml
deleted file mode 100644
index af258ff89..000000000
--- a/psalm.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/tests/AppTest.php b/tests/AppTest.php
index cd18bfd16..630f7f94b 100644
--- a/tests/AppTest.php
+++ b/tests/AppTest.php
@@ -3,159 +3,166 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Tests;
-use Prophecy\Argument;
-use Psr\Container\ContainerInterface;
-use Psr\Http\Message\ResponseFactoryInterface;
+use DI\Container;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
-use Psr\Http\Message\StreamInterface;
-use Psr\Http\Message\UriInterface;
-use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
-use Psr\Log\LoggerInterface;
-use ReflectionClass;
-use ReflectionProperty;
use RuntimeException;
use Slim\App;
-use Slim\CallableResolver;
+use Slim\Builder\AppBuilder;
+use Slim\Container\DefaultDefinitions;
+use Slim\Container\HttpDefinitions;
use Slim\Exception\HttpMethodNotAllowedException;
use Slim\Exception\HttpNotFoundException;
-use Slim\Handlers\Strategies\RequestResponseArgs;
-use Slim\Handlers\Strategies\RequestResponseNamedArgs;
-use Slim\Interfaces\CallableResolverInterface;
-use Slim\Interfaces\MiddlewareDispatcherInterface;
-use Slim\Interfaces\RouteCollectorInterface;
-use Slim\Interfaces\RouteCollectorProxyInterface;
-use Slim\Interfaces\RouteParserInterface;
+use Slim\Interfaces\RequestHandlerInvocationStrategyInterface;
+use Slim\Interfaces\ServerRequestCreatorInterface;
+use Slim\Middleware\BasePathMiddleware;
use Slim\Middleware\BodyParsingMiddleware;
-use Slim\Middleware\ErrorMiddleware;
+use Slim\Middleware\ContentLengthMiddleware;
+use Slim\Middleware\EndpointMiddleware;
+use Slim\Middleware\ErrorHandlingMiddleware;
+use Slim\Middleware\ExceptionHandlingMiddleware;
+use Slim\Middleware\ExceptionLoggingMiddleware;
+use Slim\Middleware\HeadMethodMiddleware;
+use Slim\Middleware\RoutingArgumentsMiddleware;
use Slim\Middleware\RoutingMiddleware;
-use Slim\MiddlewareDispatcher;
-use Slim\Routing\RouteCollector;
-use Slim\Routing\RouteCollectorProxy;
-use Slim\Routing\RouteContext;
-use Slim\Tests\Mocks\MockAction;
-use stdClass;
-
-use function array_key_exists;
-use function array_shift;
+use Slim\Psr7\Headers;
+use Slim\Psr7\Request;
+use Slim\Psr7\Stream;
+use Slim\Psr7\Uri;
+use Slim\Routing\RouteGroup;
+use Slim\Routing\Strategies\RequestResponseArgs;
+use Slim\Routing\Strategies\RequestResponseNamedArgs;
+use Slim\Tests\Traits\AppTestTrait;
+use SplStack;
+use UnexpectedValueException;
+
use function count;
-use function ini_set;
-use function json_encode;
use function strtolower;
-use function sys_get_temp_dir;
-use function tempnam;
-class AppTest extends TestCase
+final class AppTest extends TestCase
{
- public static function setupBeforeClass(): void
- {
- ini_set('error_log', tempnam(sys_get_temp_dir(), 'slim'));
- }
+ use AppTestTrait;
- public function testDoesNotUseContainerAsServiceLocator(): void
+ public function testAppWithExceptionAndErrorDetails(): void
{
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $app = new App($responseFactoryProphecy->reveal(), $containerProphecy->reveal());
+ $builder = new AppBuilder();
+ $builder->setSettings(['display_error_details' => true]);
+ $app = $builder->build();
- $containerProphecy->has(Argument::type('string'))->shouldNotHaveBeenCalled();
- $containerProphecy->get(Argument::type('string'))->shouldNotHaveBeenCalled();
- }
+ $app->add(RoutingMiddleware::class);
+ $app->add(ExceptionHandlingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- /********************************************************************************
- * Getter methods
- *******************************************************************************/
+ $app->get('/', fn () => throw new UnexpectedValueException('Test exception message'));
- public function testGetContainer(): void
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $app = new App($responseFactoryProphecy->reveal(), $containerProphecy->reveal());
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
- $this->assertSame($containerProphecy->reveal(), $app->getContainer());
- }
+ $request = $request->withHeader(
+ 'Accept',
+ 'text/html, application/xhtml+xml, application/xml;q=0.9, application/json , image/webp, */*;q=0.8'
+ );
- public function testGetCallableResolverReturnsInjectedInstance(): void
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $app = new App($responseFactoryProphecy->reveal(), null, $callableResolverProphecy->reveal());
+ $response = $app->handle($request);
+
+ $this->assertSame('text/html', $response->getHeaderLine('content-type'));
- $this->assertSame($callableResolverProphecy->reveal(), $app->getCallableResolver());
+ $expected = 'Test exception message';
+ $this->assertStringContainsString($expected, (string)$response->getBody());
}
- public function testCreatesCallableResolverWhenNull(): void
+ public function testGetAppFromContainer(): void
{
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $callableResolver = new CallableResolver($containerProphecy->reveal());
- $app = new App($responseFactoryProphecy->reveal(), $containerProphecy->reveal(), null);
+ $builder = new AppBuilder();
+ $app = $builder->build();
- $this->assertEquals($callableResolver, $app->getCallableResolver());
+ // should return the same instance
+ $actual = $app->getContainer()->get(App::class);
+ $this->assertSame($app, $actual);
}
- public function testGetRouteCollectorReturnsInjectedInstance(): void
+ public function testGetContainer(): void
{
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class);
- $routeParserProphecy = $this->prophesize(RouteParserInterface::class);
+ $definitions = (new DefaultDefinitions())->__invoke();
+ $definitions = array_merge($definitions, (new HttpDefinitions())->__invoke());
+ $container = new Container($definitions);
- $routeCollectorProphecy->getRouteParser()->willReturn($routeParserProphecy->reveal());
+ $builder = new AppBuilder();
+ $builder->setContainerFactory(function () use ($container) {
+ return $container;
+ });
- $app = new App($responseFactoryProphecy->reveal(), null, null, $routeCollectorProphecy->reveal());
+ $app = $builder->build();
- $this->assertSame($routeCollectorProphecy->reveal(), $app->getRouteCollector());
+ $this->assertSame($container, $app->getContainer());
}
- public function testCreatesRouteCollectorWhenNullWithInjectedContainer(): void
+ public function testAppWithMiddlewareStack(): void
{
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $routeCollector = new RouteCollector(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- $containerProphecy->reveal()
- );
- $app = new App(
- $responseFactoryProphecy->reveal(),
- $containerProphecy->reveal(),
- $callableResolverProphecy->reveal()
- );
+ $app = (new AppBuilder())->build();
- $this->assertEquals($routeCollector, $app->getRouteCollector());
+ $app->add(BasePathMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(RoutingArgumentsMiddleware::class);
+ $app->add(BodyParsingMiddleware::class);
+ $app->add(ErrorHandlingMiddleware::class);
+ $app->add(ExceptionHandlingMiddleware::class);
+ $app->add(ExceptionLoggingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+ $app->add(ContentLengthMiddleware::class);
+
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ return $response->withHeader('X-Test', 'action');
+ })->add(BodyParsingMiddleware::class);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->handle($request);
+
+ $this->assertSame('action', $response->getHeaderLine('X-Test'));
}
- public function testGetMiddlewareDispatcherGetsSeededAndReturnsInjectedInstance(): void
+ public function testGetWithInvokableClass(): void
{
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
-
- $middlewareDispatcherProphecy = $this->prophesize(MiddlewareDispatcherInterface::class);
- $middlewareDispatcherProphecy
- ->seedMiddlewareStack(Argument::any())
- ->shouldBeCalledOnce();
-
- $app = new App(
- $responseFactoryProphecy->reveal(),
- null,
- null,
- null,
- null,
- $middlewareDispatcherProphecy->reveal()
- );
+ $builder = new AppBuilder();
+ $app = $builder->build();
- $this->assertSame($middlewareDispatcherProphecy->reveal(), $app->getMiddlewareDispatcher());
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $action = new class {
+ public function __invoke($request, $response, $args)
+ {
+ return $response->withHeader('X-Test', 'action');
+ }
+ };
+
+ $app->get('/', $action::class);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->handle($request);
+
+ $this->assertSame('action', $response->getHeaderLine('X-Test'));
}
- public function lowerCaseRequestMethodsProvider(): array
+ public static function lowerCaseRequestMethodsProvider(): array
{
return [
['get'],
@@ -167,84 +174,49 @@ public function lowerCaseRequestMethodsProvider(): array
];
}
- /**
- * @param string $method
- * @dataProvider upperCaseRequestMethodsProvider()
- */
+ #[DataProvider('upperCaseRequestMethodsProvider')]
public function testGetPostPutPatchDeleteOptionsMethods(string $method): void
{
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('Hello World');
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
+ $app = $this->createApp();
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn($method);
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest($method, '/');
$methodName = strtolower($method);
- $app = new App($responseFactoryProphecy->reveal());
$app->$methodName('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
+
return $response;
});
- $response = $app->handle($requestProphecy->reveal());
+ $response = $app->handle($request);
- $this->assertSame('Hello World', (string) $response->getBody());
+ $this->assertSame('Hello World', (string)$response->getBody());
}
public function testAnyRoute(): void
{
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('Hello World');
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
+ $app = $this->createApp();
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('FOO', '/');
- $app = new App($responseFactoryProphecy->reveal());
$app->any('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
+
return $response;
});
+ $response = $app->handle($request);
- foreach ($this->upperCaseRequestMethodsProvider() as $methods) {
- $method = $methods[0];
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn($method);
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
-
- $response = $app->handle($requestProphecy->reveal());
-
- $this->assertSame('Hello World', (string) $response->getBody());
- }
+ $this->assertSame('Hello World', (string)$response->getBody());
}
- /********************************************************************************
- * Route collector proxy methods
- *******************************************************************************/
-
- public function upperCaseRequestMethodsProvider(): array
+ public static function upperCaseRequestMethodsProvider(): array
{
return [
['GET'],
@@ -256,278 +228,106 @@ public function upperCaseRequestMethodsProvider(): array
];
}
- /**
- * @param string $method
- * @dataProvider lowerCaseRequestMethodsProvider
- * @dataProvider upperCaseRequestMethodsProvider
- */
+ #[DataProvider('lowerCaseRequestMethodsProvider')]
+ #[DataProvider('upperCaseRequestMethodsProvider')]
public function testMapRoute(string $method): void
{
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('Hello World');
+ $app = $this->createApp();
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn($method);
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest($method, '/');
- $app = new App($responseFactoryProphecy->reveal());
$app->map([$method], '/', function (ServerRequestInterface $request, ResponseInterface $response) {
- return $response;
- });
- $response = $app->handle($requestProphecy->reveal());
-
- $this->assertSame('Hello World', (string) $response->getBody());
- }
-
- public function testRedirectRoute(): void
- {
- $from = '/from';
- $to = '/to';
-
- $routeCreatedResponse = $this->prophesize(ResponseInterface::class);
-
- $handlerCreatedResponse = $this->prophesize(ResponseInterface::class);
- $handlerCreatedResponse->getStatusCode()->willReturn(301);
- $handlerCreatedResponse->getHeaderLine('Location')->willReturn($to);
- $handlerCreatedResponse->withHeader(
- Argument::type('string'),
- Argument::type('string')
- )->will(function ($args) {
- $this->getHeader($args[0])->willReturn($args[1]);
- return $this;
- });
+ $response->getBody()->write('Hello World');
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($routeCreatedResponse->reveal());
- $responseFactoryProphecy->createResponse(301)->willReturn($handlerCreatedResponse->reveal());
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn($from);
- $uriProphecy->__toString()->willReturn($to);
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
+ return $response;
});
- $app = new App($responseFactoryProphecy->reveal());
- $app->redirect($from, $to, 301);
- $response = $app->handle($requestProphecy->reveal());
+ $response = $app->handle($request);
- $responseFactoryProphecy->createResponse(301)->shouldHaveBeenCalled();
- $this->assertSame(301, $response->getStatusCode());
- $this->assertSame($to, $response->getHeaderLine('Location'));
+ $this->assertSame('Hello World', (string)$response->getBody());
}
public function testRouteWithInternationalCharacters(): void
{
$path = '/новости';
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('Hello World');
+ $app = $this->createApp();
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', $path);
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ $app->get($path, function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
- $app = new App($responseFactoryProphecy->reveal());
- $app->get($path, function () use ($responseProphecy) {
- return $responseProphecy->reveal();
+ return $response;
});
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn($path);
+ $response = $app->handle($request);
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
- $response = $app->handle($requestProphecy->reveal());
-
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertSame('Hello World', (string) $response->getBody());
+ $this->assertSame('Hello World', (string)$response->getBody());
}
/********************************************************************************
* Route Patterns
*******************************************************************************/
- public function routePatternsProvider(): array
+ public static function routePatternsProvider(): array
{
return [
- [''], // Empty Route
- ['/'], // Single Slash Route
- ['foo'], // Route That Does Not Start With A Slash
- ['/foo'], // Route That Does Not End In A Slash
- ['/foo/'], // Route That Ends In A Slash
+ // Route pattern -> http uri
+ // Empty route
+ ['', '/'],
+ // Single slash route
+ ['/', '/'],
+ // Route That Does Not Start With A Slash
+ ['foo', '/foo'],
+ // Route That Does Not End In A Slash
+ ['/foo', '/foo'],
+ // Route That Ends In A Slash
+ ['/foo/', '/foo'],
+ // Route That Ends In A double Slash
+ ['/foo//', '/foo'],
+ // Route That contains In A double Slash
+ ['/foo//bar', '/foo/bar'],
];
}
- /**
- * @param string $pattern
- * @dataProvider routePatternsProvider
- */
- public function testRoutePatterns(string $pattern): void
+ #[DataProvider('routePatternsProvider')]
+ public function testRoutePatterns(string $pattern, string $uri): void
{
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
+ $app = $this->createApp();
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $app = new App($responseFactoryProphecy->reveal());
- $app->get($pattern, function () {
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', $uri);
+
+ $app->get($pattern, function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
+
+ return $response;
});
- $routeCollector = $app->getRouteCollector();
- $route = $routeCollector->lookupRoute('route0');
+ $response = $app->handle($request);
- $this->assertSame($pattern, $route->getPattern());
+ $this->assertSame('Hello World', (string)$response->getBody());
}
/********************************************************************************
* Route Groups
*******************************************************************************/
- public function routeGroupsDataProvider(): array
- {
- return [
- 'empty group with empty route' => [
- ['', ''], ''
- ],
- 'empty group with single slash route' => [
- ['', '/'], '/'
- ],
- 'empty group with segment route that does not end in aSlash' => [
- ['', '/foo'], '/foo'
- ],
- 'empty group with segment route that ends in aSlash' => [
- ['', '/foo/'], '/foo/'
- ],
- 'group single slash with empty route' => [
- ['/', ''], '/'
- ],
- 'group single slash with single slash route' => [
- ['/', '/'], '//'
- ],
- 'group single slash with segment route that does not end in aSlash' => [
- ['/', '/foo'], '//foo'
- ],
- 'group single slash with segment route that ends in aSlash' => [
- ['/', '/foo/'], '//foo/'
- ],
- 'group segment with empty route' => [
- ['/foo', ''], '/foo'
- ],
- 'group segment with single slash route' => [
- ['/foo', '/'], '/foo/'
- ],
- 'group segment with segment route that does not end in aSlash' => [
- ['/foo', '/bar'], '/foo/bar'
- ],
- 'group segment with segment route that ends in aSlash' => [
- ['/foo', '/bar/'], '/foo/bar/'
- ],
- 'empty group with nested group segment with an empty route' => [
- ['', '/foo', ''], '/foo'
- ],
- 'empty group with nested group segment with single slash route' => [
- ['', '/foo', '/'], '/foo/'
- ],
- 'group single slash with empty nested group and segment route without leading slash' => [
- ['/', '', 'foo'], '/foo'
- ],
- 'group single slash with empty nested group and segment route' => [
- ['/', '', '/foo'], '//foo'
- ],
- 'group single slash with single slash group and segment route without leading slash' => [
- ['/', '/', 'foo'], '//foo'
- ],
- 'group single slash with single slash nested group and segment route' => [
- ['/', '/', '/foo'], '///foo'
- ],
- 'group single slash with nested group segment with an empty route' => [
- ['/', '/foo', ''], '//foo'
- ],
- 'group single slash with nested group segment with single slash route' => [
- ['/', '/foo', '/'], '//foo/'
- ],
- 'group single slash with nested group segment with segment route' => [
- ['/', '/foo', '/bar'], '//foo/bar'
- ],
- 'group single slash with nested group segment with segment route that has aTrailing slash' => [
- ['/', '/foo', '/bar/'], '//foo/bar/'
- ],
- 'empty group with empty nested group and segment route without leading slash' => [
- ['', '', 'foo'], 'foo'
- ],
- 'empty group with empty nested group and segment route' => [
- ['', '', '/foo'], '/foo'
- ],
- 'empty group with single slash group and segment route without leading slash' => [
- ['', '/', 'foo'], '/foo'
- ],
- 'empty group with single slash nested group and segment route' => [
- ['', '/', '/foo'], '//foo'
- ],
- 'empty group with nested group segment with segment route' => [
- ['', '/foo', '/bar'], '/foo/bar'
- ],
- 'empty group with nested group segment with segment route that has aTrailing slash' => [
- ['', '/foo', '/bar/'], '/foo/bar/'
- ],
- 'group segment with empty nested group and segment route without leading slash' => [
- ['/foo', '', 'bar'], '/foobar'
- ],
- 'group segment with empty nested group and segment route' => [
- ['/foo', '', '/bar'], '/foo/bar'
- ],
- 'group segment with single slash nested group and segment route' => [
- ['/foo', '/', 'bar'], '/foo/bar'
- ],
- 'group segment with single slash nested group and slash segment route' => [
- ['/foo', '/', '/bar'], '/foo//bar'
- ],
- 'two group segments with empty route' => [
- ['/foo', '/bar', ''], '/foo/bar'
- ],
- 'two group segments with single slash route' => [
- ['/foo', '/bar', '/'], '/foo/bar/'
- ],
- 'two group segments with segment route' => [
- ['/foo', '/bar', '/baz'], '/foo/bar/baz'
- ],
- 'two group segments with segment route that has aTrailing slash' => [
- ['/foo', '/bar', '/baz/'], '/foo/bar/baz/'
- ],
- ];
- }
-
public function testGroupClosureIsBoundToThisClass(): void
{
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $app = new App($responseFactoryProphecy->reveal());
-
+ $app = $this->createApp();
$testCase = $this;
$app->group('/foo', function () use ($testCase) {
$testCase->assertSame($testCase, $this);
@@ -535,1612 +335,633 @@ public function testGroupClosureIsBoundToThisClass(): void
}
/**
- * @dataProvider routeGroupsDataProvider
- * @param array $sequence
- * @param string $expectedPath
- */
- public function testRouteGroupCombinations(array $sequence, string $expectedPath): void
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $app = new App($responseFactoryProphecy->reveal());
-
- $processSequence = function (RouteCollectorProxy $app, array $sequence, $processSequence) {
- $path = array_shift($sequence);
-
- /**
- * If sequence isn't on last element we use $app->group()
- * The very tail of the sequence uses the $app->get() method
- */
- if (count($sequence)) {
- $app->group($path, function (RouteCollectorProxy $group) use (&$sequence, $processSequence) {
- $processSequence($group, $sequence, $processSequence);
- });
- } else {
- $app->get($path, function () {
- });
- }
- };
-
- $processSequence($app, $sequence, $processSequence);
-
- $routeCollector = $app->getRouteCollector();
- $route = $routeCollector->lookupRoute('route0');
-
- $this->assertSame($expectedPath, $route->getPattern());
- }
-
- public function testRouteGroupPattern(): void
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
-
- /** @var ResponseFactoryInterface $responseFactoryInterface */
- $responseFactoryInterface = $responseFactoryProphecy->reveal();
- $app = new App($responseFactoryInterface);
- $group = $app->group('/foo', function () {
- });
-
- $this->assertSame('/foo', $group->getPattern());
- }
-
- /********************************************************************************
* Middleware
- *******************************************************************************/
-
- public function testAddMiddleware(): void
+ */
+ public function testAddMiddlewareUsingDeferredResolution(): void
{
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('Hello World');
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ $app = $this->createApp();
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $app = new App($responseFactoryProphecy->reveal());
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
- $middlewareProphecy = $this->prophesize(MiddlewareInterface::class);
- $middlewareProphecy->process(Argument::cetera())->will(function () use ($responseProphecy) {
- return $responseProphecy->reveal();
- });
-
- $middlewareProphecy2 = $this->prophesize(MiddlewareInterface::class);
- $middlewareProphecy2->process(
- Argument::type(ServerRequestInterface::class),
- Argument::type(RequestHandlerInterface::class)
- )->will(function ($args) {
- /** @var ServerRequestInterface $request */
- $request = $args[0];
-
- /** @var RequestHandlerInterface $handler */
- $handler = $args[1];
-
- return $handler->handle($request);
- });
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
- $app->add($middlewareProphecy->reveal());
- $app->addMiddleware($middlewareProphecy2->reveal());
- $app->get('/', function (ServerRequestInterface $request, $response) {
return $response;
});
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
+ $response = $app->handle($request);
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
-
-
- $response = $app->handle($requestProphecy->reveal());
- $middlewareProphecy->process(
- Argument::type(ServerRequestInterface::class),
- Argument::type(RequestHandlerInterface::class)
- )->shouldHaveBeenCalled();
- $middlewareProphecy2->process(
- Argument::type(ServerRequestInterface::class),
- Argument::type(RequestHandlerInterface::class)
- )->shouldHaveBeenCalled();
-
- $this->assertSame($responseProphecy->reveal(), $response);
+ $this->assertSame('Hello World', (string)$response->getBody());
}
- public function testAddMiddlewareUsingDeferredResolution(): void
+ public function testAddMiddlewareUsingClosure(): void
{
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('Hello World');
+ $app = $this->createApp();
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
+ $middleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler) {
+ $response = $handler->handle($request);
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ return $response->withHeader('X-Foo', 'Foo');
+ };
- $middlewareProphecy = $this->prophesize(MiddlewareInterface::class);
- $middlewareProphecy->process(Argument::cetera())->willReturn($responseProphecy->reveal());
+ $app->add($middleware);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $containerProphecy->has('middleware')->willReturn(true);
- $containerProphecy->get('middleware')->willReturn($middlewareProphecy);
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
- $app = new App($responseFactoryProphecy->reveal(), $containerProphecy->reveal());
- $app->add('middleware');
$app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
+
return $response;
});
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
-
- $response = $app->handle($requestProphecy->reveal());
- $this->assertSame('Hello World', (string) $response->getBody());
- }
-
- public function testAddRoutingMiddleware(): void
- {
- /** @var ResponseFactoryInterface $responseFactory */
- $responseFactory = $this->prophesize(ResponseFactoryInterface::class)->reveal();
-
- // Create the app.
- $app = new App($responseFactory);
-
- // Add the routing middleware.
- $routingMiddleware = $app->addRoutingMiddleware();
-
- // Check that the routing middleware really has been added to the tip of the app middleware stack.
- $middlewareDispatcherProperty = new ReflectionProperty(App::class, 'middlewareDispatcher');
- $middlewareDispatcherProperty->setAccessible(true);
- /** @var MiddlewareDispatcher $middlewareDispatcher */
- $middlewareDispatcher = $middlewareDispatcherProperty->getValue($app);
-
- $tipProperty = new ReflectionProperty(MiddlewareDispatcher::class, 'tip');
- $tipProperty->setAccessible(true);
- /** @var RequestHandlerInterface $tip */
- $tip = $tipProperty->getValue($middlewareDispatcher);
-
- $reflection = new ReflectionClass($tip);
- $middlewareProperty = $reflection->getProperty('middleware');
- $middlewareProperty->setAccessible(true);
-
- $this->assertSame($routingMiddleware, $middlewareProperty->getValue($tip));
- $this->assertInstanceOf(RoutingMiddleware::class, $routingMiddleware);
- }
-
- public function testAddErrorMiddleware(): void
- {
- /** @var ResponseFactoryInterface $responseFactory */
- $responseFactory = $this->prophesize(ResponseFactoryInterface::class)->reveal();
-
- /** @var LoggerInterface $logger */
- $logger = $this->prophesize(LoggerInterface::class)->reveal();
-
- // Create the app.
- $app = new App($responseFactory);
-
- // Add the error middleware.
- $errorMiddleware = $app->addErrorMiddleware(true, true, true, $logger);
+ $response = $app->handle($request);
- // Check that the error middleware really has been added to the tip of the app middleware stack.
- $middlewareDispatcherProperty = new ReflectionProperty(App::class, 'middlewareDispatcher');
- $middlewareDispatcherProperty->setAccessible(true);
- /** @var MiddlewareDispatcher $middlewareDispatcher */
- $middlewareDispatcher = $middlewareDispatcherProperty->getValue($app);
-
- $tipProperty = new ReflectionProperty(MiddlewareDispatcher::class, 'tip');
- $tipProperty->setAccessible(true);
- /** @var RequestHandlerInterface $tip */
- $tip = $tipProperty->getValue($middlewareDispatcher);
-
- $reflection = new ReflectionClass($tip);
- $middlewareProperty = $reflection->getProperty('middleware');
- $middlewareProperty->setAccessible(true);
-
- $this->assertSame($errorMiddleware, $middlewareProperty->getValue($tip));
- $this->assertInstanceOf(ErrorMiddleware::class, $errorMiddleware);
- }
-
- public function testAddBodyParsingMiddleware(): void
- {
- /** @var ResponseFactoryInterface $responseFactory */
- $responseFactory = $this->prophesize(ResponseFactoryInterface::class)->reveal();
-
- // Create the app.
- $app = new App($responseFactory);
-
- // Add the error middleware.
- $bodyParsingMiddleware = $app->addBodyParsingMiddleware();
-
- // Check that the body parsing middleware really has been added to the tip of the app middleware stack.
- $middlewareDispatcherProperty = new ReflectionProperty(App::class, 'middlewareDispatcher');
- $middlewareDispatcherProperty->setAccessible(true);
- /** @var MiddlewareDispatcher $middlewareDispatcher */
- $middlewareDispatcher = $middlewareDispatcherProperty->getValue($app);
-
- $tipProperty = new ReflectionProperty(MiddlewareDispatcher::class, 'tip');
- $tipProperty->setAccessible(true);
- /** @var RequestHandlerInterface $tip */
- $tip = $tipProperty->getValue($middlewareDispatcher);
-
- $reflection = new ReflectionClass($tip);
- $middlewareProperty = $reflection->getProperty('middleware');
- $middlewareProperty->setAccessible(true);
-
- $this->assertSame($bodyParsingMiddleware, $middlewareProperty->getValue($tip));
- $this->assertInstanceOf(BodyParsingMiddleware::class, $bodyParsingMiddleware);
+ $this->assertSame('Hello World', (string)$response->getBody());
+ $this->assertSame('Foo', $response->getHeaderLine('X-Foo'));
}
public function testAddMiddlewareOnRoute(): void
{
- $output = '';
-
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('Hello World');
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ $app = $this->createApp();
- $middlewareProphecy = $this->prophesize(MiddlewareInterface::class);
- $middlewareProphecy->process(
- Argument::type(ServerRequestInterface::class),
- Argument::type(RequestHandlerInterface::class)
- )->will(function ($args) use (&$output) {
- /** @var ServerRequestInterface $request */
- $request = $args[0];
-
- /** @var RequestHandlerInterface $handler */
- $handler = $args[1];
-
- $output .= 'In1';
-
- /** @var ResponseInterface $response */
+ $middleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler) {
$response = $handler->handle($request);
-
- $output .= 'Out1';
+ $response->getBody()->write('_MW1_');
return $response;
- });
+ };
- $middlewareProphecy2 = $this->prophesize(MiddlewareInterface::class);
- $middlewareProphecy2->process(
- Argument::type(ServerRequestInterface::class),
- Argument::type(RequestHandlerInterface::class)
- )->will(function ($args) use (&$output) {
- /** @var ServerRequestInterface $request */
- $request = $args[0];
+ $middleware2 = function (ServerRequestInterface $request, RequestHandlerInterface $handler) {
+ $response = $handler->handle($request);
+ $response->getBody()->write('_MW2_');
- /** @var RequestHandlerInterface $handler */
- $handler = $args[1];
+ return $response;
+ };
- $output .= 'In2';
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- /** @var ResponseInterface $response */
+ // This middleware should not be invoked
+ $middleware3 = function (ServerRequestInterface $request, RequestHandlerInterface $handler) {
$response = $handler->handle($request);
-
- $output .= 'Out2';
+ $response->getBody()->write('_MW3_');
return $response;
- });
+ };
+ $app->add($middleware3);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ // Add two middlewares
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('_ROUTE1_');
- $app = new App($responseFactoryProphecy->reveal());
- $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) use (&$output) {
- $output .= 'Center';
return $response;
})
- ->add($middlewareProphecy->reveal())
- ->addMiddleware($middlewareProphecy2->reveal());
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
+ ->add($middleware)
+ ->add($middleware2);
- $app->handle($requestProphecy->reveal());
+ $response = $app->handle($request);
- $this->assertSame('In2In1CenterOut1Out2', $output);
+ $this->assertSame('_ROUTE1__MW2__MW1_', (string)$response->getBody());
}
public function testAddMiddlewareOnRouteGroup(): void
{
- $output = '';
-
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('Hello World');
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ $app = $this->createApp();
- $middlewareProphecy = $this->prophesize(MiddlewareInterface::class);
- $middlewareProphecy->process(
- Argument::type(ServerRequestInterface::class),
- Argument::type(RequestHandlerInterface::class)
- )->will(function ($args) use (&$output) {
- /** @var ServerRequestInterface $request */
- $request = $args[0];
+ $trace = new SplStack();
- /** @var RequestHandlerInterface $handler */
- $handler = $args[1];
+ $authMiddleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler) use ($trace) {
+ $trace->push('_AUTH_');
- $output .= 'In1';
+ return $handler->handle($request);
+ };
- /** @var ResponseInterface $response */
+ $outgoingMiddleware = function (
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler
+ ) use ($trace) {
$response = $handler->handle($request);
-
- $output .= 'Out1';
+ $response->getBody()->write('_OUTGOING_');
+ $trace->push('_OUTGOING_');
return $response;
- });
-
- $middlewareProphecy2 = $this->prophesize(MiddlewareInterface::class);
- $middlewareProphecy2->process(
- Argument::type(ServerRequestInterface::class),
- Argument::type(RequestHandlerInterface::class)
- )->will(function ($args) use (&$output) {
- /** @var ServerRequestInterface $request */
- $request = $args[0];
-
- /** @var RequestHandlerInterface $handler */
- $handler = $args[1];
-
- $output .= 'In2';
+ };
- /** @var ResponseInterface $response */
- $response = $handler->handle($request);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $output .= 'Out2';
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/api/users');
- return $response;
- });
+ // Add middleware to group
+ $app->group('/api', function (RouteGroup $group) use ($trace) {
+ $group->get('/users', function (
+ ServerRequestInterface $request,
+ ResponseInterface $response
+ ) use ($trace) {
+ $trace->push('_ROUTE1_');
+ $response->getBody()->write('_ROUTE1_');
- $app = new App($responseFactoryProphecy->reveal());
- $app->group('/foo', function (RouteCollectorProxy $proxy) use (&$output) {
- $proxy->get('/bar', function (ServerRequestInterface $request, ResponseInterface $response) use (&$output) {
- $output .= 'Center';
return $response;
});
- })
- ->add($middlewareProphecy->reveal())
- ->addMiddleware($middlewareProphecy2->reveal());
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/foo/bar');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
+ })->add($authMiddleware)->add($outgoingMiddleware);
- $app->handle($requestProphecy->reveal());
+ $response = $app->handle($request);
- $this->assertSame('In2In1CenterOut1Out2', $output);
+ $this->assertSame('_ROUTE1__OUTGOING_', (string)$response->getBody());
+ $this->assertSame(
+ [
+ 2 => '_OUTGOING_',
+ 1 => '_ROUTE1_',
+ 0 => '_AUTH_',
+ ],
+ iterator_to_array($trace)
+ );
}
public function testAddMiddlewareOnTwoRouteGroup(): void
{
- $output = '';
-
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('Hello World');
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ $app = $this->createApp();
- $middlewareProphecy = $this->prophesize(MiddlewareInterface::class);
- $middlewareProphecy->process(
- Argument::type(ServerRequestInterface::class),
- Argument::type(RequestHandlerInterface::class)
- )->will(function ($args) use (&$output) {
- /** @var ServerRequestInterface $request */
- $request = $args[0];
+ $trace = new SplStack();
- /** @var RequestHandlerInterface $handler */
- $handler = $args[1];
-
- $output .= 'In1';
-
- /** @var ResponseInterface $response */
+ $authMiddleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler) use ($trace) {
+ $trace->push('_AUTH_');
$response = $handler->handle($request);
- $output .= 'Out1';
-
return $response;
- });
-
- $middlewareProphecy2 = $this->prophesize(MiddlewareInterface::class);
- $middlewareProphecy2->process(
- Argument::type(ServerRequestInterface::class),
- Argument::type(RequestHandlerInterface::class)
- )->will(function ($args) use (&$output) {
- /** @var ServerRequestInterface $request */
- $request = $args[0];
-
- /** @var RequestHandlerInterface $handler */
- $handler = $args[1];
-
- $output .= 'In2';
+ };
- /** @var ResponseInterface $response */
+ $usersMiddleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler) use ($trace) {
+ $trace->push('_USERS_');
$response = $handler->handle($request);
-
- $output .= 'Out2';
+ $response->getBody()->write('_USERS_');
return $response;
- });
-
- $middlewareProphecy3 = $this->prophesize(MiddlewareInterface::class);
- $middlewareProphecy3->process(
- Argument::type(ServerRequestInterface::class),
- Argument::type(RequestHandlerInterface::class)
- )->will(function ($args) use (&$output) {
- /** @var ServerRequestInterface $request */
- $request = $args[0];
+ };
- /** @var RequestHandlerInterface $handler */
- $handler = $args[1];
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $output .= 'In3';
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/api/users/123');
- /** @var ResponseInterface $response */
- $response = $handler->handle($request);
+ // Add middleware to groups
+ $app->group('/api', function (RouteGroup $group) use ($usersMiddleware, $trace) {
+ $group->group('/users', function (RouteGroup $group) use ($trace) {
+ $group->get(
+ '/{id}',
+ function (ServerRequestInterface $request, ResponseInterface $response) use ($trace) {
+ $trace->push('_ROUTE1_');
+ $response->getBody()->write('_ROUTE1_');
- $output .= 'Out3';
+ return $response;
+ }
+ );
+ })->add($usersMiddleware);
+ })->add($authMiddleware);
- return $response;
- });
+ $response = $app->handle($request);
- $app = new App($responseFactoryProphecy->reveal());
- $app->group('/foo', function (RouteCollectorProxyInterface $group) use (
- $middlewareProphecy2,
- $middlewareProphecy3,
- &$output
- ) {
- // ensure that more than one nested group at the same level doesn't break middleware
- $group->group('/fizz', function (RouteCollectorProxyInterface $group) {
- $group->get('/buzz', function (ServerRequestInterface $request, ResponseInterface $response) {
- return $response;
- });
- });
-
- $group->group('/bar', function (RouteCollectorProxyInterface $group) use (
- $middlewareProphecy3,
- &$output
- ) {
- $group->get('/baz', function (
- ServerRequestInterface $request,
- ResponseInterface $response
- ) use (&$output) {
- $output .= 'Center';
- return $response;
- })->add($middlewareProphecy3->reveal());
- })->add($middlewareProphecy2->reveal());
- })->add($middlewareProphecy->reveal());
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/foo/bar/baz');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
+ $this->assertSame('_ROUTE1__USERS_', (string)$response->getBody());
- $app->handle($requestProphecy->reveal());
-
- $this->assertSame('In1In2In3CenterOut3Out2Out1', $output);
- }
-
- public function testAddMiddlewareAsStringNotImplementingInterfaceThrowsException(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage(
- 'A middleware must be an object/class name referencing an implementation of ' .
- 'MiddlewareInterface or a callable with a matching signature.'
+ $this->assertSame(
+ [
+ 2 => '_ROUTE1_',
+ 1 => '_USERS_',
+ 0 => '_AUTH_',
+ ],
+ iterator_to_array($trace)
);
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $app = new App($responseFactoryProphecy->reveal());
- $app->add(new stdClass());
}
- /********************************************************************************
- * Runner
- *******************************************************************************/
-
public function testInvokeReturnMethodNotAllowed(): void
{
$this->expectException(HttpMethodNotAllowedException::class);
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $app = new App($responseFactoryProphecy->reveal());
- $app->get('/', function () {
- });
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
+ $app = $this->createApp();
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('POST');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
-
- $app->handle($requestProphecy->reveal());
- }
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('POST', '/');
- public function testInvokeWithMatchingRoute(): void
- {
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('Hello World');
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
-
- $app = new App($responseFactoryProphecy->reveal());
- $app->get('/', function (ServerRequestInterface $request, $response) {
- return $response;
- });
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
-
- $response = $app->handle($requestProphecy->reveal());
-
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertSame('Hello World', (string) $response->getBody());
- }
-
- public function testInvokeWithMatchingRouteWithSetArgument(): void
- {
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
-
- $app = new App($responseFactoryProphecy->reveal());
- $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
- $response->getBody()->write("Hello {$args['name']}");
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
return $response;
- })->setArgument('name', 'World');
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
});
- $response = $app->handle($requestProphecy->reveal());
-
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertSame('Hello World', (string) $response->getBody());
+ $app->handle($request);
}
- public function testInvokeWithMatchingRouteWithSetArguments(): void
+ public function testInvokeWithMatchingRoute(): void
{
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
+ $app = $this->createApp();
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
- $app = new App($responseFactoryProphecy->reveal());
- $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
- $response->getBody()->write("{$args['greeting']} {$args['name']}");
return $response;
- })->setArguments(['greeting' => 'Hello', 'name' => 'World']);
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
});
- $response = $app->handle($requestProphecy->reveal());
+ $response = $app->handle($request);
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertSame('Hello World', (string) $response->getBody());
+ $this->assertSame('Hello World', (string)$response->getBody());
}
public function testInvokeWithMatchingRouteWithNamedParameterRequestResponseStrategy(): void
{
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
-
- $app = new App($responseFactoryProphecy->reveal());
- $app->get('/Hello/{name}', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
- $response->getBody()->write("Hello {$args['name']}");
- return $response;
- });
+ $app = $this->createApp();
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/Hello/World');
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/hello/john');
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
+ $app->get(
+ '/hello/{name}',
+ function (ServerRequestInterface $request, ResponseInterface $response, $args) {
+ $this->get(App::class);
+ $response->getBody()->write("Hello {$args['name']}");
- $response = $app->handle($requestProphecy->reveal());
+ return $response;
+ }
+ );
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertSame('Hello World', (string) $response->getBody());
+ $response = $app->handle($request);
+ $this->assertSame('Hello john', (string)$response->getBody());
}
public function testInvokeWithMatchingRouteWithNamedParameterRequestResponseArgStrategy(): void
{
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
+ $builder = new AppBuilder();
+ $builder->addDefinitions([
+ RequestHandlerInvocationStrategyInterface::class => fn () => new RequestResponseArgs(),
+ ]);
+ $app = $builder->build();
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/hello/john');
- $app = new App($responseFactoryProphecy->reveal());
- $app->getRouteCollector()->setDefaultInvocationStrategy(new RequestResponseArgs());
- $app->get('/Hello/{name}', function (ServerRequestInterface $request, ResponseInterface $response, $name) {
- $response->getBody()->write("Hello {$name}");
- return $response;
- });
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/Hello/World');
+ $app->get(
+ '/hello/{name}',
+ function (ServerRequestInterface $request, ResponseInterface $response, $name) {
+ $response->getBody()->write("Hello {$name}");
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
+ return $response;
+ }
+ );
- $response = $app->handle($requestProphecy->reveal());
+ $response = $app->handle($request);
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertSame('Hello World', (string) $response->getBody());
+ $this->assertSame('Hello john', (string)$response->getBody());
}
public function testInvokeWithMatchingRouteWithNamedParameterRequestResponseNamedArgsStrategy(): void
{
- if (PHP_VERSION_ID < 80000) {
- $this->markTestSkipped('Named arguments are not supported in PHP versions prior to 8.0');
- }
+ $builder = new AppBuilder();
+ $builder->addDefinitions([
+ RequestHandlerInvocationStrategyInterface::class => fn () => new RequestResponseNamedArgs(),
+ ]);
+ $app = $builder->build();
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/hello/john');
- $app = new App($responseFactoryProphecy->reveal());
- $app->getRouteCollector()->setDefaultInvocationStrategy(new RequestResponseNamedArgs());
$app->get(
- '/{greeting}/{name}',
- function (ServerRequestInterface $request, ResponseInterface $response, $name, $greeting) {
- $response->getBody()->write("{$greeting} {$name}");
+ '/hello/{name}',
+ function (ServerRequestInterface $request, ResponseInterface $response, $name) {
+ $response->getBody()->write("Hello {$name}");
+
return $response;
}
);
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/Hello/World');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
-
- $response = $app->handle($requestProphecy->reveal());
-
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertSame('Hello World', (string) $response->getBody());
- }
-
- public function testInvokeWithMatchingRouteWithNamedParameterOverwritesSetArgument(): void
- {
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
-
- $app = new App($responseFactoryProphecy->reveal());
- $app->get('/Hello/{name}', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
- $response->getBody()->write("Hello {$args['name']}");
- return $response;
- })->setArgument('name', 'World!');
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/Hello/World');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
-
- $response = $app->handle($requestProphecy->reveal());
+ $response = $app->handle($request);
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertSame('Hello World', (string) $response->getBody());
+ $this->assertSame('Hello john', (string)$response->getBody());
}
public function testInvokeWithoutMatchingRoute(): void
{
$this->expectException(HttpNotFoundException::class);
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $app = new App($responseFactoryProphecy->reveal());
+ $app = $this->createApp();
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/nada');
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ return $response;
});
- $app->handle($requestProphecy->reveal());
+ $app->handle($request);
}
public function testInvokeWithCallableRegisteredInContainer(): void
{
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('Hello World');
+ $builder = new AppBuilder();
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
-
- $handler = new Class
- {
+ $handler = new class {
public function foo(ServerRequestInterface $request, ResponseInterface $response)
{
+ $response->getBody()->write('Hello handler:foo');
+
return $response;
}
};
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $containerProphecy->has('handler')->willReturn(true);
- $containerProphecy->get('handler')->willReturn($handler);
+ $builder->addDefinitions([
+ 'handler' => $handler,
+ ]);
- $app = new App($responseFactoryProphecy->reveal(), $containerProphecy->reveal());
- $app->get('/', 'handler:foo');
+ $app = $builder->build();
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $app->get('/', 'handler:foo');
- $response = $app->handle($requestProphecy->reveal());
+ $response = $app->handle($request);
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertSame('Hello World', (string) $response->getBody());
+ $this->assertSame('Hello handler:foo', (string)$response->getBody());
}
- public function testInvokeWithNonExistentMethodOnCallableRegisteredInContainer(): void
+ public function testInvokeWithCallableRegisteredInContainerAsFunction(): void
{
- $this->expectException(RuntimeException::class);
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
+ $builder = new AppBuilder();
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ $handler = new class {
+ public function foo(ServerRequestInterface $request, ResponseInterface $response)
+ {
+ $response->getBody()->write('Hello handler:foo');
- $handler = new Class
- {
+ return $response;
+ }
};
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $containerProphecy->has('handler')->willReturn(true);
- $containerProphecy->get('handler')->willReturn($handler);
+ $builder->addDefinitions([
+ 'handler' => function () use ($handler) {
+ return $handler;
+ },
+ ]);
- $app = new App($responseFactoryProphecy->reveal(), $containerProphecy->reveal());
- $app->get('/', 'handler:method_does_not_exist');
+ $app = $builder->build();
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())
- ->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $app->handle($requestProphecy->reveal());
- }
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
- public function testInvokeWithCallableInContainerViaCallMagicMethod(): void
- {
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
+ $app->get('/', 'handler:foo');
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ $response = $app->handle($request);
- $mockAction = new MockAction();
+ $this->assertSame('Hello handler:foo', (string)$response->getBody());
+ }
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $containerProphecy->has('handler')->willReturn(true);
- $containerProphecy->get('handler')->willReturn($mockAction);
+ public function testInvokeWithNonExistentMethodOnCallableRegisteredInContainer(): void
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('The method "method_does_not_exist" does not exists');
- $app = new App($responseFactoryProphecy->reveal(), $containerProphecy->reveal());
- $app->get('/', 'handler:foo');
+ $builder = new AppBuilder();
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
+ $builder->addDefinitions([
+ 'handler' => new class {
+ public function foo()
+ {
+ }
+ },
+ ]);
+ $app = $builder->build();
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $response = $app->handle($requestProphecy->reveal());
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
- $expectedPayload = json_encode(['name' => 'foo', 'arguments' => []]);
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertSame($expectedPayload, (string) $response->getBody());
+ $app->get('/', 'handler:method_does_not_exist');
+ $app->handle($request);
}
public function testInvokeFunctionName(): void
{
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
+ $builder = new AppBuilder();
+ $app = $builder->build();
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
// @codingStandardsIgnoreStart
function handle($request, ResponseInterface $response)
{
$response->getBody()->write('Hello World');
+
return $response;
}
// @codingStandardsIgnoreEnd
- $app = new App($responseFactoryProphecy->reveal());
$app->get('/', __NAMESPACE__ . '\handle');
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
+ $response = $app->handle($request);
- $response = $app->handle($requestProphecy->reveal());
-
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertSame('Hello World', (string) $response->getBody());
+ $this->assertSame('Hello World', (string)$response->getBody());
}
- public function testCurrentRequestAttributesAreNotLostWhenAddingRouteArguments(): void
+ public function testAddMiddleware(): void
{
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
+ $builder = new AppBuilder();
+ $app = $builder->build();
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
+ $container = $app->getContainer();
+ $routing = $container->get(RoutingMiddleware::class);
+ $endpoint = $container->get(EndpointMiddleware::class);
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ $app->addMiddleware($routing);
+ $app->add($endpoint);
- $app = new App($responseFactoryProphecy->reveal());
- $app->get('/Hello/{name}', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
- $response->getBody()->write($request->getAttribute('greeting') . ' ' . $args['name']);
- return $response;
- });
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/Hello/World');
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
+ return $response;
});
- $response = $app->handle($requestProphecy->reveal()->withAttribute('greeting', 'Hello'));
-
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertSame('Hello World', (string) $response->getBody());
+ $app->run($request);
+ $this->expectOutputString('Hello World');
}
- public function testCurrentRequestAttributesAreNotLostWhenAddingRouteArgumentsRequestResponseArg(): void
+ public function testGetBasePath(): void
{
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
-
- $app = new App($responseFactoryProphecy->reveal());
- $app->getRouteCollector()->setDefaultInvocationStrategy(new RequestResponseArgs());
- $app->get('/Hello/{name}', function (ServerRequestInterface $request, ResponseInterface $response, $name) {
- $response->getBody()->write($request->getAttribute('greeting') . ' ' . $name);
- return $response;
- });
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/Hello/World');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
+ $builder = new AppBuilder();
+ $app = $builder->build();
- $response = $app->handle($requestProphecy->reveal()->withAttribute('greeting', 'Hello'));
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertSame('Hello World', (string) $response->getBody());
+ $this->assertSame('', $app->getBasePath());
+ $app->setBasePath('/sub');
+ $this->assertSame('/sub', $app->getBasePath());
}
public function testRun(): void
{
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
- $streamProphecy->read(1)->willReturn('_');
- $streamProphecy->read('11')->will(function () {
- $this->eof()->willReturn(true);
- return $this->reveal()->__toString();
- });
- $streamProphecy->eof()->willReturn(false);
- $streamProphecy->isSeekable()->willReturn(true);
- $streamProphecy->rewind()->shouldBeCalled();
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
- $responseProphecy->getStatusCode()->willReturn(200);
- $responseProphecy->getHeaders()->willReturn(['Content-Length' => ['11']]);
- $responseProphecy->getProtocolVersion()->willReturn('1.1');
- $responseProphecy->getReasonPhrase()->willReturn('');
- $responseProphecy->getHeaderLine('Content-Length')->willReturn('11');
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
-
- $app = new App($responseFactoryProphecy->reveal());
- $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
- $response->getBody()->write('Hello World');
- return $response;
- });
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
+ $builder = new AppBuilder();
+ $app = $builder->build();
- $app->run($requestProphecy->reveal());
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $this->expectOutputString('Hello World');
- }
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
- public function testRunWithoutPassingInServerRequest(): void
- {
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
- $streamProphecy->read(1)->willReturn('_');
- $streamProphecy->read(11)->will(function () {
- $this->eof()->willReturn(true);
- return $this->reveal()->__toString();
- });
- $streamProphecy->eof()->willReturn(false);
- $streamProphecy->isSeekable()->willReturn(true);
- $streamProphecy->rewind()->shouldBeCalled();
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
- $responseProphecy->getStatusCode()->willReturn(200);
- $responseProphecy->getHeaders()->willReturn(['Content-Length' => ['11']]);
- $responseProphecy->getProtocolVersion()->willReturn('1.1');
- $responseProphecy->getReasonPhrase()->willReturn('');
- $responseProphecy->getHeaderLine('Content-Length')->willReturn('11');
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
-
- $app = new App($responseFactoryProphecy->reveal());
$app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
$response->getBody()->write('Hello World');
+
return $response;
});
- $app->run();
-
+ $app->run($request);
$this->expectOutputString('Hello World');
}
- public function testHandleReturnsEmptyResponseBodyWithHeadRequestMethod(): void
+ public function testRunWithoutPassingInServerRequest(): void
{
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
- $responseProphecy
- ->withBody(Argument::type(StreamInterface::class))
- ->will(function ($args) {
- $this->getBody()->willReturn($args[0]);
- return $this;
- });
-
- $emptyStreamProphecy = $this->prophesize(StreamInterface::class);
- $emptyStreamProphecy->__toString()->willReturn('');
- $emptyResponseProphecy = $this->prophesize(ResponseInterface::class);
- $emptyResponseProphecy->getBody()->willReturn($emptyStreamProphecy->reveal());
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn(
- $responseProphecy->reveal(),
- $emptyResponseProphecy->reveal()
+ $builder = new AppBuilder();
+
+ $builder->addDefinitions(
+ [
+ ServerRequestCreatorInterface::class => function () {
+ return new class implements ServerRequestCreatorInterface {
+ public function createServerRequestFromGlobals(): ServerRequestInterface
+ {
+ return new Request(
+ 'GET',
+ new Uri('http', 'localhost', 80, '/'),
+ new Headers(),
+ [],
+ [],
+ new Stream(fopen('php://memory', 'w+'))
+ );
+ }
+ };
+ },
+ ]
);
- $called = 0;
- $app = new App($responseFactoryProphecy->reveal());
- $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) use (&$called) {
- $called++;
- $response->getBody()->write('Hello World');
- return $response;
- });
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('HEAD');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
-
- $response = $app->handle($requestProphecy->reveal());
-
- $this->assertSame(1, $called);
- $this->assertEmpty((string) $response->getBody());
- }
-
- public function testCanBeReExecutedRecursivelyDuringDispatch(): void
- {
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
-
- $responseHeaders = [];
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
- $responseProphecy->getStatusCode()->willReturn(200);
- $responseProphecy->getHeader(Argument::type('string'))->will(function ($args) use (&$responseHeaders) {
- return $responseHeaders[$args[0]];
- });
- $responseProphecy->withAddedHeader(
- Argument::type('string'),
- Argument::type('string')
- )->will(function ($args) use (&$responseHeaders) {
- $key = $args[0];
- $value = $args[1];
- if (!isset($responseHeaders[$key])) {
- $responseHeaders[$key] = [];
- }
- $responseHeaders[$key][] = $value;
- return $this;
- });
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy
- ->createResponse(Argument::type('integer'))
- ->will(function ($args) use ($responseProphecy) {
- $responseProphecy->getStatusCode()->willReturn($args[0]);
- return $responseProphecy->reveal();
- });
-
- $app = new App($responseFactoryProphecy->reveal());
-
- $middlewareProphecy = $this->prophesize(MiddlewareInterface::class);
- $middlewareProphecy->process(
- Argument::type(ServerRequestInterface::class),
- Argument::type(RequestHandlerInterface::class)
- )->will(function ($args) use ($app, $responseFactoryProphecy) {
- /** @var ServerRequestInterface $request */
- $request = $args[0];
-
- if ($request->hasHeader('X-NESTED')) {
- return $responseFactoryProphecy
- ->reveal()
- ->createResponse(204)
- ->withAddedHeader('X-TRACE', 'nested');
- }
-
- /** @var ResponseInterface $response */
- $response = $app->handle($request->withAddedHeader('X-NESTED', '1'));
- return $response->withAddedHeader('X-TRACE', 'outer');
- });
-
- $middlewareProphecy2 = $this->prophesize(MiddlewareInterface::class);
- $middlewareProphecy2->process(
- Argument::type(ServerRequestInterface::class),
- Argument::type(RequestHandlerInterface::class)
- )->will(function ($args) {
- /** @var ServerRequestInterface $request */
- $request = $args[0];
-
- /** @var RequestHandlerInterface $handler */
- $handler = $args[1];
+ $app = $builder->build();
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- /** @var ResponseInterface $response */
- $response = $handler->handle($request);
- $response->getBody()->write('1');
-
- return $response;
- });
-
- $app
- ->add($middlewareProphecy->reveal())
- ->add($middlewareProphecy2->reveal());
$app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
- return $response;
- });
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
-
- $responseHeaders = [];
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->hasHeader(Argument::type('string'))->will(function ($args) use (&$responseHeaders) {
- return array_key_exists($args[0], $responseHeaders);
- });
- $requestProphecy->withAddedHeader(
- Argument::type('string'),
- Argument::type('string')
- )->will(function ($args) use (&$responseHeaders) {
- $key = $args[0];
- $value = $args[1];
- if (!isset($responseHeaders[$key])) {
- $responseHeaders[$key] = [];
- }
- $responseHeaders[$key][] = $value;
- return $this;
- });
-
- $response = $app->handle($requestProphecy->reveal());
-
- $this->assertSame(204, $response->getStatusCode());
- $this->assertSame(['nested', 'outer'], $response->getHeader('X-TRACE'));
- $this->assertSame('11', (string) $response->getBody());
- }
-
- // TODO: Re-add testUnsupportedMethodWithoutRoute
-
- // TODO: Re-add testUnsupportedMethodWithRoute
-
- public function testContainerSetToRoute(): void
- {
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('Hello World');
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
-
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $containerProphecy->has('handler')->willReturn(true);
- $containerProphecy->get('handler')->willReturn(function () use ($responseProphecy) {
- return $responseProphecy->reveal();
- });
-
- $app = new App($responseFactoryProphecy->reveal(), $containerProphecy->reveal());
- $routeCollector = $app->getRouteCollector();
- $routeCollector->map(['GET'], '/', 'handler');
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
-
- $response = $app->handle($requestProphecy->reveal());
-
- $this->assertSame('Hello World', (string) $response->getBody());
- }
-
- public function testAppIsARequestHandler(): void
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $app = new App($responseFactoryProphecy->reveal());
-
- $this->assertInstanceOf(RequestHandlerInterface::class, $app);
- }
-
- public function testInvokeSequentialProcessToAPathWithOptionalArgsAndWithoutOptionalArgs(): void
- {
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ $response->getBody()->write('Hello World');
- $app = new App($responseFactoryProphecy->reveal());
- $app->get('/Hello[/{name}]', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
- $response->getBody()->write((string) count($args));
return $response;
});
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/Hello/World');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
-
- $response = $app->handle($requestProphecy->reveal());
- $this->assertSame('1', (string) $response->getBody());
-
- $uriProphecy2 = $this->prophesize(UriInterface::class);
- $uriProphecy2->getPath()->willReturn('/Hello');
-
- $requestProphecy2 = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy2->getMethod()->willReturn('GET');
- $requestProphecy2->getUri()->willReturn($uriProphecy2->reveal());
- $requestProphecy2->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy2->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
+ $app->run();
- $streamProphecy->__toString()->willReturn('');
- $response = $app->handle($requestProphecy2->reveal());
- $this->assertSame('0', (string) $response->getBody());
+ $this->expectOutputString('Hello World');
}
- public function testInvokeSequentialProcessToAPathWithOptionalArgsAndWithoutOptionalArgsAndKeepSetedArgs(): void
+ public function testHandleReturnsEmptyResponseBodyWithHeadRequestMethod(): void
{
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $app->add(HeadMethodMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('HEAD', '/');
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
- $app = new App($responseFactoryProphecy->reveal());
- $app->get('/Hello[/{name}]', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
- $response->getBody()->write((string) count($args));
return $response;
- })->setArgument('extra', 'value');
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/Hello/World');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
});
- $response = $app->handle($requestProphecy->reveal());
- $this->assertSame('2', (string) $response->getBody());
-
- $uriProphecy2 = $this->prophesize(UriInterface::class);
- $uriProphecy2->getPath()->willReturn('/Hello');
-
- $requestProphecy2 = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy2->getMethod()->willReturn('GET');
- $requestProphecy2->getUri()->willReturn($uriProphecy2->reveal());
- $requestProphecy2->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy2->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
- });
-
- $streamProphecy->__toString()->willReturn('');
- $response = $app->handle($requestProphecy2->reveal());
- $this->assertSame('1', (string) $response->getBody());
+ $response = $app->handle($request);
+ $this->assertEmpty((string)$response->getBody());
}
- public function testInvokeSequentialProcessAfterAddingAnotherRouteArgument(): void
+ public function testInvokeSequentialProcessToAPathWithOptionalArgsAndWithoutOptionalArgs(): void
{
- $streamProphecy = $this->prophesize(StreamInterface::class);
- $streamProphecy->__toString()->willReturn('');
- $streamProphecy->write(Argument::type('string'))->will(function ($args) {
- $body = $this->reveal()->__toString();
- $body .= $args[0];
- $this->__toString()->willReturn($body);
- return 0;
- });
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getBody()->willReturn($streamProphecy->reveal());
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/hello/friend');
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy->createResponse()->willReturn($responseProphecy->reveal());
+ $app->get('/hello[/{name}]', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
+ $response->getBody()->write((string)count($args));
- $app = new App($responseFactoryProphecy->reveal());
- $route = $app->get('/Hello[/{name}]', function (
- ServerRequestInterface $request,
- ResponseInterface $response,
- $args
- ) {
- $response->getBody()->write((string) count($args));
return $response;
- })->setArgument('extra', 'value');
-
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy->getPath()->willReturn('/Hello/World');
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getMethod()->willReturn('GET');
- $requestProphecy->getUri()->willReturn($uriProphecy->reveal());
- $requestProphecy->getAttribute(RouteContext::ROUTE)->willReturn($route);
- $requestProphecy->getAttribute(RouteContext::ROUTING_RESULTS)->willReturn(null);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) {
- $this->getAttribute($args[0])->willReturn($args[1]);
- return $this;
});
- $response = $app->handle($requestProphecy->reveal());
- $this->assertSame('2', (string) $response->getBody());
+ $response = $app->handle($request);
+ $this->assertSame('1', (string)$response->getBody());
- $route->setArgument('extra2', 'value2');
+ // 2. test without value
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/hello');
- $streamProphecy->__toString()->willReturn('');
- $response = $app->handle($requestProphecy->reveal());
- $this->assertSame('3', (string) $response->getBody());
+ $response = $app->handle($request);
+ $this->assertSame('0', (string)$response->getBody());
}
}
diff --git a/tests/Assets/HeaderStack.php b/tests/Assets/HeaderStack.php
deleted file mode 100644
index 82664df55..000000000
--- a/tests/Assets/HeaderStack.php
+++ /dev/null
@@ -1,93 +0,0 @@
-setSettings([
+ 'key' => 'value',
+ ]);
+ $app = $builder->build();
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
+ $response->getBody()->write($this->get('settings')['key']);
+
+ return $response;
+ });
+
+ $response = $app->handle($request);
+ $this->assertSame('value', (string)$response->getBody());
+ }
+
+ public function testSetSettingsMerged(): void
+ {
+ $builder = new AppBuilder();
+ $builder->setSettings([
+ 'key' => 'value',
+ 'key2' => 'value2',
+ ]);
+ $builder->setSettings([
+ 'key' => 'value3',
+ ]);
+ $app = $builder->build();
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $settings = $this->get('settings');
+ $response->getBody()->write(json_encode($settings));
+
+ return $response;
+ });
+
+ $response = $app->handle($request);
+ $this->assertSame('{"key":"value3"}', (string)$response->getBody());
+ }
+
+ public function testSetContainerFactory(): void
+ {
+ $builder = new AppBuilder();
+ $builder->setContainerFactory(function () {
+ $defaults = (new DefaultDefinitions())->__invoke();
+ $defaults = array_merge($defaults, (new HttpDefinitions())->__invoke());
+
+ $defaults['foo'] = 'bar';
+
+ return new Container($defaults);
+ });
+ $app = $builder->build();
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write($this->get('foo'));
+
+ return $response;
+ });
+
+ $response = $app->handle($request);
+ $this->assertSame('bar', (string)$response->getBody());
+ }
+
+ public function testMiddlewareOrderFifo(): void
+ {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('OK');
+
+ return $response;
+ });
+
+ $response = $app->handle($request);
+ $this->assertSame('OK', (string)$response->getBody());
+ }
+}
diff --git a/tests/CallableResolverTest.php b/tests/CallableResolverTest.php
deleted file mode 100644
index 573121a25..000000000
--- a/tests/CallableResolverTest.php
+++ /dev/null
@@ -1,551 +0,0 @@
-containerProphecy = $this->prophesize(ContainerInterface::class);
- $this->containerProphecy->has(Argument::type('string'))->willReturn(false);
- }
-
- public function testClosure(): void
- {
- $test = function () {
- return true;
- };
- $resolver = new CallableResolver(); // No container injected
- $callable = $resolver->resolve($test);
- $callableRoute = $resolver->resolveRoute($test);
- $callableMiddleware = $resolver->resolveMiddleware($test);
-
- $this->assertTrue($callable());
- $this->assertTrue($callableRoute());
- $this->assertTrue($callableMiddleware());
- }
-
- public function testClosureContainer(): void
- {
- $this->containerProphecy->has('ultimateAnswer')->willReturn(true);
- $this->containerProphecy->get('ultimateAnswer')->willReturn(42);
-
- $that = $this;
- $test = function () use ($that) {
- $that->assertInstanceOf(ContainerInterface::class, $this);
-
- /** @var ContainerInterface $this */
- return $this->get('ultimateAnswer');
- };
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $callable = $resolver->resolve($test);
- $callableRoute = $resolver->resolveRoute($test);
- $callableMiddleware = $resolver->resolveMiddleware($test);
-
- $this->assertSame(42, $callable());
- $this->assertSame(42, $callableRoute());
- $this->assertSame(42, $callableMiddleware());
- }
-
- public function testFunctionName(): void
- {
- $resolver = new CallableResolver(); // No container injected
- $callable = $resolver->resolve(__NAMESPACE__ . '\testAdvancedCallable');
- $callableRoute = $resolver->resolveRoute(__NAMESPACE__ . '\testAdvancedCallable');
- $callableMiddleware = $resolver->resolveMiddleware(__NAMESPACE__ . '\testAdvancedCallable');
-
- $this->assertTrue($callable());
- $this->assertTrue($callableRoute());
- $this->assertTrue($callableMiddleware());
- }
-
- public function testObjMethodArray(): void
- {
- $obj = new CallableTest();
- $resolver = new CallableResolver(); // No container injected
- $callable = $resolver->resolve([$obj, 'toCall']);
- $callableRoute = $resolver->resolveRoute([$obj, 'toCall']);
- $callableMiddleware = $resolver->resolveMiddleware([$obj, 'toCall']);
-
- $callable();
- $this->assertSame(1, CallableTest::$CalledCount);
-
- $callableRoute();
- $this->assertSame(2, CallableTest::$CalledCount);
-
- $callableMiddleware();
- $this->assertSame(3, CallableTest::$CalledCount);
- }
-
- public function testSlimCallable(): void
- {
- $resolver = new CallableResolver(); // No container injected
- $callable = $resolver->resolve('Slim\Tests\Mocks\CallableTest:toCall');
- $callableRoute = $resolver->resolveRoute('Slim\Tests\Mocks\CallableTest:toCall');
- $callableMiddleware = $resolver->resolveMiddleware('Slim\Tests\Mocks\CallableTest:toCall');
-
- $callable();
- $this->assertSame(1, CallableTest::$CalledCount);
-
- $callableRoute();
- $this->assertSame(2, CallableTest::$CalledCount);
-
- $callableMiddleware();
- $this->assertSame(3, CallableTest::$CalledCount);
- }
-
- public function testSlimCallableAsArray(): void
- {
- $resolver = new CallableResolver(); // No container injected
- $callable = $resolver->resolve([CallableTest::class, 'toCall']);
- $callableRoute = $resolver->resolveRoute([CallableTest::class, 'toCall']);
- $callableMiddleware = $resolver->resolveMiddleware([CallableTest::class, 'toCall']);
-
- $callable();
- $this->assertSame(1, CallableTest::$CalledCount);
-
- $callableRoute();
- $this->assertSame(2, CallableTest::$CalledCount);
-
- $callableMiddleware();
- $this->assertSame(3, CallableTest::$CalledCount);
- }
-
- public function testSlimCallableContainer(): void
- {
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolve('Slim\Tests\Mocks\CallableTest:toCall');
- $this->assertSame($container, CallableTest::$CalledContainer);
-
- CallableTest::$CalledContainer = null;
- $resolver->resolveRoute('Slim\Tests\Mocks\CallableTest:toCall');
- $this->assertSame($container, CallableTest::$CalledContainer);
-
- CallableTest::$CalledContainer = null;
- $resolver->resolveMiddleware('Slim\Tests\Mocks\CallableTest:toCall');
- $this->assertSame($container, CallableTest::$CalledContainer);
- }
-
- public function testSlimCallableAsArrayContainer(): void
- {
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolve([CallableTest::class, 'toCall']);
- $this->assertSame($container, CallableTest::$CalledContainer);
-
- CallableTest::$CalledContainer = null;
- $resolver->resolveRoute([CallableTest::class, 'toCall']);
- $this->assertSame($container, CallableTest::$CalledContainer);
-
- CallableTest::$CalledContainer = null;
- $resolver->resolveMiddleware([CallableTest::class ,'toCall']);
- $this->assertSame($container, CallableTest::$CalledContainer);
- }
-
- public function testContainer(): void
- {
- $this->containerProphecy->has('callable_service')->willReturn(true);
- $this->containerProphecy->get('callable_service')->willReturn(new CallableTest());
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
-
- $resolver = new CallableResolver($container);
- $callable = $resolver->resolve('callable_service:toCall');
- $callableRoute = $resolver->resolveRoute('callable_service:toCall');
- $callableMiddleware = $resolver->resolveMiddleware('callable_service:toCall');
-
- $callable();
- $this->assertSame(1, CallableTest::$CalledCount);
-
- $callableRoute();
- $this->assertSame(2, CallableTest::$CalledCount);
-
- $callableMiddleware();
- $this->assertSame(3, CallableTest::$CalledCount);
- }
-
- public function testResolutionToAnInvokableClassInContainer(): void
- {
- $this->containerProphecy->has('an_invokable')->willReturn(true);
- $this->containerProphecy->get('an_invokable')->willReturn(new InvokableTest());
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
-
- $resolver = new CallableResolver($container);
- $callable = $resolver->resolve('an_invokable');
- $callableRoute = $resolver->resolveRoute('an_invokable');
- $callableMiddleware = $resolver->resolveMiddleware('an_invokable');
-
- $callable();
- $this->assertSame(1, InvokableTest::$CalledCount);
-
- $callableRoute();
- $this->assertSame(2, InvokableTest::$CalledCount);
-
- $callableMiddleware();
- $this->assertSame(3, InvokableTest::$CalledCount);
- }
-
- public function testResolutionToAnInvokableClass(): void
- {
- $resolver = new CallableResolver(); // No container injected
- $callable = $resolver->resolve('Slim\Tests\Mocks\InvokableTest');
- $callableRoute = $resolver->resolveRoute('Slim\Tests\Mocks\InvokableTest');
- $callableMiddleware = $resolver->resolveMiddleware('Slim\Tests\Mocks\InvokableTest');
-
- $callable();
- $this->assertSame(1, InvokableTest::$CalledCount);
-
- $callableRoute();
- $this->assertSame(2, InvokableTest::$CalledCount);
-
- $callableMiddleware();
- $this->assertSame(3, InvokableTest::$CalledCount);
- }
-
- public function testResolutionToAPsrRequestHandlerClass(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Slim\\Tests\\Mocks\\RequestHandlerTest is not resolvable');
-
- $resolver = new CallableResolver(); // No container injected
- $resolver->resolve(RequestHandlerTest::class);
- }
-
- public function testRouteResolutionToAPsrRequestHandlerClass(): void
- {
- $request = $this->createServerRequest('/', 'GET');
- $resolver = new CallableResolver(); // No container injected
- $callableRoute = $resolver->resolveRoute(RequestHandlerTest::class);
- $callableRoute($request);
- $this->assertSame(1, RequestHandlerTest::$CalledCount);
- }
-
- public function testMiddlewareResolutionToAPsrRequestHandlerClass(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Slim\\Tests\\Mocks\\RequestHandlerTest is not resolvable');
-
- $resolver = new CallableResolver(); // No container injected
- $resolver->resolveMiddleware(RequestHandlerTest::class);
- }
-
- public function testObjPsrRequestHandlerClass(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('{} is not resolvable');
-
- $obj = new RequestHandlerTest();
- $resolver = new CallableResolver(); // No container injected
- $resolver->resolve($obj);
- }
-
- public function testRouteObjPsrRequestHandlerClass(): void
- {
- $obj = new RequestHandlerTest();
- $request = $this->createServerRequest('/', 'GET');
- $resolver = new CallableResolver(); // No container injected
- $callableRoute = $resolver->resolveRoute($obj);
- $callableRoute($request);
- $this->assertSame(1, RequestHandlerTest::$CalledCount);
- }
-
- public function testMiddlewareObjPsrRequestHandlerClass(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('{} is not resolvable');
-
- $obj = new RequestHandlerTest();
- $resolver = new CallableResolver(); // No container injected
- $resolver->resolveMiddleware($obj);
- }
-
- public function testObjPsrRequestHandlerClassInContainer(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('a_requesthandler is not resolvable');
-
- $this->containerProphecy->has('a_requesthandler')->willReturn(true);
- $this->containerProphecy->get('a_requesthandler')->willReturn(new RequestHandlerTest());
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolve('a_requesthandler');
- }
-
- public function testRouteObjPsrRequestHandlerClassInContainer(): void
- {
- $this->containerProphecy->has('a_requesthandler')->willReturn(true);
- $this->containerProphecy->get('a_requesthandler')->willReturn(new RequestHandlerTest());
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $request = $this->createServerRequest('/', 'GET');
- $resolver = new CallableResolver($container);
- $callable = $resolver->resolveRoute('a_requesthandler');
- $callable($request);
-
- $this->assertSame(1, RequestHandlerTest::$CalledCount);
- }
-
- public function testMiddlewareObjPsrRequestHandlerClassInContainer(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('a_requesthandler is not resolvable');
-
- $this->containerProphecy->has('a_requesthandler')->willReturn(true);
- $this->containerProphecy->get('a_requesthandler')->willReturn(new RequestHandlerTest());
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolveMiddleware('a_requesthandler');
- }
-
- public function testResolutionToAPsrRequestHandlerClassWithCustomMethod(): void
- {
- $resolver = new CallableResolver(); // No container injected
- $callable = $resolver->resolve(RequestHandlerTest::class . ':custom');
- $callableRoute = $resolver->resolveRoute(RequestHandlerTest::class . ':custom');
- $callableMiddleware = $resolver->resolveMiddleware(RequestHandlerTest::class . ':custom');
-
- $this->assertIsArray($callable);
- $this->assertInstanceOf(RequestHandlerTest::class, $callable[0]);
- $this->assertSame('custom', $callable[1]);
-
- $this->assertIsArray($callableRoute);
- $this->assertInstanceOf(RequestHandlerTest::class, $callableRoute[0]);
- $this->assertSame('custom', $callableRoute[1]);
-
- $this->assertIsArray($callableMiddleware);
- $this->assertInstanceOf(RequestHandlerTest::class, $callableMiddleware[0]);
- $this->assertSame('custom', $callableMiddleware[1]);
- }
-
- public function testObjMiddlewareClass(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('{} is not resolvable');
-
- $obj = new MiddlewareTest();
- $resolver = new CallableResolver(); // No container injected
- $resolver->resolve($obj);
- }
-
- public function testRouteObjMiddlewareClass(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('{} is not resolvable');
-
- $obj = new MiddlewareTest();
- $resolver = new CallableResolver(); // No container injected
- $resolver->resolveRoute($obj);
- }
-
- public function testMiddlewareObjMiddlewareClass(): void
- {
- $obj = new MiddlewareTest();
- $request = $this->createServerRequest('/', 'GET');
- $resolver = new CallableResolver(); // No container injected
- $callableRouteMiddleware = $resolver->resolveMiddleware($obj);
- $callableRouteMiddleware($request, $this->createMock(RequestHandlerInterface::class));
- $this->assertSame(1, MiddlewareTest::$CalledCount);
- }
-
- public function testNotObjectInContainerThrowException(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('callable_service container entry is not an object');
-
- $this->containerProphecy->has('callable_service')->willReturn(true);
- $this->containerProphecy->get('callable_service')->willReturn('NOT AN OBJECT');
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolve('callable_service');
- }
-
- public function testMethodNotFoundThrowException(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('callable_service:notFound is not resolvable');
-
- $this->containerProphecy->has('callable_service')->willReturn(true);
- $this->containerProphecy->get('callable_service')->willReturn(new CallableTest());
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolve('callable_service:notFound');
- }
-
- public function testRouteMethodNotFoundThrowException(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('callable_service:notFound is not resolvable');
-
- $this->containerProphecy->has('callable_service')->willReturn(true);
- $this->containerProphecy->get('callable_service')->willReturn(new CallableTest());
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolveRoute('callable_service:notFound');
- }
-
- public function testMiddlewareMethodNotFoundThrowException(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('callable_service:notFound is not resolvable');
-
- $this->containerProphecy->has('callable_service')->willReturn(true);
- $this->containerProphecy->get('callable_service')->willReturn(new CallableTest());
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolveMiddleware('callable_service:notFound');
- }
-
- public function testFunctionNotFoundThrowException(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Callable notFound does not exist');
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolve('notFound');
- }
-
- public function testRouteFunctionNotFoundThrowException(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Callable notFound does not exist');
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolveRoute('notFound');
- }
-
- public function testMiddlewareFunctionNotFoundThrowException(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Callable notFound does not exist');
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolveMiddleware('notFound');
- }
-
- public function testClassNotFoundThrowException(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Callable Unknown::notFound() does not exist');
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolve('Unknown:notFound');
- }
-
- public function testRouteClassNotFoundThrowException(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Callable Unknown::notFound() does not exist');
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolveRoute('Unknown:notFound');
- }
-
- public function testMiddlewareClassNotFoundThrowException(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Callable Unknown::notFound() does not exist');
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolveMiddleware('Unknown:notFound');
- }
-
- public function testCallableClassNotFoundThrowException(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Callable Unknown::notFound() does not exist');
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolve(['Unknown', 'notFound']);
- }
-
- public function testRouteCallableClassNotFoundThrowException(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Callable Unknown::notFound() does not exist');
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolveRoute(['Unknown', 'notFound']);
- }
-
- public function testMiddlewareCallableClassNotFoundThrowException(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Callable Unknown::notFound() does not exist');
-
- /** @var ContainerInterface $container */
- $container = $this->containerProphecy->reveal();
- $resolver = new CallableResolver($container);
- $resolver->resolveMiddleware(['Unknown', 'notFound']);
- }
-}
diff --git a/tests/Configuration/ConfigTest.php b/tests/Configuration/ConfigTest.php
new file mode 100644
index 000000000..440590827
--- /dev/null
+++ b/tests/Configuration/ConfigTest.php
@@ -0,0 +1,105 @@
+ true,
+ 'key2' => false,
+ 'database' => [
+ 'host' => 'localhost',
+ 'port' => 3306,
+ ],
+ ];
+
+ $config = new Config($data);
+
+ // Test retrieving a top-level key
+ $this->assertTrue($config->get('key1'));
+ $this->assertFalse($config->get('key2'));
+
+ // Test retrieving a nested key
+ $this->assertSame('localhost', $config->get('database.host'));
+ $this->assertSame(3306, $config->get('database.port'));
+ }
+
+ public function testGetWithNonExistentKeyReturnsDefault(): void
+ {
+ $data = [
+ 'database' => [
+ 'name' => 'slim_test',
+ ],
+ ];
+
+ $config = new Config($data);
+
+ // Test retrieving a non-existent top-level key
+ $this->assertNull($config->get('version'));
+ $this->assertSame('default', $config->get('version', 'default'));
+
+ // Test retrieving a non-existent nested key
+ $this->assertNull($config->get('database.host'));
+ $this->assertSame(false, $config->get('database.host', false));
+ }
+
+ public function testGetWithEmptyKeyReturnsDefault(): void
+ {
+ $data = [
+ 'key1' => 'value1',
+ ];
+
+ $config = new Config($data);
+
+ // Test retrieving with an empty key
+ $this->assertSame('default', $config->get('', 'default'));
+ }
+
+ public function testGetWithPartialNestedKeyReturnsDefault(): void
+ {
+ $data = [
+ 'name' => 'Slim',
+ 'key1' => [
+ 'displayErrorDetails' => true,
+ 'key2' => [
+ 'key3' => 'debug',
+ ],
+ ],
+ ];
+
+ $config = new Config($data);
+
+ // Test retrieving a partially nested key
+ $this->assertSame('debug', $config->get('key1.key2.key3'));
+ $this->assertSame('default', $config->get('errors.logErrors.path', 'default'));
+ }
+
+ public function testGetWithDeeplyNestedKey(): void
+ {
+ $data = [
+ 'parent' => [
+ 'child' => [
+ 'grandchild' => 'value',
+ ],
+ ],
+ ];
+
+ $config = new Config($data);
+
+ // Test retrieving a deeply nested key
+ $this->assertSame('value', $config->get('parent.child.grandchild'));
+ }
+}
diff --git a/tests/Container/ContainerResolverTest.php b/tests/Container/ContainerResolverTest.php
new file mode 100644
index 000000000..a314992b7
--- /dev/null
+++ b/tests/Container/ContainerResolverTest.php
@@ -0,0 +1,295 @@
+build();
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+
+ $callable = $resolver->resolveCallable($test);
+
+ $this->assertTrue($callable());
+ }
+
+ public function testClosureContainer(): void
+ {
+ $builder = new AppBuilder();
+ $builder->addDefinitions(
+ [
+ 'ultimateAnswer' => fn () => 42,
+ ]
+ );
+ $app = $builder->build();
+ $container = $app->getContainer();
+
+ $that = $this;
+ $test = function () use ($that, $container) {
+ $that->assertInstanceOf(ContainerInterface::class, $this);
+ $that->assertSame($container, $this);
+
+ /** @var ContainerInterface $this */
+ return $this->get('ultimateAnswer');
+ };
+
+ $resolver = $container->get(ContainerResolverInterface::class);
+ $callable = $resolver->resolveRoute($test);
+
+ $this->assertSame(42, $callable());
+ }
+
+ public function testClosureFromCallable(): void
+ {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $container = $app->getContainer();
+
+ $that = $this;
+ $class = Closure::fromCallable(
+ function () use ($that, $container) {
+ $that->assertSame($container, $this);
+
+ return 42;
+ }
+ );
+
+ $test = [$class, '__invoke'];
+
+ $resolver = $container->get(ContainerResolverInterface::class);
+ $callable = $resolver->resolveRoute($test);
+
+ $this->assertSame(42, $callable());
+ }
+
+ public function testFunctionName(): void
+ {
+ $app = (new AppBuilder())->build();
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+ $callable = $resolver->resolveCallable(__NAMESPACE__ . '\testAdvancedCallable');
+
+ $this->assertTrue($callable());
+ }
+
+ public function testObjMethodArray(): void
+ {
+ $obj = new CallableTester();
+ $app = (new AppBuilder())->build();
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+ $callable = $resolver->resolveCallable([$obj, 'toCall']);
+ $this->assertSame(true, $callable());
+ }
+
+ public function testSlimCallable(): void
+ {
+ $app = (new AppBuilder())->build();
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+ $callable = $resolver->resolveCallable('Slim\Tests\Mocks\CallableTester:toCall');
+ $this->assertSame(true, $callable());
+ }
+
+ public function testSlimCallableAsArray(): void
+ {
+ $app = (new AppBuilder())->build();
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+ $callable = $resolver->resolveCallable([CallableTester::class, 'toCall']);
+
+ $this->assertSame(true, $callable());
+ }
+
+ public function testContainer(): void
+ {
+ $builder = new AppBuilder();
+ $builder->addDefinitions(
+ [
+ 'callable_service' => fn () => new CallableTester(),
+ ]
+ );
+ $app = $builder->build();
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+
+ $callable = $resolver->resolveCallable('callable_service:toCall');
+ $this->assertSame(true, $callable());
+ }
+
+ public function testResolutionToAnInvokableClassInContainer(): void
+ {
+ $builder = new AppBuilder();
+ $builder->addDefinitions(
+ [
+ 'an_invokable' => fn () => new InvokableTester(),
+ ]
+ );
+ $app = $builder->build();
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+ $callable = $resolver->resolveCallable('an_invokable');
+
+ $this->assertSame(true, $callable());
+ }
+
+ public function testResolutionToAnInvokableClass(): void
+ {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+ $callable = $resolver->resolveCallable(InvokableTester::class);
+ $this->assertSame(true, $callable());
+ }
+
+ public function testResolutionToRequestHandler(): void
+ {
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('The definition "Slim\Tests\Mocks\RequestHandlerTester" is not a callable');
+
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+
+ $resolver->resolveCallable(RequestHandlerTester::class);
+ }
+
+ public function testObjRequestHandlerInContainer(): void
+ {
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('The definition "a_requesthandler" is not a callable');
+
+ $builder = new AppBuilder();
+ $builder->addDefinitions(
+ [
+ 'a_requesthandler' => function ($container) {
+ return new RequestHandlerTester($container->get(ResponseFactoryInterface::class));
+ },
+ ]
+ );
+ $app = $builder->build();
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+
+ $resolver->resolveCallable('a_requesthandler');
+ }
+
+ public function testResolutionToAPsrRequestHandlerClassWithCustomMethod(): void
+ {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+ $callable = $resolver->resolveCallable(RequestHandlerTester::class . ':custom');
+
+ $this->assertIsArray($callable);
+ $this->assertInstanceOf(RequestHandlerTester::class, $callable[0]);
+ $this->assertSame('custom', $callable[1]);
+ }
+
+ public function testObjMiddlewareClass(): void
+ {
+ $this->expectException(TypeError::class);
+ $this->expectExceptionMessage('must be of type callable|array|string');
+
+ $obj = new MiddlewareTester();
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+ $resolver->resolveCallable($obj);
+ }
+
+ public function testNotObjectInContainerThrowException(): void
+ {
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('The definition "callable_service" is not a callable');
+
+ $builder = new AppBuilder();
+ $builder->addDefinitions(
+ [
+ 'callable_service' => fn () => 'NOT AN OBJECT',
+ ]
+ );
+ $app = $builder->build();
+
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+ $resolver->resolveCallable('callable_service');
+ }
+
+ public function testMethodNotFoundThrowException(): void
+ {
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('The method "notFound" does not exists');
+
+ $builder = new AppBuilder();
+ $builder->addDefinitions(
+ [
+ 'callable_service' => fn () => new CallableTester(),
+ ]
+ );
+ $app = $builder->build();
+
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+ $resolver->resolveCallable('callable_service:notFound');
+ }
+
+ public function testFunctionNotFoundThrowException(): void
+ {
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage("No entry or class found for 'notFound'");
+
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+ $resolver->resolveCallable('notFound');
+ }
+
+ public function testClassNotFoundThrowException(): void
+ {
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage("No entry or class found for 'Unknown'");
+
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+ $resolver->resolveCallable('Unknown:notFound');
+ }
+
+ public function testCallableClassNotFoundThrowException(): void
+ {
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage("No entry or class found for 'Unknown'");
+
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $resolver = $app->getContainer()->get(ContainerResolver::class);
+ $resolver->resolveCallable(['Unknown', 'notFound']);
+ }
+}
+
+function testAdvancedCallable()
+{
+ return true;
+}
diff --git a/tests/Container/DefaultDefinitionsTest.php b/tests/Container/DefaultDefinitionsTest.php
new file mode 100644
index 000000000..4b91eef51
--- /dev/null
+++ b/tests/Container/DefaultDefinitionsTest.php
@@ -0,0 +1,201 @@
+build()->getContainer();
+ $details = $container->get(ConfigurationInterface::class)
+ ->get('display_error_details', false);
+
+ $this->assertFalse($details);
+
+ $details = $container->get(ConfigurationInterface::class)
+ ->get('log_error_details', false);
+
+ $this->assertFalse($details);
+ }
+
+ public function testApp(): void
+ {
+ $container = (new AppBuilder())->build()->getContainer();
+ $app = $container->get(App::class);
+
+ $this->assertInstanceOf(App::class, $app);
+ }
+
+ public function testContainerResolverInterface(): void
+ {
+ $container = (new AppBuilder())->build()->getContainer();
+ $resolver = $container->get(ContainerResolverInterface::class);
+
+ $this->assertInstanceOf(ContainerResolverInterface::class, $resolver);
+ }
+
+ public function testRequestHandlerInterface(): void
+ {
+ $container = (new AppBuilder())->build()->getContainer();
+ $requestHandler = $container->get(RequestHandlerInterface::class);
+
+ $this->assertInstanceOf(RequestHandlerInterface::class, $requestHandler);
+ $this->assertInstanceOf(MiddlewareRequestHandler::class, $requestHandler);
+ }
+
+ public function testServerRequestFactoryInterface(): void
+ {
+ $container = (new AppBuilder())->build()->getContainer();
+ $requestFactory = $container->get(ServerRequestFactoryInterface::class);
+
+ $this->assertInstanceOf(ServerRequestFactoryInterface::class, $requestFactory);
+ }
+
+ #[DataProvider('serverRequestFactoryDefinitionsProvider')]
+ public function testServerRequestFactoryInterfaceWithDefinitions(callable $definition, string $instanceOf): void
+ {
+ $definitions = call_user_func(new HttpDefinitions());
+ $definitions = array_merge($definitions, call_user_func($definition));
+
+ $container = new Container($definitions);
+ $requestFactory = $container->get(ServerRequestFactoryInterface::class);
+
+ $this->assertInstanceOf(ServerRequestFactoryInterface::class, $requestFactory);
+ $this->assertInstanceOf($instanceOf, $requestFactory);
+ }
+
+ public static function serverRequestFactoryDefinitionsProvider(): array
+ {
+ return [
+ 'GuzzleDefinitions' => [new GuzzleDefinitions(), HttpFactory::class],
+ 'HttpSoftDefinitions' => [new HttpSoftDefinitions(), HttpSoftServerRequestFactory::class],
+ 'LaminasDiactorosDefinitions' => [new LaminasDiactorosDefinitions(), LaminasServerRequestFactory::class],
+ 'NyholmDefinitions' => [new NyholmDefinitions(), Psr17Factory::class],
+ 'SlimHttpDefinitions' => [new SlimHttpDefinitions(), ServerRequestFactoryInterface::class],
+ 'SlimPsr7Definitions' => [new SlimPsr7Definitions(), ServerRequestFactory::class],
+ ];
+ }
+
+ public function testResponseFactoryInterface(): void
+ {
+ $container = (new AppBuilder())->build()->getContainer();
+ $responseFactory = $container->get(ResponseFactoryInterface::class);
+
+ $this->assertInstanceOf(ResponseFactoryInterface::class, $responseFactory);
+ }
+
+ public function testStreamFactoryInterface(): void
+ {
+ $container = (new AppBuilder())->build()->getContainer();
+ $streamFactory = $container->get(StreamFactoryInterface::class);
+
+ $this->assertInstanceOf(StreamFactoryInterface::class, $streamFactory);
+ }
+
+ public function testUriFactoryInterface(): void
+ {
+ $container = (new AppBuilder())->build()->getContainer();
+ $uriFactory = $container->get(UriFactoryInterface::class);
+
+ $this->assertInstanceOf(UriFactoryInterface::class, $uriFactory);
+ }
+
+ public function testUploadedFileFactoryInterface(): void
+ {
+ $container = (new AppBuilder())->build()->getContainer();
+ $uploadedFileFactory = $container->get(UploadedFileFactoryInterface::class);
+
+ $this->assertInstanceOf(UploadedFileFactoryInterface::class, $uploadedFileFactory);
+ }
+
+ public function testEmitterInterface(): void
+ {
+ $container = (new AppBuilder())->build()->getContainer();
+ $emitter = $container->get(EmitterInterface::class);
+
+ $this->assertInstanceOf(ResponseEmitter::class, $emitter);
+ }
+
+ public function testRouter(): void
+ {
+ $container = (new AppBuilder())->build()->getContainer();
+ $router = $container->get(Router::class);
+
+ $this->assertInstanceOf(Router::class, $router);
+ }
+
+ public function testRequestHandlerInvocationStrategyInterface(): void
+ {
+ $container = (new AppBuilder())->build()->getContainer();
+ $invocationStrategy = $container->get(RequestHandlerInvocationStrategyInterface::class);
+
+ $this->assertInstanceOf(RequestResponse::class, $invocationStrategy);
+ }
+
+ public function testExceptionHandlingMiddleware(): void
+ {
+ $container = (new AppBuilder())->build()->getContainer();
+ $exceptionHandlingMiddleware = $container->get(ExceptionHandlingMiddleware::class);
+
+ $this->assertInstanceOf(ExceptionHandlingMiddleware::class, $exceptionHandlingMiddleware);
+ }
+
+ public function testBodyParsingMiddleware(): void
+ {
+ $container = (new AppBuilder())->build()->getContainer();
+ $bodyParsingMiddleware = $container->get(BodyParsingMiddleware::class);
+
+ $this->assertInstanceOf(BodyParsingMiddleware::class, $bodyParsingMiddleware);
+ }
+
+ public function testLoggerInterface(): void
+ {
+ $container = (new AppBuilder())->build()->getContainer();
+ $logger = $container->get(LoggerInterface::class);
+
+ $this->assertInstanceOf(LoggerInterface::class, $logger);
+ }
+}
diff --git a/tests/Container/GuzzleDefinitionsTest.php b/tests/Container/GuzzleDefinitionsTest.php
new file mode 100644
index 000000000..7d1e201fe
--- /dev/null
+++ b/tests/Container/GuzzleDefinitionsTest.php
@@ -0,0 +1,100 @@
+__invoke();
+ $container = new Container($definitions);
+
+ $this->assertTrue($container->has(ServerRequestFactoryInterface::class));
+ $this->assertTrue($container->has(ServerRequestCreatorInterface::class));
+ $this->assertTrue($container->has(ResponseFactoryInterface::class));
+ $this->assertTrue($container->has(StreamFactoryInterface::class));
+ $this->assertTrue($container->has(UriFactoryInterface::class));
+ $this->assertTrue($container->has(UploadedFileFactoryInterface::class));
+ }
+
+ public function testServerRequestFactoryInterface()
+ {
+ $definitions = (new GuzzleDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $serverRequestFactory = $container->get(ServerRequestFactoryInterface::class);
+
+ $this->assertInstanceOf(HttpFactory::class, $serverRequestFactory);
+ }
+
+ public function testServerRequestCreatorInterface()
+ {
+ $definitions = (new GuzzleDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $serverRequestCreator = $container->get(ServerRequestCreatorInterface::class);
+
+ $this->assertInstanceOf(ServerRequestCreatorInterface::class, $serverRequestCreator);
+ $this->assertInstanceOf(ServerRequestInterface::class, $serverRequestCreator->createServerRequestFromGlobals());
+ }
+
+ public function testResponseFactoryInterface()
+ {
+ $definitions = (new GuzzleDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $responseFactory = $container->get(ResponseFactoryInterface::class);
+
+ $this->assertInstanceOf(HttpFactory::class, $responseFactory);
+ }
+
+ public function testStreamFactoryInterface()
+ {
+ $definitions = (new GuzzleDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $streamFactory = $container->get(StreamFactoryInterface::class);
+
+ $this->assertInstanceOf(HttpFactory::class, $streamFactory);
+ }
+
+ public function testUriFactoryInterface()
+ {
+ $definitions = (new GuzzleDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $uriFactory = $container->get(UriFactoryInterface::class);
+
+ $this->assertInstanceOf(HttpFactory::class, $uriFactory);
+ }
+
+ public function testUploadedFileFactoryInterface()
+ {
+ $definitions = (new GuzzleDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $uploadedFileFactory = $container->get(UploadedFileFactoryInterface::class);
+
+ $this->assertInstanceOf(HttpFactory::class, $uploadedFileFactory);
+ }
+}
diff --git a/tests/Container/HttpDefinitionsTest.php b/tests/Container/HttpDefinitionsTest.php
new file mode 100644
index 000000000..b30d04829
--- /dev/null
+++ b/tests/Container/HttpDefinitionsTest.php
@@ -0,0 +1,49 @@
+expectException(RuntimeException::class);
+
+ // Create a mock for the class_exists function
+ $classExistsMock = fn () => false;
+
+ $httpDefinitions = new HttpDefinitions();
+
+ // Use reflection to inject the mock callable into the $classExists property
+ $reflection = new ReflectionClass($httpDefinitions);
+ $classExistsProperty = $reflection->getProperty('classExists');
+ $classExistsProperty->setAccessible(true);
+ $classExistsProperty->setValue($httpDefinitions, $classExistsMock);
+
+ $httpDefinitions();
+ }
+
+ public function testServerRequestFactoryInterface()
+ {
+ $definitions = (new HttpDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $serverRequestFactory = $container->get(ServerRequestFactoryInterface::class);
+
+ $this->assertInstanceOf(ServerRequestFactoryInterface::class, $serverRequestFactory);
+ }
+}
diff --git a/tests/Container/HttpSoftDefinitionsTest.php b/tests/Container/HttpSoftDefinitionsTest.php
new file mode 100644
index 000000000..5a26e490b
--- /dev/null
+++ b/tests/Container/HttpSoftDefinitionsTest.php
@@ -0,0 +1,104 @@
+__invoke();
+ $container = new Container($definitions);
+
+ $this->assertTrue($container->has(ServerRequestFactoryInterface::class));
+ $this->assertTrue($container->has(ServerRequestCreatorInterface::class));
+ $this->assertTrue($container->has(ResponseFactoryInterface::class));
+ $this->assertTrue($container->has(StreamFactoryInterface::class));
+ $this->assertTrue($container->has(UriFactoryInterface::class));
+ $this->assertTrue($container->has(UploadedFileFactoryInterface::class));
+ }
+
+ public function testServerRequestFactoryInterface()
+ {
+ $definitions = (new HttpSoftDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $serverRequestFactory = $container->get(ServerRequestFactoryInterface::class);
+
+ $this->assertInstanceOf(ServerRequestFactory::class, $serverRequestFactory);
+ }
+
+ public function testServerRequestCreatorInterface()
+ {
+ $definitions = (new HttpSoftDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $serverRequestCreator = $container->get(ServerRequestCreatorInterface::class);
+
+ $this->assertInstanceOf(ServerRequestCreatorInterface::class, $serverRequestCreator);
+ $this->assertInstanceOf(ServerRequestInterface::class, $serverRequestCreator->createServerRequestFromGlobals());
+ }
+
+ public function testResponseFactoryInterface()
+ {
+ $definitions = (new HttpSoftDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $responseFactory = $container->get(ResponseFactoryInterface::class);
+
+ $this->assertInstanceOf(ResponseFactory::class, $responseFactory);
+ }
+
+ public function testStreamFactoryInterface()
+ {
+ $definitions = (new HttpSoftDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $streamFactory = $container->get(StreamFactoryInterface::class);
+
+ $this->assertInstanceOf(StreamFactory::class, $streamFactory);
+ }
+
+ public function testUriFactoryInterface()
+ {
+ $definitions = (new HttpSoftDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $uriFactory = $container->get(UriFactoryInterface::class);
+
+ $this->assertInstanceOf(UriFactory::class, $uriFactory);
+ }
+
+ public function testUploadedFileFactoryInterface()
+ {
+ $definitions = (new HttpSoftDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $uploadedFileFactory = $container->get(UploadedFileFactoryInterface::class);
+
+ $this->assertInstanceOf(UploadedFileFactory::class, $uploadedFileFactory);
+ }
+}
diff --git a/tests/Container/LaminasDiactorosDefinitionsTest.php b/tests/Container/LaminasDiactorosDefinitionsTest.php
new file mode 100644
index 000000000..0f323b4c3
--- /dev/null
+++ b/tests/Container/LaminasDiactorosDefinitionsTest.php
@@ -0,0 +1,104 @@
+__invoke();
+ $container = new Container($definitions);
+
+ $this->assertTrue($container->has(ServerRequestFactoryInterface::class));
+ $this->assertTrue($container->has(ServerRequestCreatorInterface::class));
+ $this->assertTrue($container->has(ResponseFactoryInterface::class));
+ $this->assertTrue($container->has(StreamFactoryInterface::class));
+ $this->assertTrue($container->has(UriFactoryInterface::class));
+ $this->assertTrue($container->has(UploadedFileFactoryInterface::class));
+ }
+
+ public function testServerRequestFactoryInterface()
+ {
+ $definitions = (new LaminasDiactorosDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $serverRequestFactory = $container->get(ServerRequestFactoryInterface::class);
+
+ $this->assertInstanceOf(ServerRequestFactory::class, $serverRequestFactory);
+ }
+
+ public function testServerRequestCreatorInterface()
+ {
+ $definitions = (new LaminasDiactorosDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $serverRequestCreator = $container->get(ServerRequestCreatorInterface::class);
+
+ $this->assertInstanceOf(ServerRequestCreatorInterface::class, $serverRequestCreator);
+ $this->assertInstanceOf(ServerRequestInterface::class, $serverRequestCreator->createServerRequestFromGlobals());
+ }
+
+ public function testResponseFactoryInterface()
+ {
+ $definitions = (new LaminasDiactorosDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $responseFactory = $container->get(ResponseFactoryInterface::class);
+
+ $this->assertInstanceOf(ResponseFactory::class, $responseFactory);
+ }
+
+ public function testStreamFactoryInterface()
+ {
+ $definitions = (new LaminasDiactorosDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $streamFactory = $container->get(StreamFactoryInterface::class);
+
+ $this->assertInstanceOf(StreamFactory::class, $streamFactory);
+ }
+
+ public function testUriFactoryInterface()
+ {
+ $definitions = (new LaminasDiactorosDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $uriFactory = $container->get(UriFactoryInterface::class);
+
+ $this->assertInstanceOf(UriFactory::class, $uriFactory);
+ }
+
+ public function testUploadedFileFactoryInterface()
+ {
+ $definitions = (new LaminasDiactorosDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $uploadedFileFactory = $container->get(UploadedFileFactoryInterface::class);
+
+ $this->assertInstanceOf(UploadedFileFactory::class, $uploadedFileFactory);
+ }
+}
diff --git a/tests/Container/MiddlewareResolverTest.php b/tests/Container/MiddlewareResolverTest.php
new file mode 100644
index 000000000..95f95b5d8
--- /dev/null
+++ b/tests/Container/MiddlewareResolverTest.php
@@ -0,0 +1,126 @@
+build();
+ $container = $app->getContainer();
+ $containerResolver = $container->get(ContainerResolverInterface::class);
+
+ $middlewareResolver = new MiddlewareResolver(
+ $container,
+ $containerResolver
+ );
+
+ $middleware1 = $this->createCallableMiddleware();
+ $middleware2 = $this->createMiddleware();
+
+ $queue = [$middleware1, $middleware2];
+
+ $resolvedStack = $middlewareResolver->resolveStack($queue);
+
+ $this->assertCount(2, $resolvedStack);
+ $this->assertInstanceOf(MiddlewareInterface::class, $resolvedStack[0]);
+ $this->assertInstanceOf(MiddlewareInterface::class, $resolvedStack[1]);
+
+ $request = $this->createMock(ServerRequestInterface::class);
+ $handler = $this->createMock(RequestHandlerInterface::class);
+
+ $response = $resolvedStack[0]->process($request, $handler);
+ $this->assertInstanceOf(ResponseInterface::class, $response);
+
+ $response = $resolvedStack[1]->process($request, $handler);
+ $this->assertInstanceOf(ResponseInterface::class, $response);
+ }
+
+ public function testResolveMiddlewareWithValidMiddleware()
+ {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $container = $app->getContainer();
+ $containerResolver = $container->get(ContainerResolverInterface::class);
+
+ $middlewareResolver = new MiddlewareResolver(
+ $container,
+ $containerResolver
+ );
+
+ $middleware = $this->createMiddleware();
+
+ $resolvedMiddleware = $middlewareResolver->resolveStack([$middleware]);
+
+ $this->assertInstanceOf(MiddlewareInterface::class, $resolvedMiddleware[0]);
+ }
+
+ public function testResolveStackWithException(): void
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage(
+ 'A middleware must be an object or callable that implements "MiddlewareInterface".'
+ );
+
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $container = $app->getContainer();
+ $containerResolver = $container->get(ContainerResolverInterface::class);
+
+ $middlewareResolver = new MiddlewareResolver(
+ $container,
+ $containerResolver
+ );
+
+ $middlewareResolver->resolveStack([[null]]);
+ }
+
+ private function createCallableMiddleware(): callable
+ {
+ $response = $this->createMock(ResponseInterface::class);
+
+ return function () use ($response): ResponseInterface {
+ return $response;
+ };
+ }
+
+ private function createMiddleware(): MiddlewareInterface
+ {
+ $response = $this->createMock(ResponseInterface::class);
+
+ return new class ($response) implements MiddlewareInterface {
+ private ResponseInterface $response;
+
+ public function __construct(ResponseInterface $response)
+ {
+ $this->response = $response;
+ }
+
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler,
+ ): ResponseInterface {
+ return $this->response;
+ }
+ };
+ }
+}
diff --git a/tests/Container/NyholmDefinitionsTest.php b/tests/Container/NyholmDefinitionsTest.php
new file mode 100644
index 000000000..63cc84b10
--- /dev/null
+++ b/tests/Container/NyholmDefinitionsTest.php
@@ -0,0 +1,100 @@
+__invoke();
+ $container = new Container($definitions);
+
+ $this->assertTrue($container->has(ServerRequestFactoryInterface::class));
+ $this->assertTrue($container->has(ServerRequestCreatorInterface::class));
+ $this->assertTrue($container->has(ResponseFactoryInterface::class));
+ $this->assertTrue($container->has(StreamFactoryInterface::class));
+ $this->assertTrue($container->has(UriFactoryInterface::class));
+ $this->assertTrue($container->has(UploadedFileFactoryInterface::class));
+ }
+
+ public function testServerRequestFactoryInterface()
+ {
+ $definitions = (new NyholmDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $serverRequestFactory = $container->get(ServerRequestFactoryInterface::class);
+
+ $this->assertInstanceOf(Psr17Factory::class, $serverRequestFactory);
+ }
+
+ public function testServerRequestCreatorInterface()
+ {
+ $definitions = (new NyholmDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $serverRequestCreator = $container->get(ServerRequestCreatorInterface::class);
+
+ $this->assertInstanceOf(ServerRequestCreatorInterface::class, $serverRequestCreator);
+ $this->assertInstanceOf(ServerRequestInterface::class, $serverRequestCreator->createServerRequestFromGlobals());
+ }
+
+ public function testResponseFactoryInterface()
+ {
+ $definitions = (new NyholmDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $responseFactory = $container->get(ResponseFactoryInterface::class);
+
+ $this->assertInstanceOf(Psr17Factory::class, $responseFactory);
+ }
+
+ public function testStreamFactoryInterface()
+ {
+ $definitions = (new NyholmDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $streamFactory = $container->get(StreamFactoryInterface::class);
+
+ $this->assertInstanceOf(Psr17Factory::class, $streamFactory);
+ }
+
+ public function testUriFactoryInterface()
+ {
+ $definitions = (new NyholmDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $uriFactory = $container->get(UriFactoryInterface::class);
+
+ $this->assertInstanceOf(Psr17Factory::class, $uriFactory);
+ }
+
+ public function testUploadedFileFactoryInterface()
+ {
+ $definitions = (new NyholmDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $uploadedFileFactory = $container->get(UploadedFileFactoryInterface::class);
+
+ $this->assertInstanceOf(Psr17Factory::class, $uploadedFileFactory);
+ }
+}
diff --git a/tests/Container/SlimHttpDefinitionsTest.php b/tests/Container/SlimHttpDefinitionsTest.php
new file mode 100644
index 000000000..ad845c271
--- /dev/null
+++ b/tests/Container/SlimHttpDefinitionsTest.php
@@ -0,0 +1,182 @@
+__invoke();
+ $container = new Container($definitions);
+
+ $this->assertTrue($container->has(ServerRequestFactoryInterface::class));
+ $this->assertTrue($container->has(ServerRequestCreatorInterface::class));
+ $this->assertTrue($container->has(ResponseFactoryInterface::class));
+ $this->assertTrue($container->has(StreamFactoryInterface::class));
+ $this->assertTrue($container->has(UriFactoryInterface::class));
+ $this->assertTrue($container->has(UploadedFileFactoryInterface::class));
+ }
+
+ public function testServerRequestFactoryInterface()
+ {
+ $definitions = (new SlimHttpDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $serverRequestFactory = $container->get(ServerRequestFactoryInterface::class);
+
+ $this->assertInstanceOf(ServerRequestFactoryInterface::class, $serverRequestFactory);
+
+ $serverRequest = $serverRequestFactory->createServerRequest('GET', 'https://example.com');
+ $this->assertInstanceOf(ServerRequestInterface::class, $serverRequest);
+ }
+
+ public function testServerRequestCreatorInterface()
+ {
+ $definitions = (new SlimHttpDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $serverRequestCreator = $container->get(ServerRequestCreatorInterface::class);
+
+ $this->assertInstanceOf(ServerRequestCreatorInterface::class, $serverRequestCreator);
+ $this->assertInstanceOf(ServerRequestInterface::class, $serverRequestCreator->createServerRequestFromGlobals());
+ }
+
+ public function testResponseFactoryInterface()
+ {
+ $definitions = (new SlimHttpDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $responseFactory = $container->get(ResponseFactoryInterface::class);
+
+ $this->assertInstanceOf(ResponseFactoryInterface::class, $responseFactory);
+
+ if ($responseFactory instanceof DecoratedResponseFactory) {
+ $this->assertInstanceOf(DecoratedResponseFactory::class, $responseFactory);
+ }
+ }
+
+ public function testStreamFactoryInterface()
+ {
+ $definitions = (new SlimHttpDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $streamFactory = $container->get(StreamFactoryInterface::class);
+
+ $this->assertInstanceOf(StreamFactoryInterface::class, $streamFactory);
+ }
+
+ public function testUriFactoryInterface()
+ {
+ $definitions = (new SlimHttpDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $uriFactory = $container->get(UriFactoryInterface::class);
+
+ $this->assertInstanceOf(UriFactoryInterface::class, $uriFactory);
+
+ if ($uriFactory instanceof DecoratedUriFactory) {
+ $this->assertInstanceOf(DecoratedUriFactory::class, $uriFactory);
+ }
+ }
+
+ public function testUploadedFileFactoryInterface()
+ {
+ $definitions = (new SlimHttpDefinitions())->__invoke();
+
+ $container = new Container($definitions);
+ $uploadedFileFactory = $container->get(UploadedFileFactoryInterface::class);
+
+ $this->assertInstanceOf(UploadedFileFactoryInterface::class, $uploadedFileFactory);
+ }
+
+ public function testResponseFactoryInterfaceThrowsRuntimeExceptionWhenNoImplementationIsAvailable()
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Could not detect any PSR-17 ResponseFactory implementations.');
+
+ $definitions = new SlimHttpDefinitions();
+
+ // Use reflection to inject the mock callable into the $classExists property
+ $reflection = new ReflectionClass($definitions);
+ $classExistsProperty = $reflection->getProperty('classExists');
+ $classExistsProperty->setAccessible(true);
+ $classExistsProperty->setValue($definitions, fn () => false);
+
+ $container = new Container($definitions());
+ $container->get(ResponseFactoryInterface::class);
+ }
+
+ public function testStreamFactoryInterfaceThrowsRuntimeExceptionWhenNoImplementationIsAvailable()
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Could not instantiate a StreamFactory.');
+
+ $definitions = new SlimHttpDefinitions();
+
+ // Use reflection to inject the mock callable into the $classExists property
+ $reflection = new ReflectionClass($definitions);
+ $classExistsProperty = $reflection->getProperty('classExists');
+ $classExistsProperty->setAccessible(true);
+ $classExistsProperty->setValue($definitions, fn () => false);
+
+ $container = new Container($definitions());
+ $container->get(StreamFactoryInterface::class);
+ }
+
+ public function testUriFactoryInterfaceThrowsRuntimeExceptionWhenNoImplementationIsAvailable()
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Could not instantiate a UriFactory.');
+
+ $definitions = new SlimHttpDefinitions();
+
+ // Use reflection to inject the mock callable into the $classExists property
+ $reflection = new ReflectionClass($definitions);
+ $classExistsProperty = $reflection->getProperty('classExists');
+ $classExistsProperty->setAccessible(true);
+ $classExistsProperty->setValue($definitions, fn () => false);
+
+ $container = new Container($definitions());
+ $container->get(UriFactoryInterface::class);
+ }
+
+ public function testUploadedFileFactoryInterfaceThrowsRuntimeExceptionWhenNoImplementationIsAvailable()
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Could not instantiate a UploadedFileFactory.');
+
+ $definitions = new SlimHttpDefinitions();
+
+ // Use reflection to inject the mock callable into the $classExists property
+ $reflection = new ReflectionClass($definitions);
+ $classExistsProperty = $reflection->getProperty('classExists');
+ $classExistsProperty->setAccessible(true);
+ $classExistsProperty->setValue($definitions, fn () => false);
+
+ $container = new Container($definitions());
+ $container->get(UploadedFileFactoryInterface::class);
+ }
+}
diff --git a/tests/Container/SlimPsr7DefinitionsTest.php b/tests/Container/SlimPsr7DefinitionsTest.php
new file mode 100644
index 000000000..4177b0f89
--- /dev/null
+++ b/tests/Container/SlimPsr7DefinitionsTest.php
@@ -0,0 +1,103 @@
+__invoke();
+ $container = new Container($definitions);
+
+ $this->assertTrue($container->has(ServerRequestFactoryInterface::class));
+ $this->assertTrue($container->has(ServerRequestCreatorInterface::class));
+ $this->assertTrue($container->has(ResponseFactoryInterface::class));
+ $this->assertTrue($container->has(StreamFactoryInterface::class));
+ $this->assertTrue($container->has(UriFactoryInterface::class));
+ $this->assertTrue($container->has(UploadedFileFactoryInterface::class));
+ }
+
+ public function testServerRequestFactoryInterface()
+ {
+ $definitions = (new SlimPsr7Definitions())->__invoke();
+
+ $container = new Container($definitions);
+ $serverRequestFactory = $container->get(ServerRequestFactoryInterface::class);
+
+ $this->assertInstanceOf(ServerRequestFactory::class, $serverRequestFactory);
+ }
+
+ public function testServerRequestCreatorInterface()
+ {
+ $definitions = (new SlimPsr7Definitions())->__invoke();
+
+ $container = new Container($definitions);
+ $serverRequestCreator = $container->get(ServerRequestCreatorInterface::class);
+
+ $this->assertInstanceOf(ServerRequestCreatorInterface::class, $serverRequestCreator);
+ $this->assertInstanceOf(ServerRequestInterface::class, $serverRequestCreator->createServerRequestFromGlobals());
+ }
+
+ public function testResponseFactoryInterface()
+ {
+ $definitions = (new SlimPsr7Definitions())->__invoke();
+
+ $container = new Container($definitions);
+ $responseFactory = $container->get(ResponseFactoryInterface::class);
+
+ $this->assertInstanceOf(ResponseFactory::class, $responseFactory);
+ }
+
+ public function testStreamFactoryInterface()
+ {
+ $definitions = (new SlimPsr7Definitions())->__invoke();
+ $container = new Container($definitions);
+ $streamFactory = $container->get(StreamFactoryInterface::class);
+
+ $this->assertInstanceOf(StreamFactory::class, $streamFactory);
+ }
+
+ public function testUriFactoryInterface()
+ {
+ $definitions = (new SlimPsr7Definitions())->__invoke();
+
+ $container = new Container($definitions);
+ $uriFactory = $container->get(UriFactoryInterface::class);
+
+ $this->assertInstanceOf(UriFactory::class, $uriFactory);
+ }
+
+ public function testUploadedFileFactoryInterface()
+ {
+ $definitions = (new SlimPsr7Definitions())->__invoke();
+
+ $container = new Container($definitions);
+ $uploadedFileFactory = $container->get(UploadedFileFactoryInterface::class);
+
+ $this->assertInstanceOf(UploadedFileFactory::class, $uploadedFileFactory);
+ }
+}
diff --git a/tests/Emitter/HeaderStack.php b/tests/Emitter/HeaderStack.php
new file mode 100644
index 000000000..360e45b4f
--- /dev/null
+++ b/tests/Emitter/HeaderStack.php
@@ -0,0 +1,43 @@
+ $header,
+ ];
+ }
+
+ return $result;
+ }
+}
diff --git a/tests/ResponseEmitterTest.php b/tests/Emitter/ResponseEmitterTest.php
similarity index 73%
rename from tests/ResponseEmitterTest.php
rename to tests/Emitter/ResponseEmitterTest.php
index b0fadb2a1..946e7dd29 100644
--- a/tests/ResponseEmitterTest.php
+++ b/tests/Emitter/ResponseEmitterTest.php
@@ -3,19 +3,25 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
-namespace Slim\Tests;
+namespace Slim\Tests\Emitter;
+use PHPUnit\Framework\Attributes\RequiresPhpExtension;
+use PHPUnit\Framework\TestCase;
+use Psr\Http\Message\ResponseFactoryInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\StreamFactoryInterface;
use ReflectionClass;
-use Slim\ResponseEmitter;
-use Slim\Tests\Assets\HeaderStack;
+use Slim\Builder\AppBuilder;
+use Slim\Emitter\ResponseEmitter;
use Slim\Tests\Mocks\MockStream;
use Slim\Tests\Mocks\SlowPokeStream;
use Slim\Tests\Mocks\SmallChunksStream;
+use Slim\Tests\Traits\AppTestTrait;
use function base64_decode;
use function fopen;
@@ -35,8 +41,10 @@
use const STREAM_FILTER_READ;
use const STREAM_FILTER_WRITE;
-class ResponseEmitterTest extends TestCase
+final class ResponseEmitterTest extends TestCase
{
+ use AppTestTrait;
+
public function setUp(): void
{
HeaderStack::reset();
@@ -47,6 +55,15 @@ public function tearDown(): void
HeaderStack::reset();
}
+ private function createResponse(int $statusCode = 200, string $reasonPhrase = ''): ResponseInterface
+ {
+ $app = (new AppBuilder())->build();
+
+ return $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse($statusCode, $reasonPhrase);
+ }
+
public function testRespond(): void
{
$response = $this->createResponse();
@@ -60,6 +77,9 @@ public function testRespond(): void
public function testRespondWithPaddedStreamFilterOutput(): void
{
+ $builder = new AppBuilder();
+ $streamFactory = $builder->build()->getContainer()->get(StreamFactoryInterface::class);
+
$availableFilter = stream_get_filters();
$filterName = 'string.rot13';
@@ -77,7 +97,7 @@ public function testRespondWithPaddedStreamFilterOutput(): void
$stream = fopen('php://temp', 'r+');
$filter = stream_filter_append($stream, $specificFilterName, STREAM_FILTER_WRITE, [
'key' => $key,
- 'iv' => $iv
+ 'iv' => $iv,
]);
fwrite($stream, $data);
@@ -85,10 +105,10 @@ public function testRespondWithPaddedStreamFilterOutput(): void
stream_filter_remove($filter);
stream_filter_append($stream, $specificUnfilterName, STREAM_FILTER_READ, [
'key' => $key,
- 'iv' => $iv
+ 'iv' => $iv,
]);
- $body = $this->getStreamFactory()->createStreamFromResource($stream);
+ $body = $streamFactory->createStreamFromResource($stream);
$response = $this
->createResponse()
->withHeader('Content-Length', $length)
@@ -144,14 +164,14 @@ public function testResponseReplacesPreviouslySetHeaders(): void
$responseEmitter->emit($response);
$expectedStack = [
- ['header' => 'X-Foo: baz1', 'replace' => true, 'status_code' => null],
- ['header' => 'X-Foo: baz2', 'replace' => false, 'status_code' => null],
- ['header' => 'HTTP/1.1 200 OK', 'replace' => true, 'status_code' => 200],
+ ['header' => 'X-Foo: baz1'],
+ ['header' => 'X-Foo: baz2'],
];
$this->assertSame($expectedStack, HeaderStack::stack());
}
+ #[RequiresPhpExtension('xdebug')]
public function testResponseDoesNotReplacePreviouslySetSetCookieHeaders(): void
{
$response = $this
@@ -162,9 +182,8 @@ public function testResponseDoesNotReplacePreviouslySetSetCookieHeaders(): void
$responseEmitter->emit($response);
$expectedStack = [
- ['header' => 'set-cOOkie: foo=bar', 'replace' => false, 'status_code' => null],
- ['header' => 'set-cOOkie: bar=baz', 'replace' => false, 'status_code' => null],
- ['header' => 'HTTP/1.1 200 OK', 'replace' => true, 'status_code' => 200],
+ ['header' => 'set-cOOkie: foo=bar'],
+ ['header' => 'set-cOOkie: bar=baz'],
];
$this->assertSame($expectedStack, HeaderStack::stack());
@@ -172,7 +191,12 @@ public function testResponseDoesNotReplacePreviouslySetSetCookieHeaders(): void
public function testIsResponseEmptyWithNonEmptyBodyAndTriggeringStatusCode(): void
{
- $body = $this->createStream('Hello');
+ $app = (new AppBuilder())->build();
+
+ $body = $app->getContainer()
+ ->get(StreamFactoryInterface::class)
+ ->createStream('Hello');
+
$response = $this
->createResponse(204)
->withBody($body);
@@ -183,10 +207,17 @@ public function testIsResponseEmptyWithNonEmptyBodyAndTriggeringStatusCode(): vo
public function testIsResponseEmptyDoesNotReadAllDataFromNonEmptySeekableResponse(): void
{
- $body = $this->createStream('Hello');
- $response = $this
- ->createResponse(200)
+ $app = (new AppBuilder())->build();
+
+ $body = $app->getContainer()
+ ->get(StreamFactoryInterface::class)
+ ->createStream('Hello');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse()
->withBody($body);
+
$responseEmitter = new ResponseEmitter();
$responseEmitter->isResponseEmpty($response);
@@ -197,15 +228,18 @@ public function testIsResponseEmptyDoesNotReadAllDataFromNonEmptySeekableRespons
public function testIsResponseEmptyDoesNotDrainNonSeekableResponseWithContent(): void
{
+ $builder = new AppBuilder();
+ $streamFactory = $builder->build()->getContainer()->get(StreamFactoryInterface::class);
+
$resource = popen('echo 12', 'r');
- $body = $this->getStreamFactory()->createStreamFromResource($resource);
+ $body = $streamFactory->createStreamFromResource($resource);
$response = $this->createResponse(200)->withBody($body);
$responseEmitter = new ResponseEmitter();
$responseEmitter->isResponseEmpty($response);
$this->assertFalse($body->isSeekable());
- $this->assertSame('12', trim((string) $body));
+ $this->assertSame('12', trim((string)$body));
}
public function testAvoidReadFromSlowStreamAccordingToStatus(): void
@@ -232,10 +266,17 @@ public function testIsResponseEmptyWithEmptyBody(): void
public function testIsResponseEmptyWithZeroAsBody(): void
{
- $body = $this->createStream('0');
- $response = $this
- ->createResponse(200)
+ $app = (new AppBuilder())->build();
+
+ $body = $app->getContainer()
+ ->get(StreamFactoryInterface::class)
+ ->createStream('0');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse()
->withBody($body);
+
$responseEmitter = new ResponseEmitter();
$this->assertFalse($responseEmitter->isResponseEmpty($response));
@@ -243,7 +284,10 @@ public function testIsResponseEmptyWithZeroAsBody(): void
public function testWillHandleInvalidConnectionStatusWithADeterminateBody(): void
{
- $body = $this->getStreamFactory()->createStreamFromResource(fopen('php://temp', 'r+'));
+ $builder = new AppBuilder();
+ $streamFactory = $builder->build()->getContainer()->get(StreamFactoryInterface::class);
+
+ $body = $streamFactory->createStreamFromResource(fopen('php://temp', 'r+'));
$body->write('Hello!' . "\n");
$body->write('Hello!' . "\n");
@@ -266,7 +310,10 @@ public function testWillHandleInvalidConnectionStatusWithADeterminateBody(): voi
public function testWillHandleInvalidConnectionStatusWithAnIndeterminateBody(): void
{
- $body = $this->getStreamFactory()->createStreamFromResource(fopen('php://input', 'r+'));
+ $builder = new AppBuilder();
+ $streamFactory = $builder->build()->getContainer()->get(StreamFactoryInterface::class);
+
+ $body = $streamFactory->createStreamFromResource(fopen('php://input', 'r+'));
// Tell connection_status() to fail.
$GLOBALS['connection_status_return'] = CONNECTION_TIMEOUT;
@@ -275,7 +322,6 @@ public function testWillHandleInvalidConnectionStatusWithAnIndeterminateBody():
->createResponse()
->withBody($body);
-
$responseEmitter = new ResponseEmitter();
$mirror = new ReflectionClass(ResponseEmitter::class);
@@ -283,7 +329,7 @@ public function testWillHandleInvalidConnectionStatusWithAnIndeterminateBody():
$emitBodyMethod->setAccessible(true);
$emitBodyMethod->invoke($responseEmitter, $response);
- $this->expectOutputString("");
+ $this->expectOutputString('');
// Tell connection_status() to pass.
unset($GLOBALS['connection_status_return']);
diff --git a/tests/Error/AbstractErrorRendererTest.php b/tests/Error/AbstractErrorRendererTest.php
deleted file mode 100644
index 88cfa135b..000000000
--- a/tests/Error/AbstractErrorRendererTest.php
+++ /dev/null
@@ -1,263 +0,0 @@
-__invoke($exception, true);
-
- $this->assertMatchesRegularExpression(
- '/.*The application could not run because of the following error:.*/',
- $output
- );
- $this->assertStringContainsString('Oops..', $output);
- }
-
- public function testHTMLErrorRendererNoErrorDetails()
- {
- $exception = new RuntimeException('Oops..');
- $renderer = new HtmlErrorRenderer();
- $output = $renderer->__invoke($exception, false);
-
- $this->assertMatchesRegularExpression(
- '/.*A website error has occurred. Sorry for the temporary inconvenience.*/',
- $output
- );
- $this->assertStringNotContainsString('Oops..', $output);
- }
-
- public function testHTMLErrorRendererRenderFragmentMethod()
- {
- $exception = new Exception('Oops..', 500);
- $renderer = new HtmlErrorRenderer();
- $reflectionRenderer = new ReflectionClass(HtmlErrorRenderer::class);
-
- $method = $reflectionRenderer->getMethod('renderExceptionFragment');
- $method->setAccessible(true);
- $output = $method->invoke($renderer, $exception);
-
- $this->assertMatchesRegularExpression('/.*Type:*/', $output);
- $this->assertMatchesRegularExpression('/.*Code:*/', $output);
- $this->assertMatchesRegularExpression('/.*Message*/', $output);
- $this->assertMatchesRegularExpression('/.*File*/', $output);
- $this->assertMatchesRegularExpression('/.*Line*/', $output);
- }
-
- public function testHTMLErrorRendererRenderHttpException()
- {
- $exceptionTitle = 'title';
- $exceptionDescription = 'description';
-
- $httpExceptionProphecy = $this->prophesize(HttpException::class);
-
- $httpExceptionProphecy
- ->getTitle()
- ->willReturn($exceptionTitle)
- ->shouldBeCalledOnce();
-
- $httpExceptionProphecy
- ->getDescription()
- ->willReturn($exceptionDescription)
- ->shouldBeCalledOnce();
-
- $renderer = new HtmlErrorRenderer();
- $output = $renderer->__invoke($httpExceptionProphecy->reveal(), false);
-
- $this->assertStringContainsString($exceptionTitle, $output, 'Should contain http exception title');
- $this->assertStringContainsString($exceptionDescription, $output, 'Should contain http exception description');
- }
-
- public function testJSONErrorRendererDisplaysErrorDetails()
- {
- $exception = new Exception('Oops..');
- $renderer = new JsonErrorRenderer();
- $reflectionRenderer = new ReflectionClass(JsonErrorRenderer::class);
-
- $method = $reflectionRenderer->getMethod('formatExceptionFragment');
- $method->setAccessible(true);
-
- $fragment = $method->invoke($renderer, $exception);
- $output = json_encode(json_decode($renderer->__invoke($exception, true)));
- $expectedString = json_encode(['message' => 'Slim Application Error', 'exception' => [$fragment]]);
-
- $this->assertSame($output, $expectedString);
- }
-
- public function testJSONErrorRendererDoesNotDisplayErrorDetails()
- {
- $exception = new Exception('Oops..');
-
- $renderer = new JsonErrorRenderer();
- $output = json_encode(json_decode($renderer->__invoke($exception, false)));
-
- $this->assertSame($output, json_encode(['message' => 'Slim Application Error']));
- }
-
- public function testJSONErrorRendererDisplaysPreviousError()
- {
- $previousException = new Exception('Oh no!');
- $exception = new Exception('Oops..', 0, $previousException);
-
- $renderer = new JsonErrorRenderer();
- $reflectionRenderer = new ReflectionClass(JsonErrorRenderer::class);
- $method = $reflectionRenderer->getMethod('formatExceptionFragment');
- $method->setAccessible(true);
-
- $output = json_encode(json_decode($renderer->__invoke($exception, true)));
-
- $fragments = [
- $method->invoke($renderer, $exception),
- $method->invoke($renderer, $previousException),
- ];
-
- $expectedString = json_encode(['message' => 'Slim Application Error', 'exception' => $fragments]);
-
- $this->assertSame($output, $expectedString);
- }
-
- public function testJSONErrorRendererRenderHttpException()
- {
- $exceptionTitle = 'title';
-
- $httpExceptionProphecy = $this->prophesize(HttpException::class);
-
- $httpExceptionProphecy
- ->getTitle()
- ->willReturn($exceptionTitle)
- ->shouldBeCalledOnce();
-
- $renderer = new JsonErrorRenderer();
- $output = json_encode(json_decode($renderer->__invoke($httpExceptionProphecy->reveal(), false)));
-
- $this->assertSame(
- $output,
- json_encode(['message' => $exceptionTitle]),
- 'Should contain http exception title'
- );
- }
-
-
- public function testXMLErrorRendererDisplaysErrorDetails()
- {
- $previousException = new RuntimeException('Oops..');
- $exception = new Exception('Ooops...', 0, $previousException);
-
- $renderer = new XmlErrorRenderer();
-
- /** @var stdClass $output */
- $output = simplexml_load_string($renderer->__invoke($exception, true));
-
- $this->assertSame((string) $output->message[0], 'Slim Application Error');
- $this->assertSame((string) $output->exception[0]->type, 'Exception');
- $this->assertSame((string) $output->exception[0]->message, 'Ooops...');
- $this->assertSame((string) $output->exception[1]->type, 'RuntimeException');
- $this->assertSame((string) $output->exception[1]->message, 'Oops..');
- }
-
- public function testXMLErrorRendererRenderHttpException()
- {
- $exceptionTitle = 'title';
-
- $httpExceptionProphecy = $this->prophesize(HttpException::class);
-
- $httpExceptionProphecy
- ->getTitle()
- ->willReturn($exceptionTitle)
- ->shouldBeCalledOnce();
-
- $renderer = new XmlErrorRenderer();
-
- /** @var stdClass $output */
- $output = simplexml_load_string($renderer->__invoke($httpExceptionProphecy->reveal(), true));
-
- $this->assertSame((string) $output->message[0], $exceptionTitle, 'Should contain http exception title');
- }
-
- public function testPlainTextErrorRendererFormatFragmentMethod()
- {
- $message = 'Oops..
';
- $exception = new Exception($message, 500);
- $renderer = new PlainTextErrorRenderer();
- $reflectionRenderer = new ReflectionClass(PlainTextErrorRenderer::class);
-
- $method = $reflectionRenderer->getMethod('formatExceptionFragment');
- $method->setAccessible(true);
- $output = $method->invoke($renderer, $exception);
- $this->assertIsString($output);
-
- $this->assertMatchesRegularExpression('/.*Type:*/', $output);
- $this->assertMatchesRegularExpression('/.*Code:*/', $output);
- $this->assertMatchesRegularExpression('/.*Message*/', $output);
- $this->assertMatchesRegularExpression('/.*File*/', $output);
- $this->assertMatchesRegularExpression('/.*Line*/', $output);
-
- // ensure the renderer doesn't reformat the message
- $this->assertMatchesRegularExpression("/.*$message/", $output);
- }
-
- public function testPlainTextErrorRendererDisplaysErrorDetails()
- {
- $previousException = new RuntimeException('Oops..');
- $exception = new Exception('Ooops...', 0, $previousException);
-
- $renderer = new PlainTextErrorRenderer();
- $output = $renderer->__invoke($exception, true);
-
- $this->assertMatchesRegularExpression('/Ooops.../', $output);
- }
-
- public function testPlainTextErrorRendererNotDisplaysErrorDetails()
- {
- $previousException = new RuntimeException('Oops..');
- $exception = new Exception('Ooops...', 0, $previousException);
-
- $renderer = new PlainTextErrorRenderer();
- $output = $renderer->__invoke($exception, false);
-
- $this->assertSame("Slim Application Error\n", $output, 'Should show only one string');
- }
-
- public function testPlainTextErrorRendererRenderHttpException()
- {
- $exceptionTitle = 'title';
-
- $httpExceptionProphecy = $this->prophesize(HttpException::class);
-
- $httpExceptionProphecy
- ->getTitle()
- ->willReturn($exceptionTitle)
- ->shouldBeCalledOnce();
-
- $renderer = new PlainTextErrorRenderer();
- $output = $renderer->__invoke($httpExceptionProphecy->reveal(), true);
-
- $this->assertStringContainsString($exceptionTitle, $output, 'Should contain http exception title');
- }
-}
diff --git a/tests/Error/Handlers/ExceptionHandlerTest.php b/tests/Error/Handlers/ExceptionHandlerTest.php
new file mode 100644
index 000000000..369096ae8
--- /dev/null
+++ b/tests/Error/Handlers/ExceptionHandlerTest.php
@@ -0,0 +1,162 @@
+build();
+
+ $exceptionHandler = $app->getContainer()->get(ExceptionHandlerInterface::class);
+ $exceptionHandler = $exceptionHandler->withDisplayErrorDetails(true);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/')
+ ->withHeader($header, $headerValue);
+
+ $response = $exceptionHandler($request, new RuntimeException('Test Error message'));
+
+ $this->assertSame(500, $response->getStatusCode());
+ $this->assertSame('text/html', (string)$response->getHeaderLine('Content-Type'));
+ $this->assertStringContainsString('Test Error message', (string)$response->getBody());
+ }
+
+ public static function textHmlHeaderProvider(): array
+ {
+ return [
+ ['Accept', 'text/html'],
+ ['Accept', 'text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8'],
+ ['Content-Type', 'text/html'],
+ ['Content-Type', 'text/html; charset=utf-8'],
+ ];
+ }
+
+ // todo: Add test for other media types
+
+ public function testWithAcceptJson(): void
+ {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/')
+ ->withHeader('Accept', 'application/json');
+
+ $exceptionHandler = $app->getContainer()->get(ExceptionHandlerInterface::class);
+
+ $response = $exceptionHandler($request, new RuntimeException('Test exception'));
+
+ $this->assertSame(500, $response->getStatusCode());
+ $expected = [
+ 'message' => 'Application Error',
+ ];
+ $this->assertJsonResponse($expected, $response);
+ }
+
+ public function testInvokeWithDefaultHtmlRenderer(): void
+ {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $app->add(ExceptionHandlingMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $app->get('/', function () {
+ throw new Exception('Test Error message');
+ });
+
+ $response = $app->handle($request);
+
+ $this->assertSame(500, $response->getStatusCode());
+ $this->assertSame('text/html', (string)$response->getHeaderLine('Content-Type'));
+ $this->assertStringNotContainsString('Test Error message', (string)$response->getBody());
+ $this->assertStringContainsString('Application Error
', (string)$response->getBody());
+ }
+
+ public static function xmlHeaderProvider(): array
+ {
+ return [
+ ['Accept', 'application/xml'],
+ ['Accept', 'application/xml, application/json'],
+ ['Content-Type', 'application/xml'],
+ ['Content-Type', 'application/xml; charset=utf-8'],
+ ];
+ }
+
+ #[DataProvider('xmlHeaderProvider')]
+ public function testWithAcceptXml(string $header, string $headerValue): void
+ {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/')
+ ->withHeader($header, $headerValue);
+
+ /** @var ExceptionHandler $exceptionHandler */
+ $exceptionHandler = $app->getContainer()->get(ExceptionHandlerInterface::class);
+ $exceptionHandler->withDisplayErrorDetails(false);
+ $exceptionHandler
+ ->withoutHandlers()
+ ->withHandler('application/json', JsonExceptionRenderer::class)
+ ->withHandler('application/xml', XmlExceptionRenderer::class);
+
+ $response = $exceptionHandler($request, new RuntimeException('Test exception'));
+
+ $this->assertSame(500, $response->getStatusCode());
+ $expected = '
+
+ Application Error
+ ';
+
+ $dom = new DOMDocument();
+ $dom->preserveWhiteSpace = false;
+ $dom->formatOutput = true;
+ $dom->loadXML($expected);
+ $expected = $dom->saveXML();
+
+ $dom2 = new DOMDocument();
+ $dom2->preserveWhiteSpace = false;
+ $dom2->formatOutput = true;
+ $dom2->loadXML((string)$response->getBody());
+ $actual = $dom2->saveXML();
+
+ $this->assertSame($expected, $actual);
+ }
+}
diff --git a/tests/Error/Renderers/HtmlExceptionFormatterTest.php b/tests/Error/Renderers/HtmlExceptionFormatterTest.php
new file mode 100644
index 000000000..b7bff1d01
--- /dev/null
+++ b/tests/Error/Renderers/HtmlExceptionFormatterTest.php
@@ -0,0 +1,84 @@
+build();
+
+ // Create a request and response
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $exception = new Exception('Test exception message');
+
+ $formatter = $app->getContainer()->get(HtmlExceptionRenderer::class);
+ $result = $formatter($request, $response, $exception, true);
+
+ $this->assertEquals('text/html', $result->getHeaderLine('Content-Type'));
+
+ $html = (string)$result->getBody();
+ $this->assertStringContainsString('Details
', $html);
+ $this->assertStringContainsString('Test exception message', $html);
+ $this->assertStringContainsString('Type: Exception
', $html);
+ }
+
+ public function testInvokeWithExceptionAndWithoutErrorDetails()
+ {
+ // Create the Slim app
+ $app = (new AppBuilder())->build();
+
+ // Create a request and response
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $body = $app->getContainer()
+ ->get(StreamFactoryInterface::class)
+ ->createStream('');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse()
+ ->withBody($body);
+
+ $exception = new Exception('Test exception message');
+
+ // Instantiate the formatter and invoke it
+ $formatter = $app->getContainer()->get(HtmlExceptionRenderer::class);
+ $result = $formatter($request, $response, $exception, false);
+
+ // Expected HTML
+ $html = (string)$result->getBody();
+ $this->assertStringNotContainsString('Details
', $html);
+ $this->assertStringContainsString('Application Error', $html);
+ $this->assertStringContainsString(
+ 'A website error has occurred. Sorry for the temporary inconvenience.',
+ $html
+ );
+ }
+}
diff --git a/tests/Error/Renderers/JsonExceptionFormatterTest.php b/tests/Error/Renderers/JsonExceptionFormatterTest.php
new file mode 100644
index 000000000..9ada7085f
--- /dev/null
+++ b/tests/Error/Renderers/JsonExceptionFormatterTest.php
@@ -0,0 +1,107 @@
+build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $exception = new Exception('Test exception message');
+
+ // Instantiate the formatter with JsonRenderer and invoke it
+ $formatter = $app->getContainer()->get(JsonExceptionRenderer::class);
+ $result = $formatter($request, $response, $exception, true);
+
+ $this->assertEquals('application/json', $result->getHeaderLine('Content-Type'));
+
+ $json = (string)$result->getBody();
+ $data = json_decode($json, true);
+
+ // Assertions
+ $this->assertEquals('Application Error', $data['message']);
+ $this->assertArrayHasKey('exception', $data);
+ $this->assertCount(1, $data['exception']);
+ $this->assertEquals('Test exception message', $data['exception'][0]['message']);
+ }
+
+ public function testInvokeWithExceptionAndWithoutErrorDetails()
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $exception = new Exception('Test exception message');
+
+ $formatter = $app->getContainer()->get(JsonExceptionRenderer::class);
+ $result = $formatter($request, $response, $exception, false);
+
+ $this->assertEquals('application/json', $result->getHeaderLine('Content-Type'));
+
+ $json = (string)$result->getBody();
+ $data = json_decode($json, true);
+
+ // Assertions
+ $this->assertEquals('Application Error', $data['message']);
+ $this->assertArrayNotHasKey('exception', $data);
+ }
+
+ public function testInvokeWithHttpExceptionAndWithoutErrorDetails()
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse()
+ ->withStatus(404);
+
+ $exception = new HttpNotFoundException($request, 'Test exception message');
+
+ $formatter = $app->getContainer()->get(JsonExceptionRenderer::class);
+ $result = $formatter($request, $response, $exception, true);
+
+ $this->assertEquals('application/json', $result->getHeaderLine('Content-Type'));
+
+ $json = (string)$result->getBody();
+ $data = json_decode($json, true);
+
+ // Assertions
+ $this->assertEquals('404 Not Found', $data['message']);
+ $this->assertArrayHasKey('exception', $data);
+ }
+}
diff --git a/tests/Error/Renderers/PlainTextExceptionFormatterTest.php b/tests/Error/Renderers/PlainTextExceptionFormatterTest.php
new file mode 100644
index 000000000..8b4a2cf76
--- /dev/null
+++ b/tests/Error/Renderers/PlainTextExceptionFormatterTest.php
@@ -0,0 +1,108 @@
+build();
+
+ // Create a request and response
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $exception = new Exception('Test exception message');
+
+ // Instantiate the formatter and invoke it
+ $formatter = $app->getContainer()->get(PlainTextExceptionRenderer::class);
+ $result = $formatter($request, $response, $exception, true);
+
+ // Assertions
+ $this->assertEquals('text/plain', $result->getHeaderLine('Content-Type'));
+
+ $text = (string)$result->getBody();
+ $this->assertStringContainsString('Application Error', $text);
+ $this->assertStringContainsString('Test exception message', $text);
+ $this->assertStringContainsString('Type: Exception', $text);
+ $this->assertStringContainsString('Message: Test exception message', $text);
+ }
+
+ public function testInvokeWithExceptionAndWithoutErrorDetails()
+ {
+ $app = (new AppBuilder())->build();
+
+ // Create a request and response
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $exception = new Exception('Test exception message');
+
+ // Instantiate the formatter and invoke it
+ $formatter = $app->getContainer()->get(PlainTextExceptionRenderer::class);
+ $result = $formatter($request, $response, $exception, false);
+
+ // Assertions
+ $this->assertEquals('text/plain', $result->getHeaderLine('Content-Type'));
+
+ $text = (string)$result->getBody();
+ $this->assertStringContainsString('Application Error', $text);
+ $this->assertStringNotContainsString('Test exception message', $text);
+ $this->assertStringNotContainsString('Type: Exception', $text);
+ }
+
+ public function testInvokeWithNestedExceptionsAndWithErrorDetails()
+ {
+ $app = (new AppBuilder())->build();
+
+ // Create a request and response
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $innerException = new Exception('Inner exception message');
+ $outerException = new Exception('Outer exception message', 0, $innerException);
+
+ // Instantiate the formatter and invoke it
+ $formatter = $app->getContainer()->get(PlainTextExceptionRenderer::class);
+ $result = $formatter($request, $response, $outerException, true);
+
+ // Assertions
+ $this->assertEquals('text/plain', $result->getHeaderLine('Content-Type'));
+
+ $text = (string)$result->getBody();
+ $this->assertStringContainsString('Application Error', $text);
+ $this->assertStringContainsString('Outer exception message', $text);
+ $this->assertStringContainsString('Inner exception message', $text);
+ $this->assertStringContainsString('Previous Exception:', $text);
+ }
+}
diff --git a/tests/Error/Renderers/XmlExceptionFormatterTest.php b/tests/Error/Renderers/XmlExceptionFormatterTest.php
new file mode 100644
index 000000000..1137ecae4
--- /dev/null
+++ b/tests/Error/Renderers/XmlExceptionFormatterTest.php
@@ -0,0 +1,108 @@
+build();
+
+ // Create a request and response
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $exception = new Exception('Test exception message');
+
+ // Instantiate the formatter and invoke it
+ $formatter = $app->getContainer()->get(XmlExceptionRenderer::class);
+ $result = $formatter($request, $response, $exception, true);
+
+ // Assertions
+ $this->assertEquals('application/xml', $result->getHeaderLine('Content-Type'));
+
+ $xml = (string)$result->getBody();
+ $this->assertStringContainsString('Application Error', $xml);
+ $this->assertStringContainsString('', $xml);
+ $this->assertStringContainsString('Exception', $xml);
+ $this->assertStringContainsString('Test exception message', $xml);
+ }
+
+ public function testInvokeWithExceptionAndWithoutErrorDetails()
+ {
+ $app = (new AppBuilder())->build();
+
+ // Create a request and response
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $exception = new Exception('Test exception message');
+
+ // Instantiate the formatter and invoke it
+ $formatter = $app->getContainer()->get(XmlExceptionRenderer::class);
+ $result = $formatter($request, $response, $exception, false);
+
+ // Assertions
+ $this->assertEquals('application/xml', $result->getHeaderLine('Content-Type'));
+
+ $xml = (string)$result->getBody();
+ $this->assertStringContainsString('Application Error', $xml);
+ $this->assertStringNotContainsString('', $xml);
+ $this->assertStringNotContainsString('Exception', $xml);
+ }
+
+ public function testInvokeWithNestedExceptionsAndWithErrorDetails()
+ {
+ $app = (new AppBuilder())->build();
+
+ // Create a request and response
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $innerException = new Exception('Inner exception message');
+ $outerException = new Exception('Outer exception message', 0, $innerException);
+
+ // Instantiate the formatter and invoke it
+ $formatter = $app->getContainer()->get(XmlExceptionRenderer::class);
+ $result = $formatter($request, $response, $outerException, true);
+
+ // Assertions
+ $this->assertEquals('application/xml', $result->getHeaderLine('Content-Type'));
+
+ $xml = (string)$result->getBody();
+ $this->assertStringContainsString('Application Error', $xml);
+ $this->assertStringContainsString('', $xml);
+ $this->assertStringContainsString('Outer exception message', $xml);
+ $this->assertStringContainsString('Inner exception message', $xml);
+ }
+}
diff --git a/tests/Exception/HttpExceptionTest.php b/tests/Exception/HttpExceptionTest.php
index 4edd66fb8..4d5beedd4 100644
--- a/tests/Exception/HttpExceptionTest.php
+++ b/tests/Exception/HttpExceptionTest.php
@@ -3,23 +3,33 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Tests\Exception;
+use PHPUnit\Framework\TestCase;
+use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
+use Slim\Builder\AppBuilder;
use Slim\Exception\HttpMethodNotAllowedException;
use Slim\Exception\HttpNotFoundException;
-use Slim\Tests\TestCase;
+use Slim\Tests\Traits\AppTestTrait;
-class HttpExceptionTest extends TestCase
+final class HttpExceptionTest extends TestCase
{
+ use AppTestTrait;
+
public function testHttpExceptionRequestReponseGetterSetters()
{
- $request = $this->createServerRequest('/');
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
$exception = new HttpNotFoundException($request);
$this->assertInstanceOf(ServerRequestInterface::class, $exception->getRequest());
@@ -27,7 +37,11 @@ public function testHttpExceptionRequestReponseGetterSetters()
public function testHttpExceptionAttributeGettersSetters()
{
- $request = $this->createServerRequest('/');
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
$exception = new HttpNotFoundException($request);
$exception->setTitle('Title');
@@ -39,7 +53,11 @@ public function testHttpExceptionAttributeGettersSetters()
public function testHttpNotAllowedExceptionGetAllowedMethods()
{
- $request = $this->createServerRequest('/');
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
$exception = new HttpMethodNotAllowedException($request);
$exception->setAllowedMethods(['GET']);
diff --git a/tests/Exception/HttpUnauthorizedExceptionTest.php b/tests/Exception/HttpUnauthorizedExceptionTest.php
index b57b839a7..99bc3462e 100644
--- a/tests/Exception/HttpUnauthorizedExceptionTest.php
+++ b/tests/Exception/HttpUnauthorizedExceptionTest.php
@@ -3,21 +3,31 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Tests\Exception;
+use PHPUnit\Framework\TestCase;
+use Psr\Http\Message\ServerRequestFactoryInterface;
+use Slim\Builder\AppBuilder;
use Slim\Exception\HttpUnauthorizedException;
-use Slim\Tests\TestCase;
+use Slim\Tests\Traits\AppTestTrait;
-class HttpUnauthorizedExceptionTest extends TestCase
+final class HttpUnauthorizedExceptionTest extends TestCase
{
+ use AppTestTrait;
+
public function testHttpUnauthorizedException()
{
- $request = $this->createServerRequest('/');
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
$exception = new HttpUnauthorizedException($request);
$this->assertInstanceOf(HttpUnauthorizedException::class, $exception);
@@ -25,7 +35,12 @@ public function testHttpUnauthorizedException()
public function testHttpUnauthorizedExceptionWithMessage()
{
- $request = $this->createServerRequest('/');
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
$exception = new HttpUnauthorizedException($request, 'Hello World');
$this->assertSame('Hello World', $exception->getMessage());
diff --git a/tests/Factory/AppFactoryTest.php b/tests/Factory/AppFactoryTest.php
deleted file mode 100644
index 25349ffc8..000000000
--- a/tests/Factory/AppFactoryTest.php
+++ /dev/null
@@ -1,394 +0,0 @@
-setStaticPropertyValue('responseFactoryClass', DecoratedResponseFactory::class);
- }
-
- public function provideImplementations()
- {
- return [
- [SlimPsr17Factory::class, SlimResponseFactory::class],
- [HttpSoftPsr17Factory::class, HttpSoftResponseFactory::class],
- [NyholmPsr17Factory::class, Psr17Factory::class],
- [GuzzlePsr17Factory::class, HttpFactory::class],
- [LaminasDiactorosPsr17Factory::class, LaminasDiactorosResponseFactory::class],
- ];
- }
-
- /**
- * @dataProvider provideImplementations
- * @param string $psr17factory
- * @param string $expectedResponseFactoryClass
- */
- public function testCreateAppWithAllImplementations(string $psr17factory, string $expectedResponseFactoryClass)
- {
- Psr17FactoryProvider::setFactories([$psr17factory]);
- AppFactory::setSlimHttpDecoratorsAutomaticDetection(false);
-
- $app = AppFactory::create();
-
- $routeCollector = $app->getRouteCollector();
-
- $responseFactoryProperty = new ReflectionProperty(RouteCollector::class, 'responseFactory');
- $responseFactoryProperty->setAccessible(true);
-
- $responseFactory = $responseFactoryProperty->getValue($routeCollector);
-
- $this->assertInstanceOf($expectedResponseFactoryClass, $responseFactory);
- }
-
- public function testDetermineResponseFactoryReturnsDecoratedFactory()
- {
- Psr17FactoryProvider::setFactories([SlimPsr17Factory::class]);
- AppFactory::setSlimHttpDecoratorsAutomaticDetection(true);
-
- $app = AppFactory::create();
-
- $this->assertInstanceOf(DecoratedResponseFactory::class, $app->getResponseFactory());
- }
-
- public function testDetermineResponseFactoryThrowsRuntimeExceptionIfDecoratedNotInstanceOfResponseInterface()
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage(
- 'Slim\\Factory\\Psr17\\SlimHttpPsr17Factory could not instantiate a decorated response factory.'
- );
-
- $reflectionClass = new ReflectionClass(SlimHttpPsr17Factory::class);
- $reflectionClass->setStaticPropertyValue('responseFactoryClass', SlimHttpPsr17Factory::class);
-
- Psr17FactoryProvider::setFactories([SlimPsr17Factory::class]);
- AppFactory::setSlimHttpDecoratorsAutomaticDetection(true);
-
- AppFactory::create();
- }
-
- /**
- * @runInSeparateProcess - Psr17FactoryProvider::setFactories breaks other tests
- */
- public function testDetermineResponseFactoryThrowsRuntimeException()
- {
- $this->expectException(RuntimeException::class);
-
- Psr17FactoryProvider::setFactories([]);
- AppFactory::create();
- }
-
- public function testSetPsr17FactoryProvider()
- {
- $psr17FactoryProvider = new Psr17FactoryProvider();
- $psr17FactoryProvider::setFactories([SlimPsr17Factory::class]);
-
- AppFactory::setPsr17FactoryProvider($psr17FactoryProvider);
- AppFactory::setSlimHttpDecoratorsAutomaticDetection(false);
-
- $this->assertInstanceOf(SlimResponseFactory::class, AppFactory::determineResponseFactory());
- }
-
- /**
- * @runInSeparateProcess - Psr17FactoryProvider::setFactories breaks other tests
- */
- public function testResponseFactoryIsStillReturnedIfStreamFactoryIsNotAvailable()
- {
- Psr17FactoryProvider::setFactories([MockPsr17FactoryWithoutStreamFactory::class]);
- AppFactory::setSlimHttpDecoratorsAutomaticDetection(true);
-
- $app = AppFactory::create();
-
- $this->assertInstanceOf(SlimResponseFactory::class, $app->getResponseFactory());
- }
-
- /**
- * @runInSeparateProcess - AppFactory::setResponseFactory breaks other tests
- */
- public function testAppIsCreatedWithInstancesFromSetters()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class);
- $routeParserProphecy = $this->prophesize(RouteParserInterface::class);
- $routeResolverProphecy = $this->prophesize(RouteResolverInterface::class);
- $middlewareDispatcherProphecy = $this->prophesize(MiddlewareDispatcherInterface::class);
-
- $routeCollectorProphecy->getRouteParser()->willReturn($routeParserProphecy);
-
- AppFactory::setSlimHttpDecoratorsAutomaticDetection(false);
- AppFactory::setResponseFactory($responseFactoryProphecy->reveal());
- AppFactory::setContainer($containerProphecy->reveal());
- AppFactory::setCallableResolver($callableResolverProphecy->reveal());
- AppFactory::setRouteCollector($routeCollectorProphecy->reveal());
- AppFactory::setRouteResolver($routeResolverProphecy->reveal());
- AppFactory::setMiddlewareDispatcher($middlewareDispatcherProphecy->reveal());
-
- $app = AppFactory::create();
-
- $this->assertSame(
- $responseFactoryProphecy->reveal(),
- $app->getResponseFactory()
- );
-
- $this->assertSame(
- $containerProphecy->reveal(),
- $app->getContainer()
- );
-
- $this->assertSame(
- $callableResolverProphecy->reveal(),
- $app->getCallableResolver()
- );
-
- $this->assertSame(
- $routeCollectorProphecy->reveal(),
- $app->getRouteCollector()
- );
-
- $this->assertSame(
- $routeResolverProphecy->reveal(),
- $app->getRouteResolver()
- );
-
- $this->assertSame(
- $middlewareDispatcherProphecy->reveal(),
- $app->getMiddlewareDispatcher()
- );
- }
-
- /**
- * @runInSeparateProcess - AppFactory::create saves $responseFactory into static::$responseFactory,
- * this breaks other tests
- */
- public function testAppIsCreatedWithInjectedInstancesFromFunctionArguments()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class);
- $routeParserProphecy = $this->prophesize(RouteParserInterface::class);
- $routeResolverProphecy = $this->prophesize(RouteResolverInterface::class);
-
- $routeCollectorProphecy->getRouteParser()->willReturn($routeParserProphecy->reveal());
-
- AppFactory::setSlimHttpDecoratorsAutomaticDetection(false);
-
- $app = AppFactory::create(
- $responseFactoryProphecy->reveal(),
- $containerProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- $routeCollectorProphecy->reveal(),
- $routeResolverProphecy->reveal()
- );
-
- $this->assertSame(
- $responseFactoryProphecy->reveal(),
- $app->getResponseFactory()
- );
-
- $this->assertSame(
- $containerProphecy->reveal(),
- $app->getContainer()
- );
-
- $this->assertSame(
- $callableResolverProphecy->reveal(),
- $app->getCallableResolver()
- );
-
- $this->assertSame(
- $routeCollectorProphecy->reveal(),
- $app->getRouteCollector()
- );
-
- $this->assertSame(
- $routeResolverProphecy->reveal(),
- $app->getRouteResolver()
- );
- }
-
- /**
- * @runInSeparateProcess - AppFactory::setResponseFactory breaks other tests
- */
- public function testResponseAndStreamFactoryIsBeingInjectedInDecoratedResponseFactory()
- {
- $responseProphecy = $this->prophesize(ResponseInterface::class);
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy
- ->createResponse(200, '')
- ->willReturn($responseProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $streamFactoryProphecy = $this->prophesize(StreamFactoryInterface::class);
-
- AppFactory::setResponseFactory($responseFactoryProphecy->reveal());
- AppFactory::setStreamFactory($streamFactoryProphecy->reveal());
- AppFactory::setSlimHttpDecoratorsAutomaticDetection(true);
-
- $app = AppFactory::create();
-
- $responseFactory = $app->getResponseFactory();
- $response = $responseFactory->createResponse();
-
- $streamFactoryProperty = new ReflectionProperty(DecoratedResponse::class, 'streamFactory');
- $streamFactoryProperty->setAccessible(true);
-
- $this->assertSame($streamFactoryProphecy->reveal(), $streamFactoryProperty->getValue($response));
- }
-
- public function testCreateAppWithContainerUsesContainerDependenciesWhenPresent()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $routeResolverProphecy = $this->prophesize(RouteResolverInterface::class);
- $routeParserProphecy = $this->prophesize(RouteParserInterface::class);
-
- $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class);
- $routeCollectorProphecy
- ->getRouteParser()
- ->willReturn($routeParserProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $middlewareDispatcherProphecy = $this->prophesize(MiddlewareDispatcherInterface::class);
-
- $containerProphecy = $this->prophesize(ContainerInterface::class);
-
- $containerProphecy
- ->has(ResponseFactoryInterface::class)
- ->willReturn(true)
- ->shouldBeCalledOnce();
-
- $containerProphecy
- ->get(ResponseFactoryInterface::class)
- ->willReturn($responseFactoryProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $containerProphecy
- ->has(CallableResolverInterface::class)
- ->willReturn(true)
- ->shouldBeCalledOnce();
-
- $containerProphecy
- ->get(CallableResolverInterface::class)
- ->willReturn($callableResolverProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $containerProphecy
- ->has(RouteCollectorInterface::class)
- ->willReturn(true)
- ->shouldBeCalledOnce();
-
- $containerProphecy
- ->get(RouteCollectorInterface::class)
- ->willReturn($routeCollectorProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $containerProphecy
- ->has(RouteResolverInterface::class)
- ->willReturn(true)
- ->shouldBeCalledOnce();
-
- $containerProphecy
- ->get(RouteResolverInterface::class)
- ->willReturn($routeResolverProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $containerProphecy
- ->has(MiddlewareDispatcherInterface::class)
- ->willReturn(true)
- ->shouldBeCalledOnce();
-
- $containerProphecy
- ->get(MiddlewareDispatcherInterface::class)
- ->willReturn($middlewareDispatcherProphecy->reveal())
- ->shouldBeCalledOnce();
-
- AppFactory::setSlimHttpDecoratorsAutomaticDetection(false);
- $app = AppFactory::createFromContainer($containerProphecy->reveal());
-
- $this->assertSame($app->getResponseFactory(), $responseFactoryProphecy->reveal());
- $this->assertSame($app->getContainer(), $containerProphecy->reveal());
- $this->assertSame($app->getCallableResolver(), $callableResolverProphecy->reveal());
- $this->assertSame($app->getRouteCollector(), $routeCollectorProphecy->reveal());
- $this->assertSame($app->getRouteResolver(), $routeResolverProphecy->reveal());
- $this->assertSame($app->getMiddlewareDispatcher(), $middlewareDispatcherProphecy->reveal());
- }
-
- public function testCreateAppWithEmptyContainer()
- {
- $containerProphecy = $this->prophesize(ContainerInterface::class);
-
- $containerProphecy
- ->has(ResponseFactoryInterface::class)
- ->willReturn(false)
- ->shouldBeCalledOnce();
-
- $containerProphecy
- ->has(CallableResolverInterface::class)
- ->willReturn(false)
- ->shouldBeCalledOnce();
-
- $containerProphecy
- ->has(RouteCollectorInterface::class)
- ->willReturn(false)
- ->shouldBeCalledOnce();
-
- $containerProphecy
- ->has(RouteResolverInterface::class)
- ->willReturn(false)
- ->shouldBeCalledOnce();
-
- $containerProphecy
- ->has(MiddlewareDispatcherInterface::class)
- ->willReturn(false)
- ->shouldBeCalledOnce();
-
- AppFactory::setSlimHttpDecoratorsAutomaticDetection(false);
- AppFactory::createFromContainer($containerProphecy->reveal());
- }
-}
diff --git a/tests/Factory/Psr17/Psr17FactoryProviderTest.php b/tests/Factory/Psr17/Psr17FactoryProviderTest.php
deleted file mode 100644
index 291000ddb..000000000
--- a/tests/Factory/Psr17/Psr17FactoryProviderTest.php
+++ /dev/null
@@ -1,39 +0,0 @@
-assertSame([], Psr17FactoryProvider::getFactories());
- }
-
-
- /**
- * @runInSeparateProcess - Psr17FactoryProvider::setFactories breaks other tests
- */
- public function testAddFactory()
- {
- Psr17FactoryProvider::setFactories(['Factory 1']);
- Psr17FactoryProvider::addFactory('Factory 2');
-
- $this->assertSame(['Factory 2', 'Factory 1'], Psr17FactoryProvider::getFactories());
- }
-}
diff --git a/tests/Factory/Psr17/Psr17FactoryTest.php b/tests/Factory/Psr17/Psr17FactoryTest.php
deleted file mode 100644
index da5821705..000000000
--- a/tests/Factory/Psr17/Psr17FactoryTest.php
+++ /dev/null
@@ -1,43 +0,0 @@
-expectException(RuntimeException::class);
- $this->expectExceptionMessage('Slim\\Tests\\Mocks\\MockPsr17Factory could not instantiate a response factory.');
-
- MockPsr17Factory::getResponseFactory();
- }
-
- public function testGetStreamFactoryThrowsRuntimeException()
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Slim\\Tests\\Mocks\\MockPsr17Factory could not instantiate a stream factory.');
-
- MockPsr17Factory::getStreamFactory();
- }
-
- public function testGetServerRequestCreatorThrowsRuntimeException()
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Slim\\Tests\\Mocks\\MockPsr17Factory' .
- ' could not instantiate a server request creator.');
-
- MockPsr17Factory::getServerRequestCreator();
- }
-}
diff --git a/tests/Factory/Psr17/SlimHttpServerRequestCreatorTest.php b/tests/Factory/Psr17/SlimHttpServerRequestCreatorTest.php
deleted file mode 100644
index 328f4cb4c..000000000
--- a/tests/Factory/Psr17/SlimHttpServerRequestCreatorTest.php
+++ /dev/null
@@ -1,100 +0,0 @@
-prophesize(ServerRequestCreatorInterface::class);
-
- $slimHttpServerRequestCreator = new SlimHttpServerRequestCreator($serverRequestCreatorProphecy->reveal());
-
- $serverRequestDecoratorClassProperty = new ReflectionProperty(
- SlimHttpServerRequestCreator::class,
- 'serverRequestDecoratorClass'
- );
- $serverRequestDecoratorClassProperty->setAccessible(true);
- $serverRequestDecoratorClassProperty->setValue($slimHttpServerRequestCreator, ServerRequest::class);
- }
-
- public function testCreateServerRequestFromGlobals()
- {
- $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class);
-
- $serverRequestCreatorProphecy = $this->prophesize(ServerRequestCreatorInterface::class);
-
- $serverRequestCreatorProphecy
- ->createServerRequestFromGlobals()
- ->willReturn($serverRequestProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $slimHttpServerRequestCreator = new SlimHttpServerRequestCreator($serverRequestCreatorProphecy->reveal());
-
- $this->assertInstanceOf(ServerRequest::class, $slimHttpServerRequestCreator->createServerRequestFromGlobals());
- }
-
- public function testCreateServerRequestFromGlobalsThrowsRuntimeException()
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('The Slim-Http ServerRequest decorator is not available.');
-
- $serverRequestCreatorProphecy = $this->prophesize(ServerRequestCreatorInterface::class);
-
- $slimHttpServerRequestCreator = new SlimHttpServerRequestCreator($serverRequestCreatorProphecy->reveal());
-
- $serverRequestDecoratorClassProperty = new ReflectionProperty(
- SlimHttpServerRequestCreator::class,
- 'serverRequestDecoratorClass'
- );
- $serverRequestDecoratorClassProperty->setAccessible(true);
- $serverRequestDecoratorClassProperty->setValue($slimHttpServerRequestCreator, '');
-
- $slimHttpServerRequestCreator->createServerRequestFromGlobals();
- }
-
- public function testCreateServerRequestFromGlobalsThrowsRuntimeExceptionIfNotInstanceOfServerRequestInterface()
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage(
- 'Slim\\Factory\\Psr17\\SlimHttpServerRequestCreator could not instantiate a decorated server request.'
- );
-
- $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class);
-
- $serverRequestCreatorProphecy = $this->prophesize(ServerRequestCreatorInterface::class);
- $serverRequestCreatorProphecy
- ->createServerRequestFromGlobals()
- ->willReturn($serverRequestProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $slimHttpServerRequestCreator = new SlimHttpServerRequestCreator($serverRequestCreatorProphecy->reveal());
-
- $reflectionClass = new ReflectionClass(SlimHttpServerRequestCreator::class);
- $reflectionClass->setStaticPropertyValue('serverRequestDecoratorClass', stdClass::class);
-
- $slimHttpServerRequestCreator->createServerRequestFromGlobals();
- }
-}
diff --git a/tests/Factory/ServerRequestCreatorFactoryTest.php b/tests/Factory/ServerRequestCreatorFactoryTest.php
deleted file mode 100644
index e68a08382..000000000
--- a/tests/Factory/ServerRequestCreatorFactoryTest.php
+++ /dev/null
@@ -1,137 +0,0 @@
-createServerRequestFromGlobals();
-
- $this->assertInstanceOf($expectedServerRequestClass, $serverRequest);
- }
-
- public function testDetermineServerRequestCreatorReturnsDecoratedServerRequestCreator()
- {
- Psr17FactoryProvider::setFactories([SlimPsr17Factory::class]);
- ServerRequestCreatorFactory::setSlimHttpDecoratorsAutomaticDetection(true);
-
- $serverRequestCreator = ServerRequestCreatorFactory::create();
-
- $this->assertInstanceOf(SlimHttpServerRequestCreator::class, $serverRequestCreator);
- $this->assertInstanceOf(ServerRequest::class, $serverRequestCreator->createServerRequestFromGlobals());
- }
-
- /**
- * @runInSeparateProcess - Psr17FactoryProvider::setFactories breaks other tests
- */
- public function testDetermineServerRequestCreatorThrowsRuntimeException()
- {
- $this->expectException(RuntimeException::class);
-
- Psr17FactoryProvider::setFactories([]);
- ServerRequestCreatorFactory::create();
- }
-
- public function testSetPsr17FactoryProvider()
- {
- $psr17FactoryProvider = new Psr17FactoryProvider();
- $psr17FactoryProvider::setFactories([SlimPsr17Factory::class]);
-
- ServerRequestCreatorFactory::setPsr17FactoryProvider($psr17FactoryProvider);
- ServerRequestCreatorFactory::setSlimHttpDecoratorsAutomaticDetection(false);
-
- $serverRequestCreator = ServerRequestCreatorFactory::create();
-
- $this->assertInstanceOf(SlimServerRequest::class, $serverRequestCreator->createServerRequestFromGlobals());
- }
-
- /**
- * @runInSeparateProcess - ServerRequestCreatorFactory::setServerRequestCreator breaks other tests
- */
- public function testSetServerRequestCreatorWithoutDecorators()
- {
- ServerRequestCreatorFactory::setSlimHttpDecoratorsAutomaticDetection(false);
- $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class);
-
- $serverRequestCreatorProphecy = $this->prophesize(ServerRequestCreatorInterface::class);
- $serverRequestCreatorProphecy
- ->createServerRequestFromGlobals()
- ->willReturn($serverRequestProphecy->reveal())
- ->shouldBeCalledOnce();
-
- ServerRequestCreatorFactory::setServerRequestCreator($serverRequestCreatorProphecy->reveal());
-
- $serverRequestCreator = ServerRequestCreatorFactory::create();
-
- $this->assertSame($serverRequestProphecy->reveal(), $serverRequestCreator->createServerRequestFromGlobals());
- }
-
- /**
- * @runInSeparateProcess - ServerRequestCreatorFactory::setServerRequestCreator breaks other tests
- */
- public function testSetServerRequestCreatorWithDecorators()
- {
- ServerRequestCreatorFactory::setSlimHttpDecoratorsAutomaticDetection(true);
- $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class);
-
- $serverRequestCreatorProphecy = $this->prophesize(ServerRequestCreatorInterface::class);
- $serverRequestCreatorProphecy
- ->createServerRequestFromGlobals()
- ->willReturn($serverRequestProphecy->reveal())
- ->shouldBeCalledOnce();
-
- ServerRequestCreatorFactory::setServerRequestCreator($serverRequestCreatorProphecy->reveal());
-
- $serverRequestCreator = ServerRequestCreatorFactory::create();
-
- $this->assertInstanceOf(ServerRequest::class, $serverRequestCreator->createServerRequestFromGlobals());
- }
-}
diff --git a/tests/Handlers/ErrorHandlerTest.php b/tests/Handlers/ErrorHandlerTest.php
deleted file mode 100644
index b50dff4b3..000000000
--- a/tests/Handlers/ErrorHandlerTest.php
+++ /dev/null
@@ -1,429 +0,0 @@
-createMock(LoggerInterface::class);
- }
-
- public function testDetermineRenderer()
- {
- $handler = $this
- ->getMockBuilder(ErrorHandler::class)
- ->disableOriginalConstructor()
- ->getMock();
- $class = new ReflectionClass(ErrorHandler::class);
-
- $callableResolverProperty = $class->getProperty('callableResolver');
- $callableResolverProperty->setAccessible(true);
- $callableResolverProperty->setValue($handler, $this->getCallableResolver());
-
- $reflectionProperty = $class->getProperty('contentType');
- $reflectionProperty->setAccessible(true);
- $reflectionProperty->setValue($handler, 'application/json');
-
- $method = $class->getMethod('determineRenderer');
- $method->setAccessible(true);
-
- $renderer = $method->invoke($handler);
- $this->assertIsCallable($renderer);
- $this->assertInstanceOf(JsonErrorRenderer::class, $renderer[0]);
-
- $reflectionProperty->setValue($handler, 'application/xml');
- $renderer = $method->invoke($handler);
- $this->assertIsCallable($renderer);
- $this->assertInstanceOf(XmlErrorRenderer::class, $renderer[0]);
-
- $reflectionProperty->setValue($handler, 'text/plain');
- $renderer = $method->invoke($handler);
- $this->assertIsCallable($renderer);
- $this->assertInstanceOf(PlainTextErrorRenderer::class, $renderer[0]);
-
- // Test the default error renderer
- $reflectionProperty->setValue($handler, 'text/unknown');
- $renderer = $method->invoke($handler);
- $this->assertIsCallable($renderer);
- $this->assertInstanceOf(HtmlErrorRenderer::class, $renderer[0]);
- }
-
- public function testDetermineStatusCode()
- {
- $request = $this->createServerRequest('/');
- $handler = $this
- ->getMockBuilder(ErrorHandler::class)
- ->disableOriginalConstructor()
- ->getMock();
- $class = new ReflectionClass(ErrorHandler::class);
-
- $reflectionProperty = $class->getProperty('responseFactory');
- $reflectionProperty->setAccessible(true);
- $reflectionProperty->setValue($handler, $this->getResponseFactory());
-
- $reflectionProperty = $class->getProperty('exception');
- $reflectionProperty->setAccessible(true);
- $reflectionProperty->setValue($handler, new HttpNotFoundException($request));
-
- $method = $class->getMethod('determineStatusCode');
- $method->setAccessible(true);
-
- $statusCode = $method->invoke($handler);
- $this->assertSame($statusCode, 404);
-
- $reflectionProperty->setValue($handler, new MockCustomException());
-
- $statusCode = $method->invoke($handler);
- $this->assertSame($statusCode, 500);
- }
-
- /**
- * Test if we can force the content type of all error handler responses.
- */
- public function testForceContentType()
- {
- $request = $this
- ->createServerRequest('/not-defined', 'GET')
- ->withHeader('Accept', 'text/plain,text/xml');
-
- $handler = new ErrorHandler($this->getCallableResolver(), $this->getResponseFactory());
- $handler->forceContentType('application/json');
-
- $exception = new HttpNotFoundException($request);
-
- /** @var ResponseInterface $response */
- $response = $handler->__invoke($request, $exception, false, false, false);
-
- $this->assertSame(['application/json'], $response->getHeader('Content-Type'));
- }
-
- public function testHalfValidContentType()
- {
- $request = $this
- ->createServerRequest('/', 'GET')
- ->withHeader('Content-Type', 'unknown/json+');
-
- $handler = $this
- ->getMockBuilder(ErrorHandler::class)
- ->disableOriginalConstructor()
- ->getMock();
- $newErrorRenderers = [
- 'application/xml' => XmlErrorRenderer::class,
- 'text/xml' => XmlErrorRenderer::class,
- 'text/html' => HtmlErrorRenderer::class,
- ];
-
- $class = new ReflectionClass(ErrorHandler::class);
-
- $reflectionProperty = $class->getProperty('responseFactory');
- $reflectionProperty->setAccessible(true);
- $reflectionProperty->setValue($handler, $this->getResponseFactory());
-
- $reflectionProperty = $class->getProperty('errorRenderers');
- $reflectionProperty->setAccessible(true);
- $reflectionProperty->setValue($handler, $newErrorRenderers);
-
- $method = $class->getMethod('determineContentType');
- $method->setAccessible(true);
-
- $contentType = $method->invoke($handler, $request);
-
- $this->assertNull($contentType);
- }
-
- public function testDetermineContentTypeTextPlainMultiAcceptHeader()
- {
- $request = $this
- ->createServerRequest('/', 'GET')
- ->withHeader('Content-Type', 'text/plain')
- ->withHeader('Accept', 'text/plain,text/xml');
-
- $handler = $this
- ->getMockBuilder(ErrorHandler::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $errorRenderers = [
- 'text/plain' => PlainTextErrorRenderer::class,
- 'text/xml' => XmlErrorRenderer::class,
- ];
-
- $class = new ReflectionClass(ErrorHandler::class);
-
- $reflectionProperty = $class->getProperty('responseFactory');
- $reflectionProperty->setAccessible(true);
- $reflectionProperty->setValue($handler, $this->getResponseFactory());
-
- $reflectionProperty = $class->getProperty('errorRenderers');
- $reflectionProperty->setAccessible(true);
- $reflectionProperty->setValue($handler, $errorRenderers);
-
- $method = $class->getMethod('determineContentType');
- $method->setAccessible(true);
-
- $contentType = $method->invoke($handler, $request);
-
- $this->assertSame('text/xml', $contentType);
- }
-
- public function testDetermineContentTypeApplicationJsonOrXml()
- {
- $request = $this
- ->createServerRequest('/', 'GET')
- ->withHeader('Content-Type', 'text/json')
- ->withHeader('Accept', 'application/xhtml+xml');
-
- $handler = $this
- ->getMockBuilder(ErrorHandler::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $errorRenderers = [
- 'application/xml' => XmlErrorRenderer::class
- ];
-
- $class = new ReflectionClass(ErrorHandler::class);
-
- $reflectionProperty = $class->getProperty('responseFactory');
- $reflectionProperty->setAccessible(true);
- $reflectionProperty->setValue($handler, $this->getResponseFactory());
-
- $reflectionProperty = $class->getProperty('errorRenderers');
- $reflectionProperty->setAccessible(true);
- $reflectionProperty->setValue($handler, $errorRenderers);
-
- $method = $class->getMethod('determineContentType');
- $method->setAccessible(true);
-
- $contentType = $method->invoke($handler, $request);
-
- $this->assertSame('application/xml', $contentType);
- }
-
- /**
- * Ensure that an acceptable media-type is found in the Accept header even
- * if it's not the first in the list.
- */
- public function testAcceptableMediaTypeIsNotFirstInList()
- {
- $request = $this
- ->createServerRequest('/', 'GET')
- ->withHeader('Accept', 'text/plain,text/html');
-
- // provide access to the determineContentType() as it's a protected method
- $class = new ReflectionClass(ErrorHandler::class);
- $method = $class->getMethod('determineContentType');
- $method->setAccessible(true);
-
- // use a mock object here as ErrorHandler cannot be directly instantiated
- $handler = $this
- ->getMockBuilder(ErrorHandler::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- // call determineContentType()
- $return = $method->invoke($handler, $request);
-
- $this->assertSame('text/html', $return);
- }
-
- public function testRegisterErrorRenderer()
- {
- $handler = new ErrorHandler($this->getCallableResolver(), $this->getResponseFactory());
- $handler->registerErrorRenderer('application/slim', PlainTextErrorRenderer::class);
-
- $reflectionClass = new ReflectionClass(ErrorHandler::class);
- $reflectionProperty = $reflectionClass->getProperty('errorRenderers');
- $reflectionProperty->setAccessible(true);
- $errorRenderers = $reflectionProperty->getValue($handler);
-
- $this->assertArrayHasKey('application/slim', $errorRenderers);
- }
-
- public function testSetDefaultErrorRenderer()
- {
- $handler = new ErrorHandler($this->getCallableResolver(), $this->getResponseFactory());
- $handler->setDefaultErrorRenderer('text/plain', PlainTextErrorRenderer::class);
-
- $reflectionClass = new ReflectionClass(ErrorHandler::class);
- $reflectionProperty = $reflectionClass->getProperty('defaultErrorRenderer');
- $reflectionProperty->setAccessible(true);
- $defaultErrorRenderer = $reflectionProperty->getValue($handler);
-
- $defaultErrorRendererContentTypeProperty = $reflectionClass->getProperty('defaultErrorRendererContentType');
- $defaultErrorRendererContentTypeProperty->setAccessible(true);
- $defaultErrorRendererContentType = $defaultErrorRendererContentTypeProperty->getValue($handler);
-
- $this->assertSame(PlainTextErrorRenderer::class, $defaultErrorRenderer);
- $this->assertSame('text/plain', $defaultErrorRendererContentType);
- }
-
- public function testOptions()
- {
- $request = $this->createServerRequest('/', 'OPTIONS');
- $handler = new ErrorHandler($this->getCallableResolver(), $this->getResponseFactory());
- $exception = new HttpMethodNotAllowedException($request);
- $exception->setAllowedMethods(['POST', 'PUT']);
-
- /** @var ResponseInterface $res */
- $res = $handler->__invoke($request, $exception, true, false, true);
-
- $this->assertSame(200, $res->getStatusCode());
- $this->assertTrue($res->hasHeader('Allow'));
- $this->assertSame('POST, PUT', $res->getHeaderLine('Allow'));
- }
-
- public function testWriteToErrorLog()
- {
- $request = $this
- ->createServerRequest('/', 'GET')
- ->withHeader('Accept', 'application/json');
-
- $logger = $this->getMockLogger();
-
- $handler = new ErrorHandler(
- $this->getCallableResolver(),
- $this->getResponseFactory(),
- $logger
- );
-
- $logger->expects(self::once())
- ->method('error')
- ->willReturnCallback(static function (string $error) {
- self::assertStringNotContainsString(
- 'set "displayErrorDetails" to true in the ErrorHandler constructor',
- $error
- );
- });
-
- $exception = new HttpNotFoundException($request);
- $handler->__invoke($request, $exception, true, true, true);
- }
-
- public function testWriteToErrorLogShowTip()
- {
- $request = $this
- ->createServerRequest('/', 'GET')
- ->withHeader('Accept', 'application/json');
-
- $logger = $this->getMockLogger();
-
- $handler = new ErrorHandler(
- $this->getCallableResolver(),
- $this->getResponseFactory(),
- $logger
- );
-
- $logger->expects(self::once())
- ->method('error')
- ->willReturnCallback(static function (string $error) {
- self::assertStringContainsString(
- 'set "displayErrorDetails" to true in the ErrorHandler constructor',
- $error
- );
- });
-
- $exception = new HttpNotFoundException($request);
- $handler->__invoke($request, $exception, false, true, true);
- }
-
- public function testWriteToErrorLogDoesNotShowTipIfErrorLogRendererIsNotPlainText()
- {
- $request = $this
- ->createServerRequest('/', 'GET')
- ->withHeader('Accept', 'application/json');
-
- $logger = $this->getMockLogger();
-
- $handler = new ErrorHandler(
- $this->getCallableResolver(),
- $this->getResponseFactory(),
- $logger
- );
-
- $handler->setLogErrorRenderer(HtmlErrorRenderer::class);
-
- $logger->expects(self::once())
- ->method('error')
- ->willReturnCallback(static function (string $error) {
- self::assertStringNotContainsString(
- 'set "displayErrorDetails" to true in the ErrorHandler constructor',
- $error
- );
- });
-
- $exception = new HttpNotFoundException($request);
- $handler->__invoke($request, $exception, false, true, true);
- }
-
- public function testDefaultErrorRenderer()
- {
- $request = $this
- ->createServerRequest('/', 'GET')
- ->withHeader('Accept', 'application/unknown');
-
- $handler = new ErrorHandler($this->getCallableResolver(), $this->getResponseFactory());
- $exception = new RuntimeException();
-
- /** @var ResponseInterface $res */
- $res = $handler->__invoke($request, $exception, true, false, true);
-
- $this->assertTrue($res->hasHeader('Content-Type'));
- $this->assertSame('text/html', $res->getHeaderLine('Content-Type'));
- }
-
- public function testLogErrorRenderer()
- {
- $renderer = function () {
- return '';
- };
-
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $callableResolverProphecy
- ->resolve('logErrorRenderer')
- ->willReturn($renderer)
- ->shouldBeCalledOnce();
-
- $handler = new ErrorHandler($callableResolverProphecy->reveal(), $this->getResponseFactory());
- $handler->setLogErrorRenderer('logErrorRenderer');
-
- $displayErrorDetailsProperty = new ReflectionProperty($handler, 'displayErrorDetails');
- $displayErrorDetailsProperty->setAccessible(true);
- $displayErrorDetailsProperty->setValue($handler, true);
-
- $exception = new RuntimeException();
- $exceptionProperty = new ReflectionProperty($handler, 'exception');
- $exceptionProperty->setAccessible(true);
- $exceptionProperty->setValue($handler, $exception);
-
- $writeToErrorLogMethod = new ReflectionMethod($handler, 'writeToErrorLog');
- $writeToErrorLogMethod->setAccessible(true);
- $writeToErrorLogMethod->invoke($handler);
- }
-}
diff --git a/tests/Handlers/Strategies/RequestResponseNamedArgsTest.php b/tests/Handlers/Strategies/RequestResponseNamedArgsTest.php
deleted file mode 100644
index 11ff7b45b..000000000
--- a/tests/Handlers/Strategies/RequestResponseNamedArgsTest.php
+++ /dev/null
@@ -1,163 +0,0 @@
-request = $this->createMock(ServerRequestInterface::class);
- $this->response = $this->createMock(ResponseInterface::class);
- }
-
- public function testCreatingRequestResponseNamedArgsThrowsRuntimeExceptionForPHPOlderThan80()
- {
- if (PHP_VERSION_ID >= self::PHP_8_0_VERSION_ID) {
- $this->markTestSkipped('Test only valid for PHP versions older than 8.0');
- }
-
- $this->expectException(RuntimeException::class);
- new RequestResponseNamedArgs();
- }
-
- public function testCallingWithEmptyArguments()
- {
- if (PHP_VERSION_ID < self::PHP_8_0_VERSION_ID) {
- $this->markTestSkipped('Named arguments are not supported in PHP versions prior to 8.0');
- }
-
- $args = [];
- $invocationStrategy = new RequestResponseNamedArgs();
-
- $callback = function ($request, $response) {
- $this->assertSame($this->request, $request);
- $this->assertSame($this->response, $response);
-
- return $response;
- };
-
- $this->assertSame($this->response, $invocationStrategy($callback, $this->request, $this->response, $args));
- }
-
- public function testCallingWithKnownArguments()
- {
- if (PHP_VERSION_ID < self::PHP_8_0_VERSION_ID) {
- $this->markTestSkipped('Named arguments are not supported in PHP versions prior to 8.0');
- }
-
- $args = [
- 'name' => 'world',
- 'greeting' => 'hello',
- ];
-
- $invocationStrategy = new RequestResponseNamedArgs();
-
- $callback = function ($request, $response, $greeting, $name) use ($args) {
- $this->assertSame($this->request, $request);
- $this->assertSame($this->response, $response);
- $this->assertSame($greeting, $args['greeting']);
- $this->assertSame($name, $args['name']);
-
- return $response;
- };
-
- $this->assertSame($this->response, $invocationStrategy($callback, $this->request, $this->response, $args));
- }
-
- public function testCallingWithOptionalArguments()
- {
- if (PHP_VERSION_ID < 80000) {
- $this->markTestSkipped('Named arguments are not supported in PHP versions prior to 8.0');
- }
-
- $args = [
- 'name' => 'world',
- ];
-
- $invocationStrategy = new RequestResponseNamedArgs();
-
- $callback = function ($request, $response, $greeting = 'Hello', $name = 'Rob') use ($args) {
- $this->assertSame($this->request, $request);
- $this->assertSame($this->response, $response);
- $this->assertSame($greeting, 'Hello');
- $this->assertSame($name, $args['name']);
-
- return $response;
- };
-
- $this->assertSame($this->response, $invocationStrategy($callback, $this->request, $this->response, $args));
- }
-
- public function testCallingWithUnknownAndVariadic()
- {
- if (PHP_VERSION_ID < self::PHP_8_0_VERSION_ID) {
- $this->markTestSkipped('Named arguments are not supported in PHP versions prior to 8.0');
- }
-
- $args = [
- 'name' => 'world',
- 'greeting' => 'hello',
- ];
-
- $invocationStrategy = new RequestResponseNamedArgs();
-
- $callback = function ($request, $response, ...$arguments) use ($args) {
- $this->assertSame($this->request, $request);
- $this->assertSame($this->response, $response);
- $this->assertSame($args, $arguments);
-
- return $response;
- };
-
- $this->assertSame($this->response, $invocationStrategy($callback, $this->request, $this->response, $args));
- }
-
- public function testCallingWithMixedKnownAndUnknownParametersAndVariadic()
- {
- if (PHP_VERSION_ID < self::PHP_8_0_VERSION_ID) {
- $this->markTestSkipped('Named arguments are not supported in PHP versions prior to 8.0');
- }
-
- $known = [
- 'name' => 'world',
- 'greeting' => 'hello',
- ];
- $unknown = [
- 'foo' => 'foo',
- 'bar' => 'bar',
- ];
- $args = array_merge($known, $unknown);
- $invocationStrategy = new RequestResponseNamedArgs();
-
- $callback = function ($request, $response, $name, $greeting, ...$arguments) use ($known, $unknown) {
- $this->assertSame($this->request, $request);
- $this->assertSame($this->response, $response);
- $this->assertSame($name, $known['name']);
- $this->assertSame($greeting, $known['greeting']);
- $this->assertSame($unknown, $arguments);
-
- return $response;
- };
-
- $this->assertSame($this->response, $invocationStrategy($callback, $this->request, $this->response, $args));
- }
-}
diff --git a/tests/Logging/TestLogger.php b/tests/Logging/TestLogger.php
new file mode 100644
index 000000000..4576b8b6c
--- /dev/null
+++ b/tests/Logging/TestLogger.php
@@ -0,0 +1,33 @@
+logs[] = [
+ 'level' => $level,
+ 'message' => $message,
+ 'context' => $context,
+ ];
+ }
+
+ public function getLogs(): array
+ {
+ return $this->logs;
+ }
+}
diff --git a/tests/Media/MediaTypeDetectorTest.php b/tests/Media/MediaTypeDetectorTest.php
new file mode 100644
index 000000000..6449715f7
--- /dev/null
+++ b/tests/Media/MediaTypeDetectorTest.php
@@ -0,0 +1,74 @@
+build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/')
+ ->withHeader('Accept', $acceptHeader);
+
+ $mediaTypeDetector = new MediaTypeDetector();
+ $detectedMediaTypes = $mediaTypeDetector->detect($request);
+
+ $this->assertEquals($expectedMediaTypes, $detectedMediaTypes);
+ }
+
+ #[DataProvider('provideContentTypeCases')]
+ public function testDetectFromContentTypeHeader(string $contentTypeHeader, array $expectedMediaTypes)
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('POST', '/')
+ ->withHeader('Content-Type', $contentTypeHeader);
+
+ $mediaTypeDetector = new MediaTypeDetector();
+ $detectedMediaTypes = $mediaTypeDetector->detect($request);
+
+ $this->assertEquals($expectedMediaTypes, $detectedMediaTypes);
+ }
+
+ public static function provideAcceptHeaderCases(): array
+ {
+ return [
+ ['application/json', [0 => 'application/json']],
+ ['text/html', [0 => 'text/html']],
+ ['application/xml, text/html', [0 => 'application/xml', 1 => 'text/html']],
+ ['*/*', [0 => '*/*']],
+ ['', []],
+ ];
+ }
+
+ public static function provideContentTypeCases(): array
+ {
+ return [
+ ['application/json', [0 => 'application/json']],
+ ['text/html', [0 => 'text/html']],
+ ['application/xml; charset=UTF-8', [0 => 'application/xml']],
+ ['application/vnd.api+json', [0 => 'application/vnd.api+json']],
+ ['', []],
+ ];
+ }
+}
diff --git a/tests/Middleware/BasePathMiddlewareTest.php b/tests/Middleware/BasePathMiddlewareTest.php
new file mode 100644
index 000000000..5e86671c1
--- /dev/null
+++ b/tests/Middleware/BasePathMiddlewareTest.php
@@ -0,0 +1,274 @@
+addDefinitions(
+ [
+ BasePathMiddleware::class => function (ContainerInterface $container) {
+ $app = $container->get(App::class);
+
+ return new BasePathMiddleware($app, 'apache2handler');
+ },
+ ]
+ );
+ $app = $builder->build();
+
+ $app->add(BasePathMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->get('/', function ($request, ResponseInterface $response) {
+ $basePath = $this->get(App::class)->getBasePath();
+ $response->getBody()->write('basePath: ' . $basePath);
+
+ return $response;
+ });
+
+ $serverParams = [
+ 'REQUEST_URI' => '/',
+ 'SCRIPT_NAME' => '',
+ ];
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/', $serverParams);
+
+ $response = $app->handle($request);
+
+ $this->assertSame('', $app->getBasePath());
+ $this->assertSame('basePath: ', (string)$response->getBody());
+ }
+
+ public function testScriptNameWithIndexPhp(): void
+ {
+ $builder = new AppBuilder();
+ $builder->addDefinitions(
+ [
+ BasePathMiddleware::class => function (ContainerInterface $container) {
+ $app = $container->get(App::class);
+
+ return new BasePathMiddleware($app, 'apache2handler');
+ },
+ ]
+ );
+ $app = $builder->build();
+
+ $app->add(BasePathMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->get('/', function ($request, ResponseInterface $response) {
+ $basePath = $this->get(App::class)->getBasePath();
+ $response->getBody()->write('basePath: ' . $basePath);
+
+ return $response;
+ });
+
+ $serverParams = [
+ 'REQUEST_URI' => '/',
+ // PHP internal server
+ 'SCRIPT_NAME' => '/index.php',
+ ];
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/', $serverParams);
+
+ $response = $app->handle($request);
+
+ $this->assertSame('', $app->getBasePath());
+ $this->assertSame('basePath: ', (string)$response->getBody());
+ }
+
+ public function testScriptNameWithPublicIndexPhp(): void
+ {
+ $builder = new AppBuilder();
+ $builder->addDefinitions(
+ [
+ BasePathMiddleware::class => function (ContainerInterface $container) {
+ $app = $container->get(App::class);
+
+ return new BasePathMiddleware($app, 'apache2handler');
+ },
+ ]
+ );
+ $app = $builder->build();
+
+ $app->add(BasePathMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $basePath = $this->get(App::class)->getBasePath();
+ $response->getBody()->write('basePath: ' . $basePath);
+
+ return $response;
+ });
+
+ $serverParams = [
+ 'REQUEST_URI' => '/',
+ // PHP internal server
+ 'SCRIPT_NAME' => '/public/index.php',
+ ];
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/', $serverParams);
+
+ $response = $app->handle($request);
+
+ $this->assertSame('', $app->getBasePath());
+ $this->assertSame('basePath: ', (string)$response->getBody());
+ }
+
+ public function testSubDirectoryWithSlash(): void
+ {
+ $builder = new AppBuilder();
+ $builder->addDefinitions(
+ [
+ BasePathMiddleware::class => function (ContainerInterface $container) {
+ $app = $container->get(App::class);
+
+ return new BasePathMiddleware($app, 'apache2handler');
+ },
+ ]
+ );
+ $app = $builder->build();
+
+ $app->add(BasePathMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->get('/', function ($request, ResponseInterface $response) {
+ $basePath = $this->get(App::class)->getBasePath();
+ $response->getBody()->write('basePath: ' . $basePath);
+
+ return $response;
+ });
+
+ $serverParams = [
+ 'REQUEST_URI' => '/slim-hello-world/',
+ 'SCRIPT_NAME' => '/slim-hello-world/public/index.php',
+ ];
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/slim-hello-world/?key=value', $serverParams);
+
+ $response = $app->handle($request);
+
+ $this->assertSame('/slim-hello-world', $app->getBasePath());
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('basePath: /slim-hello-world', (string)$response->getBody());
+ }
+
+ public function testSubDirectoryWithoutSlash(): void
+ {
+ $builder = new AppBuilder();
+ $builder->addDefinitions(
+ [
+ BasePathMiddleware::class => function (ContainerInterface $container) {
+ $app = $container->get(App::class);
+
+ return new BasePathMiddleware($app, 'apache2handler');
+ },
+ ]
+ );
+ $app = $builder->build();
+
+ $app->add(BasePathMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->get('/foo', function ($request, ResponseInterface $response) {
+ $basePath = $this->get(App::class)->getBasePath();
+ $response->getBody()->write('basePath: ' . $basePath);
+
+ return $response;
+ });
+
+ $serverParams = [
+ 'REQUEST_URI' => '/slim-hello-world/foo',
+ 'SCRIPT_NAME' => '/slim-hello-world/public/index.php',
+ ];
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/slim-hello-world/foo?key=value', $serverParams);
+
+ $response = $app->handle($request);
+
+ $this->assertSame('/slim-hello-world', $app->getBasePath());
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('basePath: /slim-hello-world', (string)$response->getBody());
+ }
+
+ public function testSubDirectoryWithFooPath(): void
+ {
+ $builder = new AppBuilder();
+ $builder->addDefinitions(
+ [
+ BasePathMiddleware::class => function (ContainerInterface $container) {
+ $app = $container->get(App::class);
+
+ return new BasePathMiddleware($app, 'apache2handler');
+ },
+ ]
+ );
+ $app = $builder->build();
+
+ $app->add(BasePathMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->get('/foo', function ($request, ResponseInterface $response) {
+ $basePath = $this->get(App::class)->getBasePath();
+ $response->getBody()->write('basePath: ' . $basePath);
+
+ return $response;
+ });
+
+ $serverParams = [
+ 'REQUEST_URI' => '/slim-hello-world/foo',
+ 'SCRIPT_NAME' => '/slim-hello-world/public/index.php',
+ ];
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/slim-hello-world/foo/?key=value', $serverParams);
+
+ $response = $app->handle($request);
+
+ $this->assertSame('/slim-hello-world', $app->getBasePath());
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('basePath: /slim-hello-world', (string)$response->getBody());
+ }
+}
diff --git a/tests/Middleware/BodyParsingMiddlewareTest.php b/tests/Middleware/BodyParsingMiddlewareTest.php
index 30cba022c..4d16a6507 100644
--- a/tests/Middleware/BodyParsingMiddlewareTest.php
+++ b/tests/Middleware/BodyParsingMiddlewareTest.php
@@ -3,68 +3,77 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Tests\Middleware;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\StreamFactoryInterface;
+use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use RuntimeException;
+use Slim\Builder\AppBuilder;
+use Slim\Container\GuzzleDefinitions;
+use Slim\Container\HttpSoftDefinitions;
+use Slim\Container\LaminasDiactorosDefinitions;
+use Slim\Container\NyholmDefinitions;
+use Slim\Container\SlimHttpDefinitions;
+use Slim\Container\SlimPsr7Definitions;
+use Slim\Media\MediaTypeDetector;
use Slim\Middleware\BodyParsingMiddleware;
-use Slim\Tests\TestCase;
+use Slim\Middleware\ResponseFactoryMiddleware;
+use Slim\RequestHandler\Runner;
+use Slim\Tests\Traits\AppTestTrait;
-use function is_string;
use function simplexml_load_string;
-class BodyParsingMiddlewareTest extends TestCase
+final class BodyParsingMiddlewareTest extends TestCase
{
- /**
- * Create a request handler that simply assigns the $request that it receives to a public property
- * of the returned response, so that we can then inspect that request.
- */
- protected function createRequestHandler(): RequestHandlerInterface
+ use AppTestTrait;
+
+ #[DataProvider('parsingProvider')]
+ public function testParsing($contentType, $body, $expected)
{
- $response = $this->createResponse();
- return new class ($response) implements RequestHandlerInterface {
- private $response;
- public $request;
+ $builder = new AppBuilder();
- public function __construct(ResponseInterface $response)
- {
- $this->response = $response;
- }
+ // Replace or change the PSR-17 factory because slim/http has its own parser
+ $builder->addDefinitions(NyholmDefinitions::class);
+ $app = $builder->build();
- public function handle(ServerRequestInterface $request): ResponseInterface
- {
- $this->request = $request;
- return $this->response;
- }
- };
- }
+ $responseFactory = $app->getContainer()->get(ResponseFactoryMiddleware::class);
- /**
- * @param string $contentType
- * @param string $body
- * @return ServerRequestInterface
- */
- protected function createRequestWithBody($contentType, $body)
- {
- $request = $this->createServerRequest('/', 'POST');
- if (is_string($contentType)) {
- $request = $request->withHeader('Content-Type', $contentType);
- }
- if (is_string($body)) {
- $request = $request->withBody($this->createStream($body));
- }
- return $request;
- }
+ $test = $this;
+ $middlewares = [
+ $app->getContainer()->get(BodyParsingMiddleware::class),
+ $this->createCallbackMiddleware(function (ServerRequestInterface $request) use ($expected, $test) {
+ $test->assertEquals($expected, $request->getParsedBody());
+ }),
+ $responseFactory,
+ ];
+ $stream = $app->getContainer()
+ ->get(StreamFactoryInterface::class)
+ ->createStream($body);
- public function parsingProvider()
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('POST', '/')
+ ->withHeader('Accept', $contentType)
+ ->withHeader('Content-Type', $contentType)
+ ->withBody($stream);
+
+ (new Runner($middlewares))->handle($request);
+ }
+
+ public static function parsingProvider(): array
{
return [
'form' => [
@@ -73,7 +82,7 @@ public function parsingProvider()
['foo' => 'bar'],
],
'json' => [
- "application/json",
+ 'application/json',
'{"foo":"bar"}',
['foo' => 'bar'],
],
@@ -92,21 +101,11 @@ public function parsingProvider()
'John',
simplexml_load_string('John'),
],
- 'xml-suffix' => [
- 'application/hal+xml;charset=utf8',
- 'John',
- simplexml_load_string('John'),
- ],
'text-xml' => [
'text/xml',
'John',
simplexml_load_string('John'),
],
- 'invalid-json' => [
- 'application/json;charset=utf8',
- '{"foo"}/bar',
- null,
- ],
'valid-json-but-not-an-array' => [
'application/json;charset=utf8',
'"foo bar"',
@@ -122,11 +121,12 @@ public function parsingProvider()
'"foo bar"',
null,
],
- 'no-contenttype' => [
- null,
- '"foo bar"',
- null,
- ],
+ // null is not supported anymore
+ // 'no-contenttype' => [
+ // null,
+ // '"foo bar"',
+ // null,
+ // ],
'invalid-contenttype' => [
'foo',
'"foo bar"',
@@ -145,71 +145,194 @@ public function parsingProvider()
];
}
- /**
- * @dataProvider parsingProvider
- */
- public function testParsing($contentType, $body, $expected)
+ #[DataProvider('parsingInvalidJsonProvider')]
+ public function testParsingInvalidJson($contentType, $body)
{
- $request = $this->createRequestWithBody($contentType, $body);
+ $builder = new AppBuilder();
- $middleware = new BodyParsingMiddleware();
- $requestHandler = $this->createRequestHandler();
- $middleware->process($request, $requestHandler);
+ // Replace or change the PSR-17 factory because slim/http has its own parser
+ $builder->addDefinitions(SlimPsr7Definitions::class);
+ $app = $builder->build();
+ $container = $app->getContainer();
- $this->assertEquals($expected, $requestHandler->request->getParsedBody());
+ $middlewares = [
+ $container->get(BodyParsingMiddleware::class),
+ $container->get(ResponseFactoryMiddleware::class),
+ ];
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('POST', '/')
+ ->withHeader('Accept', $contentType)
+ ->withHeader('Content-Type', $contentType);
+
+ $request->getBody()->write($body);
+
+ $response = (new Runner($middlewares))->handle($request);
+
+ $this->assertSame('', (string)$response->getBody());
+ }
+
+ public static function parsingInvalidJsonProvider(): array
+ {
+ return [
+ 'invalid-json' => [
+ 'application/json;charset=utf8',
+ '{"foo"}/bar',
+ ],
+ 'invalid-json-2' => [
+ 'application/json',
+ '{',
+ ],
+ ];
}
public function testParsingWithARegisteredParser()
{
- $request = $this->createRequestWithBody('application/vnd.api+json', '{"foo":"bar"}');
+ $builder = new AppBuilder();
- $parsers = [
- 'application/vnd.api+json' => function ($input) {
- return ['data' => $input];
- },
- ];
- $middleware = new BodyParsingMiddleware($parsers);
- $requestHandler = $this->createRequestHandler();
- $middleware->process($request, $requestHandler);
+ // Replace or change the PSR-17 factory because slim/http has its own parser
+ $builder->addDefinitions(SlimHttpDefinitions::class);
+ $builder->addDefinitions(
+ [
+ BodyParsingMiddleware::class => function (ContainerInterface $container) {
+ $mediaTypeDetector = $container->get(MediaTypeDetector::class);
+ $middleware = new BodyParsingMiddleware($mediaTypeDetector);
+
+ return $middleware->withBodyParser('application/vnd.api+json', function ($input) {
+ return ['data' => json_decode($input, true)];
+ });
+ },
+ ]
+ );
+ $app = $builder->build();
+
+ $input = '{"foo":"bar"}';
+ $stream = $app->getContainer()
+ ->get(StreamFactoryInterface::class)
+ ->createStream($input);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('POST', '/')
+ ->withHeader('Accept', 'application/vnd.api+json;charset=utf8')
+ ->withBody($stream);
- $this->assertSame(['data' => '{"foo":"bar"}'], $requestHandler->request->getParsedBody());
+ $middlewares = [];
+ $middlewares[] = $app->getContainer()->get(BodyParsingMiddleware::class);
+ $middlewares[] = $this->createParsedBodyMiddleware();
+ $middlewares[] = $app->getContainer()->get(ResponseFactoryMiddleware::class);
+
+ $response = (new Runner($middlewares))->handle($request);
+
+ $this->assertJsonResponse(['data' => ['foo' => 'bar']], $response);
+ $this->assertSame(['data' => ['foo' => 'bar']], json_decode((string)$response->getBody(), true));
}
- public function testParsingFailsWhenAnInvalidTypeIsReturned()
+ #[DataProvider('httpDefinitionsProvider')]
+ public function testParsingFailsWhenAnInvalidTypeIsReturned(string $definitions)
{
- $request = $this->createRequestWithBody('application/json;charset=utf8', '{"foo":"bar"}');
+ // The slim/http package has its own body parser, so this middleware will not be used.
+ // The SlimHttpDefinitions::class will not fail here, because the body parser will not be executed.
+ if ($definitions === SlimHttpDefinitions::class) {
+ $this->assertTrue(true);
- $parsers = [
- 'application/json' => function ($input) {
- return 10; // invalid - should return null, array or object
- },
- ];
- $middleware = new BodyParsingMiddleware($parsers);
+ return;
+ }
$this->expectException(RuntimeException::class);
- $middleware->process($request, $this->createRequestHandler());
+
+ $builder = new AppBuilder();
+ $builder->addDefinitions($definitions);
+
+ $builder->addDefinitions(
+ [
+ BodyParsingMiddleware::class => function (ContainerInterface $container) {
+ $mediaTypeDetector = $container->get(MediaTypeDetector::class);
+ $middleware = new BodyParsingMiddleware($mediaTypeDetector);
+
+ $middleware = $middleware->withBodyParser('application/json', function () {
+ // invalid - should return null, array or object
+ return 10;
+ });
+
+ return $middleware;
+ },
+ ]
+ );
+ $app = $builder->build();
+
+ $stream = $app->getContainer()
+ ->get(StreamFactoryInterface::class)
+ ->createStream('{"foo":"bar"}');
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('POST', '/')
+ ->withHeader('Accept', 'application/json;charset=utf8')
+ ->withHeader('Content-Type', 'application/json;charset=utf8')
+ ->withBody($stream);
+
+ $middlewares = [];
+ $middlewares[] = $app->getContainer()->get(BodyParsingMiddleware::class);
+ $middlewares[] = $this->createParsedBodyMiddleware();
+ $middlewares[] = $app->getContainer()->get(ResponseFactoryMiddleware::class);
+
+ (new Runner($middlewares))->handle($request);
}
- public function testSettingAndGettingAParser()
+ public static function httpDefinitionsProvider(): array
{
- $middleware = new BodyParsingMiddleware();
- $parser = function ($input) {
- return ['data' => $input];
- };
+ return [
+ 'GuzzleDefinitions' => [GuzzleDefinitions::class],
+ 'HttpSoftDefinitions' => [HttpSoftDefinitions::class],
+ 'LaminasDiactorosDefinitions' => [LaminasDiactorosDefinitions::class],
+ 'NyholmDefinitions' => [NyholmDefinitions::class],
+ 'SlimHttpDefinitions' => [SlimHttpDefinitions::class],
+ 'SlimPsr7Definitions' => [SlimPsr7Definitions::class],
+ ];
+ }
- $this->assertFalse($middleware->hasBodyParser('text/foo'));
+ private function createParsedBodyMiddleware(): MiddlewareInterface
+ {
+ return new class implements MiddlewareInterface {
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler,
+ ): ResponseInterface {
+ $response = $handler->handle($request);
- $middleware->registerBodyParser('text/foo', $parser);
- $this->assertTrue($middleware->hasBodyParser('text/foo'));
+ // Return the parsed body
+ $response->getBody()->write(json_encode($request->getParsedBody()));
- $this->assertSame($parser, $middleware->getBodyParser('text/foo'));
+ return $response;
+ }
+ };
}
- public function testGettingUnknownParser()
+ private function createCallbackMiddleware(callable $callback): MiddlewareInterface
{
- $middleware = new BodyParsingMiddleware();
+ return new class ($callback) implements MiddlewareInterface {
+ /**
+ * @var callable
+ */
+ private $callback;
- $this->expectException(RuntimeException::class);
- $middleware->getBodyParser('text/foo');
+ public function __construct(callable $callback)
+ {
+ $this->callback = $callback;
+ }
+
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler,
+ ): ResponseInterface {
+ $response = $handler->handle($request);
+
+ call_user_func($this->callback, $request, $handler);
+
+ return $response;
+ }
+ };
}
}
diff --git a/tests/Middleware/ContentLengthMiddlewareTest.php b/tests/Middleware/ContentLengthMiddlewareTest.php
index d6908917f..4df444291 100644
--- a/tests/Middleware/ContentLengthMiddlewareTest.php
+++ b/tests/Middleware/ContentLengthMiddlewareTest.php
@@ -3,39 +3,49 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Tests\Middleware;
-use Psr\Http\Server\RequestHandlerInterface;
+use PHPUnit\Framework\TestCase;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestFactoryInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Slim\Builder\AppBuilder;
use Slim\Middleware\ContentLengthMiddleware;
-use Slim\Tests\TestCase;
+use Slim\Middleware\EndpointMiddleware;
+use Slim\Middleware\RoutingMiddleware;
+use Slim\Tests\Traits\AppTestTrait;
-class ContentLengthMiddlewareTest extends TestCase
+final class ContentLengthMiddlewareTest extends TestCase
{
+ use AppTestTrait;
+
public function testAddsContentLength()
{
- $request = $this->createServerRequest('/');
- $responseFactory = $this->getResponseFactory();
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $app->add(new ContentLengthMiddleware());
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $mw = function ($request, $handler) use ($responseFactory) {
- $response = $responseFactory->createResponse();
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
$response->getBody()->write('Body');
+
return $response;
- };
- $mw2 = new ContentLengthMiddleware();
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
- $middlewareDispatcher = $this->createMiddlewareDispatcher(
- $this->createMock(RequestHandlerInterface::class),
- null
- );
- $middlewareDispatcher->addCallable($mw);
- $middlewareDispatcher->addMiddleware($mw2);
- $response = $middlewareDispatcher->handle($request);
+ $response = $app->handle($request);
$this->assertSame('4', $response->getHeaderLine('Content-Length'));
+ $this->assertSame('Body', (string)$response->getBody());
}
}
diff --git a/tests/Middleware/CorsMiddlewareTest.php b/tests/Middleware/CorsMiddlewareTest.php
new file mode 100644
index 000000000..b3e27cd65
--- /dev/null
+++ b/tests/Middleware/CorsMiddlewareTest.php
@@ -0,0 +1,234 @@
+build();
+
+ // Add CORS middleware with default config
+ $app->add(CorsMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ // Add test route
+ $app->get('/test', function ($request, $response) {
+ $response->getBody()->write('Test response');
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/test');
+
+ $response = $app->handle($request);
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin'));
+ $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Headers'));
+ $this->assertSame(
+ 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
+ $response->getHeaderLine('Access-Control-Allow-Methods')
+ );
+ $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
+ }
+
+ public function testDefaultConfigurationWithOrigin(): void
+ {
+ $app = (new AppBuilder())->build();
+
+ // Add CORS middleware with default config
+ $app->add(CorsMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ // Add test route
+ $app->get('/test', function ($request, $response) {
+ $response->getBody()->write('Test response');
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/test')
+ ->withHeader('Origin', 'https://example.com');
+
+ $response = $app->handle($request);
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('https://example.com', $response->getHeaderLine('Access-Control-Allow-Origin'));
+ $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Headers'));
+ $this->assertSame(
+ 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
+ $response->getHeaderLine('Access-Control-Allow-Methods')
+ );
+ $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
+ }
+
+ public function testPreflightRequest(): void
+ {
+ $app = (new AppBuilder())->build();
+
+ // Configure CORS middleware
+ $cors = $app->getContainer()
+ ->get(CorsMiddleware::class)
+ ->withAllowedOrigins(['https://example.com'])
+ ->withAllowCredentials(true)
+ ->withMaxAge(3600);
+
+ $app->add($cors);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ // Add test routes
+ $app->get('/test', function ($request, $response) {
+ $response->getBody()->write('Test response');
+
+ return $response;
+ });
+
+ $app->options('/test', function ($request, $response) {
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('OPTIONS', '/test')
+ ->withHeader('Origin', 'https://example.com')
+ ->withHeader('Access-Control-Request-Method', 'POST');
+
+ $response = $app->handle($request);
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('https://example.com', $response->getHeaderLine('Access-Control-Allow-Origin'));
+ $this->assertSame('true', $response->getHeaderLine('Access-Control-Allow-Credentials'));
+ $this->assertSame('3600', $response->getHeaderLine('Access-Control-Max-Age'));
+ }
+
+ public function testDisallowedOrigin(): void
+ {
+ $app = (new AppBuilder())->build();
+
+ // Configure CORS middleware
+ $cors = $app->getContainer()
+ ->get(CorsMiddleware::class)
+ ->withAllowedOrigins(['https://example.com'])
+ ->withAllowCredentials(true);
+
+ $app->add($cors);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ // Add test route
+ $app->get('/test', function ($request, $response) {
+ $response->getBody()->write('Test response');
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/test')
+ ->withHeader('Origin', 'https://bad-domain.tld');
+
+ $response = $app->handle($request);
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertFalse($response->hasHeader('Access-Control-Allow-Origin'));
+ $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
+ }
+
+ public function testCustomHeadersAndMethods(): void
+ {
+ $app = (new AppBuilder())->build();
+
+ // Configure CORS middleware
+ $cors = $app->getContainer()
+ ->get(CorsMiddleware::class)
+ ->withAllowedOrigins(['https://example.com'])
+ ->withAllowedHeaders(['Content-Type', 'X-Custom-Header'])
+ ->withExposedHeaders(['X-Custom-Response'])
+ ->withAllowedMethods(['GET', 'POST'])
+ ->withMaxAge(3600)
+ ->withCache(false);
+
+ $app->add($cors);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ // Add test routes
+ $app->get('/test', function ($request, $response) {
+ $response->getBody()->write('Test response');
+
+ return $response;
+ });
+
+ $app->options('/test', function ($request, $response) {
+ return $response;
+ });
+
+ // Test preflight request
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('OPTIONS', '/test')
+ ->withHeader('Origin', 'https://example.com')
+ ->withHeader('Access-Control-Request-Method', 'POST')
+ ->withHeader('Access-Control-Request-Headers', 'X-Custom-Header');
+
+ $response = $app->handle($request);
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('https://example.com', $response->getHeaderLine('Access-Control-Allow-Origin'));
+ $this->assertSame('Content-Type, X-Custom-Header', $response->getHeaderLine('Access-Control-Allow-Headers'));
+ $this->assertSame('GET, POST', $response->getHeaderLine('Access-Control-Allow-Methods'));
+ $this->assertSame('X-Custom-Response', $response->getHeaderLine('Access-Control-Expose-Headers'));
+ $this->assertSame('3600', $response->getHeaderLine('Access-Control-Max-Age'));
+ $this->assertFalse($response->hasHeader('Cache-Control'));
+ }
+
+ public function testWildcardOriginWithCredentials(): void
+ {
+ $app = (new AppBuilder())->build();
+
+ // Configure CORS middleware
+ $cors = $app->getContainer()
+ ->get(CorsMiddleware::class)
+ ->withAllowedOrigins(null) // Wildcard
+ ->withAllowCredentials(true);
+
+ $app->add($cors);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ // Add test route
+ $app->get('/test', function ($request, $response) {
+ $response->getBody()->write('Test response');
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/test')
+ ->withHeader('Origin', 'https://example.com');
+
+ $response = $app->handle($request);
+
+ // Should use specific origin instead of wildcard when credentials are allowed
+ $this->assertSame('https://example.com', $response->getHeaderLine('Access-Control-Allow-Origin'));
+ $this->assertSame('true', $response->getHeaderLine('Access-Control-Allow-Credentials'));
+ }
+}
diff --git a/tests/Middleware/EndpointMiddlewareTest.php b/tests/Middleware/EndpointMiddlewareTest.php
new file mode 100644
index 000000000..e8ef4d0bb
--- /dev/null
+++ b/tests/Middleware/EndpointMiddlewareTest.php
@@ -0,0 +1,85 @@
+build();
+
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ // Set up a route that will be found
+ $app->get('/test', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Route found');
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/test');
+
+ $response = $app->handle($request);
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('Route found', (string)$response->getBody());
+ }
+
+ public function testProcessRouteNotFound(): void
+ {
+ $this->expectException(HttpNotFoundException::class);
+
+ $app = (new AppBuilder())->build();
+
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/non-existent-route');
+
+ $app->handle($request);
+ }
+
+ public function testProcessMethodNotAllowed(): void
+ {
+ $this->expectException(HttpMethodNotAllowedException::class);
+
+ $app = (new AppBuilder())->build();
+
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ // Set up a route with POST method only
+ $app->post('/test', function (ServerRequestInterface $request, ResponseInterface $response) {
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/test');
+
+ $app->handle($request);
+ }
+}
diff --git a/tests/Middleware/ErrorHandlingMiddlewareTest.php b/tests/Middleware/ErrorHandlingMiddlewareTest.php
new file mode 100644
index 000000000..19fd5a7bf
--- /dev/null
+++ b/tests/Middleware/ErrorHandlingMiddlewareTest.php
@@ -0,0 +1,125 @@
+expectException(ErrorException::class);
+ $this->expectExceptionMessage('Test error');
+
+ $request = $this->createMock(ServerRequestInterface::class);
+
+ $handler = $this->createMock(RequestHandlerInterface::class);
+ $handler->expects($this->once())
+ ->method('handle')
+ ->willReturnCallback(function () {
+ trigger_error('Test error', E_USER_WARNING);
+ });
+
+ error_reporting(E_USER_WARNING);
+
+ // Instantiate the middleware with a custom error level
+ $app = (new AppBuilder())->build();
+ $middleware = $app
+ ->getContainer()
+ ->get(ErrorHandlingMiddleware::class);
+
+ // Invoke the middleware process method
+ $middleware->process($request, $handler);
+ }
+
+ public function testProcessHandlesErrorSilent(): void
+ {
+ error_reporting(E_USER_ERROR);
+
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $middleware = $app
+ ->getContainer()
+ ->get(ErrorHandlingMiddleware::class);
+
+ $app->add($middleware);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->get('/', function ($request, $response) {
+ trigger_error('Test warning', E_USER_WARNING);
+
+ return $response->withHeader('X-Test', 'silent');
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->handle($request);
+
+ $this->assertInstanceOf(ResponseInterface::class, $response);
+ $this->assertSame('silent', $response->getHeaderLine('X-Test'));
+ }
+
+ public function testProcessHandlesException(): void
+ {
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Test exception');
+
+ $request = $this->createMock(ServerRequestInterface::class);
+
+ $handler = $this->createMock(RequestHandlerInterface::class);
+ $handler->expects($this->once())
+ ->method('handle')
+ ->willReturnCallback(function () {
+ throw new Exception('Test exception');
+ });
+
+ // Instantiate the middleware
+ $app = (new AppBuilder())->build();
+ $middleware = $app->getContainer()->get(ErrorHandlingMiddleware::class);
+
+ // Invoke the middleware process method
+ $middleware->process($request, $handler);
+ }
+
+ public function testProcessReturnsResponse(): void
+ {
+ // Mock the ServerRequestInterface and RequestHandlerInterface
+ $request = $this->createMock(ServerRequestInterface::class);
+ $response = $this->createMock(ResponseInterface::class);
+
+ $handler = $this->createMock(RequestHandlerInterface::class);
+ $handler->expects($this->once())
+ ->method('handle')
+ ->willReturn($response);
+
+ $app = (new AppBuilder())->build();
+ $middleware = $app->getContainer()->get(ErrorHandlingMiddleware::class);
+
+ // Invoke the middleware process method and assert the response is returned
+ $result = $middleware->process($request, $handler);
+
+ $this->assertSame($response, $result);
+ }
+}
diff --git a/tests/Middleware/ErrorMiddlewareTest.php b/tests/Middleware/ErrorMiddlewareTest.php
deleted file mode 100644
index a32956bed..000000000
--- a/tests/Middleware/ErrorMiddlewareTest.php
+++ /dev/null
@@ -1,319 +0,0 @@
-createMock(LoggerInterface::class);
- }
-
- public function testSetErrorHandler()
- {
- $responseFactory = $this->getResponseFactory();
- $app = new App($responseFactory);
- $callableResolver = $app->getCallableResolver();
- $logger = $this->getMockLogger();
-
- $routingMiddleware = new RoutingMiddleware(
- $app->getRouteResolver(),
- $app->getRouteCollector()->getRouteParser()
- );
- $app->add($routingMiddleware);
-
- $exception = HttpNotFoundException::class;
- $handler = (function () {
- $response = $this->createResponse(500);
- $response->getBody()->write('Oops..');
- return $response;
- })->bindTo($this);
-
- $errorMiddleware = new ErrorMiddleware(
- $callableResolver,
- $this->getResponseFactory(),
- false,
- false,
- false,
- $logger
- );
- $errorMiddleware->setErrorHandler($exception, $handler);
- $app->add($errorMiddleware);
-
- $request = $this->createServerRequest('/foo/baz/');
- $app->run($request);
-
- $this->expectOutputString('Oops..');
- }
-
- public function testSetDefaultErrorHandler()
- {
- $responseFactory = $this->getResponseFactory();
- $app = new App($responseFactory);
- $callableResolver = $app->getCallableResolver();
- $logger = $this->getMockLogger();
-
- $routingMiddleware = new RoutingMiddleware(
- $app->getRouteResolver(),
- $app->getRouteCollector()->getRouteParser()
- );
- $app->add($routingMiddleware);
-
- $handler = (function () {
- $response = $this->createResponse();
- $response->getBody()->write('Oops..');
- return $response;
- })->bindTo($this);
-
- $errorMiddleware = new ErrorMiddleware(
- $callableResolver,
- $this->getResponseFactory(),
- false,
- false,
- false,
- $logger
- );
- $errorMiddleware->setDefaultErrorHandler($handler);
- $app->add($errorMiddleware);
-
- $request = $this->createServerRequest('/foo/baz/');
- $app->run($request);
-
- $this->expectOutputString('Oops..');
- }
-
- public function testSetDefaultErrorHandlerThrowsException()
- {
- $this->expectException(RuntimeException::class);
-
- $responseFactory = $this->getResponseFactory();
- $app = new App($responseFactory);
- $callableResolver = $app->getCallableResolver();
- $logger = $this->getMockLogger();
-
- $errorMiddleware = new ErrorMiddleware(
- $callableResolver,
- $this->getResponseFactory(),
- false,
- false,
- false,
- $logger
- );
- $errorMiddleware->setDefaultErrorHandler('Uncallable');
- $errorMiddleware->getDefaultErrorHandler();
- }
-
- public function testGetErrorHandlerWillReturnDefaultErrorHandlerForUnhandledExceptions()
- {
- $responseFactory = $this->getResponseFactory();
- $app = new App($responseFactory);
- $callableResolver = $app->getCallableResolver();
- $logger = $this->getMockLogger();
-
- $middleware = new ErrorMiddleware($callableResolver, $this->getResponseFactory(), false, false, false, $logger);
- $exception = MockCustomException::class;
- $handler = $middleware->getErrorHandler($exception);
- $this->assertInstanceOf(ErrorHandler::class, $handler);
- }
-
- public function testSuperclassExceptionHandlerHandlesExceptionWithSubclassExactMatch()
- {
- $responseFactory = $this->getResponseFactory();
- $app = new App($responseFactory);
- $callableResolver = $app->getCallableResolver();
- $logger = $this->getMockLogger();
- $middleware = new ErrorMiddleware($callableResolver, $this->getResponseFactory(), false, false, false, $logger);
- $app->add(function ($request, $handler) {
- throw new LogicException('This is a LogicException...');
- });
- $middleware->setErrorHandler(LogicException::class, (function (ServerRequestInterface $request, $exception) {
- $response = $this->createResponse();
- $response->getBody()->write($exception->getMessage());
- return $response;
- })->bindTo($this), true); // - true; handle subclass but also LogicException explicitly
- $middleware->setDefaultErrorHandler((function () {
- $response = $this->createResponse();
- $response->getBody()->write('Oops..');
- return $response;
- })->bindTo($this));
- $app->add($middleware);
- $app->get('/foo', function (ServerRequestInterface $request, ResponseInterface $response) {
- $response->getBody()->write('...');
- return $response;
- });
- $request = $this->createServerRequest('/foo');
- $app->run($request);
- $this->expectOutputString('This is a LogicException...');
- }
-
- public function testSuperclassExceptionHandlerHandlesSubclassException()
- {
- $responseFactory = $this->getResponseFactory();
- $app = new App($responseFactory);
- $callableResolver = $app->getCallableResolver();
- $logger = $this->getMockLogger();
-
- $middleware = new ErrorMiddleware($callableResolver, $this->getResponseFactory(), false, false, false, $logger);
-
- $app->add(function ($request, $handler) {
- throw new InvalidArgumentException('This is a subclass of LogicException...');
- });
-
- $middleware->setErrorHandler(LogicException::class, (function (ServerRequestInterface $request, $exception) {
- $response = $this->createResponse();
- $response->getBody()->write($exception->getMessage());
- return $response;
- })->bindTo($this), true); // - true; handle subclass
-
- $middleware->setDefaultErrorHandler((function () {
- $response = $this->createResponse();
- $response->getBody()->write('Oops..');
- return $response;
- })->bindTo($this));
-
- $app->add($middleware);
-
- $app->get('/foo', function (ServerRequestInterface $request, ResponseInterface $response) {
- $response->getBody()->write('...');
- return $response;
- });
-
- $request = $this->createServerRequest('/foo');
- $app->run($request);
-
- $this->expectOutputString('This is a subclass of LogicException...');
- }
-
- public function testSuperclassExceptionHandlerDoesNotHandleSubclassException()
- {
- $responseFactory = $this->getResponseFactory();
- $app = new App($responseFactory);
- $callableResolver = $app->getCallableResolver();
- $logger = $this->getMockLogger();
-
- $middleware = new ErrorMiddleware($callableResolver, $this->getResponseFactory(), false, false, false, $logger);
-
- $app->add(function ($request, $handler) {
- throw new InvalidArgumentException('This is a subclass of LogicException...');
- });
-
- $middleware->setErrorHandler(LogicException::class, (function (ServerRequestInterface $request, $exception) {
- $response = $this->createResponse();
- $response->getBody()->write($exception->getMessage());
- return $response;
- })->bindTo($this), false); // - false; don't handle subclass
-
- $middleware->setDefaultErrorHandler((function () {
- $response = $this->createResponse();
- $response->getBody()->write('Oops..');
- return $response;
- })->bindTo($this));
-
- $app->add($middleware);
-
- $app->get('/foo', function (ServerRequestInterface $request, ResponseInterface $response) {
- $response->getBody()->write('...');
- return $response;
- });
-
- $request = $this->createServerRequest('/foo');
- $app->run($request);
-
- $this->expectOutputString('Oops..');
- }
-
- public function testHandleMultipleExceptionsAddedAsArray()
- {
- $responseFactory = $this->getResponseFactory();
- $app = new App($responseFactory);
- $callableResolver = $app->getCallableResolver();
- $logger = $this->getMockLogger();
-
- $middleware = new ErrorMiddleware($callableResolver, $this->getResponseFactory(), false, false, false, $logger);
-
- $app->add(function ($request, $handler) {
- throw new InvalidArgumentException('This is an invalid argument exception...');
- });
-
- $handler = (function (ServerRequestInterface $request, $exception) {
- $response = $this->createResponse();
- $response->getBody()->write($exception->getMessage());
- return $response;
- });
-
- $middleware->setErrorHandler([LogicException::class, InvalidArgumentException::class], $handler->bindTo($this));
-
- $middleware->setDefaultErrorHandler((function () {
- $response = $this->createResponse();
- $response->getBody()->write('Oops..');
- return $response;
- })->bindTo($this));
-
- $app->add($middleware);
-
- $app->get('/foo', function (ServerRequestInterface $request, ResponseInterface $response) {
- $response->getBody()->write('...');
- return $response;
- });
-
- $request = $this->createServerRequest('/foo');
- $app->run($request);
-
- $this->expectOutputString('This is an invalid argument exception...');
- }
-
- public function testErrorHandlerHandlesThrowables()
- {
- $responseFactory = $this->getResponseFactory();
- $app = new App($responseFactory);
- $callableResolver = $app->getCallableResolver();
- $logger = $this->getMockLogger();
-
- $middleware = new ErrorMiddleware($callableResolver, $this->getResponseFactory(), false, false, false, $logger);
-
- $app->add(function ($request, $handler) {
- throw new Error('Oops..');
- });
-
- $middleware->setDefaultErrorHandler((function (ServerRequestInterface $request, $exception) {
- $response = $this->createResponse();
- $response->getBody()->write($exception->getMessage());
- return $response;
- })->bindTo($this));
-
- $app->add($middleware);
-
- $app->get('/foo', function (ServerRequestInterface $request, ResponseInterface $response) {
- $response->getBody()->write('...');
- return $response;
- });
-
- $request = $this->createServerRequest('/foo');
- $app->run($request);
-
- $this->expectOutputString('Oops..');
- }
-}
diff --git a/tests/Middleware/ExceptionHandlingMiddlewareTest.php b/tests/Middleware/ExceptionHandlingMiddlewareTest.php
new file mode 100644
index 000000000..da40a6c87
--- /dev/null
+++ b/tests/Middleware/ExceptionHandlingMiddlewareTest.php
@@ -0,0 +1,219 @@
+build();
+
+ $responseFactory = $app->getContainer()->get(ResponseFactoryInterface::class);
+
+ // Custom ExceptionHandlerInterface implementation
+ $exceptionHandler = new class ($responseFactory) implements ExceptionHandlerInterface {
+ private ResponseFactoryInterface $responseFactory;
+
+ public function __construct($responseFactory)
+ {
+ $this->responseFactory = $responseFactory;
+ }
+
+ public function __invoke(ServerRequestInterface $request, Throwable $exception): ResponseInterface
+ {
+ $response = $this->responseFactory->createResponse(500, 'Internal Server Error');
+ $response->getBody()->write($exception->getMessage());
+
+ return $response;
+ }
+ };
+
+ $app->add((new ExceptionHandlingMiddleware())->withExceptionHandler($exceptionHandler));
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->get('/', function () {
+ throw new RuntimeException('Something went wrong');
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->handle($request);
+ $this->assertEquals(500, $response->getStatusCode());
+ $this->assertSame('Something went wrong', (string)$response->getBody());
+ }
+
+ public function testExceptionHandlingMiddlewarePassesThroughNonExceptionRequest()
+ {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $responseFactory = $app->getContainer()->get(ResponseFactoryInterface::class);
+
+ // This handler should not be called in this test
+ $exceptionHandler = new class ($responseFactory) implements ExceptionHandlerInterface {
+ private ResponseFactoryInterface $responseFactory;
+
+ public function __construct($responseFactory)
+ {
+ $this->responseFactory = $responseFactory;
+ }
+
+ public function __invoke(ServerRequestInterface $request, Throwable $exception): ResponseInterface
+ {
+ $response = $this->responseFactory->createResponse(500);
+ $response->getBody()->write($exception->getMessage());
+
+ return $response;
+ }
+ };
+
+ $app->add(new ExceptionHandlingMiddleware($exceptionHandler));
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->handle($request);
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertSame('Hello World', (string)$response->getBody());
+ }
+
+ public function testDefaultMediaTypeWithoutDetails(): void
+ {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $app->add(ExceptionHandlingMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $app->get('/', function () {
+ throw new RuntimeException('Test error');
+ });
+
+ $response = $app->handle($request);
+
+ $this->assertSame(500, $response->getStatusCode());
+ $this->assertSame('text/html', $response->getHeaderLine('Content-Type'));
+ $this->assertStringNotContainsString('Test Error message', (string)$response->getBody());
+ $this->assertStringContainsString('Application Error
', (string)$response->getBody());
+ }
+
+ public function testDefaultHtmlMediaTypeWithDetails(): void
+ {
+ $builder = new AppBuilder();
+ $builder->setSettings(['display_error_details' => true]);
+ $app = $builder->build();
+
+ $app->add(ExceptionHandlingMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $app->get('/', function () {
+ throw new RuntimeException('Test error', 123);
+ });
+
+ $response = $app->handle($request);
+
+ $this->assertSame(500, $response->getStatusCode());
+ $this->assertSame('text/html', (string)$response->getHeaderLine('Content-Type'));
+ $this->assertStringNotContainsString('Test Error message', (string)$response->getBody());
+ $this->assertStringContainsString('Application Error
', (string)$response->getBody());
+ }
+
+ public function testJsonMediaTypeWithDetails(): void
+ {
+ $builder = new AppBuilder();
+ $builder->setSettings(['display_error_details' => true]);
+ $app = $builder->build();
+
+ $app->add(ExceptionHandlingMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/')
+ ->withHeader('Accept', 'application/json');
+
+ $app->get('/', function () {
+ throw new RuntimeException('Test error', 123);
+ });
+
+ $response = $app->handle($request);
+
+ $actual = json_decode((string)$response->getBody(), true);
+ $this->assertSame('Application Error', $actual['message']);
+ $this->assertSame(1, count($actual['exception']));
+ $this->assertSame('RuntimeException', $actual['exception'][0]['type']);
+ $this->assertSame(123, $actual['exception'][0]['code']);
+ $this->assertSame('Test error', $actual['exception'][0]['message']);
+ }
+
+ public function testWithoutHandler(): void
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Test error');
+
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $app->add(new ExceptionHandlingMiddleware());
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $app->get('/', function () {
+ throw new RuntimeException('Test error', 123);
+ });
+
+ $app->handle($request);
+ }
+}
diff --git a/tests/Middleware/ExceptionLoggingMiddlewareTest.php b/tests/Middleware/ExceptionLoggingMiddlewareTest.php
new file mode 100644
index 000000000..f356740e4
--- /dev/null
+++ b/tests/Middleware/ExceptionLoggingMiddlewareTest.php
@@ -0,0 +1,144 @@
+expectException(ErrorException::class);
+
+ $app = (new AppBuilder())->build();
+
+ $logger = new TestLogger();
+
+ $middleware = new ExceptionLoggingMiddleware($logger);
+ $app->add($middleware->withLogErrorDetails(true));
+
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ // Set up a route that throws an ErrorException
+ $app->get('/error', function (ServerRequestInterface $request, ResponseInterface $response) {
+ throw new ErrorException('This is an error', 0, E_ERROR);
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/error');
+
+ try {
+ $app->handle($request);
+ } finally {
+ // Assert the logger captured the error
+ $logs = $logger->getLogs();
+ $this->assertCount(1, $logs);
+ $log = $logs[0];
+ $this->assertSame(LogLevel::ERROR, $log['level']);
+ $this->assertSame('This is an error', $log['message']);
+ $this->assertInstanceOf(ErrorException::class, $log['context']['exception']);
+ }
+ }
+
+ public function testThrowableIsLogged(): void
+ {
+ // Expect the RuntimeException to be thrown
+ $this->expectException(RuntimeException::class);
+
+ $app = (new AppBuilder())->build();
+
+ $logger = new TestLogger();
+
+ $middleware = new ExceptionLoggingMiddleware($logger);
+ $app->add($middleware->withLogErrorDetails(true));
+
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ // Set up a route that throws a generic Throwable
+ $app->get('/throwable', function (ServerRequestInterface $request, ResponseInterface $response) {
+ throw new RuntimeException('This is a runtime exception');
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/throwable');
+
+ try {
+ $app->handle($request);
+ } finally {
+ // Assert the logger captured the error
+ $logs = $logger->getLogs();
+ $this->assertCount(1, $logs);
+ $log = $logs[0];
+ $this->assertSame(LogLevel::ERROR, $log['level']);
+ $this->assertSame('This is a runtime exception', $log['message']);
+ $this->assertInstanceOf(RuntimeException::class, $log['context']['exception']);
+ }
+ }
+
+ /**
+ * Passing E_USER_ERROR to trigger_error() is now deprecated.
+ * RFC: https://wiki.php.net/rfc/deprecations_php_8_4#deprecate_passing_e_user_error_to_trigger_error
+ */
+ #[RequiresPhp('< 8.4.0')]
+ public function testUserLevelErrorIsLogged(): void
+ {
+ $this->expectException(ErrorException::class);
+
+ $app = (new AppBuilder())->build();
+ error_reporting(E_ALL);
+
+ $logger = new TestLogger();
+ $app->add(ErrorHandlingMiddleware::class);
+
+ $middleware = new ExceptionLoggingMiddleware($logger);
+
+ $app->add($middleware->withLogErrorDetails(true));
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->get('/error', function () {
+ trigger_error('This is an error', E_USER_ERROR);
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/error');
+
+ try {
+ $app->handle($request);
+ } finally {
+ // Assert the logger captured the error
+ $logs = $logger->getLogs();
+ $this->assertCount(1, $logs);
+ $log = $logs[0];
+ $this->assertSame(LogLevel::ERROR, $log['level']);
+ $this->assertSame('This is an error', $log['message']);
+ $this->assertInstanceOf(ErrorException::class, $log['context']['exception']);
+ }
+ }
+}
diff --git a/tests/Middleware/HeadMethodMiddlewareTest.php b/tests/Middleware/HeadMethodMiddlewareTest.php
new file mode 100644
index 000000000..e1d232a40
--- /dev/null
+++ b/tests/Middleware/HeadMethodMiddlewareTest.php
@@ -0,0 +1,73 @@
+build();
+
+ $app->add(HeadMethodMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ // Set up a route that returns a non-empty body
+ $app->get('/test', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('This is the body content');
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('HEAD', '/test');
+
+ $response = $app->handle($request);
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('', (string)$response->getBody());
+ }
+
+ public function testGetRequestResponseBodyIsUnchanged(): void
+ {
+ $app = (new AppBuilder())->build();
+
+ $app->add(HeadMethodMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ // Set up a route that returns a non-empty body
+ $app->get('/test', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('This is the body content');
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/test');
+
+ $response = $app->handle($request);
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('This is the body content', (string)$response->getBody());
+ }
+}
diff --git a/tests/Middleware/MethodOverrideMiddlewareTest.php b/tests/Middleware/MethodOverrideMiddlewareTest.php
index ecd2242cd..8c54fcc87 100644
--- a/tests/Middleware/MethodOverrideMiddlewareTest.php
+++ b/tests/Middleware/MethodOverrideMiddlewareTest.php
@@ -3,143 +3,208 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Tests\Middleware;
-use Psr\Http\Message\ServerRequestInterface as Request;
+use PHPUnit\Framework\TestCase;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestFactoryInterface;
+use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
-use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
+use Psr\Http\Server\RequestHandlerInterface;
+use Slim\Builder\AppBuilder;
+use Slim\Middleware\EndpointMiddleware;
use Slim\Middleware\MethodOverrideMiddleware;
-use Slim\Tests\TestCase;
+use Slim\Middleware\RoutingMiddleware;
+use Slim\Tests\Traits\AppTestTrait;
-class MethodOverrideMiddlewareTest extends TestCase
+final class MethodOverrideMiddlewareTest extends TestCase
{
+ use AppTestTrait;
+
public function testHeader()
{
- $responseFactory = $this->getResponseFactory();
- $middleware = (function (Request $request, RequestHandler $handler) use ($responseFactory) {
- $this->assertSame('PUT', $request->getMethod());
- return $responseFactory->createResponse();
- })->bindTo($this);
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $test = $this;
+ $middleware = (function (ServerRequestInterface $request, RequestHandlerInterface $handler) use ($test) {
+ $test->assertSame('PUT', $request->getMethod());
+
+ return $handler->handle($request);
+ });
$methodOverrideMiddleware = new MethodOverrideMiddleware();
- $request = $this
- ->createServerRequest('/', 'POST')
+ $app->add($methodOverrideMiddleware);
+ $app->add($middleware);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->put('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('POST', '/')
->withHeader('X-Http-Method-Override', 'PUT');
- $middlewareDispatcher = $this->createMiddlewareDispatcher(
- $this->createMock(RequestHandler::class),
- null
- );
- $middlewareDispatcher->addCallable($middleware);
- $middlewareDispatcher->addMiddleware($methodOverrideMiddleware);
- $middlewareDispatcher->handle($request);
+ $response = $app->handle($request);
+
+ $this->assertSame('Hello World', (string)$response->getBody());
}
public function testBodyParam()
{
- $responseFactory = $this->getResponseFactory();
- $middleware = (function (Request $request, RequestHandler $handler) use ($responseFactory) {
- $this->assertSame('PUT', $request->getMethod());
- return $responseFactory->createResponse();
- })->bindTo($this);
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $test = $this;
+ $middleware = (function (ServerRequestInterface $request, RequestHandlerInterface $handler) use ($test) {
+ $test->assertSame('PUT', $request->getMethod());
+ return $handler->handle($request);
+ });
$methodOverrideMiddleware = new MethodOverrideMiddleware();
- $request = $this
- ->createServerRequest('/', 'POST')
+ $app->add($methodOverrideMiddleware);
+ $app->add($middleware);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->put('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('POST', '/')
->withParsedBody(['_METHOD' => 'PUT']);
- $middlewareDispatcher = $this->createMiddlewareDispatcher(
- $this->createMock(RequestHandler::class),
- null
- );
- $middlewareDispatcher->addCallable($middleware);
- $middlewareDispatcher->addMiddleware($methodOverrideMiddleware);
- $middlewareDispatcher->handle($request);
+ $response = $app->handle($request);
+
+ $this->assertSame('Hello World', (string)$response->getBody());
}
public function testHeaderPreferred()
{
- $responseFactory = $this->getResponseFactory();
- $middleware = (function (Request $request, RequestHandler $handler) use ($responseFactory) {
- $this->assertSame('DELETE', $request->getMethod());
- return $responseFactory->createResponse();
- })->bindTo($this);
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $test = $this;
+ $middleware = (function (ServerRequestInterface $request, RequestHandlerInterface $handler) use ($test) {
+ $test->assertSame('DELETE', $request->getMethod());
+
+ return $handler->handle($request);
+ });
$methodOverrideMiddleware = new MethodOverrideMiddleware();
- $request = $this
- ->createServerRequest('/', 'POST')
+ $app->add($methodOverrideMiddleware);
+ $app->add($middleware);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->delete('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('POST', '/')
->withHeader('X-Http-Method-Override', 'DELETE')
- ->withParsedBody((object) ['_METHOD' => 'PUT']);
-
- $middlewareDispatcher = $this->createMiddlewareDispatcher(
- $this->createMock(RequestHandler::class),
- null
- );
- $middlewareDispatcher->addCallable($middleware);
- $middlewareDispatcher->addMiddleware($methodOverrideMiddleware);
- $middlewareDispatcher->handle($request);
+ ->withParsedBody((object)['_METHOD' => 'PUT']);
+
+ $response = $app->handle($request);
+
+ $this->assertSame('Hello World', (string)$response->getBody());
}
public function testNoOverride()
{
- $responseFactory = $this->getResponseFactory();
- $middleware = (function (Request $request, RequestHandler $handler) use ($responseFactory) {
- $this->assertSame('POST', $request->getMethod());
- return $responseFactory->createResponse();
- })->bindTo($this);
+ $builder = new AppBuilder();
+ $app = $builder->build();
+ $test = $this;
+ $middleware = (function (ServerRequestInterface $request, RequestHandlerInterface $handler) use ($test) {
+ $test->assertSame('POST', $request->getMethod());
+
+ return $handler->handle($request);
+ });
$methodOverrideMiddleware = new MethodOverrideMiddleware();
- $request = $this->createServerRequest('/', 'POST');
+ $app->add($methodOverrideMiddleware);
+ $app->add($middleware);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->post('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
- $middlewareDispatcher = $this->createMiddlewareDispatcher(
- $this->createMock(RequestHandler::class),
- null
- );
- $middlewareDispatcher->addCallable($middleware);
- $middlewareDispatcher->addMiddleware($methodOverrideMiddleware);
- $middlewareDispatcher->handle($request);
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('POST', '/');
+
+ $response = $app->handle($request);
+
+ $this->assertSame('Hello World', (string)$response->getBody());
}
public function testNoOverrideRewindEofBodyStream()
{
- $responseFactory = $this->getResponseFactory();
- $middleware = (function (Request $request, RequestHandler $handler) use ($responseFactory) {
- $this->assertSame('POST', $request->getMethod());
- return $responseFactory->createResponse();
- })->bindTo($this);
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $test = $this;
+ $middleware = (function (ServerRequestInterface $request, RequestHandlerInterface $handler) use ($test) {
+ $test->assertSame('POST', $request->getMethod());
+ return $handler->handle($request);
+ });
$methodOverrideMiddleware = new MethodOverrideMiddleware();
- $request = $this->createServerRequest('/', 'POST');
-
- // Prophesize the body stream for which `eof()` returns `true` and the
- // `rewind()` has to be called.
- $bodyProphecy = $this->prophesize(StreamInterface::class);
- /** @noinspection PhpUndefinedMethodInspection */
- $bodyProphecy->eof()
- ->willReturn(true)
- ->shouldBeCalled();
- /** @noinspection PhpUndefinedMethodInspection */
- $bodyProphecy->rewind()
- ->shouldBeCalled();
- /** @var StreamInterface $body */
- $body = $bodyProphecy->reveal();
+ $app->add($methodOverrideMiddleware);
+ $app->add($middleware);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->post('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
+
+ return $response;
+ });
+
+ /** @var ServerRequestInterface $request */
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('POST', '/');
+
+ $body = $this->createMock(StreamInterface::class);
+
+ // Configuring the mock to return true for eof() and ensure rewind() is called
+ $body->expects($this->once())
+ ->method('eof')
+ ->willReturn(true);
+
+ $body->expects($this->once())
+ ->method('rewind');
+
$request = $request->withBody($body);
- $middlewareDispatcher = $this->createMiddlewareDispatcher(
- $this->createMock(RequestHandler::class),
- null
- );
- $middlewareDispatcher->addCallable($middleware);
- $middlewareDispatcher->addMiddleware($methodOverrideMiddleware);
- $middlewareDispatcher->handle($request);
+ $response = $app->handle($request);
+
+ $this->assertSame('Hello World', (string)$response->getBody());
}
}
diff --git a/tests/Middleware/OutputBufferingMiddlewareTest.php b/tests/Middleware/OutputBufferingMiddlewareTest.php
index aa84dd019..bf2e4cda0 100644
--- a/tests/Middleware/OutputBufferingMiddlewareTest.php
+++ b/tests/Middleware/OutputBufferingMiddlewareTest.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
@@ -12,72 +12,85 @@
use Exception;
use InvalidArgumentException;
-use Psr\Http\Server\RequestHandlerInterface;
-use ReflectionProperty;
+use PHPUnit\Framework\TestCase;
+use Psr\Http\Message\ResponseFactoryInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestFactoryInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\StreamFactoryInterface;
+use Slim\Builder\AppBuilder;
+use Slim\Middleware\EndpointMiddleware;
use Slim\Middleware\OutputBufferingMiddleware;
-use Slim\Tests\TestCase;
+use Slim\Middleware\RoutingMiddleware;
+use Slim\Tests\Traits\AppTestTrait;
use function ob_get_contents;
-class OutputBufferingMiddlewareTest extends TestCase
+final class OutputBufferingMiddlewareTest extends TestCase
{
- public function testStyleDefaultValid()
- {
- $middleware = new OutputBufferingMiddleware($this->getStreamFactory());
-
- $reflectionProperty = new ReflectionProperty($middleware, 'style');
- $reflectionProperty->setAccessible(true);
- $value = $reflectionProperty->getValue($middleware);
-
- $this->assertSame('append', $value);
- }
+ use AppTestTrait;
public function testStyleCustomValid()
{
- $middleware = new OutputBufferingMiddleware($this->getStreamFactory(), 'prepend');
+ $this->expectNotToPerformAssertions();
- $reflectionProperty = new ReflectionProperty($middleware, 'style');
- $reflectionProperty->setAccessible(true);
- $value = $reflectionProperty->getValue($middleware);
+ $builder = new AppBuilder();
+ $streamFactory = $builder->build()->getContainer()->get(StreamFactoryInterface::class);
- $this->assertSame('prepend', $value);
+ new OutputBufferingMiddleware($streamFactory, OutputBufferingMiddleware::APPEND);
+ new OutputBufferingMiddleware($streamFactory, OutputBufferingMiddleware::PREPEND);
}
public function testStyleCustomInvalid()
{
$this->expectException(InvalidArgumentException::class);
- new OutputBufferingMiddleware($this->getStreamFactory(), 'foo');
+ $builder = new AppBuilder();
+ $streamFactory = $builder->build()->getContainer()->get(StreamFactoryInterface::class);
+
+ new OutputBufferingMiddleware($streamFactory, 'foo');
}
public function testAppend()
{
- $responseFactory = $this->getResponseFactory();
- $middleware = function ($request, $handler) use ($responseFactory) {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $responseFactory = $app->getContainer()->get(ResponseFactoryInterface::class);
+ $streamFactory = $app->getContainer()->get(StreamFactoryInterface::class);
+
+ $outputBufferingMiddleware = new OutputBufferingMiddleware($streamFactory, OutputBufferingMiddleware::APPEND);
+ $app->add($outputBufferingMiddleware);
+
+ $middleware = function () use ($responseFactory) {
$response = $responseFactory->createResponse();
$response->getBody()->write('Body');
echo 'Test';
return $response;
};
- $outputBufferingMiddleware = new OutputBufferingMiddleware($this->getStreamFactory(), 'append');
+ $app->add($middleware);
- $request = $this->createServerRequest('/', 'GET');
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
- $middlewareDispatcher = $this->createMiddlewareDispatcher(
- $this->createMock(RequestHandlerInterface::class),
- null
- );
- $middlewareDispatcher->addCallable($middleware);
- $middlewareDispatcher->addMiddleware($outputBufferingMiddleware);
- $response = $middlewareDispatcher->handle($request);
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
- $this->assertSame('BodyTest', (string) $response->getBody());
+ $response = $app->handle($request);
+
+ $this->assertSame('BodyTest', (string)$response->getBody());
}
public function testPrepend()
{
- $responseFactory = $this->getResponseFactory();
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $responseFactory = $app->getContainer()->get(ResponseFactoryInterface::class);
+ $streamFactory = $app->getContainer()->get(StreamFactoryInterface::class);
+
$middleware = function ($request, $handler) use ($responseFactory) {
$response = $responseFactory->createResponse();
$response->getBody()->write('Body');
@@ -85,42 +98,58 @@ public function testPrepend()
return $response;
};
- $outputBufferingMiddleware = new OutputBufferingMiddleware($this->getStreamFactory(), 'prepend');
- $request = $this->createServerRequest('/', 'GET');
+ $outputBufferingMiddleware = new OutputBufferingMiddleware($streamFactory, OutputBufferingMiddleware::PREPEND);
+
+ $app->add($outputBufferingMiddleware);
+ $app->add($middleware);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
- $middlewareDispatcher = $this->createMiddlewareDispatcher(
- $this->createMock(RequestHandlerInterface::class),
- null
- );
- $middlewareDispatcher->addCallable($middleware);
- $middlewareDispatcher->addMiddleware($outputBufferingMiddleware);
- $response = $middlewareDispatcher->handle($request);
+ $response = $app->handle($request);
- $this->assertSame('TestBody', (string) $response->getBody());
+ $this->assertSame('TestBody', (string)$response->getBody());
}
public function testOutputBufferIsCleanedWhenThrowableIsCaught()
{
- $this->getResponseFactory();
- $middleware = (function ($request, $handler) {
- echo "Test";
- $this->assertSame('Test', ob_get_contents());
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $streamFactory = $app->getContainer()->get(StreamFactoryInterface::class);
+
+ $test = $this;
+ $middleware = (function ($request, $handler) use ($test) {
+ echo 'Test';
+ $test->assertSame('Test', ob_get_contents());
throw new Exception('Oops...');
- })->bindTo($this);
- $outputBufferingMiddleware = new OutputBufferingMiddleware($this->getStreamFactory(), 'prepend');
+ });
+
+ $outputBufferingMiddleware = new OutputBufferingMiddleware($streamFactory, OutputBufferingMiddleware::PREPEND);
- $request = $this->createServerRequest('/', 'GET');
+ $app->add($outputBufferingMiddleware);
+ $app->add($middleware);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
+ return $response;
+ });
- $middlewareDispatcher = $this->createMiddlewareDispatcher(
- $this->createMock(RequestHandlerInterface::class),
- null
- );
- $middlewareDispatcher->addCallable($middleware);
- $middlewareDispatcher->addMiddleware($outputBufferingMiddleware);
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
try {
- $middlewareDispatcher->handle($request);
+ $app->handle($request);
} catch (Exception $e) {
$this->assertSame('', ob_get_contents());
}
diff --git a/tests/Middleware/ResponseFactoryMiddlewareTest.php b/tests/Middleware/ResponseFactoryMiddlewareTest.php
new file mode 100644
index 000000000..e29845723
--- /dev/null
+++ b/tests/Middleware/ResponseFactoryMiddlewareTest.php
@@ -0,0 +1,80 @@
+build();
+
+ $app->add(function ($request, $handler) {
+ $response = $handler->handle($request);
+ $response->getBody()->write('Expected Response');
+
+ return $response;
+ });
+ $app->add(ResponseFactoryMiddleware::class);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/test');
+
+ $response = $app->handle($request);
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('Expected Response', (string)$response->getBody());
+ }
+
+ public function testProcessReturnsResponseFromFactory(): void
+ {
+ $app = (new AppBuilder())->build();
+
+ $responseFactory = $app->getContainer()->get(ResponseFactoryInterface::class);
+
+ // Create a response with a specific content
+ $expectedResponse = $responseFactory->createResponse();
+ $expectedResponse->getBody()->write('Expected Response');
+
+ // Mock the ResponseFactoryInterface to always return the expected response
+ $responseFactory = new class ($expectedResponse) implements ResponseFactoryInterface {
+ private ResponseInterface $response;
+
+ public function __construct(ResponseInterface $response)
+ {
+ $this->response = $response;
+ }
+
+ public function createResponse(int $status = 200, string $reasonPhrase = ''): ResponseInterface
+ {
+ return $this->response;
+ }
+ };
+
+ $app->add(new ResponseFactoryMiddleware($responseFactory));
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/test');
+
+ $response = $app->handle($request);
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('Expected Response', (string)$response->getBody());
+ }
+}
diff --git a/tests/Middleware/RoutingArgumentsMiddlewareTest.php b/tests/Middleware/RoutingArgumentsMiddlewareTest.php
new file mode 100644
index 000000000..27d5d8f9e
--- /dev/null
+++ b/tests/Middleware/RoutingArgumentsMiddlewareTest.php
@@ -0,0 +1,78 @@
+build();
+
+ $app->add(RoutingMiddleware::class);
+ $app->add(RoutingArgumentsMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ // Define a route with arguments
+ $app->get('/test/{id}', function (ServerRequestInterface $request, ResponseInterface $response) {
+ // Verify that the 'id' attribute has been added to the request
+ $id = $request->getAttribute('id');
+ $response->getBody()->write("ID: $id");
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/test/123');
+
+ $response = $app->handle($request);
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('ID: 123', (string)$response->getBody());
+ }
+
+ public function testProcessNoRoutingArguments(): void
+ {
+ $app = (new AppBuilder())->build();
+
+ $app->add(RoutingArgumentsMiddleware::class);
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ // Define a route without any arguments
+ $app->get('/no-args', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $id = $request->getAttribute('id') ?? 'No arguments';
+ $response->getBody()->write("ID: $id");
+
+ return $response;
+ });
+
+ // Create a server request without any routing arguments
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/no-args');
+
+ $response = $app->handle($request);
+
+ // Assertions
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('ID: No arguments', (string)$response->getBody());
+ }
+}
diff --git a/tests/Middleware/RoutingMiddlewareTest.php b/tests/Middleware/RoutingMiddlewareTest.php
index 7ca0c02d0..9bcd28b4f 100644
--- a/tests/Middleware/RoutingMiddlewareTest.php
+++ b/tests/Middleware/RoutingMiddlewareTest.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
@@ -11,181 +11,192 @@
namespace Slim\Tests\Middleware;
use FastRoute\Dispatcher;
-use Prophecy\Argument;
+use PHPUnit\Framework\TestCase;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
-use RuntimeException;
-use Slim\CallableResolver;
+use Slim\Builder\AppBuilder;
use Slim\Exception\HttpMethodNotAllowedException;
use Slim\Exception\HttpNotFoundException;
-use Slim\Interfaces\RouteParserInterface;
-use Slim\Interfaces\RouteResolverInterface;
+use Slim\Interfaces\UrlGeneratorInterface;
+use Slim\Middleware\EndpointMiddleware;
use Slim\Middleware\RoutingMiddleware;
-use Slim\Routing\RouteCollector;
use Slim\Routing\RouteContext;
-use Slim\Routing\RouteParser;
-use Slim\Routing\RouteResolver;
use Slim\Routing\RoutingResults;
-use Slim\Tests\TestCase;
-class RoutingMiddlewareTest extends TestCase
+final class RoutingMiddlewareTest extends TestCase
{
- protected function getRouteCollector()
- {
- $callableResolver = new CallableResolver();
- $responseFactory = $this->getResponseFactory();
- $routeCollector = new RouteCollector($responseFactory, $callableResolver);
- $routeCollector->map(['GET'], '/hello/{name}', null);
- return $routeCollector;
- }
-
public function testRouteIsStoredOnSuccessfulMatch()
{
- $responseFactory = $this->getResponseFactory();
- $middleware = (function (ServerRequestInterface $request) use ($responseFactory) {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $test = $this;
+ $middleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler) use ($test) {
+ // routingResults is available
+ /** @var RoutingResults $routingResults */
+ $routingResults = $request->getAttribute(RouteContext::ROUTING_RESULTS);
+ $test->assertInstanceOf(RoutingResults::class, $routingResults);
+
// route is available
- $route = $request->getAttribute(RouteContext::ROUTE);
- $this->assertNotNull($route);
- $this->assertSame('foo', $route->getArgument('name'));
+ $route = $routingResults->getRoute();
+ $test->assertNotNull($route);
// routeParser is available
- $routeParser = $request->getAttribute(RouteContext::ROUTE_PARSER);
- $this->assertNotNull($routeParser);
- $this->assertInstanceOf(RouteParserInterface::class, $routeParser);
+ $urlGenerator = $request->getAttribute(RouteContext::URL_GENERATOR);
+ $test->assertNotNull($urlGenerator);
+ $test->assertInstanceOf(UrlGeneratorInterface::class, $urlGenerator);
- // routingResults is available
- $routingResults = $request->getAttribute(RouteContext::ROUTING_RESULTS);
- $this->assertInstanceOf(RoutingResults::class, $routingResults);
- return $responseFactory->createResponse();
- })->bindTo($this);
-
- $routeCollector = $this->getRouteCollector();
- $routeParser = new RouteParser($routeCollector);
- $routeResolver = new RouteResolver($routeCollector);
- $routingMiddleware = new RoutingMiddleware($routeResolver, $routeParser);
-
- $request = $this->createServerRequest('https://example.com:443/hello/foo', 'GET');
-
- $middlewareDispatcher = $this->createMiddlewareDispatcher(
- $this->createMock(RequestHandlerInterface::class),
- null
- );
- $middlewareDispatcher->addCallable($middleware);
- $middlewareDispatcher->addMiddleware($routingMiddleware);
- $middlewareDispatcher->handle($request);
+ return $handler->handle($request);
+ };
+
+ $app->add(RoutingMiddleware::class);
+ $app->add($middleware);
+ $app->add(EndpointMiddleware::class);
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', 'https://example.com:443/hello/foo');
+
+ $app->get('/hello/foo', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
+
+ return $response;
+ });
+
+ $response = $app->handle($request);
+
+ $this->assertSame('Hello World', (string)$response->getBody());
}
public function testRouteIsNotStoredOnMethodNotAllowed()
{
- $routeCollector = $this->getRouteCollector();
- $routeParser = new RouteParser($routeCollector);
- $routeResolver = new RouteResolver($routeCollector);
- $routingMiddleware = new RoutingMiddleware($routeResolver, $routeParser);
+ $this->expectException(HttpMethodNotAllowedException::class);
- $request = $this->createServerRequest('https://example.com:443/hello/foo', 'POST');
- $requestHandlerProphecy = $this->prophesize(RequestHandlerInterface::class);
- /** @var RequestHandlerInterface $requestHandler */
- $requestHandler = $requestHandlerProphecy->reveal();
+ $builder = new AppBuilder();
+ $app = $builder->build();
- $middlewareDispatcher = $this->createMiddlewareDispatcher($requestHandler, null);
- $middlewareDispatcher->addMiddleware($routingMiddleware);
+ $test = $this;
+ $middleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler) use ($test) {
+ try {
+ return $handler->handle($request);
+ } catch (HttpMethodNotAllowedException $exception) {
+ $request = $exception->getRequest();
- try {
- $middlewareDispatcher->handle($request);
- $this->fail('HTTP method should not have been allowed');
- } catch (HttpMethodNotAllowedException $exception) {
- $request = $exception->getRequest();
+ // routingResults is available
+ /** @var RoutingResults $routingResults */
+ $routingResults = $request->getAttribute(RouteContext::ROUTING_RESULTS);
+ $test->assertInstanceOf(RoutingResults::class, $routingResults);
+ $test->assertSame(Dispatcher::METHOD_NOT_ALLOWED, $routingResults->getRouteStatus());
- // route is not available
- $route = $request->getAttribute(RouteContext::ROUTE);
- $this->assertNull($route);
+ // route is not available
+ $route = $routingResults->getRoute();
+ $test->assertNull($route);
- // routeParser is available
- $routeParser = $request->getAttribute(RouteContext::ROUTE_PARSER);
- $this->assertNotNull($routeParser);
- $this->assertInstanceOf(RouteParserInterface::class, $routeParser);
+ // routeParser is available
+ $urlParser = $request->getAttribute(RouteContext::URL_GENERATOR);
+ $test->assertNotNull($urlParser);
+ $test->assertInstanceOf(UrlGeneratorInterface::class, $urlParser);
- // routingResults is available
- $routingResults = $request->getAttribute(RouteContext::ROUTING_RESULTS);
- $this->assertInstanceOf(RoutingResults::class, $routingResults);
- $this->assertSame(Dispatcher::METHOD_NOT_ALLOWED, $routingResults->getRouteStatus());
- }
+ // Re-throw to keep the behavior consistent
+ throw $exception;
+ }
+ };
+
+ $app->add(RoutingMiddleware::class);
+ $app->add($middleware);
+ $app->add(EndpointMiddleware::class);
+
+ $app->post('/hello/foo', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Hello World');
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/hello/foo');
+
+ $app->handle($request);
}
public function testRouteIsNotStoredOnNotFound()
{
- $routeCollector = $this->getRouteCollector();
- $routeParser = new RouteParser($routeCollector);
- $routeResolver = new RouteResolver($routeCollector);
- $routingMiddleware = new RoutingMiddleware($routeResolver, $routeParser);
+ $this->expectException(HttpNotFoundException::class);
- $request = $this->createServerRequest('https://example.com:443/goodbye', 'GET');
- $requestHandlerProphecy = $this->prophesize(RequestHandlerInterface::class);
- /** @var RequestHandlerInterface $requestHandler */
- $requestHandler = $requestHandlerProphecy->reveal();
+ $builder = new AppBuilder();
+ $app = $builder->build();
- $middlewareDispatcher = $this->createMiddlewareDispatcher($requestHandler, null);
- $middlewareDispatcher->addMiddleware($routingMiddleware);
+ $test = $this;
+ $middleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler) use ($test) {
+ try {
+ return $handler->handle($request);
+ } catch (HttpNotFoundException $exception) {
+ $request = $exception->getRequest();
- try {
- $middlewareDispatcher->handle($request);
- $this->fail('HTTP route should not have been found');
- } catch (HttpNotFoundException $exception) {
- $request = $exception->getRequest();
+ // routingResults is available
+ /** @var RoutingResults $routingResults */
+ $routingResults = $request->getAttribute(RouteContext::ROUTING_RESULTS);
+ $test->assertInstanceOf(RoutingResults::class, $routingResults);
+ $test->assertSame(Dispatcher::NOT_FOUND, $routingResults->getRouteStatus());
- // route is not available
- $route = $request->getAttribute(RouteContext::ROUTE);
- $this->assertNull($route);
+ // route is not available
+ $route = $routingResults->getRoute();
+ $test->assertNull($route);
- // routeParser is available
- $routeParser = $request->getAttribute(RouteContext::ROUTE_PARSER);
- $this->assertNotNull($routeParser);
- $this->assertInstanceOf(RouteParserInterface::class, $routeParser);
+ // routeParser is available
+ $urlGenerator = $request->getAttribute(RouteContext::URL_GENERATOR);
+ $test->assertNotNull($urlGenerator);
+ $test->assertInstanceOf(UrlGeneratorInterface::class, $urlGenerator);
- // routingResults is available
- $routingResults = $request->getAttribute(RouteContext::ROUTING_RESULTS);
- $this->assertInstanceOf(RoutingResults::class, $routingResults);
- $this->assertSame(Dispatcher::NOT_FOUND, $routingResults->getRouteStatus());
- }
+ // Re-throw to keep the behavior consistent
+ throw $exception;
+ }
+ };
+
+ $app->add(RoutingMiddleware::class);
+ $app->add($middleware);
+ $app->add(EndpointMiddleware::class);
+
+ // No route is defined for '/hello/foo'
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/hello/foo');
+
+ $app->handle($request);
}
- public function testPerformRoutingThrowsExceptionOnInvalidRoutingResultsRouteStatus()
+ public function testRoutingWithBasePath(): void
{
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('An unexpected error occurred while performing routing.');
-
- // Prophesize the `RoutingResults` instance that would return an invalid route
- // status when the method `getRouteStatus()` gets called.
- $routingResultsProphecy = $this->prophesize(RoutingResults::class);
- /** @noinspection PhpUndefinedMethodInspection */
- $routingResultsProphecy->getRouteStatus()
- ->willReturn(-1)
- ->shouldBeCalledOnce();
- /** @var RoutingResults $routingResults */
- $routingResults = $routingResultsProphecy->reveal();
-
- // Prophesize the `RouteParserInterface` instance will be created.
- $routeParserProphecy = $this->prophesize(RouteParser::class);
- /** @var RouteParserInterface $routeParser */
- $routeParser = $routeParserProphecy->reveal();
-
- // Prophesize the `RouteResolverInterface` that would return the `RoutingResults`
- // defined above, when the method `computeRoutingResults()` gets called.
- $routeResolverProphecy = $this->prophesize(RouteResolverInterface::class);
- /** @noinspection PhpUndefinedMethodInspection */
- $routeResolverProphecy->computeRoutingResults(Argument::any(), Argument::any())
- ->willReturn($routingResults)
- ->shouldBeCalled();
- /** @var RouteResolverInterface $routeResolver */
- $routeResolver = $routeResolverProphecy->reveal();
-
- // Create the server request.
- $request = $this->createServerRequest('https://example.com:443/hello/foo', 'GET');
-
- // Create the routing middleware with the `RouteResolverInterface` defined
- // above. Perform the routing, which should throw the RuntimeException.
- $middleware = new RoutingMiddleware($routeResolver, $routeParser);
- /** @noinspection PhpUnhandledExceptionInspection */
- $middleware->performRouting($request);
+ $app = (new AppBuilder())->build();
+ $app->setBasePath('/api');
+
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ // Define a route with arguments
+ $app->get('/users/{id}', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
+ $urlGenerator = RouteContext::fromRequest($request)->getUrlGenerator();
+
+ $url = $urlGenerator->relativeUrlFor('user.show', ['id' => $args['id']], ['page' => 2]);
+ $response = $response->withHeader('X-relativeUrlFor', $url);
+
+ $url2 = $urlGenerator->fullUrlFor($request->getUri(), 'user.show', ['id' => $args['id']], ['page' => 2]);
+ $response = $response->withHeader('X-fullUrlFor', $url2);
+
+ return $response;
+ })->setName('user.show');
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/api/users/123');
+
+ $response = $app->handle($request);
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('/api/users/123?page=2', $response->getHeaderLine('X-relativeUrlFor'));
+ $this->assertSame('/api/users/123?page=2', $response->getHeaderLine('X-fullUrlFor'));
}
}
diff --git a/tests/MiddlewareDispatcherTest.php b/tests/MiddlewareDispatcherTest.php
deleted file mode 100644
index ad87fedfc..000000000
--- a/tests/MiddlewareDispatcherTest.php
+++ /dev/null
@@ -1,596 +0,0 @@
-handle($request);
- }
- }
-
- public function testAddMiddleware(): void
- {
- $responseFactory = $this->getResponseFactory();
- $callable = function ($request, $handler) use ($responseFactory) {
- return $responseFactory->createResponse();
- };
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestHandlerProphecy = $this->prophesize(RequestHandlerInterface::class);
-
- $middlewareDispatcher = $this->createMiddlewareDispatcher($requestHandlerProphecy->reveal());
- $middlewareDispatcher->add($callable);
-
- $response = $middlewareDispatcher->handle($requestProphecy->reveal());
- $this->assertInstanceOf(ResponseInterface::class, $response);
- }
-
- public function testNamedFunctionIsResolved(): void
- {
- $handler = new MockRequestHandler();
-
- $middlewareDispatcher = $this->createMiddlewareDispatcher($handler, null);
- $middlewareDispatcher->addDeferred(__NAMESPACE__ . '\testProcessRequest');
-
- $request = $this->createServerRequest('/');
- $middlewareDispatcher->handle($request);
-
- $this->assertSame(1, $handler->getCalledCount());
- }
-
- public function testDeferredResolvedCallable(): void
- {
- $callable = function (ServerRequestInterface $request, RequestHandlerInterface $handler) {
- return $handler->handle($request);
- };
-
- $containerProphecy = $this->prophesize(ContainerInterface::class);
-
- $containerProphecy
- ->has('callable')
- ->willReturn(true)
- ->shouldBeCalledOnce();
-
- $containerProphecy
- ->get('callable')
- ->willReturn($callable)
- ->shouldBeCalledOnce();
-
- $handler = new MockRequestHandler();
-
- $middlewareDispatcher = $this->createMiddlewareDispatcher($handler, $containerProphecy->reveal());
- $middlewareDispatcher->addDeferred('callable');
-
- $request = $this->createServerRequest('/');
- $middlewareDispatcher->handle($request);
-
- $this->assertSame(1, $handler->getCalledCount());
- }
-
- public function testDeferredResolvedCallableWithoutContainerAndNonAdvancedCallableResolver(): void
- {
- $callable = function (ServerRequestInterface $request, RequestHandlerInterface $handler) {
- return $handler->handle($request);
- };
-
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $callableResolverProphecy
- ->resolve('callable')
- ->willReturn($callable)
- ->shouldBeCalledOnce();
-
- $handler = new MockRequestHandler();
-
- $middlewareDispatcher = $this->createMiddlewareDispatcher($handler, null, $callableResolverProphecy->reveal());
- $middlewareDispatcher->addDeferred('callable');
-
- $request = $this->createServerRequest('/');
- $middlewareDispatcher->handle($request);
-
- $this->assertSame(1, $handler->getCalledCount());
- }
-
- public function testDeferredResolvedCallableWithDirectConstructorCall(): void
- {
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $callableResolverProphecy
- ->resolve(MockMiddlewareWithoutConstructor::class)
- ->willThrow(new RuntimeException('Callable not available from resolver'))
- ->shouldBeCalledOnce();
-
- $handler = new MockRequestHandler();
-
- $middlewareDispatcher = $this->createMiddlewareDispatcher($handler, null, $callableResolverProphecy->reveal());
- $middlewareDispatcher->addDeferred(MockMiddlewareWithoutConstructor::class);
-
- $request = $this->createServerRequest('/');
- $middlewareDispatcher->handle($request);
-
- $this->assertSame(1, $handler->getCalledCount());
- }
-
- public function deferredCallableProvider(): array
- {
- return [
- [MockMiddlewareSlimCallable::class . ':custom', new MockMiddlewareSlimCallable()],
- ['MiddlewareInstance', new MockMiddlewareWithoutConstructor()],
- ['NamedFunction', __NAMESPACE__ . '\testProcessRequest'],
- ['Callable', function (ServerRequestInterface $request, RequestHandlerInterface $handler) {
- return $handler->handle($request);
- }],
- ['MiddlewareInterfaceNotImplemented', 'MiddlewareInterfaceNotImplemented']
- ];
- }
-
- /**
- * @dataProvider deferredCallableProvider
- *
- * @param string $callable
- * @param callable|MiddlewareInterface
- */
- public function testDeferredResolvedCallableWithContainerAndNonAdvancedCallableResolverUnableToResolveCallable(
- $callable,
- $result
- ): void {
- if ($callable === 'MiddlewareInterfaceNotImplemented') {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Middleware MiddlewareInterfaceNotImplemented is not resolvable');
- }
-
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $callableResolverProphecy
- ->resolve($callable)
- ->willThrow(RuntimeException::class)
- ->shouldBeCalledOnce();
-
- $containerProphecy = $this->prophesize(ContainerInterface::class);
-
- $containerProphecy
- ->has(Argument::any())
- ->willReturn(true)
- ->shouldBeCalledOnce();
-
- $containerProphecy
- ->get(Argument::any())
- ->willReturn($result)
- ->shouldBeCalledOnce();
-
- $handler = new MockRequestHandler();
-
- $middlewareDispatcher = $this->createMiddlewareDispatcher(
- $handler,
- $containerProphecy->reveal(),
- $callableResolverProphecy->reveal()
- );
- $middlewareDispatcher->addDeferred($callable);
-
- $request = $this->createServerRequest('/');
- $middlewareDispatcher->handle($request);
-
- $this->assertSame(1, $handler->getCalledCount());
- }
-
- public function testDeferredResolvedSlimCallable(): void
- {
- $handler = new MockRequestHandler();
- $middlewareDispatcher = $this->createMiddlewareDispatcher($handler, null);
- $middlewareDispatcher->addDeferred(MockMiddlewareSlimCallable::class . ':custom');
-
- $request = $this->createServerRequest('/');
- $middlewareDispatcher->handle($request);
-
- $this->assertSame(1, $handler->getCalledCount());
- }
-
- public function testDeferredResolvedClosureIsBoundToContainer(): void
- {
- $containerProphecy = $this->prophesize(ContainerInterface::class);
-
- $self = $this;
- $callable = function (
- ServerRequestInterface $request,
- RequestHandlerInterface $handler
- ) use ($self) {
- $self->assertInstanceOf(ContainerInterface::class, $this);
- return $handler->handle($request);
- };
-
- $containerProphecy->has('callable')->willReturn(true);
- $containerProphecy->get('callable')->willReturn($callable);
-
- $handler = new MockRequestHandler();
- $middlewareDispatcher = $this->createMiddlewareDispatcher($handler, $containerProphecy->reveal());
- $middlewareDispatcher->addDeferred('callable');
-
- $request = $this->createServerRequest('/');
- $middlewareDispatcher->handle($request);
- }
-
- public function testAddCallableBindsClosureToContainer(): void
- {
- $containerProphecy = $this->prophesize(ContainerInterface::class);
-
- $self = $this;
- $callable = function (
- ServerRequestInterface $request,
- RequestHandlerInterface $handler
- ) use (
- $self,
- $containerProphecy
- ) {
- $self->assertSame($containerProphecy->reveal(), $this);
- return $handler->handle($request);
- };
-
- $handler = new MockRequestHandler();
- $middlewareDispatcher = $this->createMiddlewareDispatcher($handler, $containerProphecy->reveal());
- $middlewareDispatcher->addCallable($callable);
-
- $request = $this->createServerRequest('/');
- $middlewareDispatcher->handle($request);
- }
-
- public function testResolvableReturnsInstantiatedObject(): void
- {
- MockMiddlewareWithoutConstructor::$CalledCount = 0;
-
- $handler = new MockRequestHandler();
- $middlewareDispatcher = $this->createMiddlewareDispatcher($handler, null);
- $middlewareDispatcher->addDeferred(MockMiddlewareWithoutConstructor::class);
-
- $request = $this->createServerRequest('/');
- $middlewareDispatcher->handle($request);
-
- $this->assertSame(1, MockMiddlewareWithoutConstructor::$CalledCount);
- $this->assertSame(1, $handler->getCalledCount());
- }
-
- public function testResolveThrowsExceptionWhenResolvableDoesNotImplementMiddlewareInterface(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('MiddlewareInterfaceNotImplemented is not resolvable');
-
- $containerProphecy = $this->prophesize(ContainerInterface::class);
-
- $containerProphecy
- ->has('MiddlewareInterfaceNotImplemented')
- ->willReturn(true)
- ->shouldBeCalledOnce();
-
- $containerProphecy
- ->get('MiddlewareInterfaceNotImplemented')
- ->willReturn(new stdClass())
- ->shouldBeCalledOnce();
-
- $handler = new MockRequestHandler();
- $middlewareDispatcher = $this->createMiddlewareDispatcher($handler, $containerProphecy->reveal());
- $middlewareDispatcher->addDeferred('MiddlewareInterfaceNotImplemented');
-
- $request = $this->createServerRequest('/');
- $middlewareDispatcher->handle($request);
- }
-
- public function testResolveThrowsExceptionWithoutContainerAndUnresolvableClass(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessageMatches('/(Middleware|Callable) Unresolvable::class does not exist/');
-
- $handler = new MockRequestHandler();
- $middlewareDispatcher = $this->createMiddlewareDispatcher($handler, null);
- $middlewareDispatcher->addDeferred('Unresolvable::class');
-
- $request = $this->createServerRequest('/');
- $middlewareDispatcher->handle($request);
- }
-
- public function testResolveThrowsExceptionWithoutContainerNonAdvancedCallableResolverAndUnresolvableClass(): void
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessageMatches('/(Middleware|Callable) Unresolvable::class does not exist/');
-
- $unresolvable = 'Unresolvable::class';
-
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $callableResolverProphecy
- ->resolve($unresolvable)
- ->willThrow(RuntimeException::class)
- ->shouldBeCalledOnce();
-
- $handler = new MockRequestHandler();
- $middlewareDispatcher = $this->createMiddlewareDispatcher($handler, null, $callableResolverProphecy->reveal());
- $middlewareDispatcher->addDeferred($unresolvable);
-
- $request = $this->createServerRequest('/');
- $middlewareDispatcher->handle($request);
- }
-
- public function testExecutesKernelWithEmptyMiddlewareStack(): void
- {
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $kernelProphecy = $this->prophesize(RequestHandlerInterface::class);
- $kernelProphecy->handle(Argument::type(ServerRequestInterface::class))->willReturn($responseProphecy->reveal());
- /** @var RequestHandlerInterface $kernel */
- $kernel = $kernelProphecy->reveal();
-
- $dispatcher = $this->createMiddlewareDispatcher($kernel, null);
-
- $response = $dispatcher->handle($requestProphecy->reveal());
-
- $kernelProphecy->handle(Argument::type(ServerRequestInterface::class))->shouldHaveBeenCalled();
- $this->assertSame($responseProphecy->reveal(), $response);
- }
-
- public function testExecutesMiddlewareLastInFirstOut(): void
- {
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->getHeader(Argument::type('string'))->willReturn([]);
- $requestProphecy->withAddedHeader(Argument::type('string'), Argument::type('string'))->will(function ($args) {
- $headers = $this->reveal()->getHeader($args[0]);
-
- $headers[] = $args[1];
- $this->getHeader($args[0])->willReturn($headers);
- $this->hasHeader($args[0])->willReturn(true);
- return $this;
- });
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy->getHeader(Argument::type('string'))->willReturn([]);
- $responseProphecy->withHeader(Argument::type('string'), Argument::type('array'))->will(function ($args) {
- $this->getHeader($args[0])->willReturn($args[1]);
- $this->hasHeader($args[0])->willReturn(true);
- return $this;
- });
- $responseProphecy->withAddedHeader(Argument::type('string'), Argument::type('string'))->will(function ($args) {
- $headers = $this->reveal()->getHeader($args[0]);
-
- $headers[] = $args[1];
- $this->getHeader($args[0])->willReturn($headers);
- $this->hasHeader($args[0])->willReturn(true);
- return $this;
- });
- $responseProphecy->withStatus(Argument::type('int'))->will(function ($args) {
- $this->getStatusCode()->willReturn($args[0]);
- return $this;
- });
-
- $kernelProphecy = $this->prophesize(RequestHandlerInterface::class);
- $kernelProphecy->handle(Argument::type(ServerRequestInterface::class))
- ->will(function ($args) use ($responseProphecy): ResponseInterface {
- $request = $args[0];
- return $responseProphecy->reveal()
- ->withStatus(204)
- ->withHeader('X-SEQ-PRE-REQ-HANDLER', $request->getHeader('X-SEQ-PRE-REQ-HANDLER'));
- });
-
- $middleware0Prophecy = $this->prophesize(MiddlewareInterface::class);
- $middleware0Prophecy
- ->process(
- Argument::type(ServerRequestInterface::class),
- Argument::type(RequestHandlerInterface::class)
- )
- ->will(function ($args): ResponseInterface {
- return $args[1]->handle($args[0]->withAddedHeader('X-SEQ-PRE-REQ-HANDLER', '0'))
- ->withAddedHeader('X-SEQ-POST-REQ-HANDLER', '0');
- });
-
- $middleware1Prophecy = $this->prophesize(MiddlewareInterface::class);
- $middleware1Prophecy
- ->process(
- Argument::type(ServerRequestInterface::class),
- Argument::type(RequestHandlerInterface::class)
- )
- ->will(function ($args): ResponseInterface {
- return $args[1]->handle($args[0]->withAddedHeader('X-SEQ-PRE-REQ-HANDLER', '1'))
- ->withAddedHeader('X-SEQ-POST-REQ-HANDLER', '1');
- });
-
- MockSequenceMiddleware::$id = '2';
-
- $middleware3Prophecy = $this->prophesize(MiddlewareInterface::class);
- $middleware3Prophecy
- ->process(
- Argument::type(ServerRequestInterface::class),
- Argument::type(RequestHandlerInterface::class)
- )
- ->will(function ($args): ResponseInterface {
- return $args[1]->handle($args[0]->withAddedHeader('X-SEQ-PRE-REQ-HANDLER', '3'))
- ->withAddedHeader('X-SEQ-POST-REQ-HANDLER', '3');
- });
-
- /** @var RequestHandlerInterface $kernel */
- $kernel = $kernelProphecy->reveal();
- $dispatcher = $this->createMiddlewareDispatcher($kernel, null);
- $dispatcher->add($middleware0Prophecy->reveal());
- $dispatcher->addMiddleware($middleware1Prophecy->reveal());
- $dispatcher->addDeferred(MockSequenceMiddleware::class);
- $dispatcher->add($middleware3Prophecy->reveal());
-
- $response = $dispatcher->handle($requestProphecy->reveal());
-
- $this->assertSame(['3', '2', '1', '0'], $response->getHeader('X-SEQ-PRE-REQ-HANDLER'));
- $this->assertSame(['0', '1', '2', '3'], $response->getHeader('X-SEQ-POST-REQ-HANDLER'));
- $this->assertSame(204, $response->getStatusCode());
- }
-
- public function testDoesNotInstantiateDeferredMiddlewareInCaseOfAnEarlyReturningOuterMiddleware(): void
- {
- $kernelProphecy = $this->prophesize(RequestHandlerInterface::class);
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $responseProphecy = $this->prophesize(ResponseInterface::class);
-
- $middlewareProphecy = $this->prophesize(MiddlewareInterface::class);
- $middlewareProphecy->process(Argument::cetera())->willReturn($responseProphecy->reveal());
-
- MockSequenceMiddleware::$hasBeenInstantiated = false;
- /** @var RequestHandlerInterface $kernel */
- $kernel = $kernelProphecy->reveal();
- $dispatcher = $this->createMiddlewareDispatcher($kernel, null);
- $dispatcher->addDeferred(MockSequenceMiddleware::class);
- $dispatcher->addMiddleware($middlewareProphecy->reveal());
- $response = $dispatcher->handle($requestProphecy->reveal());
-
- $this->assertFalse(MockSequenceMiddleware::$hasBeenInstantiated);
- $this->assertSame($responseProphecy->reveal(), $response);
- $kernelProphecy->handle(Argument::type(ServerRequestInterface::class))->shouldNotHaveBeenCalled();
- }
-
- public function testThrowsExceptionForDeferredNonMiddlewareInterfaceClasses(): void
- {
- $this->expectException(RuntimeException::class);
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $kernelProphecy = $this->prophesize(RequestHandlerInterface::class);
-
- /** @var RequestHandlerInterface $kernel */
- $kernel = $kernelProphecy->reveal();
- $dispatcher = $this->createMiddlewareDispatcher($kernel, null);
- $dispatcher->addDeferred(stdClass::class);
- $dispatcher->handle($requestProphecy->reveal());
-
- $kernelProphecy->handle(Argument::type(ServerRequestInterface::class))->shouldNotHaveBeenCalled();
- }
-
- public function testCanBeExecutedMultipleTimes(): void
- {
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $kernelProphecy = $this->prophesize(RequestHandlerInterface::class);
- $middlewareProphecy = $this->prophesize(MiddlewareInterface::class);
- $middlewareProphecy->process(Argument::cetera())->willReturn($responseProphecy->reveal());
-
- /** @var RequestHandlerInterface $kernel */
- $kernel = $kernelProphecy->reveal();
- $dispatcher = $this->createMiddlewareDispatcher($kernel, null);
- $dispatcher->add($middlewareProphecy->reveal());
-
- $response1 = $dispatcher->handle($requestProphecy->reveal());
- $response2 = $dispatcher->handle($requestProphecy->reveal());
-
- $this->assertSame($responseProphecy->reveal(), $response1);
- $this->assertSame($responseProphecy->reveal(), $response2);
- $kernelProphecy->handle(Argument::type(ServerRequestInterface::class))->shouldNotHaveBeenCalled();
- }
-
- public function testCanBeReExecutedRecursivelyDuringDispatch(): void
- {
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $kernelProphecy = $this->prophesize(RequestHandlerInterface::class);
-
- $requestProphecy->hasHeader('X-NESTED')->willReturn(false);
- $requestProphecy->withAddedHeader('X-NESTED', '1')->will(function () {
- $this->hasHeader('X-NESTED')->willReturn(true);
- return $this;
- });
-
- $responseProphecy->getHeader(Argument::type('string'))->willReturn([]);
- $responseProphecy->withAddedHeader(Argument::type('string'), Argument::type('string'))->will(function ($args) {
- $headers = $this->reveal()->getHeader($args[0]);
-
- $headers[] = $args[1];
- $this->getHeader($args[0])->willReturn($headers);
- $this->hasHeader($args[0])->willReturn(true);
- return $this;
- });
-
- /** @var RequestHandlerInterface $kernel */
- $kernel = $kernelProphecy->reveal();
- $dispatcher = $this->createMiddlewareDispatcher($kernel, null);
-
- $middlewareProphecy = $this->prophesize(MiddlewareInterface::class);
- $middlewareProphecy
- ->process(
- Argument::type(ServerRequestInterface::class),
- Argument::type(RequestHandlerInterface::class)
- )
- ->will(function ($args) use ($dispatcher, $responseProphecy): ResponseInterface {
- $request = $args[0];
- if ($request->hasHeader('X-NESTED')) {
- return $responseProphecy->reveal()->withAddedHeader('X-TRACE', 'nested');
- }
-
- $response = $dispatcher->handle($request->withAddedHeader('X-NESTED', '1'));
-
- return $response->withAddedHeader('X-TRACE', 'outer');
- });
- $dispatcher->add($middlewareProphecy->reveal());
-
- $response = $dispatcher->handle($requestProphecy->reveal());
-
- $this->assertSame(['nested', 'outer'], $response->getHeader('X-TRACE'));
- }
-
- public function testFetchesMiddlewareFromContainer(): void
- {
- $kernelProphecy = $this->prophesize(RequestHandlerInterface::class);
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $responseProphecy = $this->prophesize(ResponseInterface::class);
-
- $middlewareProphecy = $this->prophesize(MiddlewareInterface::class);
- $middlewareProphecy->process(Argument::cetera())->willReturn($responseProphecy->reveal());
-
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $containerProphecy->has('somemiddlewarename')->willReturn(true);
- $containerProphecy->get('somemiddlewarename')->willReturn($middlewareProphecy->reveal());
- /** @var ContainerInterface $container */
- $container = $containerProphecy->reveal();
- /** @var RequestHandlerInterface $kernel */
- $kernel = $kernelProphecy->reveal();
- $dispatcher = $this->createMiddlewareDispatcher($kernel, $container);
- $dispatcher->addDeferred('somemiddlewarename');
- $response = $dispatcher->handle($requestProphecy->reveal());
-
- $this->assertSame($responseProphecy->reveal(), $response);
- $kernelProphecy->handle(Argument::type(ServerRequestInterface::class))->shouldNotHaveBeenCalled();
- }
-
- public function testMiddlewareGetsInstantiatedWithContainer(): void
- {
- $kernelProphecy = $this->prophesize(RequestHandlerInterface::class);
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
-
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $containerProphecy->has(MockMiddlewareWithConstructor::class)->willReturn(false);
- /** @var ContainerInterface $container */
- $container = $containerProphecy->reveal();
- /** @var RequestHandlerInterface $kernel */
- $kernel = $kernelProphecy->reveal();
- $dispatcher = $this->createMiddlewareDispatcher($kernel, $container);
- $dispatcher->addDeferred(MockMiddlewareWithConstructor::class);
- $dispatcher->handle($requestProphecy->reveal());
-
- $this->assertSame($containerProphecy->reveal(), MockMiddlewareWithConstructor::$container);
- }
-}
diff --git a/tests/Mocks/CallableTest.php b/tests/Mocks/CallableTest.php
deleted file mode 100644
index 19b271927..000000000
--- a/tests/Mocks/CallableTest.php
+++ /dev/null
@@ -1,33 +0,0 @@
-createResponse();
- }
-}
diff --git a/tests/Mocks/CallableTester.php b/tests/Mocks/CallableTester.php
new file mode 100644
index 000000000..f6c7ff981
--- /dev/null
+++ b/tests/Mocks/CallableTester.php
@@ -0,0 +1,19 @@
+getResponseFactory();
-
- $response = $responseFactory
- ->createResponse()
- ->withHeader('Content-Type', 'text/plain');
- $calledCount = static::$CalledCount;
- $response->getBody()->write("{$calledCount}");
-
- return $response;
- }
-}
diff --git a/tests/Mocks/MiddlewareTester.php b/tests/Mocks/MiddlewareTester.php
new file mode 100644
index 000000000..0c88b2997
--- /dev/null
+++ b/tests/Mocks/MiddlewareTester.php
@@ -0,0 +1,25 @@
+ $arguments[2]]);
- $arguments[1]->getBody()->write($contents);
-
- return $response;
- }
-}
diff --git a/tests/Mocks/MockCustomException.php b/tests/Mocks/MockCustomException.php
index 9d1f966d7..792fd2297 100644
--- a/tests/Mocks/MockCustomException.php
+++ b/tests/Mocks/MockCustomException.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
diff --git a/tests/Mocks/MockMiddlewareSlimCallable.php b/tests/Mocks/MockMiddlewareSlimCallable.php
deleted file mode 100644
index 50de90196..000000000
--- a/tests/Mocks/MockMiddlewareSlimCallable.php
+++ /dev/null
@@ -1,28 +0,0 @@
-handle($request);
- }
-}
diff --git a/tests/Mocks/MockMiddlewareWithConstructor.php b/tests/Mocks/MockMiddlewareWithConstructor.php
deleted file mode 100644
index f49ea195b..000000000
--- a/tests/Mocks/MockMiddlewareWithConstructor.php
+++ /dev/null
@@ -1,41 +0,0 @@
-prophesize(ResponseInterface::class);
- return $responseProphecy->reveal();
- }
-}
diff --git a/tests/Mocks/MockMiddlewareWithoutConstructor.php b/tests/Mocks/MockMiddlewareWithoutConstructor.php
deleted file mode 100644
index 6fb9adac8..000000000
--- a/tests/Mocks/MockMiddlewareWithoutConstructor.php
+++ /dev/null
@@ -1,38 +0,0 @@
-getAttribute('appendToOutput');
- if ($appendToOutput !== null) {
- $appendToOutput('Hello World');
- }
-
- static::$CalledCount++;
-
- return $handler->handle($request);
- }
-}
diff --git a/tests/Mocks/MockMiddlewareWithoutInterface.php b/tests/Mocks/MockMiddlewareWithoutInterface.php
index 11eaea4a2..2288b8bbc 100644
--- a/tests/Mocks/MockMiddlewareWithoutInterface.php
+++ b/tests/Mocks/MockMiddlewareWithoutInterface.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
diff --git a/tests/Mocks/MockPsr17Factory.php b/tests/Mocks/MockPsr17Factory.php
deleted file mode 100644
index 63356a381..000000000
--- a/tests/Mocks/MockPsr17Factory.php
+++ /dev/null
@@ -1,21 +0,0 @@
-getResponseFactory();
-
- $this->calledCount += 1;
- return $responseFactory->createResponse();
- }
-
- /**
- * @return int
- */
- public function getCalledCount(): int
- {
- return $this->calledCount;
- }
-}
diff --git a/tests/Mocks/MockSequenceMiddleware.php b/tests/Mocks/MockSequenceMiddleware.php
deleted file mode 100644
index 500d0b930..000000000
--- a/tests/Mocks/MockSequenceMiddleware.php
+++ /dev/null
@@ -1,42 +0,0 @@
-withAddedHeader('X-SEQ-PRE-REQ-HANDLER', static::$id);
- $response = $handler->handle($request);
-
- return $response->withAddedHeader('X-SEQ-POST-REQ-HANDLER', static::$id);
- }
-}
diff --git a/tests/Mocks/MockStream.php b/tests/Mocks/MockStream.php
index 1c81c04a1..3bba2f7e3 100644
--- a/tests/Mocks/MockStream.php
+++ b/tests/Mocks/MockStream.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
@@ -47,7 +47,7 @@ class MockStream implements StreamInterface
/** @var bool */
private $writable;
- /** @var array|mixed|null|void */
+ /** @var array|mixed|void|null */
private $uri;
/** @var int|null */
@@ -56,21 +56,47 @@ class MockStream implements StreamInterface
/** @var array Hash of readable and writable stream types */
private static $readWriteHash = [
'read' => [
- 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true,
- 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true,
- 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true,
- 'x+t' => true, 'c+t' => true, 'a+' => true,
+ 'r' => true,
+ 'w+' => true,
+ 'r+' => true,
+ 'x+' => true,
+ 'c+' => true,
+ 'rb' => true,
+ 'w+b' => true,
+ 'r+b' => true,
+ 'x+b' => true,
+ 'c+b' => true,
+ 'rt' => true,
+ 'w+t' => true,
+ 'r+t' => true,
+ 'x+t' => true,
+ 'c+t' => true,
+ 'a+' => true,
],
'write' => [
- 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true,
- 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true,
- 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true,
- 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true,
+ 'w' => true,
+ 'w+' => true,
+ 'rw' => true,
+ 'r+' => true,
+ 'x+' => true,
+ 'c+' => true,
+ 'wb' => true,
+ 'w+b' => true,
+ 'r+b' => true,
+ 'x+b' => true,
+ 'c+b' => true,
+ 'w+t' => true,
+ 'r+t' => true,
+ 'x+t' => true,
+ 'c+t' => true,
+ 'a' => true,
+ 'a+' => true,
],
];
/**
* MockStream constructor.
+ *
* @param string|resource $body
*/
public function __construct($body = '')
diff --git a/tests/Mocks/RequestHandlerInvocationStrategyTester.php b/tests/Mocks/RequestHandlerInvocationStrategyTester.php
new file mode 100644
index 000000000..60b5538bb
--- /dev/null
+++ b/tests/Mocks/RequestHandlerInvocationStrategyTester.php
@@ -0,0 +1,41 @@
+getResponseFactory();
-
- $response = $responseFactory
- ->createResponse()
- ->withHeader('Content-Type', 'text/plain');
- $calledCount = static::$CalledCount;
- $response->getBody()->write("{$calledCount}");
-
- return $response;
- }
-
- public function custom(ServerRequestInterface $request): ResponseInterface
- {
- $psr7ObjectProvider = new PSR7ObjectProvider();
- $responseFactory = $psr7ObjectProvider->getResponseFactory();
-
- return $responseFactory->createResponse();
- }
-}
diff --git a/tests/Mocks/RequestHandlerTester.php b/tests/Mocks/RequestHandlerTester.php
new file mode 100644
index 000000000..b4f169d86
--- /dev/null
+++ b/tests/Mocks/RequestHandlerTester.php
@@ -0,0 +1,42 @@
+responseFactory = $responseFactory;
+ }
+
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $response = $this->responseFactory
+ ->createResponse()
+ ->withHeader('Content-Type', 'text/plain');
+
+ $response->getBody()->write('CALLED');
+
+ return $response;
+ }
+
+ public function custom(ServerRequestInterface $request): ResponseInterface
+ {
+ return $this->responseFactory->createResponse();
+ }
+}
diff --git a/tests/Mocks/SlowPokeStream.php b/tests/Mocks/SlowPokeStream.php
index 2e6c0776f..ed57b6489 100644
--- a/tests/Mocks/SlowPokeStream.php
+++ b/tests/Mocks/SlowPokeStream.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
@@ -40,6 +40,7 @@ public function __toString(): string
while (!$this->eof()) {
$content .= $this->read(self::CHUNK_SIZE);
}
+
return $content;
}
@@ -92,6 +93,7 @@ public function read($length): string
usleep(1);
$size = min($this->amountToRead, self::CHUNK_SIZE, $length);
$this->amountToRead -= $size;
+
return str_repeat('.', $size);
}
diff --git a/tests/Mocks/SmallChunksStream.php b/tests/Mocks/SmallChunksStream.php
index c4b0169db..7b36fac31 100644
--- a/tests/Mocks/SmallChunksStream.php
+++ b/tests/Mocks/SmallChunksStream.php
@@ -3,7 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
diff --git a/tests/Providers/PSR7ObjectProvider.php b/tests/Providers/PSR7ObjectProvider.php
deleted file mode 100644
index 3d0e73bf1..000000000
--- a/tests/Providers/PSR7ObjectProvider.php
+++ /dev/null
@@ -1,112 +0,0 @@
- 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
- 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.3',
- 'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.8',
- 'HTTP_HOST' => 'localhost',
- 'HTTP_USER_AGENT' => 'Slim Framework',
- 'QUERY_STRING' => '',
- 'REMOTE_ADDR' => '127.0.0.1',
- 'REQUEST_METHOD' => $method,
- 'REQUEST_TIME' => time(),
- 'REQUEST_TIME_FLOAT' => microtime(true),
- 'REQUEST_URI' => '',
- 'SCRIPT_NAME' => '/index.php',
- 'SERVER_NAME' => 'localhost',
- 'SERVER_PORT' => 80,
- 'SERVER_PROTOCOL' => 'HTTP/1.1',
- ], $data);
-
- return $this
- ->getServerRequestFactory()
- ->createServerRequest($method, $uri, $headers);
- }
-
- /**
- * @return ServerRequestFactoryInterface
- */
- public function getServerRequestFactory(): ServerRequestFactoryInterface
- {
- return new Psr17Factory();
- }
-
- /**
- * @param int $statusCode
- * @param string $reasonPhrase
- * @return ResponseInterface
- */
- public function createResponse(int $statusCode = 200, string $reasonPhrase = ''): ResponseInterface
- {
- return $this
- ->getResponseFactory()
- ->createResponse($statusCode, $reasonPhrase);
- }
-
- /**
- * @return ResponseFactoryInterface
- */
- public function getResponseFactory(): ResponseFactoryInterface
- {
- return new Psr17Factory();
- }
-
- /**
- * @param string $contents
- * @return StreamInterface
- */
- public function createStream(string $contents = ''): StreamInterface
- {
- return $this
- ->getStreamFactory()
- ->createStream($contents);
- }
-
- /**
- * @return StreamFactoryInterface
- */
- public function getStreamFactory(): StreamFactoryInterface
- {
- return new Psr17Factory();
- }
-}
diff --git a/tests/Providers/PSR7ObjectProviderInterface.php b/tests/Providers/PSR7ObjectProviderInterface.php
deleted file mode 100644
index 94d969a90..000000000
--- a/tests/Providers/PSR7ObjectProviderInterface.php
+++ /dev/null
@@ -1,62 +0,0 @@
-build();
+
+ return $app->getContainer()->get(StreamFactoryInterface::class);
+ }
+
+ public function testJsonRendersCorrectly(): void
+ {
+ $app = (new AppBuilder())->build();
+ $renderer = $app->getContainer()->get(JsonRenderer::class);
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ // Mock JSON data
+ $jsonData = ['key' => 'value'];
+ $jsonOptions = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR;
+ $jsonString = json_encode($jsonData, $jsonOptions);
+
+ $response = $renderer->json($response, $jsonData);
+
+ $this->assertEquals('application/json', $response->getHeaderLine('Content-Type'));
+ $this->assertEquals($jsonString, (string)$response->getBody());
+ }
+
+ public function testSetContentType(): void
+ {
+ $app = (new AppBuilder())->build();
+ $renderer = $app->getContainer()->get(JsonRenderer::class);
+
+ $renderer = $renderer->withContentType('application/vnd.api+json');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+ $response = $renderer->json($response, ['key' => 'value']);
+
+ $this->assertEquals('application/vnd.api+json', $response->getHeaderLine('Content-Type'));
+ }
+
+ public function testSetJsonOptions(): void
+ {
+ $app = (new AppBuilder())->build();
+ $renderer = $app->getContainer()->get(JsonRenderer::class);
+
+ $renderer = $renderer->withJsonOptions(JSON_UNESCAPED_UNICODE);
+
+ // Mock JSON data
+ $jsonData = ['key' => 'value'];
+ $jsonString = json_encode($jsonData, JSON_UNESCAPED_UNICODE);
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $response = $renderer->json($response, $jsonData);
+
+ $this->assertEquals('application/json', $response->getHeaderLine('Content-Type'));
+ $this->assertEquals($jsonString, (string)$response->getBody());
+ }
+}
diff --git a/tests/RequestHandler/MiddlewareRequestHandlerTest.php b/tests/RequestHandler/MiddlewareRequestHandlerTest.php
new file mode 100644
index 000000000..0d19721fe
--- /dev/null
+++ b/tests/RequestHandler/MiddlewareRequestHandlerTest.php
@@ -0,0 +1,203 @@
+build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $middleware = [
+ function ($req, $handler) {
+ $response = $handler->handle($req);
+
+ return $response->withHeader('X-Middleware-1', 'Processed-1');
+ },
+ function ($req, $handler) {
+ $response = $handler->handle($req);
+
+ return $response->withHeader('X-Middleware-2', 'Processed-2');
+ },
+ ResponseFactoryMiddleware::class,
+ ];
+
+ $request = $request->withAttribute(MiddlewareRequestHandler::MIDDLEWARE, $middleware);
+
+ $handler = $app->getContainer()
+ ->get(MiddlewareRequestHandler::class);
+
+ $response = $handler->handle($request);
+
+ $this->assertSame('Processed-1', $response->getHeaderLine('X-Middleware-1'));
+ $this->assertSame('Processed-2', $response->getHeaderLine('X-Middleware-2'));
+ }
+
+ public function testHandleWithoutMiddlewareStack()
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('No middleware found. Add a response factory middleware.');
+
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $request = $request->withAttribute(MiddlewareRequestHandler::MIDDLEWARE, []);
+
+ $handler = $app->getContainer()
+ ->get(MiddlewareRequestHandler::class);
+
+ $response = $handler->handle($request);
+
+ $this->assertSame('Final', $response->getHeaderLine('X-Result'));
+ }
+
+ public function testHandleWithClassMiddlewareStack()
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $middleware = [];
+ $middleware[] = new class implements MiddlewareInterface {
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler,
+ ): ResponseInterface {
+ $response = $handler->handle($request);
+
+ return $response->withHeader('X-Middleware-1', 'Processed-1');
+ }
+ };
+
+ $middleware[] = ResponseFactoryMiddleware::class;
+
+ $request = $request->withAttribute(MiddlewareRequestHandler::MIDDLEWARE, $middleware);
+
+ $handler = $app->getContainer()
+ ->get(MiddlewareRequestHandler::class);
+
+ $response = $handler->handle($request);
+
+ $this->assertSame('Processed-1', $response->getHeaderLine('X-Middleware-1'));
+ }
+
+ public function testHandleWithNoMiddlewareAttribute()
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('No middleware found. Add a response factory middleware.');
+
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $request = $request->withoutAttribute(MiddlewareRequestHandler::MIDDLEWARE);
+
+ $handler = $app->getContainer()
+ ->get(MiddlewareRequestHandler::class);
+
+ $response = $handler->handle($request);
+
+ $this->assertSame('Processed-1', $response->getHeaderLine('X-Middleware-1'));
+ }
+
+ public function testHandleWithInvalidMiddleware()
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage(
+ 'A middleware must be an object or callable that implements "MiddlewareInterface".'
+ );
+
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $middleware = [];
+
+ // invalid middleware
+ $middleware[] = [];
+
+ $middleware[] = ResponseFactoryMiddleware::class;
+
+ $request = $request->withAttribute(MiddlewareRequestHandler::MIDDLEWARE, $middleware);
+
+ $handler = $app->getContainer()
+ ->get(MiddlewareRequestHandler::class);
+
+ $handler->handle($request);
+ }
+
+ public function testHandleWithFifoMiddlewareStack()
+ {
+ $builder = new AppBuilder();
+ // $builder->setMiddlewareOrder(MiddlewareOrder::FIFO);
+ $app = $builder->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $middleware = [];
+
+ $middleware[] = new class implements MiddlewareInterface {
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler,
+ ): ResponseInterface {
+ $response = $handler->handle($request);
+ $response->getBody()->write('2');
+
+ return $response;
+ }
+ };
+
+ $middleware[] = new class implements MiddlewareInterface {
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler,
+ ): ResponseInterface {
+ $response = $handler->handle($request);
+ $response->getBody()->write('1');
+
+ return $response;
+ }
+ };
+
+ $middleware[] = ResponseFactoryMiddleware::class;
+
+ $request = $request->withAttribute(MiddlewareRequestHandler::MIDDLEWARE, $middleware);
+
+ $handler = $app->getContainer()
+ ->get(MiddlewareRequestHandler::class);
+
+ $response = $handler->handle($request);
+
+ $this->assertSame('12', (string)$response->getBody());
+ }
+}
diff --git a/tests/RequestHandler/RunnerTest.php b/tests/RequestHandler/RunnerTest.php
new file mode 100644
index 000000000..c51150070
--- /dev/null
+++ b/tests/RequestHandler/RunnerTest.php
@@ -0,0 +1,160 @@
+build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/')
+ ->withHeader('X-Test', 'Modified');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $middleware = new class implements MiddlewareInterface {
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler
+ ): ResponseInterface {
+ $response = $handler->handle($request);
+
+ return $response->withHeader('X-Middleware', 'Processed');
+ }
+ };
+
+ $runner = new Runner(
+ [
+ $middleware,
+ function () use ($response) {
+ return $response->withHeader('X-Result', 'Success');
+ },
+ ]
+ );
+
+ $result = $runner->handle($request);
+
+ $this->assertSame('Processed', $result->getHeaderLine('X-Middleware'));
+ $this->assertSame('Success', $result->getHeaderLine('X-Result'));
+ }
+
+ public function testHandleWithRequestHandlerInterface()
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $handler = new class ($response) implements RequestHandlerInterface {
+ private ResponseInterface $response;
+
+ public function __construct(ResponseInterface $response)
+ {
+ $this->response = $response;
+ }
+
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ return $this->response->withHeader('X-Handler', 'Handled');
+ }
+ };
+
+ $runner = new Runner([$handler]);
+
+ $result = $runner->handle($request);
+
+ $this->assertSame('Handled', $result->getHeaderLine('X-Handler'));
+ }
+
+ public function testHandleWithCallableMiddleware()
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $runner = new Runner([
+ function (ServerRequestInterface $req, RequestHandlerInterface $handler) use ($response) {
+ return $response->withHeader('X-Callable', 'Called');
+ },
+ ]);
+
+ $result = $runner->handle($request);
+
+ $this->assertSame('Called', $result->getHeaderLine('X-Callable'));
+ }
+
+ public function testHandleWithEmptyQueueThrowsException()
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('No middleware found. Add a response factory middleware.');
+
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $runner = new Runner([]);
+ $runner->handle($request);
+ }
+
+ public function testHandleWithInvalidObjectMiddlewareThrowsException()
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Invalid middleware queue entry "object"');
+
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $runner = new Runner([new stdClass()]);
+ $runner->handle($request);
+ }
+
+ public function testHandleWithInvalidMiddlewareStringThrowsException()
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Invalid middleware queue entry "foo"');
+
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $runner = new Runner(['foo']);
+ $runner->handle($request);
+ }
+}
diff --git a/tests/Routing/DispatcherTest.php b/tests/Routing/DispatcherTest.php
deleted file mode 100644
index cefd15192..000000000
--- a/tests/Routing/DispatcherTest.php
+++ /dev/null
@@ -1,142 +0,0 @@
-prophesize(CallableResolverInterface::class);
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $dispatcher = new Dispatcher($routeCollector);
-
- $method = new ReflectionMethod(Dispatcher::class, 'createDispatcher');
- $method->setAccessible(true);
-
- $this->assertInstanceOf(FastRouteDispatcher::class, $method->invoke($dispatcher));
- }
-
- /**
- * Test cached routes file is created & that it holds our routes.
- */
- public function testRouteCacheFileCanBeDispatched()
- {
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $dispatcher = new Dispatcher($routeCollector);
-
- $route = $routeCollector->map(['GET'], '/', function () {
- });
- $route->setName('foo');
-
- $cacheFile = __DIR__ . '/' . uniqid((string) microtime(true));
- $routeCollector->setCacheFile($cacheFile);
-
- $method = new ReflectionMethod(Dispatcher::class, 'createDispatcher');
- $method->setAccessible(true);
- $method->invoke($dispatcher);
- $this->assertFileExists($cacheFile, 'cache file was not created');
-
- $routeCollector2 = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $routeCollector2->setCacheFile($cacheFile);
- $dispatcher2 = new Dispatcher($routeCollector2);
-
- $method = new ReflectionMethod(Dispatcher::class, 'createDispatcher');
- $method->setAccessible(true);
- $method->invoke($dispatcher2);
-
- /** @var RoutingResults $result */
- $result = $dispatcher2->dispatch('GET', '/');
- $this->assertSame(FastRouteDispatcher::FOUND, $result->getRouteStatus());
-
- unlink($cacheFile);
- }
-
- /**
- * Calling createDispatcher as second time should give you back the same
- * dispatcher as when you called it the first time.
- */
- public function testCreateDispatcherReturnsSameDispatcherASecondTime()
- {
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $dispatcher = new Dispatcher($routeCollector);
-
- $method = new ReflectionMethod(Dispatcher::class, 'createDispatcher');
- $method->setAccessible(true);
-
- $fastRouteDispatcher = $method->invoke($dispatcher);
- $fastRouteDispatcher2 = $method->invoke($dispatcher);
-
- $this->assertSame($fastRouteDispatcher, $fastRouteDispatcher2);
- }
-
- public function testGetAllowedMethods()
- {
- $methods = ['GET', 'POST', 'PUT'];
- $uri = '/';
-
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
-
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $routeCollector->map($methods, $uri, function () {
- });
-
- $dispatcher = new Dispatcher($routeCollector);
- $results = $dispatcher->getAllowedMethods('/');
-
- $this->assertSame($methods, $results);
- }
-
- public function testDispatch()
- {
- $methods = ['GET', 'POST'];
- $uri = '/hello/{name}';
-
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
-
- $callable = function () {
- };
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $route = $routeCollector->map($methods, $uri, $callable);
-
- $dispatcher = new Dispatcher($routeCollector);
- $results = $dispatcher->dispatch('GET', '/hello/Foo%20Bar');
-
- $this->assertSame(RoutingResults::FOUND, $results->getRouteStatus());
- $this->assertSame('GET', $results->getMethod());
- $this->assertSame('/hello/Foo%20Bar', $results->getUri());
- $this->assertSame($route->getIdentifier(), $results->getRouteIdentifier());
- $this->assertSame(['name' => 'Foo Bar'], $results->getRouteArguments());
- $this->assertSame(['name' => 'Foo%20Bar'], $results->getRouteArguments(false));
- $this->assertSame($methods, $results->getAllowedMethods());
- $this->assertSame($dispatcher, $results->getDispatcher());
- }
-}
diff --git a/tests/Routing/FastRouteDispatcherTest.php b/tests/Routing/FastRouteDispatcherTest.php
deleted file mode 100644
index 0f49c42d9..000000000
--- a/tests/Routing/FastRouteDispatcherTest.php
+++ /dev/null
@@ -1,622 +0,0 @@
-generateDispatcherOptions());
-
- $results = $dispatcher->dispatch($method, $uri);
-
- $this->assertSame($dispatcher::FOUND, $results[0]);
- $this->assertSame($handler, $results[1]);
- $this->assertSame($argDict, $results[2]);
- }
-
- /**
- * Set appropriate options for the specific Dispatcher class we're testing
- */
- private function generateDispatcherOptions()
- {
- return [
- 'dataGenerator' => $this->getDataGeneratorClass(),
- 'dispatcher' => $this->getDispatcherClass()
- ];
- }
-
- protected function getDataGeneratorClass()
- {
- return GroupCountBased::class;
- }
-
- protected function getDispatcherClass()
- {
- return FastRouteDispatcher::class;
- }
-
- /**
- * @dataProvider provideNotFoundDispatchCases
- */
- public function testNotFoundDispatches($method, $uri, $callback)
- {
- /** @var FastRouteDispatcher $dispatcher */
- $dispatcher = simpleDispatcher($callback, $this->generateDispatcherOptions());
-
- $results = $dispatcher->dispatch($method, $uri);
-
- $this->assertSame($dispatcher::NOT_FOUND, $results[0]);
- }
-
- /**
- * @dataProvider provideMethodNotAllowedDispatchCases
- * @param $method
- * @param $uri
- * @param $callback
- */
- public function testMethodNotAllowedDispatches($method, $uri, $callback)
- {
- /** @var FastRouteDispatcher $dispatcher */
- $dispatcher = simpleDispatcher($callback, $this->generateDispatcherOptions());
-
- $results = $dispatcher->dispatch($method, $uri);
-
- $this->assertSame($dispatcher::METHOD_NOT_ALLOWED, $results[0]);
- }
-
- /**
- * @dataProvider provideMethodNotAllowedDispatchCases
- * @param $method
- * @param $uri
- * @param $callback
- * @param $allowedMethods
- */
- public function testGetAllowedMethods($method, $uri, $callback, $allowedMethods)
- {
- /** @var FastRouteDispatcher $dispatcher */
- $dispatcher = simpleDispatcher($callback, $this->generateDispatcherOptions());
-
- $results = $dispatcher->getAllowedMethods($uri);
-
- $this->assertSame($results, $allowedMethods);
- }
-
- public function testDuplicateVariableNameError()
- {
- $this->expectException(BadRouteException::class);
- $this->expectExceptionMessage('Cannot use the same placeholder "test" twice');
-
- simpleDispatcher(function (RouteCollector $r) {
- $r->addRoute('GET', '/foo/{test}/{test:\d+}', 'handler0');
- }, $this->generateDispatcherOptions());
- }
-
- public function testDuplicateVariableRoute()
- {
- $this->expectException(BadRouteException::class);
- $this->expectExceptionMessage('Cannot register two routes matching "/user/([^/]+)" for method "GET"');
-
- simpleDispatcher(function (RouteCollector $r) {
- $r->addRoute('GET', '/user/{id}', 'handler0'); // oops, forgot \d+ restriction ;)
- $r->addRoute('GET', '/user/{name}', 'handler1');
- }, $this->generateDispatcherOptions());
- }
-
- public function testDuplicateStaticRoute()
- {
- $this->expectException(BadRouteException::class);
- $this->expectExceptionMessage('Cannot register two routes matching "/user" for method "GET"');
-
- simpleDispatcher(function (RouteCollector $r) {
- $r->addRoute('GET', '/user', 'handler0');
- $r->addRoute('GET', '/user', 'handler1');
- }, $this->generateDispatcherOptions());
- }
-
- /**
- * @codingStandardsIgnoreStart
- * @codingStandardsIgnoreEnd
- */
- public function testShadowedStaticRoute()
- {
- $this->expectException(BadRouteException::class);
- $this->expectExceptionMessage('Static route "/user/nikic" is shadowed by previously defined variable route' .
- ' "/user/([^/]+)" for method "GET"');
-
- simpleDispatcher(function (RouteCollector $r) {
- $r->addRoute('GET', '/user/{name}', 'handler0');
- $r->addRoute('GET', '/user/nikic', 'handler1');
- }, $this->generateDispatcherOptions());
- }
-
- public function testCapturing()
- {
- $this->expectException(BadRouteException::class);
- $this->expectExceptionMessage('Regex "(en|de)" for parameter "lang" contains a capturing group');
-
- simpleDispatcher(function (RouteCollector $r) {
- $r->addRoute('GET', '/{lang:(en|de)}', 'handler0');
- }, $this->generateDispatcherOptions());
- }
-
- public function provideFoundDispatchCases()
- {
- $cases = [];
-
- // 0 -------------------------------------------------------------------------------------->
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/resource/123/456', 'handler0');
- };
-
- $method = 'GET';
- $uri = '/resource/123/456';
- $handler = 'handler0';
- $argDict = [];
-
- $cases[] = [$method, $uri, $callback, $handler, $argDict];
-
- // 1 -------------------------------------------------------------------------------------->
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/handler0', 'handler0');
- $r->addRoute('GET', '/handler1', 'handler1');
- $r->addRoute('GET', '/handler2', 'handler2');
- };
-
- $method = 'GET';
- $uri = '/handler2';
- $handler = 'handler2';
- $argDict = [];
-
- $cases[] = [$method, $uri, $callback, $handler, $argDict];
-
- // 2 -------------------------------------------------------------------------------------->
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
- $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1');
- $r->addRoute('GET', '/user/{name}', 'handler2');
- };
-
- $method = 'GET';
- $uri = '/user/rdlowrey';
- $handler = 'handler2';
- $argDict = ['name' => 'rdlowrey'];
-
- $cases[] = [$method, $uri, $callback, $handler, $argDict];
-
- // 3 -------------------------------------------------------------------------------------->
-
- // reuse $callback from #2
-
- $method = 'GET';
- $uri = '/user/12345';
- $handler = 'handler1';
- $argDict = ['id' => '12345'];
-
- $cases[] = [$method, $uri, $callback, $handler, $argDict];
-
- // 4 -------------------------------------------------------------------------------------->
-
- // reuse $callback from #3
-
- $method = 'GET';
- $uri = '/user/NaN';
- $handler = 'handler2';
- $argDict = ['name' => 'NaN'];
-
- $cases[] = [$method, $uri, $callback, $handler, $argDict];
-
- // 5 -------------------------------------------------------------------------------------->
-
- // reuse $callback from #4
-
- $method = 'GET';
- $uri = '/user/rdlowrey/12345';
- $handler = 'handler0';
- $argDict = ['name' => 'rdlowrey', 'id' => '12345'];
-
- $cases[] = [$method, $uri, $callback, $handler, $argDict];
-
- // 6 -------------------------------------------------------------------------------------->
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler0');
- $r->addRoute('GET', '/user/12345/extension', 'handler1');
- $r->addRoute('GET', '/user/{id:[0-9]+}.{extension}', 'handler2');
- };
-
- $method = 'GET';
- $uri = '/user/12345.svg';
- $handler = 'handler2';
- $argDict = ['id' => '12345', 'extension' => 'svg'];
-
- $cases[] = [$method, $uri, $callback, $handler, $argDict];
-
- // 7 ----- Test GET method fallback on HEAD route miss ------------------------------------>
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/user/{name}', 'handler0');
- $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler1');
- $r->addRoute('GET', '/static0', 'handler2');
- $r->addRoute('GET', '/static1', 'handler3');
- $r->addRoute('HEAD', '/static1', 'handler4');
- };
-
- $method = 'HEAD';
- $uri = '/user/rdlowrey';
- $handler = 'handler0';
- $argDict = ['name' => 'rdlowrey'];
-
- $cases[] = [$method, $uri, $callback, $handler, $argDict];
-
- // 8 ----- Test GET method fallback on HEAD route miss ------------------------------------>
-
- // reuse $callback from #7
-
- $method = 'HEAD';
- $uri = '/user/rdlowrey/1234';
- $handler = 'handler1';
- $argDict = ['name' => 'rdlowrey', 'id' => '1234'];
-
- $cases[] = [$method, $uri, $callback, $handler, $argDict];
-
- // 9 ----- Test GET method fallback on HEAD route miss ------------------------------------>
-
- // reuse $callback from #8
-
- $method = 'HEAD';
- $uri = '/static0';
- $handler = 'handler2';
- $argDict = [];
-
- $cases[] = [$method, $uri, $callback, $handler, $argDict];
-
- // 10 ---- Test existing HEAD route used if available (no fallback) ----------------------->
-
- // reuse $callback from #9
-
- $method = 'HEAD';
- $uri = '/static1';
- $handler = 'handler4';
- $argDict = [];
-
- $cases[] = [$method, $uri, $callback, $handler, $argDict];
-
- // 11 ---- More specified routes are not shadowed by less specific of another method ------>
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/user/{name}', 'handler0');
- $r->addRoute('POST', '/user/{name:[a-z]+}', 'handler1');
- };
-
- $method = 'POST';
- $uri = '/user/rdlowrey';
- $handler = 'handler1';
- $argDict = ['name' => 'rdlowrey'];
-
- $cases[] = [$method, $uri, $callback, $handler, $argDict];
-
- // 12 ---- Handler of more specific routes is used, if it occurs first -------------------->
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/user/{name}', 'handler0');
- $r->addRoute('POST', '/user/{name:[a-z]+}', 'handler1');
- $r->addRoute('POST', '/user/{name}', 'handler2');
- };
-
- $method = 'POST';
- $uri = '/user/rdlowrey';
- $handler = 'handler1';
- $argDict = ['name' => 'rdlowrey'];
-
- $cases[] = [$method, $uri, $callback, $handler, $argDict];
-
- // 13 ---- Route with constant suffix ----------------------------------------------------->
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/user/{name}', 'handler0');
- $r->addRoute('GET', '/user/{name}/edit', 'handler1');
- };
-
- $method = 'GET';
- $uri = '/user/rdlowrey/edit';
- $handler = 'handler1';
- $argDict = ['name' => 'rdlowrey'];
-
- $cases[] = [$method, $uri, $callback, $handler, $argDict];
-
- // 14 ---- Handle multiple methods with the same handler ---------------------------------->
-
- $callback = function (RouteCollector $r) {
- $r->addRoute(['GET', 'POST'], '/user', 'handlerGetPost');
- $r->addRoute(['DELETE'], '/user', 'handlerDelete');
- $r->addRoute([], '/user', 'handlerNone');
- };
-
- $argDict = [];
- $cases[] = ['GET', '/user', $callback, 'handlerGetPost', $argDict];
- $cases[] = ['POST', '/user', $callback, 'handlerGetPost', $argDict];
- $cases[] = ['DELETE', '/user', $callback, 'handlerDelete', $argDict];
-
- // 17 ----
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('POST', '/user.json', 'handler0');
- $r->addRoute('GET', '/{entity}.json', 'handler1');
- };
-
- $cases[] = ['GET', '/user.json', $callback, 'handler1', ['entity' => 'user']];
-
- // 18 ----
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '', 'handler0');
- };
-
- $cases[] = ['GET', '', $callback, 'handler0', []];
-
- // 19 ----
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('HEAD', '/a/{foo}', 'handler0');
- $r->addRoute('GET', '/b/{foo}', 'handler1');
- };
-
- $cases[] = ['HEAD', '/b/bar', $callback, 'handler1', ['foo' => 'bar']];
-
- // 20 ----
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('HEAD', '/a', 'handler0');
- $r->addRoute('GET', '/b', 'handler1');
- };
-
- $cases[] = ['HEAD', '/b', $callback, 'handler1', []];
-
- // 21 ----
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/foo', 'handler0');
- $r->addRoute('HEAD', '/{bar}', 'handler1');
- };
-
- $cases[] = ['HEAD', '/foo', $callback, 'handler1', ['bar' => 'foo']];
-
- // 22 ----
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('*', '/user', 'handler0');
- $r->addRoute('*', '/{user}', 'handler1');
- $r->addRoute('GET', '/user', 'handler2');
- };
-
- $cases[] = ['GET', '/user', $callback, 'handler2', []];
-
- // 23 ----
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('*', '/user', 'handler0');
- $r->addRoute('GET', '/user', 'handler1');
- };
-
- $cases[] = ['POST', '/user', $callback, 'handler0', []];
-
- // 24 ----
-
- $cases[] = ['HEAD', '/user', $callback, 'handler1', []];
-
- // 25 ----
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/{bar}', 'handler0');
- $r->addRoute('*', '/foo', 'handler1');
- };
-
- $cases[] = ['GET', '/foo', $callback, 'handler0', ['bar' => 'foo']];
-
- // 26 ----
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/user', 'handler0');
- $r->addRoute('*', '/{foo:.*}', 'handler1');
- };
-
- $cases[] = ['POST', '/bar', $callback, 'handler1', ['foo' => 'bar']];
-
- // 27 --- International characters
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/новости/{name}', 'handler0');
- };
-
- $cases[] = ['GET', '/новости/rdlowrey', $callback, 'handler0', ['name' => 'rdlowrey']];
-
- // x -------------------------------------------------------------------------------------->
-
- return $cases;
- }
-
- public function provideNotFoundDispatchCases()
- {
- $cases = [];
-
- // 0 -------------------------------------------------------------------------------------->
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/resource/123/456', 'handler0');
- };
-
- $method = 'GET';
- $uri = '/not-found';
-
- $cases[] = [$method, $uri, $callback];
-
- // 1 -------------------------------------------------------------------------------------->
-
- // reuse callback from #0
- $method = 'POST';
- $uri = '/not-found';
-
- $cases[] = [$method, $uri, $callback];
-
- // 2 -------------------------------------------------------------------------------------->
-
- // reuse callback from #1
- $method = 'PUT';
- $uri = '/not-found';
-
- $cases[] = [$method, $uri, $callback];
-
- // 3 -------------------------------------------------------------------------------------->
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/handler0', 'handler0');
- $r->addRoute('GET', '/handler1', 'handler1');
- $r->addRoute('GET', '/handler2', 'handler2');
- };
-
- $method = 'GET';
- $uri = '/not-found';
-
- $cases[] = [$method, $uri, $callback];
-
- // 4 -------------------------------------------------------------------------------------->
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
- $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1');
- $r->addRoute('GET', '/user/{name}', 'handler2');
- };
-
- $method = 'GET';
- $uri = '/not-found';
-
- $cases[] = [$method, $uri, $callback];
-
- // 5 -------------------------------------------------------------------------------------->
-
- // reuse callback from #4
- $method = 'GET';
- $uri = '/user/rdlowrey/12345/not-found';
-
- $cases[] = [$method, $uri, $callback];
-
- // 6 -------------------------------------------------------------------------------------->
-
- // reuse callback from #5
- $method = 'HEAD';
-
- $cases[] = [$method, $uri, $callback];
-
- // x -------------------------------------------------------------------------------------->
-
- return $cases;
- }
-
- public function provideMethodNotAllowedDispatchCases()
- {
- $cases = [];
-
- // 0 -------------------------------------------------------------------------------------->
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/resource/123/456', 'handler0');
- };
-
- $method = 'POST';
- $uri = '/resource/123/456';
- $allowedMethods = ['GET'];
-
- $cases[] = [$method, $uri, $callback, $allowedMethods];
-
- // 1 -------------------------------------------------------------------------------------->
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/resource/123/456', 'handler0');
- $r->addRoute('POST', '/resource/123/456', 'handler1');
- $r->addRoute('PUT', '/resource/123/456', 'handler2');
- $r->addRoute('*', '/', 'handler3');
- };
-
- $method = 'DELETE';
- $uri = '/resource/123/456';
- $allowedMethods = ['GET', 'POST', 'PUT'];
-
- $cases[] = [$method, $uri, $callback, $allowedMethods];
-
- // 2 -------------------------------------------------------------------------------------->
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
- $r->addRoute('POST', '/user/{name}/{id:[0-9]+}', 'handler1');
- $r->addRoute('PUT', '/user/{name}/{id:[0-9]+}', 'handler2');
- $r->addRoute('PATCH', '/user/{name}/{id:[0-9]+}', 'handler3');
- };
-
- $method = 'DELETE';
- $uri = '/user/rdlowrey/42';
- $allowedMethods = ['GET', 'POST', 'PUT', 'PATCH'];
-
- $cases[] = [$method, $uri, $callback, $allowedMethods];
-
- // 3 -------------------------------------------------------------------------------------->
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('POST', '/user/{name}', 'handler1');
- $r->addRoute('PUT', '/user/{name:[a-z]+}', 'handler2');
- $r->addRoute('PATCH', '/user/{name:[a-z]+}', 'handler3');
- };
-
- $method = 'GET';
- $uri = '/user/rdlowrey';
- $allowedMethods = ['POST', 'PUT', 'PATCH'];
-
- $cases[] = [$method, $uri, $callback, $allowedMethods];
-
- // 4 -------------------------------------------------------------------------------------->
-
- $callback = function (RouteCollector $r) {
- $r->addRoute(['GET', 'POST'], '/user', 'handlerGetPost');
- $r->addRoute(['DELETE'], '/user', 'handlerDelete');
- $r->addRoute([], '/user', 'handlerNone');
- };
-
- $cases[] = ['PUT', '/user', $callback, ['GET', 'POST', 'DELETE']];
-
- // 5
-
- $callback = function (RouteCollector $r) {
- $r->addRoute('POST', '/user.json', 'handler0');
- $r->addRoute('GET', '/{entity}.json', 'handler1');
- };
-
- $cases[] = ['PUT', '/user.json', $callback, ['POST', 'GET']];
-
- // x -------------------------------------------------------------------------------------->
-
- return $cases;
- }
-}
diff --git a/tests/Routing/RouteCollectorProxyTest.php b/tests/Routing/RouteCollectorProxyTest.php
deleted file mode 100644
index 2b2843fa3..000000000
--- a/tests/Routing/RouteCollectorProxyTest.php
+++ /dev/null
@@ -1,458 +0,0 @@
-prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $routeCollectorProxy = new RouteCollectorProxy(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal()
- );
-
- $this->assertSame(
- $responseFactoryProphecy->reveal(),
- $routeCollectorProxy->getResponseFactory()
- );
- }
-
- public function testGetCallableResolver()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $routeCollectorProxy = new RouteCollectorProxy(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal()
- );
-
- $this->assertSame(
- $callableResolverProphecy->reveal(),
- $routeCollectorProxy->getCallableResolver()
- );
- }
-
- public function testGetContainerReturnsInjectedInstance()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $containerProphecy = $this->prophesize(ContainerInterface::class);
-
- $routeCollectorProxy = new RouteCollectorProxy(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- $containerProphecy->reveal()
- );
-
- $this->assertSame(
- $containerProphecy->reveal(),
- $routeCollectorProxy->getContainer()
- );
- }
-
- public function testGetRouteCollectorReturnsInjectedInstance()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class);
-
- $routeCollectorProxy = new RouteCollectorProxy(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- $containerProphecy->reveal(),
- $routeCollectorProphecy->reveal()
- );
-
- $this->assertSame(
- $routeCollectorProphecy->reveal(),
- $routeCollectorProxy->getRouteCollector()
- );
- }
-
- public function testGetSetBasePath()
- {
- $basePath = '/base/path';
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $containerProphecy = $this->prophesize(ContainerInterface::class);
-
- $routeCollectorProxy = new RouteCollectorProxy(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- $containerProphecy->reveal()
- );
-
- $routeCollectorProxy->setBasePath($basePath);
-
- $this->assertSame($basePath, $routeCollectorProxy->getBasePath());
-
- $newBasePath = '/new/base/path';
- $routeCollectorProxy->setBasePath('/new/base/path');
-
- $this->assertSame($newBasePath, $routeCollectorProxy->getBasePath());
- }
-
- public function testGet()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $pattern = '/';
- $callable = function () {
- };
-
- $routeProphecy = $this->prophesize(RouteInterface::class);
- $routeProphecy
- ->getPattern()
- ->willReturn($pattern)
- ->shouldBeCalledOnce();
-
- $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class);
- $routeCollectorProphecy
- ->map(['GET'], $pattern, Argument::is($callable))
- ->willReturn($routeProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $routeCollectorProxy = new RouteCollectorProxy(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- null,
- $routeCollectorProphecy->reveal()
- );
-
- $route = $routeCollectorProxy->get($pattern, $callable);
-
- $this->assertSame($pattern, $route->getPattern());
- }
-
- public function testPost()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $pattern = '/';
- $callable = function () {
- };
-
- $routeProphecy = $this->prophesize(RouteInterface::class);
- $routeProphecy
- ->getPattern()
- ->willReturn($pattern)
- ->shouldBeCalledOnce();
-
- $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class);
- $routeCollectorProphecy
- ->map(['POST'], $pattern, Argument::is($callable))
- ->willReturn($routeProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $routeCollectorProxy = new RouteCollectorProxy(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- null,
- $routeCollectorProphecy->reveal()
- );
-
- $route = $routeCollectorProxy->post($pattern, $callable);
-
- $this->assertSame($pattern, $route->getPattern());
- }
-
- public function testPut()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $pattern = '/';
- $callable = function () {
- };
-
- $routeProphecy = $this->prophesize(RouteInterface::class);
- $routeProphecy
- ->getPattern()
- ->willReturn($pattern)
- ->shouldBeCalledOnce();
-
- $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class);
- $routeCollectorProphecy
- ->map(['PUT'], $pattern, Argument::is($callable))
- ->willReturn($routeProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $routeCollectorProxy = new RouteCollectorProxy(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- null,
- $routeCollectorProphecy->reveal()
- );
-
- $route = $routeCollectorProxy->put($pattern, $callable);
-
- $this->assertSame($pattern, $route->getPattern());
- }
-
- public function testPatch()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $pattern = '/';
- $callable = function () {
- };
-
- $routeProphecy = $this->prophesize(RouteInterface::class);
- $routeProphecy
- ->getPattern()
- ->willReturn($pattern)
- ->shouldBeCalledOnce();
-
- $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class);
- $routeCollectorProphecy
- ->map(['PATCH'], $pattern, Argument::is($callable))
- ->willReturn($routeProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $routeCollectorProxy = new RouteCollectorProxy(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- null,
- $routeCollectorProphecy->reveal()
- );
-
- $route = $routeCollectorProxy->patch($pattern, $callable);
-
- $this->assertSame($pattern, $route->getPattern());
- }
-
- public function testDelete()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $pattern = '/';
- $callable = function () {
- };
-
- $routeProphecy = $this->prophesize(RouteInterface::class);
- $routeProphecy
- ->getPattern()
- ->willReturn($pattern)
- ->shouldBeCalledOnce();
-
- $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class);
- $routeCollectorProphecy
- ->map(['DELETE'], $pattern, Argument::is($callable))
- ->willReturn($routeProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $routeCollectorProxy = new RouteCollectorProxy(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- null,
- $routeCollectorProphecy->reveal()
- );
-
- $route = $routeCollectorProxy->delete($pattern, $callable);
-
- $this->assertSame($pattern, $route->getPattern());
- }
-
- public function testOptions()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $pattern = '/';
- $callable = function () {
- };
-
- $routeProphecy = $this->prophesize(RouteInterface::class);
- $routeProphecy
- ->getPattern()
- ->willReturn($pattern)
- ->shouldBeCalledOnce();
-
- $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class);
- $routeCollectorProphecy
- ->map(['OPTIONS'], $pattern, Argument::is($callable))
- ->willReturn($routeProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $routeCollectorProxy = new RouteCollectorProxy(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- null,
- $routeCollectorProphecy->reveal()
- );
-
- $route = $routeCollectorProxy->options($pattern, $callable);
-
- $this->assertSame($pattern, $route->getPattern());
- }
-
- public function testAny()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $pattern = '/';
- $callable = function () {
- };
-
- $routeProphecy = $this->prophesize(RouteInterface::class);
- $routeProphecy
- ->getPattern()
- ->willReturn($pattern)
- ->shouldBeCalledOnce();
-
- $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class);
- $routeCollectorProphecy
- ->map(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], $pattern, Argument::is($callable))
- ->willReturn($routeProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $routeCollectorProxy = new RouteCollectorProxy(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- null,
- $routeCollectorProphecy->reveal()
- );
-
- $route = $routeCollectorProxy->any($pattern, $callable);
-
- $this->assertSame($pattern, $route->getPattern());
- }
-
- public function testMap()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $pattern = '/';
- $methods = ['GET', 'POST'];
- $callable = function () {
- };
-
- $routeProphecy = $this->prophesize(RouteInterface::class);
- $routeProphecy
- ->getPattern()
- ->willReturn($pattern)
- ->shouldBeCalledOnce();
-
- $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class);
- $routeCollectorProphecy
- ->map($methods, $pattern, Argument::is($callable))
- ->willReturn($routeProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $routeCollectorProxy = new RouteCollectorProxy(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- null,
- $routeCollectorProphecy->reveal()
- );
-
- $route = $routeCollectorProxy->map($methods, $pattern, $callable);
-
- $this->assertSame($pattern, $route->getPattern());
- }
-
- public function testRedirect()
- {
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $callableResolver = new CallableResolver($containerProphecy->reveal());
-
- $from = '/from';
- $to = '/to';
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $responseProphecy
- ->withHeader('Location', $to)
- ->willReturn($responseProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy
- ->createResponse()
- ->will(function () use ($responseProphecy) {
- $this
- ->createResponse(302)
- ->willReturn($responseProphecy)
- ->shouldBeCalledOnce();
- return $responseProphecy->reveal();
- })
- ->shouldBeCalledOnce();
-
- $routeCollector = new RouteCollector(
- $responseFactoryProphecy->reveal(),
- $callableResolver
- );
-
- $routeCollectorProxy = new RouteCollectorProxy(
- $responseFactoryProphecy->reveal(),
- $callableResolver,
- $containerProphecy->reveal(),
- $routeCollector
- );
-
- $route = $routeCollectorProxy->redirect($from, $to);
- $request = $this->createServerRequest('/');
- $response = $route->run($request);
-
- $this->assertSame($responseProphecy->reveal(), $response);
- }
-
- public function testGroup()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $pattern = '/group';
- $callable = function () {
- };
-
- $routeGroupProphecy = $this->prophesize(RouteGroupInterface::class);
-
- $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class);
- $routeCollectorProphecy
- ->group($pattern, Argument::is($callable))
- ->willReturn($routeGroupProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $routeCollectorProxy = new RouteCollectorProxy(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- null,
- $routeCollectorProphecy->reveal()
- );
-
- $routeCollectorProxy->group($pattern, $callable);
- }
-}
diff --git a/tests/Routing/RouteCollectorTest.php b/tests/Routing/RouteCollectorTest.php
deleted file mode 100644
index e73cdb74c..000000000
--- a/tests/Routing/RouteCollectorTest.php
+++ /dev/null
@@ -1,206 +0,0 @@
-cacheFile && file_exists($this->cacheFile)) {
- unlink($this->cacheFile);
- }
- }
-
- public function testGetSetBasePath()
- {
- $basePath = '/app';
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $routeCollector->setBasePath($basePath);
-
- $this->assertSame($basePath, $routeCollector->getBasePath());
- }
-
- public function testMap()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $route = $routeCollector->map(['GET'], '/', function () {
- });
-
- $routes = $routeCollector->getRoutes();
- $this->assertSame($route, $routes[$route->getIdentifier()]);
- }
-
- public function testMapPrependsGroupPattern()
- {
- $self = $this;
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
-
- $callable = function (RouteCollectorProxy $proxy) use ($self) {
- $route = $proxy->get('/test', function () {
- });
-
- $self->assertSame('/prefix/test', $route->getPattern());
- };
-
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $callableResolverProphecy
- ->resolve(Argument::is($callable))
- ->willReturn($callable)
- ->shouldBeCalledOnce();
-
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $routeCollector->group('/prefix', $callable);
- }
-
- public function testGetRouteInvocationStrategy()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $invocationStrategyProphecy = $this->prophesize(InvocationStrategyInterface::class);
-
- $routeCollector = new RouteCollector(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- $containerProphecy->reveal(),
- $invocationStrategyProphecy->reveal()
- );
-
- $this->assertSame($invocationStrategyProphecy->reveal(), $routeCollector->getDefaultInvocationStrategy());
- }
-
- public function testRemoveNamedRoute()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $routeCollector->setBasePath('/base/path');
-
- $route = $routeCollector->map(['GET'], '/test', function () {
- });
- $route->setName('test');
-
- $routes = $routeCollector->getRoutes();
- $this->assertCount(1, $routes);
-
- $routeCollector->removeNamedRoute('test');
- $routes = $routeCollector->getRoutes();
- $this->assertCount(0, $routes);
- }
-
- public function testRemoveNamedRouteWithARouteThatDoesNotExist()
- {
- $this->expectException(RuntimeException::class);
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $routeCollector->removeNamedRoute('missing');
- }
-
- public function testLookupRouteThrowsExceptionIfRouteNotFound()
- {
- $this->expectException(RuntimeException::class);
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $routeCollector->lookupRoute('missing');
- }
-
- /**
- * Test cache file exists but is not writable
- */
- public function testCacheFileExistsAndIsNotReadable()
- {
- $this->cacheFile = __DIR__ . '/non-readable.cache';
- file_put_contents($this->cacheFile, '');
-
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage(sprintf('Route collector cache file `%s` is not readable', $this->cacheFile));
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $routeCollector->setCacheFile($this->cacheFile);
- }
-
- /**
- * Test cache file does not exist and directory is not writable
- */
- public function testCacheFileDoesNotExistsAndDirectoryIsNotWritable()
- {
- $cacheFile = __DIR__ . '/non-writable-directory/router.cache';
-
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage(sprintf(
- 'Route collector cache file directory `%s` is not writable',
- dirname($cacheFile)
- ));
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $routeCollector->setCacheFile($cacheFile);
- }
-
- public function testSetCacheFileViaConstructor()
- {
- $cacheFile = __DIR__ . '/router.cache';
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $routeCollector = new RouteCollector(
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- null,
- null,
- null,
- $cacheFile
- );
- $this->assertSame($cacheFile, $routeCollector->getCacheFile());
- }
-}
diff --git a/tests/Routing/RouteContextTest.php b/tests/Routing/RouteContextTest.php
index 871843792..1d560da99 100644
--- a/tests/Routing/RouteContextTest.php
+++ b/tests/Routing/RouteContextTest.php
@@ -3,104 +3,332 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Tests\Routing;
-use Psr\Http\Message\ServerRequestInterface;
+use PHPUnit\Framework\TestCase;
+use Psr\Http\Message\ServerRequestFactoryInterface;
use RuntimeException;
-use Slim\Interfaces\RouteInterface;
-use Slim\Interfaces\RouteParserInterface;
+use Slim\Builder\AppBuilder;
+use Slim\Routing\Route;
use Slim\Routing\RouteContext;
use Slim\Routing\RoutingResults;
-use Slim\Tests\TestCase;
+use Slim\Routing\UrlGenerator;
class RouteContextTest extends TestCase
{
- public function testCanCreateInstanceFromServerRequest(): void
+ /**
+ * Tests that a RouteContext instance is correctly created with all required attributes.
+ * Verifies that URL generator, routing results, and base path are properly set.
+ */
+ public function testFromRequestCreatesInstanceWithValidAttributes(): void
{
- $route = $this->createMock(RouteInterface::class);
- $routeParser = $this->createMock(RouteParserInterface::class);
- $routingResults = $this->createMock(RoutingResults::class);
+ $app = (new AppBuilder())->build();
- $serverRequest = $this->createServerRequest('/')
- ->withAttribute(RouteContext::BASE_PATH, '')
- ->withAttribute(RouteContext::ROUTE, $route)
- ->withAttribute(RouteContext::ROUTE_PARSER, $routeParser)
- ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults);
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
- $routeContext = RouteContext::fromRequest($serverRequest);
+ $urlGenerator = $app->getContainer()->get(UrlGenerator::class);
- $this->assertSame($route, $routeContext->getRoute());
- $this->assertSame($routeParser, $routeContext->getRouteParser());
+ $routingResults = new RoutingResults(200, null, 'GET', '/test', []);
+ $basePath = '/base-path';
+
+ $request = $request
+ ->withAttribute(RouteContext::URL_GENERATOR, $urlGenerator)
+ ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults)
+ ->withAttribute(RouteContext::BASE_PATH, $basePath);
+
+ $routeContext = RouteContext::fromRequest($request);
+
+ $this->assertInstanceOf(RouteContext::class, $routeContext);
+ $this->assertSame($urlGenerator, $routeContext->getUrlGenerator());
$this->assertSame($routingResults, $routeContext->getRoutingResults());
- $this->assertSame('', $routeContext->getBasePath());
+ $this->assertSame($basePath, $routeContext->getBasePath());
}
- public function testCanCreateInstanceWithoutRoute(): void
+ /**
+ * Tests that an exception is thrown when attempting to create a RouteContext
+ * without a URL generator attribute set in the request.
+ */
+ public function testFromRequestThrowsExceptionIfUrlGeneratorIsMissing(): void
{
- $serverRequest = $this->createServerRequestWithRouteAttributes();
+ $app = (new AppBuilder())->build();
- // Route attribute is not required
- $serverRequest = $serverRequest->withoutAttribute(RouteContext::ROUTE);
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
- $routeContext = RouteContext::fromRequest($serverRequest);
- $this->assertNull($routeContext->getRoute());
- $this->assertNotNull($routeContext->getRouteParser());
- $this->assertNotNull($routeContext->getRoutingResults());
- $this->assertNotNull($routeContext->getBasePath());
+ $routingResults = new RoutingResults(200, null, 'GET', '/test', []);
+
+ $request = $request
+ ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults);
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage(
+ 'Cannot create RouteContext before routing has been completed. Add UrlGeneratorMiddleware to fix this.'
+ );
+
+ RouteContext::fromRequest($request);
}
- public function testCanCreateInstanceWithoutBasePathAndThrowExceptionIfGetBasePathIsCalled(): void
+ /**
+ * Tests that an exception is thrown when attempting to create a RouteContext
+ * without routing results attribute set in the request.
+ */
+ public function testFromRequestThrowsExceptionIfRoutingResultsAreMissing(): void
{
- $serverRequest = $this->createServerRequestWithRouteAttributes();
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
- // Route attribute is not required
- $serverRequest = $serverRequest->withoutAttribute(RouteContext::BASE_PATH);
+ $urlGenerator = $app->getContainer()->get(UrlGenerator::class);
- $routeContext = RouteContext::fromRequest($serverRequest);
- $this->assertNotNull($routeContext->getRoute());
- $this->assertNotNull($routeContext->getRouteParser());
- $this->assertNotNull($routeContext->getRoutingResults());
+ $request = $request
+ ->withAttribute(RouteContext::URL_GENERATOR, $urlGenerator);
$this->expectException(RuntimeException::class);
- $routeContext->getBasePath();
+ $this->expectExceptionMessage(
+ 'Cannot create RouteContext before routing has been completed. Add RoutingMiddleware to fix this.'
+ );
+
+ RouteContext::fromRequest($request);
}
- public function requiredRouteContextRequestAttributes(): array
+ /**
+ * Tests that the URL generator instance returned by getUrlGenerator matches
+ * the one originally provided in the request attributes.
+ */
+ public function testGetUrlGeneratorReturnsCorrectInstance(): void
{
- return [
- [RouteContext::ROUTE_PARSER],
- [RouteContext::ROUTING_RESULTS],
- ];
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $urlGenerator = $app->getContainer()->get(UrlGenerator::class);
+
+ $routingResults = new RoutingResults(200, null, 'GET', '/test', []);
+
+ $request = $request
+ ->withAttribute(RouteContext::URL_GENERATOR, $urlGenerator)
+ ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults);
+
+ $routeContext = RouteContext::fromRequest($request);
+
+ $this->assertSame($urlGenerator, $routeContext->getUrlGenerator());
}
/**
- * @dataProvider requiredRouteContextRequestAttributes
- * @param string $attribute
+ * Tests that the RoutingResults instance returned by getRoutingResults matches
+ * the one originally provided in the request attributes.
*/
- public function testCannotCreateInstanceIfRequestIsMissingAttributes(string $attribute): void
+ public function testGetRoutingResultsReturnsCorrectInstance(): void
{
- $this->expectException(RuntimeException::class);
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
- $serverRequest = $this->createServerRequestWithRouteAttributes()->withoutAttribute($attribute);
+ $urlGenerator = $app->getContainer()->get(UrlGenerator::class);
- RouteContext::fromRequest($serverRequest);
+ $routingResults = new RoutingResults(200, null, 'GET', '/test', []);
+
+ $request = $request
+ ->withAttribute(RouteContext::URL_GENERATOR, $urlGenerator)
+ ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults);
+
+ $routeContext = RouteContext::fromRequest($request);
+
+ $this->assertSame($routingResults, $routeContext->getRoutingResults());
}
- private function createServerRequestWithRouteAttributes(): ServerRequestInterface
+ /**
+ * Tests that the base path value returned by getBasePath matches
+ * the one originally provided in the request attributes.
+ */
+ public function testGetBasePathReturnsCorrectValue(): void
{
- $route = $this->createMock(RouteInterface::class);
- $routeParser = $this->createMock(RouteParserInterface::class);
- $routingResults = $this->createMock(RoutingResults::class);
-
- return $this->createServerRequest('/')
- ->withAttribute(RouteContext::BASE_PATH, '')
- ->withAttribute(RouteContext::ROUTE, $route)
- ->withAttribute(RouteContext::ROUTE_PARSER, $routeParser)
- ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults);
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $urlGenerator = $app->getContainer()->get(UrlGenerator::class);
+
+ $routingResults = new RoutingResults(200, null, 'GET', '/test', []);
+ $basePath = '/base-path';
+
+ $request = $request
+ ->withAttribute(RouteContext::URL_GENERATOR, $urlGenerator)
+ ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults)
+ ->withAttribute(RouteContext::BASE_PATH, $basePath);
+
+ $routeContext = RouteContext::fromRequest($request);
+
+ $this->assertSame($basePath, $routeContext->getBasePath());
+ }
+
+ /**
+ * Tests that getBasePath returns null when no base path attribute
+ * was set in the request.
+ */
+ public function testGetBasePathReturnsNullIfNotSet(): void
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $urlGenerator = $app->getContainer()->get(UrlGenerator::class);
+
+ $routingResults = new RoutingResults(200, null, 'GET', '/test', []);
+
+ $request = $request
+ ->withAttribute(RouteContext::URL_GENERATOR, $urlGenerator)
+ ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults);
+
+ $routeContext = RouteContext::fromRequest($request);
+
+ $this->assertNull($routeContext->getBasePath());
+ }
+
+ /**
+ * Tests that getRoute() returns the correct Route instance when a route is matched
+ */
+ public function testGetRouteReturnsCorrectInstance(): void
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $urlGenerator = $app->getContainer()->get(UrlGenerator::class);
+
+ // Create a route for testing
+ $route = $app->get('/test', function () {
+ })->setName('test-route');
+ $routingResults = new RoutingResults(200, $route, 'GET', '/test', []);
+
+ $request = $request
+ ->withAttribute(RouteContext::URL_GENERATOR, $urlGenerator)
+ ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults);
+
+ $routeContext = RouteContext::fromRequest($request);
+
+ $this->assertInstanceOf(Route::class, $routeContext->getRoute());
+ $this->assertSame($route, $routeContext->getRoute());
+ $this->assertSame('test-route', $routeContext->getRoute()->getName());
+ }
+
+ /**
+ * Tests that getRoute() returns null when no route is matched
+ */
+ public function testGetRouteReturnsNullWhenNoRouteMatched(): void
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $urlGenerator = $app->getContainer()->get(UrlGenerator::class);
+
+ $routingResults = new RoutingResults(404, null, 'GET', '/not-found', []);
+
+ $request = $request
+ ->withAttribute(RouteContext::URL_GENERATOR, $urlGenerator)
+ ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults);
+
+ $routeContext = RouteContext::fromRequest($request);
+
+ $this->assertNull($routeContext->getRoute());
+ }
+
+ /**
+ * Tests that getArguments() returns all route arguments correctly
+ */
+ public function testGetArgumentsReturnsCorrectValues(): void
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $urlGenerator = $app->getContainer()->get(UrlGenerator::class);
+
+ $arguments = ['id' => '123', 'name' => 'test'];
+ $routingResults = new RoutingResults(200, null, 'GET', '/test', $arguments);
+
+ $request = $request
+ ->withAttribute(RouteContext::URL_GENERATOR, $urlGenerator)
+ ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults);
+
+ $routeContext = RouteContext::fromRequest($request);
+
+ $this->assertSame($arguments, $routeContext->getArguments());
+ }
+
+ /**
+ * Tests that getArgument() returns the correct value for a specific argument key
+ */
+ public function testGetArgumentReturnsCorrectValue(): void
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $urlGenerator = $app->getContainer()->get(UrlGenerator::class);
+
+ $arguments = ['id' => '123', 'name' => 'test'];
+ $routingResults = new RoutingResults(200, null, 'GET', '/test', $arguments);
+
+ $request = $request
+ ->withAttribute(RouteContext::URL_GENERATOR, $urlGenerator)
+ ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults);
+
+ $routeContext = RouteContext::fromRequest($request);
+
+ $this->assertSame('123', $routeContext->getArgument('id'));
+ $this->assertSame('test', $routeContext->getArgument('name'));
+ }
+
+ /**
+ * Tests that getArgument() returns null when the requested key doesn't exist
+ */
+ public function testGetArgumentReturnsNullForNonExistentKey(): void
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $urlGenerator = $app->getContainer()->get(UrlGenerator::class);
+
+ $arguments = ['id' => '123'];
+ $routingResults = new RoutingResults(200, null, 'GET', '/test', $arguments);
+
+ $request = $request
+ ->withAttribute(RouteContext::URL_GENERATOR, $urlGenerator)
+ ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults);
+
+ $routeContext = RouteContext::fromRequest($request);
+
+ $this->assertNull($routeContext->getArgument('non-existent'));
}
}
diff --git a/tests/Routing/RouteGroupTest.php b/tests/Routing/RouteGroupTest.php
new file mode 100644
index 000000000..a36c0b39b
--- /dev/null
+++ b/tests/Routing/RouteGroupTest.php
@@ -0,0 +1,96 @@
+createRouter();
+ $callback = function () {
+ };
+ $prefix = '/test';
+ $routeGroup = new RouteGroup($prefix, $callback, $router);
+
+ $this->assertSame('/test', $routeGroup->getPrefix());
+ $this->assertSame($callback, $routeGroup->getRouteGroup() === null ? $callback : null);
+ $this->assertSame($router, $routeGroup->getRouteGroup() === null ? $router : null);
+ $this->assertNull($routeGroup->getRouteGroup());
+ }
+
+ public function testConstructorWithParentGroup(): void
+ {
+ $router = $this->createRouter();
+ $parentGroupCallback = function () {
+ };
+ $childGroupCallback = function () {
+ };
+ $parentGroup = new RouteGroup('/parent', $parentGroupCallback, $router);
+ $childGroup = new RouteGroup('/child', $childGroupCallback, $router, $parentGroup);
+
+ $this->assertSame('/child', $childGroup->getPrefix());
+ $this->assertSame($parentGroup, $childGroup->getRouteGroup());
+ }
+
+ public function testInvokeExecutesCallback(): void
+ {
+ $router = $this->createRouter();
+ $called = false;
+ $callback = function () use (&$called) {
+ $called = true;
+ };
+ $routeGroup = new RouteGroup('/test', $callback, $router);
+
+ $routeGroup();
+ $this->assertTrue($called);
+ }
+
+ public function testMapCreatesAndRegistersRoute(): void
+ {
+ $router = $this->createRouter();
+ $callback = function () {
+ };
+ $routeGroup = new RouteGroup('/test', $callback, $router);
+
+ $route = $routeGroup->map(['GET'], '/foo', 'handler');
+ $this->assertInstanceOf(Route::class, $route);
+ $this->assertSame(['GET'], $route->getMethods());
+ $this->assertSame('/test/foo', $route->getPattern());
+ }
+
+ public function testGroupCreatesAndRegistersNestedRouteGroup(): void
+ {
+ $router = $this->createRouter();
+ $callback = function () {
+ };
+ $routeGroup = new RouteGroup('/test', $callback, $router);
+
+ $nestedGroup = $routeGroup->group('/nested', function () {
+ });
+
+ $this->assertInstanceOf(RouteGroup::class, $nestedGroup);
+ $this->assertSame('/test/nested', $nestedGroup->getPrefix());
+ $this->assertSame($routeGroup, $nestedGroup->getRouteGroup());
+ }
+
+ private function createRouter(): Router
+ {
+ $app = (new AppBuilder())->build();
+
+ return $app->getContainer()->get(Router::class);
+ }
+}
diff --git a/tests/Routing/RouteParserTest.php b/tests/Routing/RouteParserTest.php
deleted file mode 100644
index d855d820c..000000000
--- a/tests/Routing/RouteParserTest.php
+++ /dev/null
@@ -1,198 +0,0 @@
- [
- true,
- '/{first}/{second}',
- ['first' => 'hello', 'second' => 'world'],
- [],
- '/app/hello/world',
- ],
- 'without base path' => [
- false,
- '/{first}/{second}',
- ['first' => 'hello', 'second' => 'world'],
- [],
- '/hello/world',
- ],
- 'without query parameters' => [
- false,
- '/{first}/{second}',
- ['first' => 'hello', 'second' => 'world'],
- ['a' => 'b', 'c' => 'd'],
- '/hello/world?a=b&c=d',
- ],
- 'with argument without optional parameter' => [
- false,
- '/archive/{year}[/{month:[\d:{2}]}[/d/{day}]]',
- ['year' => '2015'],
- [],
- '/archive/2015',
- ],
- 'with argument and optional parameter' => [
- false,
- '/archive/{year}[/{month:[\d:{2}]}[/d/{day}]]',
- ['year' => '2015', 'month' => '07'],
- [],
- '/archive/2015/07',
- ],
- 'with argument and optional parameters' => [
- false,
- '/archive/{year}[/{month:[\d:{2}]}[/d/{day}]]',
- ['year' => '2015', 'month' => '07', 'day' => '19'],
- [],
- '/archive/2015/07/d/19',
- ],
- ];
- }
-
- public function testRelativePathForWithNoBasePath()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
-
- $route = $routeCollector->map(['GET'], '/{first}/{second}', function () {
- });
- $route->setName('test');
-
- $routeParser = $routeCollector->getRouteParser();
- $results = $routeParser->relativeUrlFor('test', ['first' => 'hello', 'second' => 'world']);
-
- $this->assertSame('/hello/world', $results);
- }
-
- public function testBasePathIsIgnoreInRelativePathFor()
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $routeCollector->setBasePath('/app');
-
- $route = $routeCollector->map(['GET'], '/{first}/{second}', function () {
- });
- $route->setName('test');
-
- $routeParser = $routeCollector->getRouteParser();
- $results = $routeParser->relativeUrlFor('test', ['first' => 'hello', 'second' => 'world']);
-
- $this->assertSame('/hello/world', $results);
- }
-
- /**
- * @dataProvider urlForCases
- * @param $withBasePath
- * @param $pattern
- * @param $arguments
- * @param $queryParams
- * @param $expectedResult
- */
- public function testUrlForWithBasePath($withBasePath, $pattern, $arguments, $queryParams, $expectedResult)
- {
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
-
- if ($withBasePath) {
- $routeCollector->setBasePath('/app');
- }
-
- $route = $routeCollector->map(['GET'], $pattern, function () {
- });
- $route->setName('test');
-
- $routeParser = $routeCollector->getRouteParser();
- $results = $routeParser->urlFor('test', $arguments, $queryParams);
-
- $this->assertSame($expectedResult, $results);
- }
-
- public function testUrlForWithMissingSegmentData()
- {
- $this->expectException(InvalidArgumentException::class);
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $route = $routeCollector->map(['GET'], '/{first}/{last}', function () {
- });
- $route->setName('test');
-
- $routeParser = $routeCollector->getRouteParser();
- $routeParser->urlFor('test', ['last' => 'world']);
- }
-
- public function testUrlForRouteThatDoesNotExist()
- {
- $this->expectException(RuntimeException::class);
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal());
- $routeParser = $routeCollector->getRouteParser();
-
- $routeParser->urlFor('test');
- }
-
- public function testFullUrlFor()
- {
- $uriProphecy = $this->prophesize(UriInterface::class);
- $uriProphecy
- ->getScheme()
- ->willReturn('http')
- ->shouldBeCalledOnce();
-
- $uriProphecy
- ->getAuthority()
- ->willReturn('example.com:8080')
- ->shouldBeCalledOnce();
-
- $routeProphecy = $this->prophesize(RouteInterface::class);
- $routeProphecy
- ->getPattern()
- ->willReturn('/{token}')
- ->shouldBeCalledOnce();
-
- $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class);
-
- $routeCollectorProphecy
- ->getBasePath()
- ->willReturn('/app')
- ->shouldBeCalledOnce();
-
- $routeCollectorProphecy
- ->getNamedRoute('test')
- ->willReturn($routeProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $routeParser = new RouteParser($routeCollectorProphecy->reveal());
- $result = $routeParser->fullUrlFor($uriProphecy->reveal(), 'test', ['token' => '123']);
-
- $expectedResult = 'http://example.com:8080/app/123';
- $this->assertSame($expectedResult, $result);
- }
-}
diff --git a/tests/Routing/RouteResolverTest.php b/tests/Routing/RouteResolverTest.php
deleted file mode 100644
index 2a3a7e288..000000000
--- a/tests/Routing/RouteResolverTest.php
+++ /dev/null
@@ -1,91 +0,0 @@
-prophesize(RouteCollectorInterface::class);
- $routingResultsProphecy = $this->prophesize(RoutingResults::class);
-
- $dispatcherProphecy = $this->prophesize(DispatcherInterface::class);
- $dispatcherProphecy
- ->dispatch(Argument::type('string'), Argument::type('string'))
- ->will(function ($args) use ($routingResultsProphecy, $expectedUri) {
- if ($args[1] !== $expectedUri) {
- throw new Error(sprintf(
- "URI transformation failed.\n Received: '%s'\n Expected: '%s'",
- $args[1],
- $expectedUri
- ));
- }
- return $routingResultsProphecy->reveal();
- })
- ->shouldBeCalledOnce();
-
- $routeResolver = new RouteResolver(
- $routeCollectorProphecy->reveal(),
- $dispatcherProphecy->reveal()
- );
-
- $routeResolver->computeRoutingResults($uri, $method);
- }
-
- public function testResolveRoute()
- {
- $identifier = 'test';
-
- $routeProphecy = $this->prophesize(RouteInterface::class);
- $dispatcherProphecy = $this->prophesize(DispatcherInterface::class);
-
- $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class);
- $routeCollectorProphecy
- ->lookupRoute($identifier)
- ->willReturn($routeProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $routeResolver = new RouteResolver(
- $routeCollectorProphecy->reveal(),
- $dispatcherProphecy->reveal()
- );
-
- $routeResolver->resolveRoute($identifier);
- }
-}
diff --git a/tests/Routing/RouteRunnerTest.php b/tests/Routing/RouteRunnerTest.php
deleted file mode 100644
index 10d7ce1f2..000000000
--- a/tests/Routing/RouteRunnerTest.php
+++ /dev/null
@@ -1,53 +0,0 @@
-getAttribute(RouteContext::ROUTE_PARSER);
- $this->assertInstanceOf(RouteParser::class, $routeParser);
-
- $routingResults = $request->getAttribute(RouteContext::ROUTING_RESULTS);
- $this->assertInstanceOf(RoutingResults::class, $routingResults);
-
- return $response;
- })->bindTo($this);
-
- $callableResolver = $this->getCallableResolver();
- $responseFactory = $this->getResponseFactory();
-
- $routeCollector = new RouteCollector($responseFactory, $callableResolver);
- $routeCollector->map(['GET'], '/hello/{name}', $handler);
-
- $routeParser = new RouteParser($routeCollector);
- $routeResolver = new RouteResolver($routeCollector);
-
- $request = $this->createServerRequest('https://example.com:443/hello/foo', 'GET');
- $dispatcher = new RouteRunner($routeResolver, $routeParser);
-
- $middlewareDispatcher = new MiddlewareDispatcher($dispatcher, $callableResolver);
- $middlewareDispatcher->handle($request);
- }
-}
diff --git a/tests/Routing/RouteTest.php b/tests/Routing/RouteTest.php
index 9198df15b..0c855a1e9 100644
--- a/tests/Routing/RouteTest.php
+++ b/tests/Routing/RouteTest.php
@@ -3,872 +3,156 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
declare(strict_types=1);
-namespace Slim\Tests;
+namespace Slim\Tests\Routing;
-use Closure;
-use Exception;
-use Prophecy\Argument;
-use Psr\Container\ContainerInterface;
-use Psr\Http\Message\ResponseFactoryInterface;
+use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
-use Psr\Http\Message\StreamInterface;
+use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
-use RuntimeException;
-use Slim\CallableResolver;
-use Slim\Handlers\Strategies\RequestHandler;
-use Slim\Handlers\Strategies\RequestResponse;
-use Slim\Interfaces\CallableResolverInterface;
-use Slim\Interfaces\InvocationStrategyInterface;
-use Slim\Interfaces\RouteCollectorProxyInterface;
+use Slim\Builder\AppBuilder;
use Slim\Routing\Route;
use Slim\Routing\RouteGroup;
-use Slim\Tests\Mocks\CallableTest;
-use Slim\Tests\Mocks\InvocationStrategyTest;
-use Slim\Tests\Mocks\MockCustomRequestHandlerInvocationStrategy;
-use Slim\Tests\Mocks\MockMiddlewareWithoutConstructor;
-use Slim\Tests\Mocks\MockMiddlewareWithoutInterface;
-use Slim\Tests\Mocks\RequestHandlerTest;
-
-use function is_callable;
-use function is_string;
-use function ob_end_clean;
-use function ob_start;
+use Slim\Routing\Router;
class RouteTest extends TestCase
{
- /**
- * @param string|array $methods
- * @param string $pattern
- * @param Closure|string|null $callable
- * @return Route
- */
- public function createRoute($methods = 'GET', string $pattern = '/', $callable = null): Route
+ public function testGetHandlerReturnsCorrectHandler(): void
{
- $callable ??= function (ServerRequestInterface $request, ResponseInterface $response) {
- return $response;
+ $methods = ['GET'];
+ $pattern = '/test';
+ $handler = function () {
+ return 'handler';
};
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $callableResolverProphecy
- ->resolve(Argument::is($callable))
- ->willReturn($callable);
- $callableResolverProphecy
- ->resolve(MockMiddlewareWithoutConstructor::class)
- ->will(function ($args) {
- return [new MockMiddlewareWithoutConstructor(), 'process'];
- });
-
- $streamProphecy = $this->prophesize(StreamInterface::class);
-
- $value = '';
- $streamProphecy
- ->write(Argument::type('string'))
- ->will(function ($args) use ($value) {
- $value .= $args[0];
- $this->__toString()->willReturn($value);
- return strlen($value);
- });
-
- $streamProphecy
- ->__toString()
- ->willReturn($value);
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
-
- $responseProphecy
- ->getBody()
- ->willReturn($streamProphecy->reveal());
-
- $responseProphecy
- ->withStatus(Argument::type('integer'))
- ->will(function ($args) {
- $this->getStatusCode()->willReturn($args[0]);
- return $this->reveal();
- });
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy
- ->createResponse()
- ->willReturn($responseProphecy->reveal());
-
- $methods = is_string($methods) ? [$methods] : $methods;
- return new Route(
- $methods,
- $pattern,
- $callable,
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal()
- );
- }
-
- public function testConstructor()
- {
- $methods = ['GET', 'POST'];
- $pattern = '/hello/{name}';
- $callable = function ($request, $response, $args) {
- return $response;
- };
- $route = $this->createRoute($methods, $pattern, $callable);
-
- $this->assertSame($methods, $route->getMethods());
- $this->assertSame($pattern, $route->getPattern());
- $this->assertSame($callable, $route->getCallable());
- }
-
- public function testGetMethodsReturnsArrayWhenConstructedWithString()
- {
- $route = $this->createRoute();
-
- $this->assertSame(['GET'], $route->getMethods());
- }
-
- public function testGetMethods()
- {
- $methods = ['GET', 'POST'];
- $route = $this->createRoute($methods);
-
- $this->assertSame($methods, $route->getMethods());
- }
+ $route = new Route($methods, $pattern, $handler);
- public function testGetPattern()
- {
- $route = $this->createRoute();
-
- $this->assertSame('/', $route->getPattern());
+ $this->assertSame($handler, $route->getHandler());
}
- public function testGetCallable()
+ public function testGetMiddlewareStackWithoutGroup(): void
{
- $route = $this->createRoute();
-
- $this->assertTrue(is_callable($route->getCallable()));
- }
-
- public function testGetCallableResolver()
- {
- $callable = function (ServerRequestInterface $request, ResponseInterface $response) {
- return $response;
+ $methods = ['GET'];
+ $pattern = '/test';
+ $handler = function () {
+ return 'handler';
};
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
+ $route = new Route($methods, $pattern, $handler);
- $route = new Route(
- ['GET'],
- '/',
- $callable,
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal()
- );
+ // Adding middleware
+ $middleware1 = $this->createMiddleware();
+ $middleware2 = $this->createMiddleware();
+ $route->add($middleware1)->add($middleware2);
- $this->assertSame($callableResolverProphecy->reveal(), $route->getCallableResolver());
- }
+ $middlewareStack = $route->getMiddlewareStack();
- public function testGetInvocationStrategy()
- {
- $callable = function (ServerRequestInterface $request, ResponseInterface $response) {
- return $response;
- };
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $strategyProphecy = $this->prophesize(InvocationStrategyInterface::class);
-
- $route = new Route(
- ['GET'],
- '/',
- $callable,
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- $containerProphecy->reveal(),
- $strategyProphecy->reveal()
- );
-
- $this->assertSame($strategyProphecy->reveal(), $route->getInvocationStrategy());
+ $this->assertCount(2, $middlewareStack);
+ $this->assertSame([$middleware1, $middleware2], $middlewareStack);
}
- public function testSetInvocationStrategy()
+ public function testGetMiddlewareStackWithGroup(): void
{
- $callable = function (ServerRequestInterface $request, ResponseInterface $response) {
- return $response;
+ $methods = ['GET'];
+ $pattern = '/test';
+ $handler = function () {
+ return 'handler';
};
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $strategyProphecy = $this->prophesize(InvocationStrategyInterface::class);
+ // Creating middlewares
+ $middleware1 = $this->createMiddleware();
+ $middleware2 = $this->createMiddleware();
+ $groupMiddleware = $this->createMiddleware();
- $route = new Route(
- ['GET'],
- '/',
- $callable,
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal()
- );
- $route->setInvocationStrategy($strategyProphecy->reveal());
+ // Create a Router instance
+ $router = $this->createRouter();
- $this->assertSame($strategyProphecy->reveal(), $route->getInvocationStrategy());
- }
+ // Create a RouteGroup with middleware
+ $routeGroup = new RouteGroup('/group', function (RouteGroup $group) use ($groupMiddleware) {
+ $group->add($groupMiddleware);
+ }, $router);
- public function testGetGroups()
- {
- $callable = function (ServerRequestInterface $request, ResponseInterface $response) {
- return $response;
- };
+ $route = new Route($methods, $pattern, $handler, $routeGroup);
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $strategyProphecy = $this->prophesize(InvocationStrategyInterface::class);
- $routeCollectorProxyProphecy = $this->prophesize(RouteCollectorProxyInterface::class);
-
- $routeGroup = new RouteGroup(
- '/group',
- $callable,
- $callableResolverProphecy->reveal(),
- $routeCollectorProxyProphecy->reveal()
- );
- $groups = [$routeGroup];
-
- $route = new Route(
- ['GET'],
- '/',
- $callable,
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- null,
- $strategyProphecy->reveal(),
- $groups
- );
-
- $this->assertSame($groups, $route->getGroups());
- }
+ // Adding middleware to route
+ $route->add($middleware1)->add($middleware2);
- public function testArgumentSetting()
- {
- $route = $this->createRoute();
- $route->setArguments(['foo' => 'FOO', 'bar' => 'BAR']);
- $this->assertSame($route->getArguments(), ['foo' => 'FOO', 'bar' => 'BAR']);
+ // Simulate fastroute group collector
+ $routeGroup();
- $route->setArgument('bar', 'bar');
- $this->assertSame($route->getArguments(), ['foo' => 'FOO', 'bar' => 'bar']);
+ $middlewareStack = $route->getMiddlewareStack();
- $route->setArgument('baz', 'BAZ');
- $this->assertSame($route->getArguments(), ['foo' => 'FOO', 'bar' => 'bar', 'baz' => 'BAZ']);
+ // The stack should contain route middlewares followed by group middleware
+ $this->assertCount(2, $middlewareStack);
+ $this->assertSame([$middleware1, $middleware2], $middlewareStack);
- $route->setArguments(['a' => 'b']);
- $this->assertSame($route->getArguments(), ['a' => 'b']);
- $this->assertSame($route->getArgument('a', 'default'), 'b');
- $this->assertSame($route->getArgument('b', 'default'), 'default');
+ $groupMiddlewares = $routeGroup->getMiddlewareStack();
+ $this->assertCount(1, $groupMiddlewares);
+ $this->assertSame($groupMiddleware, $groupMiddlewares[0]);
}
- public function testAddMiddleware()
+ public function testSetNameAndGetName(): void
{
- $route = $this->createRoute();
- $called = 0;
-
- $mw = function (ServerRequestInterface $request, RequestHandlerInterface $handler) use (&$called) {
- $called++;
- return $handler->handle($request);
+ $methods = ['GET'];
+ $pattern = '/test';
+ $handler = function () {
+ return 'handler';
};
- $route->add($mw);
-
- $request = $this->createServerRequest('/');
- $route->run($request);
-
- $this->assertSame($called, 1);
- }
-
- public function testAddMiddlewareOnGroup()
- {
- $callable = function (ServerRequestInterface $request, ResponseInterface $response) {
- return $response;
- };
-
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
- $callableResolverProphecy
- ->resolve(Argument::is($callable))
- ->willReturn($callable)
- ->shouldBeCalledOnce();
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy
- ->createResponse()
- ->willReturn($responseProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $routeCollectorProxyProphecy = $this->prophesize(RouteCollectorProxyInterface::class);
- $strategy = new RequestResponse();
-
- $called = 0;
- $mw = function (ServerRequestInterface $request, RequestHandlerInterface $handler) use (&$called) {
- $called++;
- return $handler->handle($request);
- };
-
- $routeGroup = new RouteGroup(
- '/group',
- $callable,
- $callableResolverProphecy->reveal(),
- $routeCollectorProxyProphecy->reveal()
- );
- $routeGroup->add($mw);
- $groups = [$routeGroup];
-
- $route = new Route(
- ['GET'],
- '/',
- $callable,
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal(),
- null,
- $strategy,
- $groups
- );
-
- $request = $this->createServerRequest('/');
- $route->run($request);
-
- $this->assertSame($called, 1);
- }
-
- public function testAddClosureMiddleware()
- {
- $route = $this->createRoute();
- $called = 0;
-
- $route->add(function (ServerRequestInterface $request, RequestHandlerInterface $handler) use (&$called) {
- $called++;
- return $handler->handle($request);
- });
-
- $request = $this->createServerRequest('/');
- $route->run($request);
-
- $this->assertSame($called, 1);
- }
-
- public function testAddMiddlewareUsingDeferredResolution()
- {
- $route = $this->createRoute();
- $route->add(MockMiddlewareWithoutConstructor::class);
-
- $output = '';
- $appendToOutput = function (string $value) use (&$output) {
- $output .= $value;
- };
- $request = $this->createServerRequest('/')->withAttribute('appendToOutput', $appendToOutput);
- $route->run($request);
-
- $this->assertSame('Hello World', $output);
- }
-
- public function testAddMiddlewareAsStringNotImplementingInterfaceThrowsException()
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage(
- 'A middleware must be an object/class name referencing an implementation of ' .
- 'MiddlewareInterface or a callable with a matching signature.'
- );
-
- $route = $this->createRoute();
- $route->add(new MockMiddlewareWithoutInterface());
- }
-
- public function testIdentifier()
- {
- $route = $this->createRoute();
- $this->assertSame('route0', $route->getIdentifier());
- }
- public function testSetName()
- {
- $route = $this->createRoute();
- $this->assertSame($route, $route->setName('foo'));
- $this->assertSame('foo', $route->getName());
- }
-
- public function testControllerMethodAsStringResolvesWithoutContainer()
- {
- $callableResolver = new CallableResolver();
- $responseFactory = $this->getResponseFactory();
-
- $deferred = $callableResolver->resolve('\Slim\Tests\Mocks\CallableTest:toCall');
- $route = new Route(['GET'], '/', $deferred, $responseFactory, $callableResolver);
-
- CallableTest::$CalledCount = 0;
-
- $request = $this->createServerRequest('/');
- $response = $route->run($request);
-
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertSame(1, CallableTest::$CalledCount);
- }
-
- public function testControllerMethodAsStringResolvesWithContainer()
- {
- $self = $this;
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
- $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class);
-
- $callable = 'callable';
-
- $callableResolverProphecy
- ->resolve(Argument::is($callable))
- ->willReturn(function (
- ServerRequestInterface $request,
- ResponseInterface $response
- ) use (
- $self,
- $responseProphecy
- ) {
- $self->assertSame($responseProphecy->reveal(), $response);
- return $response;
- })
- ->shouldBeCalledOnce();
-
- $deferred = $callableResolverProphecy->reveal()->resolve($callable);
- $callableResolverProphecy
- ->resolve(Argument::is($deferred))
- ->willReturn($deferred)
- ->shouldBeCalledOnce();
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy
- ->createResponse()
- ->willReturn($responseProphecy->reveal());
-
- $route = new Route(
- ['GET'],
- '/',
- $deferred,
- $responseFactoryProphecy->reveal(),
- $callableResolverProphecy->reveal()
- );
-
- $request = $this->createServerRequest('/');
- $response = $route->run($request);
-
- $this->assertInstanceOf(ResponseInterface::class, $response);
- }
-
- /**
- * Ensure that the response returned by a route callable is the response
- * object that is returned by __invoke().
- */
- public function testProcessWhenReturningAResponse()
- {
- $callable = function (ServerRequestInterface $request, ResponseInterface $response) {
- $response->getBody()->write('foo');
- return $response;
- };
- $route = $this->createRoute(['GET'], '/', $callable);
+ $route = new Route($methods, $pattern, $handler);
- $request = $this->createServerRequest('/');
- $response = $route->run($request);
+ $route->setName('test-route');
- $this->assertSame('foo', (string) $response->getBody());
+ $this->assertSame('test-route', $route->getName());
}
- /**
- * Ensure that anything echo'd in a route callable is, by default, NOT
- * added to the response object body.
- */
- public function testRouteCallableDoesNotAppendEchoedOutput()
+ public function testGetPatternReturnsCorrectPattern(): void
{
- $callable = function (ServerRequestInterface $request, ResponseInterface $response) {
- echo "foo";
- return $response->withStatus(201);
+ $methods = ['GET'];
+ $pattern = '/test';
+ $handler = function () {
+ return 'handler';
};
- $route = $this->createRoute(['GET'], '/', $callable);
-
- $request = $this->createServerRequest('/');
- // We capture output buffer here only to clean test CLI output
- ob_start();
- $response = $route->run($request);
- ob_end_clean();
+ $route = new Route($methods, $pattern, $handler);
- // Output buffer is ignored without optional middleware
- $this->assertSame('', (string) $response->getBody());
- $this->assertSame(201, $response->getStatusCode());
+ $this->assertSame($pattern, $route->getPattern());
}
- /**
- * Ensure that if a string is returned by a route callable, then it is
- * added to the response object that is returned by __invoke().
- */
- public function testRouteCallableAppendsCorrectOutputToResponse()
+ public function testGetMethodsReturnsCorrectMethods(): void
{
- $callable = function (ServerRequestInterface $request, ResponseInterface $response) {
- $response->getBody()->write('foo');
- return $response;
+ $methods = ['GET', 'POST'];
+ $pattern = '/test';
+ $handler = function () {
+ return 'handler';
};
- $route = $this->createRoute(['GET'], '/', $callable);
- $request = $this->createServerRequest('/');
- $response = $route->run($request);
+ $route = new Route($methods, $pattern, $handler);
- $this->assertSame('foo', (string) $response->getBody());
+ $this->assertSame($methods, $route->getMethods());
}
- public function testInvokeWithException()
+ private function createMiddleware(): MiddlewareInterface
{
- $this->expectException(Exception::class);
-
- $callable = function (ServerRequestInterface $request, ResponseInterface $response) {
- throw new Exception();
+ return new class implements MiddlewareInterface {
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler,
+ ): ResponseInterface {
+ return $handler->handle($request);
+ }
};
- $route = $this->createRoute(['GET'], '/', $callable);
-
- $request = $this->createServerRequest('/');
- $route->run($request);
}
- /**
- * Ensure that `foundHandler` is called on actual callable
- */
- public function testInvokeDeferredCallableWithNoContainer()
+ private function createRouter(): Router
{
- $responseProphecy = $this->prophesize(ResponseInterface::class);
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy
- ->createResponse()
- ->willReturn($responseProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $callableResolver = new CallableResolver();
- $invocationStrategy = new InvocationStrategyTest();
-
- $deferred = '\Slim\Tests\Mocks\CallableTest:toCall';
- $route = new Route(
- ['GET'],
- '/',
- $deferred,
- $responseFactoryProphecy->reveal(),
- $callableResolver,
- null,
- $invocationStrategy
- );
-
- $request = $this->createServerRequest('/');
- $response = $route->run($request);
-
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertEquals([new CallableTest(), 'toCall'], InvocationStrategyTest::$LastCalledFor);
- }
-
- /**
- * Ensure that `foundHandler` is called on actual callable
- */
- public function testInvokeDeferredCallableWithContainer()
- {
- $responseProphecy = $this->prophesize(ResponseInterface::class);
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy
- ->createResponse()
- ->willReturn($responseProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $containerProphecy->has('\Slim\Tests\Mocks\CallableTest')->willReturn(true);
- $containerProphecy->get('\Slim\Tests\Mocks\CallableTest')->willReturn(new CallableTest());
-
- $callableResolver = new CallableResolver($containerProphecy->reveal());
- $strategy = new InvocationStrategyTest();
-
- $deferred = '\Slim\Tests\Mocks\CallableTest:toCall';
- $route = new Route(
- ['GET'],
- '/',
- $deferred,
- $responseFactoryProphecy->reveal(),
- $callableResolver,
- $containerProphecy->reveal(),
- $strategy
- );
-
- $request = $this->createServerRequest('/');
- $response = $route->run($request);
-
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertEquals([new CallableTest(), 'toCall'], InvocationStrategyTest::$LastCalledFor);
- }
+ $app = (new AppBuilder())->build();
- public function testInvokeUsesRequestHandlerStrategyForRequestHandlers()
- {
- $responseProphecy = $this->prophesize(ResponseInterface::class);
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy
- ->createResponse()
- ->willReturn($responseProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $containerProphecy->has(RequestHandlerTest::class)->willReturn(true);
- $containerProphecy->get(RequestHandlerTest::class)->willReturn(new RequestHandlerTest());
-
- $callableResolver = new CallableResolver($containerProphecy->reveal());
-
- $deferred = RequestHandlerTest::class;
- $route = new Route(
- ['GET'],
- '/',
- $deferred,
- $responseFactoryProphecy->reveal(),
- $callableResolver,
- $containerProphecy->reveal()
- );
-
- $request = $this->createServerRequest('/', 'GET');
- $route->run($request);
-
- /** @var InvocationStrategyInterface $strategy */
- $strategy = $containerProphecy
- ->reveal()
- ->get(RequestHandlerTest::class)::$strategy;
-
- $this->assertSame(RequestHandler::class, $strategy);
- }
-
- public function testInvokeUsesUserSetStrategyForRequestHandlers()
- {
- $responseProphecy = $this->prophesize(ResponseInterface::class);
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy
- ->createResponse()
- ->willReturn($responseProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $containerProphecy->has(RequestHandlerTest::class)->willReturn(true);
- $containerProphecy->get(RequestHandlerTest::class)->willReturn(new RequestHandlerTest());
-
- $callableResolver = new CallableResolver($containerProphecy->reveal());
-
- $deferred = RequestHandlerTest::class;
- $route = new Route(
- ['GET'],
- '/',
- $deferred,
- $responseFactoryProphecy->reveal(),
- $callableResolver,
- $containerProphecy->reveal()
- );
-
- $strategy = new MockCustomRequestHandlerInvocationStrategy();
- $route->setInvocationStrategy($strategy);
-
- $request = $this->createServerRequest('/', 'GET');
- $route->run($request);
-
- $this->assertSame(1, $strategy::$CalledCount);
- }
-
- public function testRequestHandlerStrategyAppendsRouteArgumentsAsAttributesToRequest()
- {
- $self = $this;
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy
- ->createResponse()
- ->willReturn($responseProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $containerProphecy->has(RequestHandlerTest::class)->willReturn(true);
- $containerProphecy->get(RequestHandlerTest::class)->willReturn(new RequestHandlerTest());
-
- $callableResolver = new CallableResolver($containerProphecy->reveal());
-
- $deferred = RequestHandlerTest::class;
- $route = new Route(
- ['GET'],
- '/',
- $deferred,
- $responseFactoryProphecy->reveal(),
- $callableResolver,
- $containerProphecy->reveal()
- );
-
- $strategy = new RequestHandler(true);
- $route->setInvocationStrategy($strategy);
- $route->setArguments(['id' => 1]);
-
- $requestProphecy = $this->prophesize(ServerRequestInterface::class);
- $requestProphecy->withAttribute(Argument::type('string'), Argument::any())->will(function ($args) use ($self) {
- $name = $args[0];
- $value = $args[1];
-
- $self->assertSame('id', $name);
- $self->assertSame(1, $value);
-
- return $this;
- })->shouldBeCalledOnce();
-
- $route->run($requestProphecy->reveal());
- }
-
- /**
- * Ensure that the pattern can be dynamically changed
- */
- public function testPatternCanBeChanged()
- {
- $route = $this->createRoute();
- $route->setPattern('/hola/{nombre}');
-
- $this->assertSame('/hola/{nombre}', $route->getPattern());
- }
-
- /**
- * Ensure that the callable can be changed
- */
- public function testChangingCallableWithNoContainer()
- {
- $responseProphecy = $this->prophesize(ResponseInterface::class);
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy
- ->createResponse()
- ->willReturn($responseProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $callableResolver = new CallableResolver();
- $strategy = new InvocationStrategyTest();
-
- $deferred = 'NonExistent:toCall';
- $route = new Route(
- ['GET'],
- '/',
- $deferred,
- $responseFactoryProphecy->reveal(),
- $callableResolver,
- null,
- $strategy
- );
- $route->setCallable('\Slim\Tests\Mocks\CallableTest:toCall'); //Then we fix it here.
-
- $request = $this->createServerRequest('/');
- $response = $route->run($request);
-
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertEquals([new CallableTest(), 'toCall'], InvocationStrategyTest::$LastCalledFor);
- }
-
- /**
- * Ensure that the callable can be changed
- */
- public function testChangingCallableWithContainer()
- {
- $responseProphecy = $this->prophesize(ResponseInterface::class);
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy
- ->createResponse()
- ->willReturn($responseProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $containerProphecy->has('CallableTest2')->willReturn(true);
- $containerProphecy->get('CallableTest2')->willReturn(new CallableTest());
-
- $callableResolver = new CallableResolver($containerProphecy->reveal());
- $strategy = new InvocationStrategyTest();
-
- $deferred = 'NonExistent:toCall';
- $route = new Route(
- ['GET'],
- '/',
- $deferred,
- $responseFactoryProphecy->reveal(),
- $callableResolver,
- $containerProphecy->reveal(),
- $strategy
- );
- $route->setCallable('CallableTest2:toCall'); //Then we fix it here.
-
- $request = $this->createServerRequest('/');
- $response = $route->run($request);
-
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertSame(
- [$containerProphecy->reveal()->get('CallableTest2'), 'toCall'],
- InvocationStrategyTest::$LastCalledFor
- );
- }
-
- public function testRouteCallableIsResolvedUsingContainerWhenCallableResolverIsPresent()
- {
- $streamProphecy = $this->prophesize(StreamInterface::class);
-
- $value = '';
- $streamProphecy
- ->write(Argument::type('string'))
- ->will(function ($args) use ($value) {
- $value .= $args[0];
- $this->__toString()->willReturn($value);
- return strlen($value);
- });
-
- $streamProphecy
- ->__toString()
- ->willReturn($value);
-
- $responseProphecy = $this->prophesize(ResponseInterface::class);
-
- $responseProphecy
- ->getBody()
- ->willReturn($streamProphecy->reveal())
- ->shouldBeCalledTimes(2);
-
- $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class);
- $responseFactoryProphecy
- ->createResponse()
- ->willReturn($responseProphecy->reveal())
- ->shouldBeCalledOnce();
-
- $containerProphecy = $this->prophesize(ContainerInterface::class);
- $containerProphecy->has('CallableTest3')->willReturn(true);
- $containerProphecy->get('CallableTest3')->willReturn(new CallableTest());
- $containerProphecy->has('ClosureMiddleware')->willReturn(true);
- $containerProphecy->get('ClosureMiddleware')->willReturn(function () use ($responseFactoryProphecy) {
- $response = $responseFactoryProphecy->reveal()->createResponse();
- $response->getBody()->write('Hello');
- return $response;
- });
-
- $callableResolver = new CallableResolver($containerProphecy->reveal());
- $strategy = new InvocationStrategyTest();
-
- $deferred = 'CallableTest3';
- $route = new Route(
- ['GET'],
- '/',
- $deferred,
- $responseFactoryProphecy->reveal(),
- $callableResolver,
- $containerProphecy->reveal(),
- $strategy
- );
- $route->add('ClosureMiddleware');
-
- $request = $this->createServerRequest('/');
- $response = $route->run($request);
-
- $this->assertSame('Hello', (string) $response->getBody());
+ return $app->getContainer()->get(Router::class);
}
}
diff --git a/tests/Routing/RouterTest.php b/tests/Routing/RouterTest.php
new file mode 100644
index 000000000..855febb94
--- /dev/null
+++ b/tests/Routing/RouterTest.php
@@ -0,0 +1,363 @@
+build();
+ $router = $app->getContainer()->get(Router::class);
+
+ // Define a route using the HTTP method from the data provider
+ $route = $router->{$methodName}($path, $handler);
+
+ // Verify the route is mapped correctly
+ $this->assertInstanceOf(Route::class, $route);
+ $this->assertEquals($handler, $route->getHandler());
+ $this->assertSame($path, $route->getPattern());
+
+ // Verify that all expected methods are present in the route's methods
+ foreach ($expectedMethods as $expectedMethod) {
+ $this->assertContains(
+ $expectedMethod,
+ $route->getMethods(),
+ "Method $expectedMethod not found in route methods"
+ );
+ }
+ }
+
+ public static function httpMethodProvider(): array
+ {
+ return [
+ [
+ 'any',
+ '/any',
+ function () {
+ return 'any_handler';
+ },
+ ['*'],
+ ],
+ [
+ 'delete',
+ '/delete',
+ function () {
+ return 'delete_handler';
+ },
+ ['DELETE'],
+ ],
+ [
+ 'get',
+ '/get',
+ function () {
+ return 'get_handler';
+ },
+ ['GET'],
+ ],
+ [
+ 'options',
+ '/options',
+ function () {
+ return 'options_handler';
+ },
+ ['OPTIONS'],
+ ],
+ [
+ 'patch',
+ '/patch',
+ function () {
+ return 'patch_handler';
+ },
+ ['PATCH'],
+ ],
+ [
+ 'post',
+ '/post',
+ function () {
+ return 'post_handler';
+ },
+ ['POST'],
+ ],
+ [
+ 'put',
+ '/put',
+ function () {
+ return 'put_handler';
+ },
+ ['PUT'],
+ ],
+ ];
+ }
+
+ public function testMapCreatesRoute(): void
+ {
+ $app = (new AppBuilder())->build();
+ $router = $app->getContainer()->get(Router::class);
+
+ $methods = ['GET'];
+ $path = '/test-route';
+ $handler = function () {
+ return 'Test Handler';
+ };
+
+ $route = $router->map($methods, $path, $handler);
+
+ $this->assertInstanceOf(Route::class, $route);
+ $this->assertSame($methods, $route->getMethods());
+ $this->assertSame($router->getBasePath() . $path, $route->getPattern());
+ $this->assertSame($handler, $route->getHandler());
+ }
+
+ public function testGroupCreatesRouteGroup(): void
+ {
+ $app = (new AppBuilder())->build();
+ $router = $app->getContainer()->get(Router::class);
+
+ $pattern = '/group';
+ $handler = function (RouteGroup $group) {
+ $group->map(['GET'], '/foo', 'foo_handler');
+ };
+
+ $routeGroup = $router->group($pattern, $handler);
+
+ $this->assertInstanceOf(RouteGroup::class, $routeGroup);
+ $this->assertSame($router->getBasePath() . $pattern, $routeGroup->getPrefix());
+ }
+
+ public function testGetRouteCollectorReturnsCollector(): void
+ {
+ $app = (new AppBuilder())->build();
+ $router = $app->getContainer()->get(Router::class);
+
+ $collector = $router->getRouteCollector();
+ $this->assertInstanceOf(RouteCollector::class, $collector);
+ }
+
+ public function testSetAndGetBasePath(): void
+ {
+ $app = (new AppBuilder())->build();
+ $router = $app->getContainer()->get(Router::class);
+
+ $basePath = '/base-path';
+ $router->setBasePath($basePath);
+
+ $this->assertSame($basePath, $router->getBasePath());
+ }
+
+ public function testMapWithBasePath(): void
+ {
+ $app = (new AppBuilder())->build();
+ $router = $app->getContainer()->get(Router::class);
+
+ $basePath = '/base-path';
+ $router->setBasePath($basePath);
+
+ $methods = ['GET'];
+ $path = '/test-route';
+ $handler = function () {
+ return 'Test Handler';
+ };
+
+ $route = $router->map($methods, $path, $handler);
+
+ $this->assertInstanceOf(Route::class, $route);
+ $this->assertSame($methods, $route->getMethods());
+ $this->assertSame($path, $route->getPattern());
+ $this->assertSame($handler, $route->getHandler());
+ }
+
+ public function testOptionsAnyCorsRoute(): void
+ {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $app->add(new ContentLengthMiddleware());
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->options('/{routes:.+}', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Body');
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('OPTIONS', '/test');
+
+ $response = $app->handle($request);
+ $this->assertSame('Body', (string)$response->getBody());
+ }
+
+ public function testOptionsAnyRoute(): void
+ {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $app->add(new ContentLengthMiddleware());
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->options('/{any:.*}', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('Body');
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('OPTIONS', '/test');
+
+ $response = $app->handle($request);
+ $this->assertSame('Body', (string)$response->getBody());
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('OPTIONS', '/');
+
+ $response = $app->handle($request);
+ $this->assertSame('Body', (string)$response->getBody());
+ }
+
+ public function testRouteWithParameters(): void
+ {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $app->add(new ContentLengthMiddleware());
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->get('/books/{id}', function (ServerRequestInterface $request, ResponseInterface $response, array $args) {
+ $response->getBody()->write(json_encode($args));
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/books/123');
+
+ $response = $app->handle($request);
+ $this->assertSame('{"id":"123"}', (string)$response->getBody());
+ }
+
+ public function testCustomRoute(): void
+ {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $app->add(new ContentLengthMiddleware());
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->map(['GET', 'POST'], '/books', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $response->getBody()->write('OK');
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/books');
+
+ $response = $app->handle($request);
+ $this->assertSame('OK', (string)$response->getBody());
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('POST', '/books');
+
+ $response = $app->handle($request);
+ $this->assertSame('OK', (string)$response->getBody());
+ }
+
+ public function testRegexRoute(): void
+ {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $app->add(new ContentLengthMiddleware());
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->get(
+ '/users/{id:[0-9]+}',
+ function (ServerRequestInterface $request, ResponseInterface $response, array $args) {
+ $response->getBody()->write($args['id']);
+
+ return $response;
+ }
+ );
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/users/123');
+
+ $response = $app->handle($request);
+ $this->assertSame('123', (string)$response->getBody());
+ }
+
+ public function testMultipleOptionalParameters(): void
+ {
+ $builder = new AppBuilder();
+ $app = $builder->build();
+
+ $app->add(new ContentLengthMiddleware());
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ $app->get(
+ '/news[/{year}[/{month}]]',
+ function (ServerRequestInterface $request, ResponseInterface $response, array $args) {
+ $response->getBody()->write(json_encode($args));
+
+ return $response;
+ }
+ );
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/news');
+
+ $response = $app->handle($request);
+ $this->assertSame('[]', (string)$response->getBody());
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/news/2038');
+
+ $response = $app->handle($request);
+ $this->assertSame('{"year":"2038"}', (string)$response->getBody());
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/news/2038/01');
+
+ $response = $app->handle($request);
+ $this->assertSame('{"year":"2038","month":"01"}', (string)$response->getBody());
+ }
+}
diff --git a/tests/Routing/RoutingResultsTest.php b/tests/Routing/RoutingResultsTest.php
new file mode 100644
index 000000000..ceafaed92
--- /dev/null
+++ b/tests/Routing/RoutingResultsTest.php
@@ -0,0 +1,109 @@
+ 'value1'];
+ $allowedMethods = ['GET', 'POST'];
+
+ // Create RoutingResults instance
+ $routingResults = new RoutingResults(
+ $status,
+ $route,
+ $method,
+ $uri,
+ $routeArguments,
+ $allowedMethods
+ );
+
+ $this->assertSame($status, $routingResults->getRouteStatus());
+ $this->assertSame($route, $routingResults->getRoute());
+ $this->assertSame($method, $routingResults->getMethod());
+ $this->assertSame($uri, $routingResults->getUri());
+ $this->assertSame('value1', $routingResults->getRouteArgument('arg1'));
+ $this->assertSame(null, $routingResults->getRouteArgument('nada'));
+ $this->assertSame($routeArguments, $routingResults->getRouteArguments());
+ $this->assertSame($allowedMethods, $routingResults->getAllowedMethods());
+ }
+
+ public function testGettersWithNullRoute(): void
+ {
+ // Define test parameters with null route
+ $status = RoutingResults::NOT_FOUND;
+ $method = 'POST';
+ $uri = '/not-found';
+ $routeArguments = [];
+ $allowedMethods = ['GET'];
+
+ // Create RoutingResults instance with null route
+ $routingResults = new RoutingResults(
+ $status,
+ null,
+ $method,
+ $uri,
+ $routeArguments,
+ $allowedMethods
+ );
+
+ $this->assertSame($status, $routingResults->getRouteStatus());
+ $this->assertNull($routingResults->getRoute());
+ $this->assertSame($method, $routingResults->getMethod());
+ $this->assertSame($uri, $routingResults->getUri());
+ $this->assertSame($routeArguments, $routingResults->getRouteArguments());
+ $this->assertSame($allowedMethods, $routingResults->getAllowedMethods());
+ }
+
+ public function testRoutingArgumentsFromRouteContext(): void
+ {
+ $app = (new AppBuilder())->build();
+
+ $app->add(RoutingMiddleware::class);
+ $app->add(EndpointMiddleware::class);
+
+ // Define a route with arguments
+ $app->get('/test/{id}', function (ServerRequestInterface $request, ResponseInterface $response) {
+ $args = RouteContext::fromRequest($request)->getRoutingResults()->getRouteArguments();
+ $response->getBody()->write('ID: ' . $args['id']);
+
+ return $response;
+ });
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/test/123');
+
+ $response = $app->handle($request);
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('ID: 123', (string)$response->getBody());
+ }
+}
diff --git a/tests/Routing/UrlGeneratorTest.php b/tests/Routing/UrlGeneratorTest.php
new file mode 100644
index 000000000..5234376a6
--- /dev/null
+++ b/tests/Routing/UrlGeneratorTest.php
@@ -0,0 +1,115 @@
+build();
+ $router = $app->getContainer()->get(Router::class);
+ $urlGenerator = new UrlGenerator($router);
+
+ $router->map(['GET'], '/user/{id}', 'user_handler')
+ ->setName('user.show');
+
+ // Generate relative URL
+ $url = $urlGenerator->relativeUrlFor('user.show', ['id' => 123], ['page' => 2]);
+
+ $this->assertSame('/user/123?page=2', $url);
+ }
+
+ public function testUrlFor(): void
+ {
+ $app = (new AppBuilder())->build();
+ $router = $app->getContainer()->get(Router::class);
+ $urlGenerator = new UrlGenerator($router);
+
+ $router->map(['GET'], '/user/{id}', 'user_handler')
+ ->setName('user.show');
+
+ $url = $urlGenerator->urlFor('user.show', ['id' => 456], ['sort' => 'asc']);
+
+ $this->assertSame('/user/456?sort=asc', $url);
+ }
+
+ public function testFullUrlFor(): void
+ {
+ $app = (new AppBuilder())->build();
+ $router = $app->getContainer()->get(Router::class);
+ $urlGenerator = new UrlGenerator($router);
+
+ $router->map(['GET'], '/user/{id}', 'user_handler')
+ ->setName('user.show');
+
+ $uri = $this->createMock(UriInterface::class);
+ $uri->method('getScheme')->willReturn('https');
+ $uri->method('getAuthority')->willReturn('example.com');
+
+ // Generate full URL
+ $fullUrl = $urlGenerator->fullUrlFor($uri, 'user.show', ['id' => 789], ['filter' => 'active']);
+
+ // Check generated full URL
+ $this->assertSame('https://example.com/user/789?filter=active', $fullUrl);
+ }
+
+ public function testGetNamedRouteThrowsExceptionIfRouteNotFound(): void
+ {
+ $this->expectException(UnexpectedValueException::class);
+
+ $app = (new AppBuilder())->build();
+ $router = $app->getContainer()->get(Router::class);
+ $urlGenerator = new UrlGenerator($router);
+
+ // Attempt to get a non-existent named route
+ $urlGenerator->relativeUrlFor('nonexistent.route');
+ }
+
+ public function testGetSegmentsThrowsExceptionIfDataIsMissing(): void
+ {
+ $app = (new AppBuilder())->build();
+ $router = $app->getContainer()->get(Router::class);
+ $urlGenerator = new UrlGenerator($router);
+
+ // Define a route with a parameter
+ $router->map(['GET'], '/user/{id}', 'user_handler')
+ ->setName('user.show');
+
+ $this->expectException(InvalidArgumentException::class);
+
+ // Attempt to generate a URL with missing data for the route parameter
+ $urlGenerator->relativeUrlFor('user.show');
+ }
+
+ public function testRelativeUrlForWithBasePath(): void
+ {
+ $app = (new AppBuilder())->build();
+ $router = $app->getContainer()->get(Router::class);
+ $router->setBasePath('/api');
+ $urlGenerator = new UrlGenerator($router);
+
+ $router->map(['GET'], '/user/{id}', 'user_handler')
+ ->setName('user.show');
+
+ // Generate relative URL with base path
+ $url = $urlGenerator->relativeUrlFor('user.show', ['id' => 123], ['page' => 2]);
+
+ $this->assertSame('/api/user/123?page=2', $url);
+ }
+}
diff --git a/tests/Strategies/RequestHandlerTest.php b/tests/Strategies/RequestHandlerTest.php
new file mode 100644
index 000000000..a65136e06
--- /dev/null
+++ b/tests/Strategies/RequestHandlerTest.php
@@ -0,0 +1,73 @@
+build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $invocationStrategy = $app->getContainer()->get(RequestHandler::class);
+
+ $callback = function (ServerRequestInterface $request) use ($response) {
+ return $response->withHeader('X-Result', 'Success');
+ };
+
+ $resultResponse = $invocationStrategy($callback, $request, $response, []);
+
+ $this->assertSame('Success', $resultResponse->getHeaderLine('X-Result'));
+ }
+
+ public function testInvokeWithModifiedRequest()
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/')
+ ->withHeader('X-Test', 'Modified');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $invocationStrategy = $app->getContainer()->get(RequestHandler::class);
+
+ $callback = function (ServerRequestInterface $request) use ($response) {
+ $headerValue = $request->getHeaderLine('X-Test');
+
+ return $response->withHeader('X-Test-Result', $headerValue);
+ };
+
+ $resultResponse = $invocationStrategy($callback, $request, $response, []);
+
+ $this->assertSame('Modified', $resultResponse->getHeaderLine('X-Test-Result'));
+ }
+}
diff --git a/tests/Strategies/RequestResponseArgsTest.php b/tests/Strategies/RequestResponseArgsTest.php
new file mode 100644
index 000000000..dd699df97
--- /dev/null
+++ b/tests/Strategies/RequestResponseArgsTest.php
@@ -0,0 +1,105 @@
+build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $invocationStrategy = $app->getContainer()->get(RequestResponseArgs::class);
+
+ $args = [
+ 'name' => 'John',
+ 'age' => '30',
+ ];
+
+ $callback = function ($request, $response, $name, $age) {
+ return $response->withHeader('X-Name', $name)
+ ->withHeader('X-Age', $age);
+ };
+
+ $response = $invocationStrategy($callback, $request, $response, $args);
+
+ $this->assertSame('John', $response->getHeaderLine('X-Name'));
+ $this->assertSame('30', $response->getHeaderLine('X-Age'));
+ }
+
+ public function testInvokeWithSingleArgument()
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $invocationStrategy = $app->getContainer()->get(RequestResponseArgs::class);
+
+ $args = [
+ 'name' => 'John',
+ ];
+
+ $callback = function ($request, $response, $name) {
+ return $response->withHeader('X-Name', $name);
+ };
+
+ $response = $invocationStrategy($callback, $request, $response, $args);
+
+ $this->assertSame('John', $response->getHeaderLine('X-Name'));
+ }
+
+ public function testInvokeWithoutArguments()
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $invocationStrategy = $app->getContainer()->get(RequestResponseArgs::class);
+
+ $args = [];
+
+ $callback = function ($request, $response) {
+ return $response->withHeader('X-Status', 'NoArgs');
+ };
+
+ $response = $invocationStrategy($callback, $request, $response, $args);
+
+ $this->assertSame('NoArgs', $response->getHeaderLine('X-Status'));
+ }
+}
diff --git a/tests/Strategies/RequestResponseNamedArgsTest.php b/tests/Strategies/RequestResponseNamedArgsTest.php
new file mode 100644
index 000000000..7507167e9
--- /dev/null
+++ b/tests/Strategies/RequestResponseNamedArgsTest.php
@@ -0,0 +1,169 @@
+build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $args = [];
+ $invocationStrategy = new RequestResponseNamedArgs();
+
+ $callback = function ($request, $response) {
+ return $response;
+ };
+
+ $this->assertSame($response, $invocationStrategy($callback, $request, $response, $args));
+ }
+
+ public function testCallingWithKnownArguments()
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $args = [
+ 'name' => 'world',
+ 'greeting' => 'hello',
+ ];
+
+ $invocationStrategy = new RequestResponseNamedArgs();
+
+ $callback = function ($request, $response, $greeting, string $name) use ($args) {
+ $this->assertSame($greeting, $args['greeting']);
+ $this->assertSame($name, $args['name']);
+
+ return $response;
+ };
+
+ $this->assertSame($response, $invocationStrategy($callback, $request, $response, $args));
+ }
+
+ public function testCallingWithOptionalArguments()
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $args = [
+ 'name' => 'world',
+ ];
+
+ $invocationStrategy = new RequestResponseNamedArgs();
+
+ $callback = function ($request, $response, $greeting = 'Hello', $name = 'Rob') use ($args) {
+ $this->assertSame('Hello', $greeting);
+ $this->assertSame($args['name'], $name);
+
+ return $response;
+ };
+
+ $this->assertSame($response, $invocationStrategy($callback, $request, $response, $args));
+ }
+
+ public function testCallingWithUnknownAndVariadic()
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $args = [
+ 'name' => 'world',
+ 'greeting' => 'hello',
+ ];
+
+ $invocationStrategy = new RequestResponseNamedArgs();
+
+ $callback = function ($request, $response, ...$arguments) use ($args) {
+ $this->assertSame($args, $arguments);
+
+ return $response;
+ };
+
+ $this->assertSame($response, $invocationStrategy($callback, $request, $response, $args));
+ }
+
+ public function testCallingWithMixedKnownAndUnknownParametersAndVariadic()
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $known = [
+ 'name' => 'world',
+ 'greeting' => 'hello',
+ ];
+ $unknown = [
+ 'foo' => 'foo',
+ 'bar' => 'bar',
+ ];
+ $args = array_merge($known, $unknown);
+ $invocationStrategy = new RequestResponseNamedArgs();
+
+ $callback = function ($request, $response, $name, $greeting, ...$arguments) use ($known, $unknown) {
+ $this->assertSame($name, $known['name']);
+ $this->assertSame($greeting, $known['greeting']);
+ $this->assertSame($unknown, $arguments);
+
+ return $response;
+ };
+
+ $this->assertSame($response, $invocationStrategy($callback, $request, $response, $args));
+ }
+}
diff --git a/tests/Strategies/RequestResponseTest.php b/tests/Strategies/RequestResponseTest.php
new file mode 100644
index 000000000..a72b24618
--- /dev/null
+++ b/tests/Strategies/RequestResponseTest.php
@@ -0,0 +1,79 @@
+build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $invocationStrategy = $app->getContainer()->get(RequestResponse::class);
+
+ $args = [
+ 'name' => 'John',
+ 'foo' => 'bar',
+ ];
+
+ $callback = function ($request, $response, $args) {
+ return $response
+ ->withHeader('X-Name', $args['name'])
+ ->withHeader('X-Foo', $args['foo']);
+ };
+
+ $response = $invocationStrategy($callback, $request, $response, $args);
+
+ $this->assertSame('John', $response->getHeaderLine('X-Name'));
+ $this->assertSame('bar', $response->getHeaderLine('X-Foo'));
+ }
+
+ public function testInvokeWithoutArguments()
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $invocationStrategy = $app->getContainer()->get(RequestResponse::class);
+
+ $callback = function ($request, $response) {
+ return $response->withHeader('X-Foo', 'Default');
+ };
+
+ $args = [];
+
+ $response = $invocationStrategy($callback, $request, $response, $args);
+
+ $this->assertSame('Default', $response->getHeaderLine('X-Foo'));
+ }
+}
diff --git a/tests/Strategies/RequestResponseTypedArgsTest.php b/tests/Strategies/RequestResponseTypedArgsTest.php
new file mode 100644
index 000000000..365b0b06f
--- /dev/null
+++ b/tests/Strategies/RequestResponseTypedArgsTest.php
@@ -0,0 +1,143 @@
+build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $invocationStrategy = $app->getContainer()->get(RequestResponseTypedArgs::class);
+
+ $args = [
+ 'name' => 'John',
+ ];
+
+ $callback = function ($request, $response, $name) {
+ return $response->withHeader('X-Foo', $name);
+ };
+
+ $response = $invocationStrategy($callback, $request, $response, $args);
+
+ $this->assertSame('John', $response->getHeaderLine('X-Foo'));
+ }
+
+ // https://github.com/slimphp/Slim/issues/3198
+ public function testCallingWithKnownArguments()
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $invocationStrategy = $app->getContainer()->get(RequestResponseTypedArgs::class);
+
+ $args = [
+ 'name' => 'John',
+ 'id' => '123',
+ ];
+
+ $callback = function ($request, $response, string $name, int $id) {
+ $this->assertSame('John', $name);
+ $this->assertSame(123, $id);
+
+ return $response->withHeader('X-Foo', $name);
+ };
+
+ $response = $invocationStrategy($callback, $request, $response, $args);
+
+ $this->assertSame('John', $response->getHeaderLine('X-Foo'));
+ }
+
+ public function testCallingWithOptionalArguments()
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $invocationStrategy = $app->getContainer()->get(RequestResponseTypedArgs::class);
+
+ $args = [
+ 'name' => 'world',
+ ];
+
+ $callback = function ($request, $response, string $greeting = 'Hello', string $name = 'Rob') {
+ $this->assertSame('Hello', $greeting);
+ $this->assertSame('world', $name);
+
+ return $response->withHeader('X-Foo', $name);
+ };
+
+ $response = $invocationStrategy($callback, $request, $response, $args);
+
+ $this->assertSame('world', $response->getHeaderLine('X-Foo'));
+ }
+
+ public function testCallingWithNotEnoughParameters()
+ {
+ $app = (new AppBuilder())->build();
+
+ $request = $app->getContainer()
+ ->get(ServerRequestFactoryInterface::class)
+ ->createServerRequest('GET', '/');
+
+ $response = $app->getContainer()
+ ->get(ResponseFactoryInterface::class)
+ ->createResponse();
+
+ $invocationStrategy = $app->getContainer()->get(RequestResponseTypedArgs::class);
+
+ $this->expectException(NotEnoughParametersException::class);
+ $args = [
+ 'greeting' => 'hello',
+ ];
+
+ $callback = function ($request, $response, $arguments) use ($args) {
+ $this->assertSame($args, $arguments);
+
+ return $response->withHeader('X-Foo', $args['greeting']);
+ };
+
+ $response = $invocationStrategy($callback, $request, $response, $args);
+
+ $this->assertSame('hello', $response->getHeaderLine('X-Foo'));
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
deleted file mode 100644
index aef12efae..000000000
--- a/tests/TestCase.php
+++ /dev/null
@@ -1,123 +0,0 @@
-getServerRequestFactory();
- }
-
- /**
- * @return ResponseFactoryInterface
- */
- protected function getResponseFactory(): ResponseFactoryInterface
- {
- $psr7ObjectProvider = new PSR7ObjectProvider();
- return $psr7ObjectProvider->getResponseFactory();
- }
-
- /**
- * @return StreamFactoryInterface
- */
- protected function getStreamFactory(): StreamFactoryInterface
- {
- $psr7ObjectProvider = new PSR7ObjectProvider();
- return $psr7ObjectProvider->getStreamFactory();
- }
-
- /**
- * @param ContainerInterface|null $container
- *
- * @return CallableResolverInterface
- */
- protected function getCallableResolver(?ContainerInterface $container = null): CallableResolverInterface
- {
- return new CallableResolver($container);
- }
-
- /**
- * @param RequestHandlerInterface $requestHandler
- * @param ContainerInterface|null $container
- * @param CallableResolverInterface|null $callableResolver
- *
- * @return MiddlewareDispatcher
- */
- protected function createMiddlewareDispatcher(
- RequestHandlerInterface $requestHandler,
- ?ContainerInterface $container = null,
- ?CallableResolverInterface $callableResolver = null
- ): MiddlewareDispatcher {
- return new MiddlewareDispatcher(
- $requestHandler,
- $callableResolver ?? $this->getCallableResolver($container),
- $container
- );
- }
-
- /**
- * @param string $uri
- * @param string $method
- * @param array $data
- * @return ServerRequestInterface
- */
- protected function createServerRequest(
- string $uri,
- string $method = 'GET',
- array $data = []
- ): ServerRequestInterface {
- $psr7ObjectProvider = new PSR7ObjectProvider();
- return $psr7ObjectProvider->createServerRequest($uri, $method, $data);
- }
-
- /**
- * @param int $statusCode
- * @param string $reasonPhrase
- * @return ResponseInterface
- */
- protected function createResponse(int $statusCode = 200, string $reasonPhrase = ''): ResponseInterface
- {
- $psr7ObjectProvider = new PSR7ObjectProvider();
- return $psr7ObjectProvider->createResponse($statusCode, $reasonPhrase);
- }
-
- /**
- * @param string $contents
- * @return StreamInterface
- */
- protected function createStream(string $contents = ''): StreamInterface
- {
- $psr7ObjectProvider = new PSR7ObjectProvider();
- return $psr7ObjectProvider->createStream($contents);
- }
-}
diff --git a/tests/Traits/AppTestTrait.php b/tests/Traits/AppTestTrait.php
new file mode 100644
index 000000000..163c9b6ed
--- /dev/null
+++ b/tests/Traits/AppTestTrait.php
@@ -0,0 +1,36 @@
+addDefinitions($definitions);
+
+ return $builder->build();
+ }
+
+ protected function assertJsonResponse(mixed $expected, ResponseInterface $actual, string $message = ''): void
+ {
+ self::assertThat(
+ json_decode((string)$actual->getBody(), true),
+ new IsIdentical($expected),
+ $message,
+ );
+ }
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 00d3896a4..2f73ba9dc 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -3,46 +3,7 @@
/**
* Slim Framework (https://slimframework.com)
*
- * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
+ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/
-declare(strict_types=1);
-
-use AdrianSuter\Autoload\Override\Override;
-use Slim\ResponseEmitter;
-use Slim\Routing\RouteCollector;
-use Slim\Tests\Assets\HeaderStack;
-
-$classLoader = require __DIR__ . '/../vendor/autoload.php';
-
-Override::apply($classLoader, [
- ResponseEmitter::class => [
- 'connection_status' => function (): int {
- if (isset($GLOBALS['connection_status_return'])) {
- return $GLOBALS['connection_status_return'];
- }
-
- return connection_status();
- },
- 'header' => function (string $string, bool $replace = true, int $statusCode = null): void {
- HeaderStack::push(
- [
- 'header' => $string,
- 'replace' => $replace,
- 'status_code' => $statusCode,
- ]
- );
- },
- 'headers_sent' => function (): bool {
- return false;
- }
- ],
- RouteCollector::class => [
- 'is_readable' => function (string $file): bool {
- return stripos($file, 'non-readable.cache') === false;
- },
- 'is_writable' => function (string $path): bool {
- return stripos($path, 'non-writable-directory') === false;
- }
- ]
-]);
+require_once __DIR__ . '/../vendor/autoload.php';