├── .codecov.yml ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .phpunit.result.cache ├── .styleci.yml ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── clover.xml ├── composer.json ├── composer.lock ├── config └── pipes.php ├── phpunit.xml ├── readme.md ├── routes └── api.php ├── src ├── Contracts │ └── Registrar.php ├── Events │ ├── IncomingPipeRequest.php │ └── IncomingPipeResponse.php ├── Exceptions │ ├── NoKeysSpecifiedException.php │ └── NotFoundPipeException.php ├── Facades │ └── Pipe.php ├── Http │ └── Middleware │ │ └── SubstituteBindings.php ├── Jobs │ └── ExecutePipeRequest.php ├── Kernel.php ├── LaravelPipesServiceProvider.php ├── Matching │ ├── CueValidator.php │ ├── KeyValidator.php │ ├── PatternValidator.php │ └── ValidatorInterface.php ├── Pipe.php ├── PipeAction.php ├── PipeCollection.php ├── PipeGroup.php ├── PipeParameterBinder.php ├── PipeRegistrar.php ├── Piper.php ├── Request.php ├── Response.php └── Testing │ ├── Fakes │ └── PipeFake.php │ └── MakesPipeRequests.php └── tests ├── Fixtures ├── Controllers │ └── TestController.php ├── Migrations │ └── 2014_10_12_000000_create_todos_table.php ├── Models │ └── Todo.php └── pipes.php ├── PipeRequestTest.php ├── SubstituteBindingsTest.php └── TestCase.php /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: “70…100” 9 | 10 | status: 11 | project: yes 12 | patch: yes 13 | changes: no 14 | 15 | comment: 16 | layout: "reach, diff, flags, files, footer" 17 | behavior: default 18 | require_changes: no 19 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: 'Laravel Tests' 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | tests-on-php71: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | php: [7.1] 16 | laravel: [5.8.x] 17 | 18 | name: PHP${{ matrix.php }} with Laravel ${{ matrix.laravel }} 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v1 23 | 24 | - name: Cache dependencies 25 | uses: actions/cache@v2 26 | with: 27 | path: ~/.composer/cache/files 28 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 29 | 30 | - name: Setup PHP 31 | uses: shivammathur/setup-php@v2 32 | with: 33 | php-version: ${{ matrix.php }} 34 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath 35 | coverage: none 36 | 37 | - name: Install dependencies 38 | run: | 39 | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update 40 | composer update --prefer-dist --no-interaction --no-suggest 41 | - name: Execute tests 42 | run: vendor/bin/phpunit --exclude-group=redis-auth 43 | env: 44 | REDIS_HOST: redis 45 | 46 | tests-on-php72: 47 | runs-on: ubuntu-latest 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | php: [7.2] 52 | laravel: [7.x, 6.x, 5.8.x] 53 | 54 | name: PHP${{ matrix.php }} with Laravel ${{ matrix.laravel }} 55 | 56 | steps: 57 | - name: Checkout code 58 | uses: actions/checkout@v1 59 | 60 | - name: Cache dependencies 61 | uses: actions/cache@v2 62 | with: 63 | path: ~/.composer/cache/files 64 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 65 | 66 | - name: Setup PHP 67 | uses: shivammathur/setup-php@v2 68 | with: 69 | php-version: ${{ matrix.php }} 70 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath 71 | coverage: none 72 | 73 | - name: Install dependencies 74 | run: | 75 | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update 76 | composer update --prefer-dist --no-interaction --no-suggest 77 | - name: Execute tests 78 | run: vendor/bin/phpunit --exclude-group=redis-auth 79 | env: 80 | REDIS_HOST: redis 81 | 82 | 83 | tests-on-php73: 84 | runs-on: ubuntu-latest 85 | strategy: 86 | fail-fast: false 87 | matrix: 88 | php: [7.3] 89 | laravel: [8.x, 7.x, 6.x, 5.8.x] 90 | 91 | name: PHP${{ matrix.php }} with Laravel ${{ matrix.laravel }} 92 | 93 | steps: 94 | - name: Checkout code 95 | uses: actions/checkout@v1 96 | 97 | - name: Cache dependencies 98 | uses: actions/cache@v2 99 | with: 100 | path: ~/.composer/cache/files 101 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 102 | 103 | - name: Setup PHP 104 | uses: shivammathur/setup-php@v2 105 | with: 106 | php-version: ${{ matrix.php }} 107 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath 108 | coverage: none 109 | 110 | - name: Install dependencies 111 | run: | 112 | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update 113 | composer update --prefer-dist --no-interaction --no-suggest 114 | - name: Execute tests 115 | run: vendor/bin/phpunit --exclude-group=redis-auth 116 | env: 117 | REDIS_HOST: redis 118 | 119 | tests-on-php74: 120 | runs-on: ubuntu-latest 121 | strategy: 122 | fail-fast: false 123 | matrix: 124 | php: [7.4] 125 | laravel: [8.x, 7.x, 6.x, 5.8.x] 126 | 127 | name: PHP${{ matrix.php }} with Laravel ${{ matrix.laravel }} 128 | 129 | steps: 130 | - name: Checkout code 131 | uses: actions/checkout@v1 132 | 133 | - name: Cache dependencies 134 | uses: actions/cache@v2 135 | with: 136 | path: ~/.composer/cache/files 137 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 138 | 139 | - name: Setup PHP 140 | uses: shivammathur/setup-php@v2 141 | with: 142 | php-version: ${{ matrix.php }} 143 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath 144 | coverage: none 145 | 146 | - name: Install dependencies 147 | run: | 148 | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update 149 | composer update --prefer-dist --no-interaction --no-suggest 150 | - name: Execute tests 151 | run: vendor/bin/phpunit --exclude-group=redis-auth 152 | env: 153 | REDIS_HOST: redis 154 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /vendor -------------------------------------------------------------------------------- /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | C:37:"PHPUnit\Runner\DefaultTestResultCache":5534:{a:2:{s:7:"defects";a:24:{s:128:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_identify_aliases_of_pipes_even_when_the_request_does_only_start_with_the_alias";i:3;s:78:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_bind_models_to_pipe_requests";i:4;s:73:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_returns_a_ok_200er_response";i:3;s:100:"Mshule\LaravelPipes\Tests\PipeRequestTest::the_response_can_be_changed_through_the_response_resolver";i:3;s:125:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_fires_an_incoming_pipe_response_event_when_a_response_is_returned_by_the_kernel";i:3;s:76:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_resolve_pipes_to_callbacks";i:3;s:77:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_matches_everything_to_lowercase";i:3;s:85:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_resolve_pipes_to_controller_actions";i:3;s:114:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_resolve_pipes_to_controller_actions_through_using_the_fluent_api";i:3;s:80:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_resolve_pipes_with_middlewares";i:3;s:111:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_resolve_grouped_pipes_and_pass_all_key_to_the_contained_pipes";i:3;s:127:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_add_fallback_pipes_to_handle_any_request_which_could_not_be_matched_otherwise";i:3;s:74:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_match_dynamic_parameters";i:4;s:140:"Mshule\LaravelPipes\Tests\PipeRequestTest::its_dynamic_pattern_match_checks_first_for_placeholder_name_in_request_then_for_default_cue_value";i:3;s:83:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_match_multiple_dynamic_parameters";i:4;s:89:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_match_dynamic_parameters_to_any_request";i:3;s:123:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_doesnt_match_dynamic_parameters_if_the_request_contains_only_parts_of_the_cue";i:3;s:84:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_add_conditions_to_pipe_definitions";i:3;s:70:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_add_aliases_to_pipes";i:3;s:81:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_add_aliases_predefined_to_pipes";i:3;s:71:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_load_pipes_from_files";i:3;s:84:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_ignores_global_helper_functions_as_cue";i:3;s:110:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_does_not_match_one_character_to_a_pipe_if_it_is_no_dynamic_param";i:3;s:85:"Mshule\LaravelPipes\Tests\SubstituteBindingsTest::it_can_bind_models_to_pipe_requests";i:3;}s:5:"times";a:26:{s:106:"Mshule\LaravelPipes\Tests\PipeRequestTest::a_not_found_pipe_exception_is_thrown_if_no_controller_was_found";d:0.068;s:73:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_returns_a_ok_200er_response";d:0.015;s:100:"Mshule\LaravelPipes\Tests\PipeRequestTest::the_response_can_be_changed_through_the_response_resolver";d:0.005;s:122:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_fires_an_incoming_pipe_request_event_when_a_request_is_handled_by_the_kernel";d:0.006;s:125:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_fires_an_incoming_pipe_response_event_when_a_response_is_returned_by_the_kernel";d:0.005;s:76:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_resolve_pipes_to_callbacks";d:0.005;s:77:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_matches_everything_to_lowercase";d:0.004;s:85:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_resolve_pipes_to_controller_actions";d:0.004;s:114:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_resolve_pipes_to_controller_actions_through_using_the_fluent_api";d:0.004;s:80:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_resolve_pipes_with_middlewares";d:0.005;s:111:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_resolve_grouped_pipes_and_pass_all_key_to_the_contained_pipes";d:0.005;s:127:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_add_fallback_pipes_to_handle_any_request_which_could_not_be_matched_otherwise";d:0.007;s:74:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_match_dynamic_parameters";d:0.004;s:140:"Mshule\LaravelPipes\Tests\PipeRequestTest::its_dynamic_pattern_match_checks_first_for_placeholder_name_in_request_then_for_default_cue_value";d:0.005;s:83:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_match_multiple_dynamic_parameters";d:0.007;s:89:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_match_dynamic_parameters_to_any_request";d:0.007;s:84:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_add_conditions_to_pipe_definitions";d:0.006;s:70:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_add_aliases_to_pipes";d:0.004;s:81:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_add_aliases_predefined_to_pipes";d:0.004;s:78:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_bind_models_to_pipe_requests";d:0.465;s:71:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_load_pipes_from_files";d:0.004;s:84:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_ignores_global_helper_functions_as_cue";d:0.004;s:128:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_can_identify_aliases_of_pipes_even_when_the_request_does_only_start_with_the_alias";d:0.004;s:123:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_doesnt_match_dynamic_parameters_if_the_request_contains_only_parts_of_the_cue";d:0.006;s:110:"Mshule\LaravelPipes\Tests\PipeRequestTest::it_does_not_match_one_character_to_a_pipe_if_it_is_no_dynamic_param";d:0.005;s:85:"Mshule\LaravelPipes\Tests\SubstituteBindingsTest::it_can_bind_models_to_pipe_requests";d:0.062;}}} -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | - 7.3 7 | - 7.4 8 | 9 | env: 10 | global: 11 | - coverage=yes 12 | matrix: 13 | - setup=basic 14 | - setup=stable 15 | - setup=lowest 16 | 17 | before_install: 18 | - composer config discard-changes true 19 | 20 | before_script: 21 | - if [[ $setup = 'stable' ]]; then travis_retry composer update --prefer-dist --no-interaction --no-suggest --prefer-stable; fi 22 | - if [[ $setup = 'lowest' ]]; then travis_retry composer update --prefer-dist --no-interaction --no-suggest --prefer-lowest --prefer-stable; fi 23 | 24 | script: 25 | - if [[ $coverage = 'yes' ]]; then vendor/bin/phpunit -c phpunit.xml --coverage-clover=coverage.clover; fi 26 | - if [[ $coverage = 'no' ]]; then vendor/bin/phpunit -c phpunit.xml; fi 27 | 28 | after_script: 29 | - if [[ $coverage = 'yes' ]]; then wget https://scrutinizer-ci.com/ocular.phar; php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v1.0.1](https://github.com/m-shule/laravel-pipes/tree/v1.0.1) (2019-05-22) 4 | [Full Changelog](https://github.com/m-shule/laravel-pipes/compare/v1.0...v1.0.1) 5 | 6 | **Merged pull requests:** 7 | 8 | - Fix Param matching [\#3](https://github.com/m-shule/laravel-pipes/pull/3) ([Naoray](https://github.com/Naoray)) 9 | - Apply fixes from StyleCI [\#2](https://github.com/m-shule/laravel-pipes/pull/2) ([Naoray](https://github.com/Naoray)) 10 | - Apply fixes from StyleCI [\#1](https://github.com/m-shule/laravel-pipes/pull/1) ([Naoray](https://github.com/Naoray)) 11 | 12 | ## [v1.0](https://github.com/m-shule/laravel-pipes/tree/v1.0) (2019-05-20) 13 | 14 | 15 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/mshule/laravel-pipes). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 M-Shule Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /clover.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mshule/laravel-pipes", 3 | "description": "A description for laravel-pipes.", 4 | "type": "package", 5 | "license": "MIT", 6 | "keywords": [ 7 | "laravel" 8 | ], 9 | "authors": [ 10 | { 11 | "name": "Krishan Koenig, Otieno Julie", 12 | "email": "krishan.koenig@googlemail.com" 13 | } 14 | ], 15 | "require": { 16 | "illuminate/support": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0", 17 | "illuminate/http": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0", 18 | "illuminate/routing": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Mshule\\LaravelPipes\\": "./src" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Mshule\\LaravelPipes\\Tests\\": "tests" 28 | } 29 | }, 30 | "scripts": { 31 | "test": "vendor/bin/phpunit" 32 | }, 33 | "extra": { 34 | "laravel": { 35 | "providers": [ 36 | "Mshule\\LaravelPipes\\LaravelPipesServiceProvider" 37 | ], 38 | "aliases": { 39 | "Pipe": "Mshule\\LaravelPipes\\Facades\\Pipe" 40 | } 41 | } 42 | }, 43 | "require-dev": { 44 | "orchestra/testbench": "~3.8.0|^4.0|^5.0|^6.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /config/pipes.php: -------------------------------------------------------------------------------- 1 | false, 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Namespace 22 | |-------------------------------------------------------------------------- 23 | | 24 | | This value determines the default namespace of your pipes. By default 25 | | it is set to `App\Pipes\Controllers`. 26 | | 27 | */ 28 | 29 | 'namespace' => 'App\Pipes\Controllers', 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Incoming Request Path 34 | |-------------------------------------------------------------------------- 35 | | 36 | | This value is the name of your request path for handling incoming 37 | | api requests which will then be dispatched through your pipes. 38 | | You only need this route as endpoint for your api provider. 39 | | 40 | */ 41 | 42 | 'incoming_request_path' => 'handle-notification', 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Queue 47 | |-------------------------------------------------------------------------- 48 | | 49 | | Sets the queue on which the job is run which boots the 50 | | akernel and starts the whole laravel-pipe lifecycle 51 | | 52 | */ 53 | 54 | 'queue' => env('PIPE_QUEUE', 'default'), 55 | ]; 56 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | src/ 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # laravel-pipes 2 | 3 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 4 | [![Build Status](https://travis-ci.org/m-shule/laravel-pipes.svg?branch=master)](https://travis-ci.org/m-shule/laravel-pipes) 5 | [![codecov](https://codecov.io/gh/m-shule/laravel-pipes/branch/master/graph/badge.svg)](https://codecov.io/gh/m-shule/laravel-pipes) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/mshule/laravel-pipes.svg?style=flat-square)](https://packagist.org/packages/mshule/laravel-pipes) 7 | 8 | Handling notifications from API's is simple when they only require to handle one type of notification, but if you have to handle multiple requests e.g. from an SMS API it can get messy. Similar to [Botman](https://botman.io)'s hear() method, this package provides a slightly different approach which could be used as part of another Botman like implementation. This package offers a similar API as you are used to from the [Laravel Routes](https://laravel.com/docs/5.8/routing). 9 | 10 | ## Install 11 | 12 | `composer require mshule/laravel-pipes` 13 | 14 | The incoming web request will be handled by `your_app_domain` + whatever you put in the `pipes.incoming_request_path` config. By default, the path will result in `your_app_domain/handle-notification`. 15 | 16 | **Optional: Create a separate route file for your pipes.** 17 | 18 | 1. add a new file `routes/pipes.php` 19 | 2. set the `pipes.load_routes_file` to `true` 20 | 21 | ## Usage 22 | 23 | To get an overview of all functionalities this package offers, you can check the `tests/PipeRequestTest.php`. 24 | 25 | ### Handling Pipes 26 | 27 | Pipes are matched by the keys and values of the request's data attributes. 28 | 29 | ```php 30 | // define pipe match for `foo` => `bar` 31 | // key, value, action 32 | Pipe::match('foo', 'bar', function () { 33 | return 'matched'; 34 | }); 35 | 36 | // same as 37 | Pipe::match('foo:bar', function () { 38 | return 'matched'; 39 | }) 40 | 41 | $this->pipe(['foo' => 'bar']) 42 | ->assertSee('matched'); // true 43 | ``` 44 | 45 | Attributes can be bound dynamically to the pipe-request. 46 | 47 | ```php 48 | Pipe::match('foo:{bar}', function ($bar) { 49 | return $bar; 50 | }); 51 | 52 | $this->pipe(['foo' => 'bar']) 53 | ->assertSee('bar'); // true 54 | 55 | $this->pipe(['foo' => 'other']) 56 | ->assertSee('other'); // true 57 | ``` 58 | 59 | Instead of handling all pipe requests inside a callback, you can also redirect to a controller action. 60 | 61 | ```php 62 | Pipe::match('foo:{bar}', 'SomeController@index'); 63 | ``` 64 | 65 | If you want to handle multiple requests with different attribute keys you can use the `Pipe::any()` method. 66 | 67 | ```php 68 | Pipe::any('{bar}', 'SomeController@index'); 69 | ``` 70 | 71 | ### Other Options 72 | 73 | **alias()** 74 | Sometimes the user might have a typo in their message or you simply want to have different cues available to trigger a Pipe. 75 | 76 | ```php 77 | Pipe::any('bar', 'FooBarController') 78 | ->alias(['ba', 'b-r', 'bas']); 79 | ``` 80 | 81 | The `FooBarController` will now be called upon `ba`, `b-r`, `bas` or as originally intended on `bar`. 82 | 83 | **namespace()** 84 | As you have probably noted the `routes/pipes.php` file is bound to a namespace configurable in the `config/pipes.php`. If you want to define a group with a different namespace, you can use the `namespace()` method: 85 | 86 | ```php 87 | Pipe::middleware('pipe') 88 | ->namespace(config('pipes.namespace')) 89 | ->group(function () { 90 | // define your namespaced pipes here 91 | }); 92 | ``` 93 | 94 | **key()** 95 | Like demonstrated in the first section of the *Handling Pipes* documentation, you can define Pipe routes in man different ways. 96 | 97 | ```php 98 | Pipe::match('foo', 'bar', function () {}); 99 | 100 | // same as 101 | Pipe::match('foo:bar', function () {}); 102 | ``` 103 | 104 | There is a third option to specify the `key` of a Pipe by using the `key()` method. 105 | 106 | ```php 107 | Pipe::key('foo')->match('bar', function () {}); 108 | ``` 109 | 110 | The key method is handy if you have got several pipe routes which react to the same key. 111 | 112 | ```php 113 | Pipe::key('text') 114 | ->group(function () { 115 | // all pipe definitions within here will check for the `text` as key in the incoming request 116 | Pipe::match('some-text', function () {}); 117 | }); 118 | ``` 119 | 120 | **where()** 121 | To further specify which request should be sent to a specific handler you can define conditions on each pipe like you are used to with [Laravel routes](https://laravel.com/docs/5.8/routing#parameters-regular-expression-constraints). 122 | 123 | ```php 124 | Pipe::any('{foo}', function ($foo) { 125 | return $foo; 126 | })->where('foo', 'bar'); 127 | 128 | Pipe::any('{foo}', function ($foo) { 129 | return $foo; 130 | })->where('foo', '[a-z]+'); 131 | ``` 132 | 133 | **Understanding Pipe Life Cycle** 134 | 135 | The laravel-pipes lifecycle starts with a `post` request which is sent to the `pipes.incoming_request_path`. The `ExecutePipeRequest` Job is dispatched and an HTTP response returned - this is important since the pipe request is handled asynchronously if you have another queue driver than `sync`. In the Job, the `$request` is passed to the Pipe-Kernel's `handle()` method where it is passed through the global pipe-middlewares. The request is matched with the registered pipes and if a match is found the response is returned, otherwise a `NotFoundPipeException` is thrown. 136 | 137 | **Define the queue** 138 | 139 | As explained in the section above, a job is triggered to start the pipe-lifecycle. With the `pipes.queue` option you can define a seperate queue to run the pipe job on. 140 | 141 | **Testing Pipes** 142 | 143 | This package provides a simple trait to perform pipe requests. The `MakesPipeRequests` Trait provides a `pipe()` method to perform a pipe-request. The method fires a `post` request to the specified endpoint in `pipes.incoming_request_path`, but it is much easier to write `$this->pipe(...)` than `$this->post(config('pipes.incoming_request_path), [...])`. 144 | 145 | Since the pipe request is executed through a job, you have to use the `Pipe::fake()` method to get access to your responses. 146 | 147 | ```php 148 | Pipe::fake(); 149 | 150 | $this->pipe(...); 151 | 152 | Pipe::assertResponded(function ($response) { 153 | $response->assertOk() 154 | ->assertSee(...); 155 | }); 156 | ``` 157 | 158 | Behind the scenes the `Pipe::fake()` method simply triggers the `Event::fake()` with the `IncomingPipeRequest` and `IncomingPipeResonse` events. 159 | 160 | ## Testing 161 | 162 | Run the tests with: 163 | 164 | ```bash 165 | vendor/bin/phpunit 166 | ``` 167 | 168 | ## Changelog 169 | 170 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 171 | 172 | ## Contributing 173 | 174 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 175 | 176 | ## Security 177 | 178 | If you discover any security-related issues, please email DummyAuthorEmail instead of using the issue tracker. 179 | 180 | ## License 181 | 182 | The MIT License (MIT). Please see [License File](/LICENSE.md) for more information. 183 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | onQueue(config('pipes.queue')); 12 | 13 | return Pipe::response($request); 14 | }); 15 | -------------------------------------------------------------------------------- /src/Contracts/Registrar.php: -------------------------------------------------------------------------------- 1 | request = $request; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Events/IncomingPipeResponse.php: -------------------------------------------------------------------------------- 1 | response = $response; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Exceptions/NoKeysSpecifiedException.php: -------------------------------------------------------------------------------- 1 | method()} {$request->url()}", $previous, $headers, $code); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Facades/Pipe.php: -------------------------------------------------------------------------------- 1 | 0 24 | ? $eventsToFake 25 | : [IncomingPipeRequest::class, IncomingPipeResponse::class] 26 | ); 27 | 28 | static::swap($fake = new PipeFake()); 29 | 30 | return $fake; 31 | } 32 | 33 | /** 34 | * Get the registered name of the component. 35 | * 36 | * @return string 37 | */ 38 | protected static function getFacadeAccessor() 39 | { 40 | return 'piper'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Http/Middleware/SubstituteBindings.php: -------------------------------------------------------------------------------- 1 | pipe()); 20 | 21 | Pipe::substituteImplicitBindings($pipe); 22 | 23 | return $next($request); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Jobs/ExecutePipeRequest.php: -------------------------------------------------------------------------------- 1 | data = $data; 32 | } 33 | 34 | /** 35 | * Execute the job. 36 | */ 37 | public function handle() 38 | { 39 | $kernel = resolve(Kernel::class); 40 | $request = Request::reconstruct($this->data); 41 | 42 | event(new IncomingPipeRequest($request)); 43 | 44 | $response = $kernel->handle($request); 45 | 46 | event(new IncomingPipeResponse($response)); 47 | 48 | $kernel->terminate($request, $response); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | [ 30 | \Mshule\LaravelPipes\Http\Middleware\SubstituteBindings::class, 31 | ], 32 | ]; 33 | 34 | /** 35 | * The application's route middleware. 36 | * 37 | * @var array 38 | */ 39 | protected $routeMiddleware = [ 40 | 'bindings' => \Mshule\LaravelPipes\Http\Middleware\SubstituteBindings::class, 41 | ]; 42 | 43 | /** 44 | * The priority-sorted list of middleware. 45 | * 46 | * Forces non-global middleware to always be in the given order. 47 | * 48 | * @var array 49 | */ 50 | protected $middlewarePriority = [ 51 | \Mshule\LaravelPipes\Http\Middleware\SubstituteBindings::class, 52 | ]; 53 | 54 | /** 55 | * Create a new PipeRequestHandler instance. 56 | * 57 | * @param \Illuminate\Contracts\Foundation\Application $app 58 | * @param \Mshule\LaravelPipes\Piper $piper 59 | */ 60 | public function __construct(Application $app, Piper $piper) 61 | { 62 | $this->piper = $piper; 63 | 64 | parent::__construct($app, $piper); 65 | } 66 | 67 | /** 68 | * Handle an incoming HTTP request. 69 | * 70 | * @param \Mshule\LaravelPipes\Request $request 71 | * 72 | * @return \Illuminate\Http\Response 73 | */ 74 | public function handle($request) 75 | { 76 | try { 77 | $response = $this->sendRequestThroughPipes($request); 78 | } catch (NotFoundPipeException $e) { 79 | throw new NotFoundPipeException($request); 80 | } catch (Exception $e) { 81 | $this->reportException($e); 82 | 83 | $response = $this->renderException($request, $e); 84 | } catch (Throwable $e) { 85 | $this->reportException($e = new FatalThrowableError($e)); 86 | 87 | $response = $this->renderException($request, $e); 88 | } 89 | 90 | return Response::from($response); 91 | } 92 | 93 | /** 94 | * Send the given request through the middleware / pipes. 95 | * 96 | * @param \Illuminate\Http\Request $request 97 | * 98 | * @return \Illuminate\Http\Response 99 | */ 100 | protected function sendRequestThroughPipes($request) 101 | { 102 | $this->app->instance('request', $request); 103 | 104 | Facade::clearResolvedInstance('request'); 105 | 106 | $this->bootstrap(); 107 | 108 | return (new Pipeline($this->app)) 109 | ->send($request) 110 | ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) 111 | ->then($this->dispatchToPiper()); 112 | } 113 | 114 | /** 115 | * Get the pipe dispatcher callback. 116 | * 117 | * @return \Closure 118 | */ 119 | protected function dispatchToPiper() 120 | { 121 | return function ($request) { 122 | $this->app->instance('request', $request); 123 | 124 | return $this->piper->dispatch($request); 125 | }; 126 | } 127 | 128 | /** 129 | * Gather the pipe middleware for the given request. 130 | * 131 | * @param \Illuminate\Http\Request $request 132 | * 133 | * @return array 134 | */ 135 | protected function gatherRouteMiddleware($request) 136 | { 137 | if ($pipe = $request->route()) { 138 | return $this->piper->gatherRouteMiddleware($pipe); 139 | } 140 | 141 | return []; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/LaravelPipesServiceProvider.php: -------------------------------------------------------------------------------- 1 | performBindings(); 16 | 17 | $this->publishes([ 18 | __DIR__.'/../config/pipes.php' => config_path('pipes.php'), 19 | ]); 20 | } 21 | 22 | /** 23 | * Perform all needed bindings. 24 | */ 25 | protected function performBindings() 26 | { 27 | $this->app->singleton('piper', function ($app) { 28 | return new Piper($app); 29 | }); 30 | 31 | $this->app->singleton(Kernel::class, function ($app) { 32 | return new Kernel($app, resolve('piper')); 33 | }); 34 | 35 | $this->app->alias('piper', \Mshule\LaravelPipes\Piper::class); 36 | 37 | $this->app->bind('pipe_any', function () { 38 | return '*any*'; 39 | }); 40 | } 41 | 42 | /** 43 | * Bootstrap services. 44 | */ 45 | public function boot() 46 | { 47 | $this->mergeConfigFrom( 48 | __DIR__.'/../config/pipes.php', 49 | 'pipes' 50 | ); 51 | 52 | $this->loadRoutes(); 53 | } 54 | 55 | /** 56 | * Load all routes for using pipes. 57 | */ 58 | protected function loadRoutes() 59 | { 60 | $this->loadRoutesFrom(__DIR__.'/../routes/api.php'); 61 | 62 | if (config('pipes.load_routes_file')) { 63 | $this->mapPipeRoutes(); 64 | } 65 | } 66 | 67 | /** 68 | * Define the "pipe" routes for the application. 69 | * 70 | * These routes are typically stateless. 71 | */ 72 | protected function mapPipeRoutes() 73 | { 74 | Pipe::middleware('pipe') 75 | ->namespace(config('pipes.namespace')) 76 | ->group(base_path('routes/pipes.php')); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Matching/CueValidator.php: -------------------------------------------------------------------------------- 1 | cueStartsWithPlaceholder()) { 25 | return true; 26 | } 27 | 28 | $any = resolve('pipe_any'); 29 | $keys = $pipe->key() === $any ? $request->all() : $request->only($pipe->key()); 30 | $values = array_map('strtolower', array_values($keys)); 31 | 32 | array_push($values, $any); 33 | 34 | $matched = collect($values)->contains(function ($value) use ($pipe) { 35 | if (! $pipe->cueContainsPlaceholder()) { 36 | return Str::startsWith($value, $pipe->cue()); 37 | } 38 | 39 | if (! $pipe->cueStartsWithPlaceholder()) { 40 | return Str::startsWith($value, trim(Str::before($pipe->cue(), '{'))); 41 | } 42 | 43 | // to be able to match starting strings with $cue and including a 44 | // param `trigger {param}` inside the cue we will figure out 45 | // which string is longer and use this for our truth test. 46 | [$haystack, $needle] = strlen($value) >= strlen($pipe->cue()) 47 | ? [$value, $pipe->cue()] 48 | : [$pipe->cue(), $value]; 49 | 50 | return Str::startsWith($haystack, $needle); 51 | }); 52 | 53 | if ($matched || ! $pipe->hasAlias()) { 54 | return $matched; 55 | } 56 | 57 | // if a pipe has alias specified we will check whether 58 | // the alias and request values have a common subset 59 | return collect($values)->contains(function ($value) use ($pipe) { 60 | return $pipe->hasAlias($value); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Matching/KeyValidator.php: -------------------------------------------------------------------------------- 1 | keys(); 21 | 22 | array_push($keys, resolve('pipe_any')); 23 | 24 | return in_array($pipe->key(), $keys); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Matching/PatternValidator.php: -------------------------------------------------------------------------------- 1 | compiled->getRegex(), $pipe->path($request)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Matching/ValidatorInterface.php: -------------------------------------------------------------------------------- 1 | cue = strtolower($cue); 63 | $this->key = strtolower($key); 64 | $this->alias = Arr::get($this->action, 'alias', []); 65 | } 66 | 67 | /** 68 | * Parse the route action into a standard array. 69 | * 70 | * @param callable|array|null $action 71 | * 72 | * @throws \UnexpectedValueException 73 | * 74 | * @return array 75 | */ 76 | protected function parseAction($action) 77 | { 78 | return PipeAction::parse($this->cue, $action); 79 | } 80 | 81 | /** 82 | * Set the piper instance on the pipe. 83 | * 84 | * @param \Mshule\LaravelPipes\Piper $piper 85 | * 86 | * @return $this 87 | */ 88 | public function setPiper(Piper $piper) 89 | { 90 | $this->piper = $piper; 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * Get the cue associated with the pipe. 97 | * 98 | * @return string 99 | */ 100 | public function cue() 101 | { 102 | return $this->cue; 103 | } 104 | 105 | /** 106 | * Get the key the pipe responds to. 107 | * 108 | * @return array 109 | */ 110 | public function key() 111 | { 112 | if ('placeholder' === $this->key) { 113 | $this->key = null; 114 | } 115 | 116 | if ($this->key) { 117 | return $this->key; 118 | } 119 | 120 | $key = $this->key ?? Arr::get($this->action, 'key', null); 121 | 122 | if (! $key) { 123 | throw new NoKeysSpecifiedException("No key were defined for {$this->pipe->cue()}"); 124 | } 125 | 126 | return $this->key = strtolower($key); 127 | } 128 | 129 | /** 130 | * Bind the pipe to a given request for execution. 131 | * 132 | * @param \Illuminate\Http\Request $request 133 | * 134 | * @return $this 135 | */ 136 | public function bind(Request $request) 137 | { 138 | $this->compileRoute(); 139 | 140 | $this->parameters = (new PipeParameterBinder($this)) 141 | ->parameters($request); 142 | 143 | $this->originalParameters = $this->parameters; 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * Get the pipe validators for the instance. 150 | * 151 | * @return array 152 | */ 153 | public static function getValidators() 154 | { 155 | if (isset(static::$validators)) { 156 | return static::$validators; 157 | } 158 | 159 | // To match the pipe, we will use a chain of responsibility pattern with the 160 | // validator implementations. We will spin through each one making sure it 161 | // passes and then we will know if the pipe as a whole matches request. 162 | return static::$validators = [ 163 | new KeyValidator(), new CueValidator(), new PatternValidator(), 164 | ]; 165 | } 166 | 167 | /** 168 | * Get the full path for the pipe with the replaced attributes from the request. 169 | * 170 | * @param Request $request 171 | */ 172 | public function path(Request $request) 173 | { 174 | $replacements = collect($this->parameterNames())->map(function ($param) use ($request) { 175 | return $request->{$this->paramKey($param)}; 176 | })->toArray(); 177 | 178 | $path = preg_replace_array('/\\{[a-zA-Z]+\\}/', $replacements, $this->uri()); 179 | 180 | return Str::startsWith($path, '/') ? $path : '/'.$path; 181 | } 182 | 183 | /** 184 | * Set a regular expression requirement on the route. 185 | * 186 | * @param array|string $name 187 | * 188 | * @return $this 189 | */ 190 | public function alias($name) 191 | { 192 | foreach ($this->parseAlias($name) as $name) { 193 | $this->alias[] = strtolower($name); 194 | } 195 | 196 | return $this; 197 | } 198 | 199 | /** 200 | * Parse arguments to the alias method into an array. 201 | * 202 | * @param array|string $name 203 | * @param string $expression 204 | * 205 | * @return array 206 | */ 207 | protected function parseAlias($name) 208 | { 209 | return is_array($name) ? $name : [$name]; 210 | } 211 | 212 | /** 213 | * Checks if a pipe does have alias specified. 214 | * 215 | * @param string|null $key 216 | * 217 | * @return bool 218 | */ 219 | public function hasAlias($key = null) 220 | { 221 | if (is_null($key)) { 222 | return (bool) count($this->getAlias()); 223 | } 224 | 225 | return collect($this->getAlias())->contains(function ($alias) use ($key) { 226 | return Str::startsWith($key, $alias); 227 | }); 228 | } 229 | 230 | /** 231 | * Get all alias of a pipe. 232 | * 233 | * @return array 234 | */ 235 | public function getAlias() 236 | { 237 | return $this->alias; 238 | } 239 | 240 | /** 241 | * Checks if cue of this pipe starts with a placeholder. 242 | * 243 | * @return bool 244 | */ 245 | public function cueStartsWithPlaceholder() 246 | { 247 | return Str::startsWith($this->cue(), '{'); 248 | } 249 | 250 | /** 251 | * Checks if the pipes cue contains placeholders. 252 | * 253 | * @return bool 254 | */ 255 | public function cueContainsPlaceholder() 256 | { 257 | return Str::contains($this->cue(), ['{', '}']); 258 | } 259 | 260 | /** 261 | * Checks if cue only contains one placeholder. 262 | * 263 | * @return bool 264 | */ 265 | public function cueIsPlaceholder() 266 | { 267 | return preg_match('/^\\{[A-Za-z]+\\}$/', $this->cue()); 268 | } 269 | 270 | /** 271 | * Get param key for the path matching of a pipe. 272 | * 273 | * @return string 274 | */ 275 | public function paramKey($param) 276 | { 277 | if ($this->key() === resolve('pipe_any')) { 278 | return $param; 279 | } 280 | 281 | return $this->cueIsPlaceholder() 282 | ? $this->key() 283 | : $param ?? $this->key(); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/PipeAction.php: -------------------------------------------------------------------------------- 1 | function () use ($cue) { 21 | throw new LogicException("Pipe for [{$cue}] has no action."); 22 | }]; 23 | } 24 | 25 | /** 26 | * Make an action for an invokable controller. 27 | * 28 | * @param string $action 29 | * 30 | * @throws \UnexpectedValueException 31 | * 32 | * @return string 33 | */ 34 | protected static function makeInvokable($action) 35 | { 36 | if (! method_exists($action, '__invoke')) { 37 | throw new UnexpectedValueException("Invalid pipe action: [{$action}]."); 38 | } 39 | 40 | return $action.'@__invoke'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/PipeCollection.php: -------------------------------------------------------------------------------- 1 | addToCollections($pipe); 41 | 42 | return $pipe; 43 | } 44 | 45 | /** 46 | * Add the given pipe to the arrays of pipes. 47 | * 48 | * @param \Mshule\LaravelPipes\Pipe $pipe 49 | */ 50 | protected function addToCollections($pipe) 51 | { 52 | $cue = $pipe->cue(); 53 | $key = $pipe->key(); 54 | 55 | $this->pipes[$key][$cue] = $pipe; 56 | $this->allPipes[$key.$cue] = $pipe; 57 | } 58 | 59 | /** 60 | * Find the first pipe matching a given request. 61 | * 62 | * @param \Illuminate\Http\Request $request 63 | * @param \Mshule\LaravelPipes\Pipe $pipe 64 | * 65 | * @throws Mshule\LaravelPipes\Exceptions\NotFoundPipeException 66 | */ 67 | public function match(Request $request) 68 | { 69 | $pipes = $this->get($request->keys()); 70 | 71 | // First, we will see if we can find a matching pipe for this current request 72 | // method. If we can, great, we can just return it so that it can be called 73 | // by the consumer. 74 | $pipe = $this->matchAgainstPipes($pipes, $request); 75 | 76 | if (! is_null($pipe)) { 77 | return $pipe->bind($request); 78 | } 79 | 80 | throw new NotFoundPipeException($request); 81 | } 82 | 83 | /** 84 | * Get pipes from the collection by attribute. 85 | * 86 | * @param string|null $ 87 | * 88 | * @return array 89 | */ 90 | public function get($keys = []) 91 | { 92 | if (0 === count($keys)) { 93 | return $this->getPipes(); 94 | } 95 | 96 | array_push($keys, resolve('pipe_any')); 97 | 98 | return collect($keys)->map(function ($key) { 99 | return Arr::get($this->pipes, $key, []); 100 | }) 101 | ->flatten() 102 | ->toArray(); 103 | } 104 | 105 | /** 106 | * Determine if a pipe in the array matches the request. 107 | * 108 | * @param array $pipes 109 | * @param \Illuminate\Http\Request $request 110 | * 111 | * @return \Mshule\LaravelPipes\Pipe $pipe|null 112 | */ 113 | protected function matchAgainstPipes(array $pipes, $request) 114 | { 115 | [$fallbacks, $pipes] = collect($pipes)->partition(function ($route) { 116 | return $route->isFallback; 117 | }); 118 | 119 | return $pipes->merge($fallbacks)->first(function ($value) use ($request) { 120 | return $this->fetchMatchedPipes($value, $request); 121 | }); 122 | } 123 | 124 | protected function fetchMatchedPipes($value, $request) 125 | { 126 | if (!$value->matches($request)) { 127 | foreach ([new KeyValidator(), new CueValidator(), new PatternValidator()] as $validator) { 128 | if (! $validator->matches($value, $request)) { 129 | return false; 130 | } 131 | } 132 | } 133 | 134 | return true; 135 | } 136 | 137 | /** 138 | * Get all of the pipes in the collection. 139 | * 140 | * @return array 141 | */ 142 | public function getPipes() 143 | { 144 | return array_values($this->allPipes); 145 | } 146 | 147 | /** 148 | * Get an iterator for the items. 149 | * 150 | * @return \ArrayIterator 151 | */ 152 | public function getIterator() 153 | { 154 | return new ArrayIterator($this->getPipes()); 155 | } 156 | 157 | /** 158 | * Count the number of items in the collection. 159 | * 160 | * @return int 161 | */ 162 | public function count() 163 | { 164 | return count($this->getPipes()); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/PipeGroup.php: -------------------------------------------------------------------------------- 1 | static::formatNamespace($new, $old), 22 | 'key' => static::formatKey($new, $old), 23 | 'where' => static::formatWhere($new, $old), 24 | ]); 25 | 26 | return array_merge_recursive(Arr::except( 27 | $old, 28 | ['namespace', 'key', 'where'] 29 | ), $new); 30 | } 31 | 32 | /** 33 | * Format the attributes of the new group attributes. 34 | * 35 | * @param array $new 36 | * @param array $old 37 | * 38 | * @return string|null 39 | */ 40 | public static function formatKey($new, $old) 41 | { 42 | return isset($new['key']) 43 | ? $new['key'] 44 | : ($old['key'] ?? null); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/PipeParameterBinder.php: -------------------------------------------------------------------------------- 1 | route->compiled->getRegex(), $this->route->path($request), $matches); 18 | 19 | return $this->matchToKeys(array_slice($matches, 1)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/PipeRegistrar.php: -------------------------------------------------------------------------------- 1 | piper = $piper; 44 | } 45 | 46 | /** 47 | * Set the value for a given attribute. 48 | * 49 | * @param string $key 50 | * @param mixed $value 51 | * 52 | * @throws \InvalidArgumentException 53 | * 54 | * @return $this 55 | */ 56 | public function attribute($key, $value) 57 | { 58 | if (! in_array($key, $this->allowedAttributes)) { 59 | throw new InvalidArgumentException("Attribute [{$key}] does not exist."); 60 | } 61 | 62 | $this->attributes[$key] = $value; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Create a route group with shared attributes. 69 | * 70 | * @param \Closure|string $callback 71 | */ 72 | public function group($callback) 73 | { 74 | $this->piper->group($this->attributes, $callback); 75 | } 76 | 77 | /** 78 | * Register a new pipe with the given verbs. 79 | * 80 | * @param string $inputs 81 | * @param string $cue 82 | * @param \Closure|array|string|callable $action 83 | * 84 | * @return \Mshule\LaravelPipes\Pipe 85 | */ 86 | public function match($inputs, $cue, $action = null) 87 | { 88 | return $this->piper->match($inputs, $cue, $this->compileAction($action)); 89 | } 90 | 91 | /** 92 | * Register a new pipe with the piper. 93 | * 94 | * @param string $method 95 | * @param string $uri 96 | * @param \Closure|array|string|null $action 97 | * 98 | * @return \Mshule\LaravelPipes\Pipe 99 | */ 100 | protected function registerPipe($method, $cue, $action = null) 101 | { 102 | if (! is_array($action)) { 103 | $action = array_merge($this->attributes, $action ? ['uses' => $action] : []); 104 | } 105 | 106 | return $this->piper->{$method}($cue, $this->compileAction($action)); 107 | } 108 | 109 | /** 110 | * Compile the action into an array including the attributes. 111 | * 112 | * @param \Closure|array|string|null $action 113 | * 114 | * @return array 115 | */ 116 | protected function compileAction($action) 117 | { 118 | if (is_null($action)) { 119 | return $this->attributes; 120 | } 121 | 122 | if (is_string($action) || $action instanceof Closure) { 123 | $action = ['uses' => $action]; 124 | } 125 | 126 | return array_merge($this->attributes, $action); 127 | } 128 | 129 | /** 130 | * Dynamically handle calls into the route registrar. 131 | * 132 | * @param string $method 133 | * @param array $parameters 134 | * 135 | * @throws \BadMethodCallException 136 | * 137 | * @return \Mshule\LaravelPipes\Pipe|$this 138 | */ 139 | public function __call($method, $parameters) 140 | { 141 | if (in_array($method, $this->passthru)) { 142 | return $this->registerPipe($method, ...$parameters); 143 | } 144 | 145 | if (in_array($method, $this->allowedAttributes)) { 146 | if ('middleware' === $method) { 147 | return $this->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters); 148 | } 149 | 150 | return $this->attribute($method, $parameters[0]); 151 | } 152 | 153 | throw new BadMethodCallException(sprintf( 154 | 'Method %s::%s does not exist.', 155 | static::class, 156 | $method 157 | )); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Piper.php: -------------------------------------------------------------------------------- 1 | pipes = new PipeCollection(); 34 | $this->container = $container ?: new Container(); 35 | } 36 | 37 | /** 38 | * Register a new pipe with the given verbs. 39 | * 40 | * @param string $inputs 41 | * @param string $cue 42 | * @param \Closure|array|string|callable $action 43 | * 44 | * @return \Mshule\LaravelPipes\Pipe 45 | */ 46 | public function any($cue, $action = []) 47 | { 48 | return $this->addPipe(resolve('pipe_any'), $cue, $action); 49 | } 50 | 51 | /** 52 | * Register a new Fallback pipe with the piper. 53 | * 54 | * @param \Closure|array|string|callable|null $action 55 | * 56 | * @return \Mshule\LaravelPipes\Pipe 57 | */ 58 | public function fallback($action) 59 | { 60 | $placeholder = 'fallbackPlaceholder'; 61 | 62 | return $this->addPipe( 63 | resolve('pipe_any'), 64 | "{{$placeholder}}", 65 | $action 66 | )->where($placeholder, '.*')->fallback(); 67 | } 68 | 69 | /** 70 | * Register a new pipe with the given verbs. 71 | * 72 | * @param string $key 73 | * @param string $cue 74 | * @param \Closure|array|string|callable $action 75 | * 76 | * @return \Mshule\LaravelPipes\Pipe 77 | */ 78 | public function match($key, $cue, $action = []) 79 | { 80 | // If only two arguments were entered and the first 81 | // does not contain a colon (:), we assume the 82 | // user either wants to allow any input or 83 | // will specify a specific input later on 84 | if (2 === count(func_get_args()) && (is_string($key) && ! Str::contains($key, ':'))) { 85 | return $this->addPipe('placeholder', $key, $cue); 86 | } 87 | 88 | return $this->addPipe($key, $cue, $action); 89 | } 90 | 91 | /** 92 | * Merge the given array with the last group stack. 93 | * 94 | * @param array $new 95 | * 96 | * @return array 97 | */ 98 | public function mergeWithLastGroup($new, $prependExistingPrefix = true) 99 | { 100 | return PipeGroup::merge($new, end($this->groupStack), $prependExistingPrefix); 101 | } 102 | 103 | /** 104 | * Add a pipe to the underlying pipe collection. 105 | * 106 | * @param string $inputs 107 | * @param string $cue 108 | * @param \Closure|array|string|callable $action 109 | * 110 | * @return \Mshule\LaravelPipes\Pipe 111 | */ 112 | public function addPipe($key, $cue, $action = []) 113 | { 114 | return $this->pipes->add($this->createPipe($key, $cue, $action)); 115 | } 116 | 117 | /** 118 | * Add a route to the underlying route collection. 119 | * 120 | * @param array|string $methods 121 | * @param string $uri 122 | * @param \Closure|array|string|callable|null $action 123 | */ 124 | public function addRoute($methods, $uri, $action) 125 | { 126 | $this->handleNotIntendedMethods(); 127 | } 128 | 129 | /** 130 | * Create a new pipe instance. 131 | * 132 | * @param string $key 133 | * @param string $cue 134 | * @param mixed $action 135 | * 136 | * @return \Mshule\LaravelPipes\Pipe 137 | */ 138 | protected function createPipe($key, $cue, $action = []) 139 | { 140 | // if the input was passed in combination with the cue 141 | // seperated by a colon (:), the values need to 142 | // be reassigned to the right variable. 143 | if (is_string($key) && Str::contains($key, ':')) { 144 | [$key, $cue, $action] = array_merge( 145 | explode(':', $key), 146 | [array_merge(['uses' => $cue], $action)] 147 | ); 148 | } 149 | 150 | // if the input was passed through the fluent api the 151 | // order of the func argument have to be rearranged. 152 | if (($cue instanceof Closure && is_callable($cue)) || Str::contains($cue, '@')) { 153 | [$key, $cue, $action] = [$action, $key, $cue]; 154 | } 155 | 156 | // If the pipe is pointing to a controller we will parse the pipe action into 157 | // an acceptable array format before registering it and creating this pipe 158 | // instance itself. We need to build the Closure that will call this out. 159 | if ($this->actionReferencesController($action)) { 160 | $action = $this->convertToControllerAction($action); 161 | } 162 | 163 | $pipe = $this->newPipe( 164 | $cue, 165 | $action, 166 | $key 167 | ); 168 | 169 | // If we have groups that need to be merged, we will merge them now after this 170 | // pipe has already been created and is ready to go. After we're done with 171 | // the merge we will be ready to return the pipe back out to the caller. 172 | if ($this->hasGroupStack()) { 173 | $this->mergeGroupAttributesIntoRoute($pipe); 174 | } 175 | 176 | $this->addWhereClausesToRoute($pipe); 177 | 178 | return $pipe; 179 | } 180 | 181 | /** 182 | * Create a new Pipe object. 183 | * 184 | * @param string $cue 185 | * @param mixed $action 186 | * @param array|string $key 187 | * 188 | * @return \Mshule\LaravelPipes\Pipe 189 | */ 190 | protected function newPipe($cue, $action, $key = '') 191 | { 192 | return (new Pipe($cue, $action, $key)) 193 | ->setPiper($this) 194 | ->setContainer($this->container); 195 | } 196 | 197 | /** 198 | * Dispatch the request to a pipe and return the response. 199 | * 200 | * @param \Illuminate\Http\Request $request 201 | * 202 | * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse 203 | */ 204 | public function dispatchToRoute(Request $request) 205 | { 206 | return $this->runPipe($request, $this->findPipe($request)); 207 | } 208 | 209 | /** 210 | * Find the pipe matching a given request. 211 | * 212 | * @param \Illuminate\Http\Request $request 213 | * @param \Mshule\LaravelPipes\Pipe $pipe 214 | */ 215 | protected function findPipe($request) 216 | { 217 | $this->current = $pipe = $this->pipes->match($request); 218 | 219 | return $pipe; 220 | } 221 | 222 | /** 223 | * Return the response for the given pipe. 224 | * 225 | * @param \Illuminate\Http\Request $request 226 | * @param \Mshule\LaravelPipes\Pipe $pipe 227 | * 228 | * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse 229 | */ 230 | protected function runPipe(Request $request, Pipe $pipe) 231 | { 232 | $request->setPipeResolver(function () use ($pipe) { 233 | return $pipe; 234 | }); 235 | 236 | return $this->prepareResponse( 237 | $request, 238 | $this->runRouteWithinStack($pipe, $request) 239 | ); 240 | } 241 | 242 | /** 243 | * Get the response resolver callback. 244 | * 245 | * @return \Closure 246 | */ 247 | public function getResponseResolver() 248 | { 249 | return $this->responseResolver ?: function ($request) { 250 | return response('ok'); 251 | }; 252 | } 253 | 254 | /** 255 | * Set the response resolver callback. 256 | * 257 | * @param \Closure $callback 258 | * 259 | * @return $this 260 | */ 261 | public function setResponseResolver(Closure $callback) 262 | { 263 | $this->responseResolver = $callback; 264 | 265 | return $this; 266 | } 267 | 268 | /** 269 | * Return standard response. 270 | * 271 | * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse 272 | */ 273 | public function response($request) 274 | { 275 | return call_user_func($this->getResponseResolver(), $request); 276 | } 277 | 278 | /** 279 | * Throws exceptions to notify user of methods not allowed to use in a pipe context. 280 | * 281 | * @throws Exception 282 | */ 283 | private function handleNotIntendedMethods() 284 | { 285 | throw new Exception('The methods solely used by the router instance are not intended to be used in a pipe context!'); 286 | } 287 | 288 | /** 289 | * Dynamically handle calls into the piper instance. 290 | * 291 | * @param string $method 292 | * @param array $parameters 293 | * 294 | * @return mixed 295 | */ 296 | public function __call($method, $parameters) 297 | { 298 | if (static::hasMacro($method)) { 299 | return $this->macroCall($method, $parameters); 300 | } 301 | 302 | if ('middleware' === $method) { 303 | return (new PipeRegistrar($this))->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters); 304 | } 305 | 306 | return (new PipeRegistrar($this))->attribute($method, $parameters[0]); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/Request.php: -------------------------------------------------------------------------------- 1 | query(), 28 | $request->post(), 29 | $request->input(), 30 | $request->cookie(), 31 | $request->file(), 32 | $request->server(), 33 | $request->getContent(), 34 | ]; 35 | } 36 | 37 | /** 38 | * Reconstruct request from array. 39 | * 40 | * @param array $data 41 | * 42 | * @return self 43 | */ 44 | public static function reconstruct($data, $request = self::class) 45 | { 46 | return tap(resolve($request), function ($request) use ($data) { 47 | $request->initialize(...$data); 48 | }); 49 | } 50 | 51 | /** 52 | * Get the pipe resolver callback. 53 | * 54 | * @return \Closure 55 | */ 56 | public function getPipeResolver() 57 | { 58 | return $this->pipeResolver ?: function () { 59 | }; 60 | } 61 | 62 | /** 63 | * Set the Pipe resolver callback. 64 | * 65 | * @param \Closure $callback 66 | * 67 | * @return $this 68 | */ 69 | public function setPipeResolver(Closure $callback) 70 | { 71 | $this->pipeResolver = $callback; 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Get the pipe handling the request. 78 | * 79 | * @param string|null $param 80 | * @param mixed $default 81 | * 82 | * @return \Mshule\LaravelPipes\Pipe|object|string 83 | */ 84 | public function pipe($param = null, $default = null) 85 | { 86 | $pipe = call_user_func($this->getPipeResolver()); 87 | 88 | if (is_null($pipe) || is_null($param)) { 89 | return $pipe; 90 | } 91 | 92 | return $pipe->parameter($param, $default); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | environment('testing')) { 19 | return $response; 20 | } 21 | 22 | if (version_compare(Application::VERSION, '7.0.0', '>=')) { 23 | return new \Illuminate\Testing\TestResponse($response); 24 | } 25 | 26 | return new \Illuminate\Foundation\Testing\TestResponse($response); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Testing/Fakes/PipeFake.php: -------------------------------------------------------------------------------- 1 | getTruthTestCallback($callback, 'request'); 20 | 21 | Event::assertDispatched(IncomingPipeRequest::class, $truthTestCallback); 22 | } 23 | 24 | /** 25 | * Assert if a response was dispatched based on a truth-test callback. 26 | * 27 | * @param \Closure $callback 28 | */ 29 | public function assertResponded($callback = null) 30 | { 31 | $truthTestCallback = $this->getTruthTestCallback($callback, 'response'); 32 | 33 | Event::assertDispatched(IncomingPipeResponse::class, $truthTestCallback); 34 | } 35 | 36 | /** 37 | * Get truth test callback. 38 | * 39 | * @param \Closure|null $callback 40 | * @param string|null $property 41 | * 42 | * @return \Closure 43 | */ 44 | protected function getTruthTestCallback($callback = null, $property = null) 45 | { 46 | return is_callable($callback) 47 | ? function ($event) use ($callback, $property) { 48 | $callback($event->{$property}); 49 | 50 | return true; 51 | } 52 | : null; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Testing/MakesPipeRequests.php: -------------------------------------------------------------------------------- 1 | post(config('pipes.incoming_request_path'), $data); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Fixtures/Controllers/TestController.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('name'); 19 | $table->timestamp('done_at')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists('todos'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Fixtures/Models/Todo.php: -------------------------------------------------------------------------------- 1 | withoutExceptionHandling(); 19 | $this->expectException(NotFoundPipeException::class); 20 | 21 | $this->pipe(); 22 | } 23 | 24 | /** @test */ 25 | public function it_returns_a_ok_200er_response() 26 | { 27 | $this->pipe() 28 | ->assertOk() 29 | ->assertSee('ok'); 30 | } 31 | 32 | /** @test */ 33 | public function the_response_can_be_changed_through_the_response_resolver() 34 | { 35 | Pipe::setResponseResolver(function ($request) { 36 | return $request->message; 37 | }); 38 | 39 | $this->pipe(['message' => 'test']) 40 | ->assertOk() 41 | ->assertSee('test'); 42 | } 43 | 44 | /** @test */ 45 | public function it_fires_an_incoming_pipe_request_event_when_a_request_is_handled_by_the_kernel() 46 | { 47 | Pipe::fake(); 48 | 49 | $this->pipe(); 50 | 51 | Pipe::assertRequested(); 52 | } 53 | 54 | /** @test */ 55 | public function it_fires_an_incoming_pipe_response_event_when_a_response_is_returned_by_the_kernel() 56 | { 57 | Pipe::fake(); 58 | 59 | $this->pipe(); 60 | 61 | Pipe::assertResponded(); 62 | } 63 | 64 | /** @test */ 65 | public function it_can_resolve_pipes_to_callbacks() 66 | { 67 | Pipe::fake(); 68 | 69 | Pipe::match('text', 'test', function () { 70 | return response('pipe was resolved', 200); 71 | }); 72 | 73 | $this->pipe(['text' => 'test']); 74 | 75 | Pipe::assertResponded(function ($response) { 76 | $response->assertOk() 77 | ->assertSee('pipe was resolved'); 78 | }); 79 | } 80 | 81 | /** @test */ 82 | public function it_matches_everything_to_lowercase() 83 | { 84 | Pipe::fake(); 85 | 86 | Pipe::match('text', 'test', function () { 87 | return response('pipe was resolved', 200); 88 | }); 89 | 90 | $this->pipe(['text' => 'TEST']); 91 | 92 | Pipe::assertResponded(function ($response) { 93 | $response->assertOk() 94 | ->assertSee('pipe was resolved'); 95 | }); 96 | } 97 | 98 | /** @test */ 99 | public function it_can_resolve_pipes_to_controller_actions() 100 | { 101 | Pipe::fake(); 102 | 103 | Pipe::match('text', 'something', '\Mshule\LaravelPipes\Tests\Fixtures\Controllers\TestController@doSomething'); 104 | 105 | $this->pipe([ 106 | 'text' => 'something', 107 | ]); 108 | 109 | Pipe::assertResponded(function ($response) { 110 | $response->assertOk() 111 | ->assertSee('did something'); 112 | }); 113 | } 114 | 115 | /** @test */ 116 | public function it_can_resolve_pipes_to_controller_actions_through_using_the_fluent_api() 117 | { 118 | Pipe::fake(); 119 | 120 | Pipe::match('text:something', '\Mshule\LaravelPipes\Tests\Fixtures\Controllers\TestController@doSomething'); 121 | 122 | $this->pipe([ 123 | 'text' => 'something', 124 | ]); 125 | 126 | Pipe::assertResponded(function ($response) { 127 | $response->assertOk() 128 | ->assertSee('did something'); 129 | }); 130 | } 131 | 132 | /** @test */ 133 | public function it_can_resolve_pipes_with_middlewares() 134 | { 135 | Pipe::fake(); 136 | 137 | Pipe::middleware(function ($request, $next) { 138 | return 'middleware succeeded'; 139 | })->match('text', 'middle', function () { 140 | return 'middleware failed'; 141 | }); 142 | 143 | $this->pipe([ 144 | 'text' => 'middle', 145 | ]); 146 | 147 | Pipe::assertResponded(function ($response) { 148 | $response->assertOk() 149 | ->assertSee('middleware succeeded'); 150 | }); 151 | } 152 | 153 | /** @test */ 154 | public function it_can_resolve_grouped_pipes_and_pass_all_key_to_the_contained_pipes() 155 | { 156 | Pipe::fake(); 157 | 158 | Pipe::key('text')->group(function () { 159 | Pipe::match('something', function () { 160 | return 'did one'; 161 | }); 162 | }); 163 | 164 | $this->pipe([ 165 | 'text' => 'something', 166 | ]); 167 | 168 | Pipe::assertResponded(function ($response) { 169 | $response->assertOk() 170 | ->assertSee('did one'); 171 | }); 172 | } 173 | 174 | /** @test */ 175 | public function it_can_add_fallback_pipes_to_handle_any_request_which_could_not_be_matched_otherwise() 176 | { 177 | Pipe::fake(); 178 | 179 | Pipe::fallback(function () { 180 | return 'no other pipe did match up'; 181 | }); 182 | 183 | $this->pipe(['foo' => 'bar']); 184 | 185 | Pipe::assertResponded(function ($response) { 186 | $response->assertOk() 187 | ->assertSee('no other pipe did match up'); 188 | }); 189 | } 190 | 191 | /** @test */ 192 | public function it_can_match_dynamic_parameters() 193 | { 194 | $this->withoutExceptionHandling(); 195 | Pipe::fake(); 196 | 197 | Pipe::match('trigger:name {text}', function ($text) { 198 | return "you said {$text}"; 199 | }); 200 | 201 | $this->pipe(['trigger' => 'name', 'text' => 'something']); 202 | 203 | Pipe::assertResponded(function ($response) { 204 | $response->assertOk() 205 | ->assertSee('you said something'); 206 | }); 207 | } 208 | 209 | /** @test */ 210 | public function its_dynamic_pattern_match_checks_first_for_placeholder_name_in_request_then_for_default_cue_value() 211 | { 212 | Pipe::fake(); 213 | 214 | Pipe::match('trigger:{text}', function ($text) { 215 | return "you said {$text}"; 216 | }); 217 | 218 | $this->pipe(['trigger' => 'something']); 219 | 220 | Pipe::assertResponded(function ($response) { 221 | $response->assertOk() 222 | ->assertSee('you said something'); 223 | }); 224 | 225 | Event::listen(IncomingPipeResponse::class, function () { 226 | $this->pipe(['trigger' => 'something', 'text' => 'another']); 227 | 228 | Pipe::assertResponded(function ($response) { 229 | $response->assertOk() 230 | ->assertSee('you said another'); 231 | }); 232 | }); 233 | } 234 | 235 | /** @test */ 236 | public function it_can_match_multiple_dynamic_parameters() 237 | { 238 | $this->withoutExceptionHandling(); 239 | Pipe::fake(); 240 | 241 | Pipe::any('{name} {other}', function ($name, $other) { 242 | return "$name $other"; 243 | }); 244 | 245 | $this->pipe(['name' => 'foo', 'other' => 'bar']); 246 | 247 | Pipe::assertResponded(function ($response) { 248 | $response->assertOk() 249 | ->assertSee('foo bar'); 250 | }); 251 | } 252 | 253 | /** @test */ 254 | public function it_can_match_dynamic_parameters_to_any_request() 255 | { 256 | Pipe::fake(); 257 | 258 | Pipe::any('name {text}', function ($text) { 259 | return "you said {$text}"; 260 | }); 261 | 262 | $this->pipe(['bla' => 'name', 'text' => 'something']); 263 | 264 | Pipe::assertResponded(function ($response) { 265 | $response->assertOk() 266 | ->assertSee('you said something'); 267 | }); 268 | } 269 | 270 | /** @test */ 271 | public function it_doesnt_match_dynamic_parameters_if_the_request_contains_only_parts_of_the_cue() 272 | { 273 | Pipe::fake(); 274 | 275 | Pipe::any('name {text}', function ($text) { 276 | return "you said {$text}"; 277 | }); 278 | 279 | $this->pipe(['bla' => 'nam', 'text' => 'something']); 280 | 281 | Pipe::assertResponded(function ($response) { 282 | $response->assertNotFound(); 283 | }); 284 | } 285 | 286 | /** @test */ 287 | public function it_can_add_conditions_to_pipe_definitions() 288 | { 289 | Pipe::fake(); 290 | 291 | Pipe::any('{name}', function ($name) { 292 | return $name; 293 | })->where('name', 'foo'); 294 | 295 | $this->pipe(['name' => 'foo']); 296 | 297 | Pipe::assertResponded(function ($response) { 298 | $response->assertOk() 299 | ->assertSee('foo'); 300 | }); 301 | } 302 | 303 | /** @test */ 304 | public function it_can_add_aliases_to_pipes() 305 | { 306 | Pipe::fake(); 307 | 308 | Pipe::any('mshule', function () { 309 | return 'matched'; 310 | })->alias(['mhule', 'mule']); 311 | 312 | $this->pipe(['foo' => 'mule']); 313 | 314 | Pipe::assertResponded(function ($response) { 315 | $response->assertOk() 316 | ->assertSee('matched'); 317 | }); 318 | } 319 | 320 | /** @test */ 321 | public function it_can_identify_aliases_of_pipes_even_when_the_request_does_only_start_with_the_alias() 322 | { 323 | Pipe::fake(); 324 | 325 | Pipe::any('mshule', function () { 326 | return 'matched'; 327 | })->alias(['mhule', 'mule']); 328 | 329 | $this->pipe(['foo' => 'mule1']); 330 | 331 | Pipe::assertResponded(function ($response) { 332 | $response->assertOk() 333 | ->assertSee('matched'); 334 | }); 335 | } 336 | 337 | /** @test */ 338 | public function it_can_add_aliases_predefined_to_pipes() 339 | { 340 | Pipe::fake(); 341 | 342 | Pipe::alias(['mhule', 'mule'])->any('mshule', function () { 343 | return 'matched'; 344 | }); 345 | 346 | $this->pipe(['foo' => 'mule']); 347 | 348 | Pipe::assertResponded(function ($response) { 349 | $response->assertOk() 350 | ->assertSee('matched'); 351 | }); 352 | } 353 | 354 | /** @test */ 355 | public function it_can_load_pipes_from_files() 356 | { 357 | Pipe::fake(); 358 | 359 | Pipe::namespace('Test')->group(__DIR__.'/Fixtures/pipes.php'); 360 | 361 | $this->pipe(['test' => 'ping']); 362 | 363 | Pipe::assertResponded(function ($response) { 364 | $response->assertOk() 365 | ->assertSee('pong'); 366 | }); 367 | } 368 | 369 | /** @test */ 370 | public function it_ignores_global_helper_functions_as_cue() 371 | { 372 | Pipe::fake(); 373 | 374 | Pipe::key('text')->group(function () { 375 | Pipe::match('report', function () { 376 | return 'it ignored report()'; 377 | }); 378 | }); 379 | 380 | $this->pipe(['text' => 'report']); 381 | 382 | Pipe::assertResponded(function ($response) { 383 | $response->assertOk() 384 | ->assertSee('it ignored report()'); 385 | }); 386 | } 387 | 388 | /** @test */ 389 | public function it_does_not_match_one_character_to_a_pipe_if_it_is_no_dynamic_param() 390 | { 391 | Pipe::fake(); 392 | 393 | Pipe::key('text')->group(function () { 394 | Pipe::match('config', function () { 395 | return 'config pipe triggered'; 396 | }); 397 | }); 398 | 399 | $this->pipe(['text' => 'c']); 400 | 401 | Pipe::assertResponded(function ($response) { 402 | $response->assertNotFound(); 403 | }); 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /tests/SubstituteBindingsTest.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__.'/Fixtures/Migrations'); 19 | } 20 | 21 | /** @test */ 22 | public function it_can_bind_models_to_pipe_requests() 23 | { 24 | Pipe::fake(); 25 | 26 | $todo = Todo::create(['name' => 'Foo Bar']); 27 | 28 | Pipe::middleware('pipe')->any('{todo}', function (Todo $todo) { 29 | return "You fetched {$todo->name} Todo"; 30 | }); 31 | 32 | $this->pipe(['todo' => $todo->id]); 33 | 34 | Pipe::assertResponded(function ($response) { 35 | $response->assertOk() 36 | ->assertSee('You fetched Foo Bar Todo'); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 |