Compare commits

...

413 Commits

Author SHA1 Message Date
Andrei V. Goryunov ca3bb2cb0a upgrade to wellrested v5.1.1, fix 2022-07-29 09:51:37 +03:00
Andrei V. Goryunov c5f49214b5 upgrade to wellrested 5.1.1 2022-07-27 11:32:47 +03:00
Basemaster c153bc0028
provide interfaces prs/http-*-implementation 2021-04-11 14:04:34 +03:00
Andy Green e22d5889b0 back to php 7.2 - remove CI environment 2021-04-11 11:56:55 +03:00
Basemaster fb2b2ab527
Update composer.json 2021-04-11 11:47:52 +03:00
Andy Green 00ea49ded6 remove require-dev dependency 2021-04-11 11:45:07 +03:00
Basemaster 4c4b6104e4
Update README.md 2021-04-11 11:36:14 +03:00
Basemaster 6b31620314
Update README.md 2021-04-11 11:34:37 +03:00
Andy Green e6bb814a76 back to php 7.2 2021-04-11 11:30:03 +03:00
PJ Dietz 19c03b9b8b
Merge pull request #22 from wellrestedphp/fix-travis
Add xdebug.ini for Travis
2021-01-24 14:46:29 -05:00
PJ Dietz b94b01453a Add xdebug.ini for Travis 2021-01-24 14:09:01 -05:00
PJ Dietz 9db267c427 Update XDebug configuration 2021-01-22 15:56:32 -05:00
PJ Dietz 8379dd69a0 Fix copyright date in docs 2021-01-22 14:06:56 -05:00
PJ Dietz 9a4b78b84a
Merge pull request #21 from wellrestedphp/fix-extra-leading-slash-in-path
Fix issue in Router with paths beginning with multiple slashes
2020-08-27 12:41:51 -04:00
PJ Dietz 2c80da2f79 Fix issue in Router with paths beginning with multiple slashes 2020-08-27 12:29:25 -04:00
PJ Dietz a15b5396e9 Fix php-cs-fixer instructions in README 2020-08-16 18:33:26 -04:00
PJ Dietz aeb9d733cc
Merge pull request #20 from wellrestedphp/v5
V5
2020-08-16 18:31:10 -04:00
PJ Dietz c74a468f3e Update documentation for v5 2020-08-16 18:03:17 -04:00
PJ Dietz 84b4bce04f Merge branch 'master' into v5 2020-08-16 17:48:13 -04:00
PJ Dietz 337baa2e26
Merge pull request #19 from wellrestedphp/archive
Archive
2020-08-16 17:41:05 -04:00
PJ Dietz bba0602122 Update README for v5 2020-08-16 17:39:41 -04:00
PJ Dietz 66ff6d2fc1
Merge pull request #18 from wellrestedphp/fix-tests
Fix broken test for Stream
2020-08-16 12:11:56 -04:00
PJ Dietz 198ebb60f7 Fix broken test for Stream 2020-08-16 12:08:57 -04:00
PJ Dietz 8f2206a65f Set min verision for Travis to 7.3 2020-08-16 11:55:00 -04:00
PJ Dietz 8b467193d7 Set minimum version to PHP 7.3 2020-08-16 11:54:28 -04:00
PJ Dietz 95c3be85c9 Create implementations for PSR-17 Response- and StreamFactoryInterface 2020-08-16 10:48:42 -04:00
PJ Dietz 9243dd7663 Refactor ServerRequestMarshaller and ServerRequest 2020-08-16 08:52:33 -04:00
PJ Dietz 20012dc671 Message constructor accepts $headers values as string or string[] 2020-08-16 08:49:47 -04:00
PJ Dietz 79d23e37a4 Reorganize Tests 2020-08-15 10:11:06 -04:00
PJ Dietz 5ba8771e93 Ensure all constants have visibility 2020-08-15 07:54:08 -04:00
PJ Dietz fe0f1ff8f9 Ensure typehints for bool are bool and not boolean 2020-08-15 07:53:03 -04:00
PJ Dietz 36df1f33c1 Refactor URI; fix coverage on UploadedFile 2020-08-15 07:51:30 -04:00
PJ Dietz c137a2066a Minor change to Server docblock 2020-08-15 07:24:57 -04:00
PJ Dietz 1d71f06e71 Router accepts custom RouteFactory through constructor; removes protected methods 2020-08-15 07:18:54 -04:00
PJ Dietz 997582f8d7 Router implements MiddlewareInterface; cleanup Router test 2020-08-14 07:49:33 -04:00
PJ Dietz 56503da35e Remove RouteFactoryInterface 2020-08-14 07:44:00 -04:00
PJ Dietz 79c4799a7b Clean up tests for routes 2020-08-14 07:38:38 -04:00
PJ Dietz fec5a4d405 Remove RouteInterface 2020-08-14 07:24:00 -04:00
PJ Dietz 4eec56b582 Mark Routing\Route classes as @internal 2020-08-14 07:00:50 -04:00
PJ Dietz c75168afae Stop tracking .env and set default in docker-compose.yml 2020-08-14 06:57:13 -04:00
PJ Dietz 288705b77a Edit comments for Dispatch namespace and clean up tests 2020-08-14 06:31:09 -04:00
PJ Dietz f542aaf3a9 Normalize length of dividers 2020-08-13 07:38:00 -04:00
PJ Dietz 2d7db1ed83 Change visibility from protected to private where possible 2020-08-13 07:36:30 -04:00
PJ Dietz 4796e1d5c5 Refactor Transmitter 2020-08-13 07:26:19 -04:00
PJ Dietz 8649090774 Fix visibility for setUp, tearDown to match PHPUnit base class 2020-08-13 07:13:14 -04:00
PJ Dietz d8294d3ac3 Refactor Uri 2020-08-13 07:08:41 -04:00
PJ Dietz 899ebb2492 Move UploadedFileState test components into test file 2020-08-13 06:37:51 -04:00
PJ Dietz 83c2290a2f Refactor streams 2020-08-13 06:21:19 -04:00
PJ Dietz 4a3545cd3c Refactor Message classes 2020-08-12 07:42:33 -04:00
PJ Dietz 2e3475b882 Refactor HeaderCollection 2020-08-12 07:12:42 -04:00
PJ Dietz 168867206e Extract server request marshalling to own class. 2020-08-12 06:49:10 -04:00
PJ Dietz cd2e4448e2 Update CS rules 2020-08-11 06:41:48 -04:00
PJ Dietz e6d1398bb1 Normalize imports for global namespace 2020-08-10 07:31:06 -04:00
PJ Dietz ff28f3c6eb Update CS rules 2020-08-10 07:30:04 -04:00
PJ Dietz 002bdb7541 Convert simple string literals to single quotes 2020-08-10 07:13:50 -04:00
PJ Dietz fb18d2ee1e Code style fixes 2020-08-10 07:09:48 -04:00
PJ Dietz a73ad17ddd Add php-cs-fixer as dev dependency 2020-08-10 06:48:32 -04:00
PJ Dietz d98789ebfd Fix test broken by Transmitter change 2020-08-09 14:54:26 -04:00
PJ Dietz 09dd1d7a32 Add type hints to Server; construct defaults for most dependencies 2020-08-09 14:51:43 -04:00
PJ Dietz 98014d8c59 Add type hints to Transmitter 2020-08-09 14:30:19 -04:00
PJ Dietz ca204a07e7 Add type hints to Router 2020-08-09 14:27:29 -04:00
PJ Dietz 967b6ac2a4 Add type hints for Routes 2020-08-09 13:58:24 -04:00
PJ Dietz c339512f01 Add type hints to Stream, UploadedFile, and Uri 2020-08-09 13:29:05 -04:00
PJ Dietz 7ade042b4b Change Request constructor signature
Various updates to Message classes
2020-08-09 13:10:14 -04:00
PJ Dietz bdc5ac40d9 Add notes to .gitignore 2020-08-09 13:08:50 -04:00
PJ Dietz ecc077a1be Add type hints to Dispatch classes 2020-08-09 11:45:41 -04:00
PJ Dietz e9fb474eb7 Fix minor issues found by Psalm 2020-08-09 11:08:33 -04:00
PJ Dietz a7b08ad8a3 Stream detects read/write more accurately; fix issues after detach() 2020-08-09 10:55:37 -04:00
PJ Dietz fe780e6b92 Set Psalm error level to 3; fix possibly null reason phrase in Response 2020-08-09 10:54:26 -04:00
PJ Dietz 29cfa34f17 Set minimum PHP version to 7.2 2020-08-09 10:52:47 -04:00
PJ Dietz 08ddb0aa2f Fix issues detected by Psalm 2020-08-08 12:05:33 -04:00
PJ Dietz 2cf65def5c Configure Psalm 2020-08-08 12:05:18 -04:00
PJ Dietz 4485675c11 Use ProphecyTrait in tests with Prophecy mocks 2020-08-08 10:59:46 -04:00
PJ Dietz fbd1c10ebe Upgrade PHPUnit to v9 2020-08-08 10:59:18 -04:00
PJ Dietz e320e7e6c3 Do not include composer.lock in archive 2020-08-08 10:27:19 -04:00
PJ Dietz c7b2c335a6 Update .gitattributes to reduce archive size 2020-08-08 10:17:56 -04:00
PJ Dietz 0bea30f434 Fix issue when reading Stream size and fstat fails 2020-05-01 10:45:10 -04:00
PJ Dietz d1c7076929 ServerRequest does not include empty Content-type and -length headers 2020-03-17 12:10:20 -04:00
PJ Dietz d78537809b
Merge pull request #16 from gintechsystems/master
Content Headers Bug Fix / PHP Unit 8.5
2020-02-06 10:31:10 -05:00
Joe Ginley a1a0dc0f45 Added server request test against content headers.
Updated getServerRequestHeaders to be more efficient and accurate.
Wrong variable used in dependency injection documentation.
2020-02-05 18:43:06 -05:00
Joe Ginley 17c58ae362 Added php minimum version badge and removed the requirements text.
Updated composer libraries, fixed phpunit errors after updating.
Updated docker-compose version.
Updated docker to use php 7.4.
2020-02-04 00:05:42 -05:00
Joe Ginley f6a273dbb5 Updated apache request headers to return if available, when false return empty array so nothing breaks. 2020-02-03 22:48:26 -05:00
Joe Ginley 2eaa8c8697 Added 7.4 to travis.yml.
Fixed a bug where not all request headers were available when running under apache server.
2020-02-03 22:44:09 -05:00
PJ Dietz 645bcf227c Upgrade PHPUnit to v8 2019-06-17 16:04:55 -04:00
PJ Dietz e4cc02dc8a Fix link in documentation; fix version in README 2019-06-17 15:01:42 -04:00
PJ Dietz e558d613ab Documentation edits 2018-08-02 16:27:51 -04:00
PJ Dietz e676a17cac Rename Router->addMiddleware to Router->add 2018-06-29 16:29:46 -04:00
PJ Dietz 677cdb4d7d Change Router::continue to Router::continueOnNotFound; update docs 2018-06-28 16:52:36 -04:00
PJ Dietz 0a0d3c3bc9 Router responds 404 by default for non-matched routes 2018-06-28 09:46:59 -04:00
PJ Dietz 36b03b6ca2 Update documentation for version 4.0 2018-06-26 16:51:33 -04:00
PJ Dietz de46c8e089 Rework Server to be configured with setters 2018-06-25 15:46:37 -04:00
PJ Dietz be3d007961 Set a default chunk size for Transmitter 2018-06-25 09:11:58 -04:00
PJ Dietz 64628c4065 Move MethodMap to Route namespace 2018-06-22 15:54:01 -04:00
PJ Dietz ac8bdce037 Remove MethodMapInterface 2018-06-22 15:10:50 -04:00
PJ Dietz 73b6e4ab83 Router registers handlers directly with routes 2018-06-22 14:53:03 -04:00
PJ Dietz 9b29f2a09e Add Route::register method to delegate to MethodMap 2018-06-22 14:44:43 -04:00
PJ Dietz 72d5df244d Clean up MessageTest and documentation for Message classes 2018-06-22 13:43:44 -04:00
PJ Dietz b82ebf6d95 Update comments and documentation for Dispatcher and related classes 2018-06-22 12:17:30 -04:00
PJ Dietz 5e9e7f154b Update Routing integration test 2018-06-21 16:53:17 -04:00
PJ Dietz 9aab0d780e Router can provide middleware called only for matched routes. 2018-06-21 16:00:11 -04:00
PJ Dietz 29cad3687e Router delegates on failure and does not return 404 2018-06-21 12:48:11 -04:00
PJ Dietz f016b74c38 Server responds with a default 404 response when request is unhandled 2018-06-21 12:48:11 -04:00
PJ Dietz 6f247bccfa Make local dev site port configurable 2018-06-21 12:48:11 -04:00
PJ Dietz 9ce784c897 Update Docker images and add dumb-init 2018-06-21 12:48:11 -04:00
PJ Dietz 04c7b100db Upgrade PHPUnit to v7 2018-06-21 10:20:33 -04:00
PJ Dietz cd9cc09afe Remove unused ivar from Transmitter 2018-03-13 15:08:35 -04:00
PJ Dietz 6849c9456f Add local dev info to the README 2018-03-13 14:32:01 -04:00
PJ Dietz d5f9dfa37b Use alias instead of symlink for documentation on local site 2018-03-13 14:15:38 -04:00
PJ Dietz 473d103739 Documentation proofreading 2018-03-13 13:23:15 -04:00
PJ Dietz cd5f25ba5e Update documentation pages to use PSR-15 examples 2018-03-13 13:23:09 -04:00
PJ Dietz ac9f40be5f Update documentation for overview, getting started, messages 2018-03-13 13:23:01 -04:00
PJ Dietz 6395a6177c Update documentation home page 2018-03-13 13:22:00 -04:00
PJ Dietz af1bb538dd Remove Vagrant-related files 2018-03-12 15:18:37 -04:00
PJ Dietz 7caf5343d4 Update README with PSR-15 interface 2018-03-12 15:05:19 -04:00
PJ Dietz 6ddcb03fe8 Add local development example site. 2018-03-12 15:05:19 -04:00
PJ Dietz a9ba30fa79 Add "docs" service to for generating documentation with Sphinx 2018-03-12 15:05:19 -04:00
PJ Dietz e531af0da5 Add official PSR-15 interfaces via composer 2018-03-12 15:05:19 -04:00
PJ Dietz 4c40db8ecc Update draft PSR-15 interfaces 2018-03-12 15:05:19 -04:00
PJ Dietz 1dd9bf0f9c Dispatcher can dispatch PSR-15 HandlerInterface and MiddlewareInterface (Drafts) 2018-03-12 15:05:18 -04:00
PJ Dietz af3eef4657 Add locally provided PSR-15 interfaces to work against while in draft 2018-03-12 15:02:58 -04:00
PJ Dietz 4b1ec94e3f Update NextMock 2018-03-12 15:02:58 -04:00
PJ Dietz b8b87a8032 ServerRequest copies request body to temp stream to allow multiple reads 2017-12-18 09:28:11 -05:00
PJ Dietz 50f1004be5 Test cleanup 2017-08-03 14:29:54 -04:00
PJ Dietz 3a77d99e00 Minor refactor of Router; various cleanup 2017-08-03 14:15:08 -04:00
PJ Dietz 76d952b076 Update Travis to test using PHP 7.1 to meet dependency requirements for tests. 2017-08-03 14:09:40 -04:00
PJ Dietz 83381bf5d5 Update PhpDoc return types as static to match updated PSR-7 interfaces 2017-08-03 14:05:02 -04:00
PJ Dietz 36a170bcff Upgrade PHPUnit to ^6 2017-07-22 15:21:41 -04:00
PJ Dietz 353b48394b Setup Docker 2017-07-22 14:39:10 -04:00
PJ Dietz 54d1aecda3 Update Composer metadata 2016-05-26 22:43:07 -04:00
PJ Dietz 6772bd1ae0 New Message instances have an empty Stream instead of NullStream by default. 2016-05-22 12:44:40 -04:00
PJ Dietz 409ffe9371 new Stream() with no arguments creates an empty temp steam 2016-05-22 12:42:59 -04:00
PJ Dietz 887b885eb9 Run Travis tests with PHP 5.6 and 7.0 2016-05-21 12:22:53 -04:00
PJ Dietz 92294a2e67 Move doubles to Doubles namespace 2016-05-21 12:08:17 -04:00
PJ Dietz a294a7eaf5 Fix test namespaces 2016-05-21 12:06:26 -04:00
PJ Dietz e0b5c836db Refactor Server test 2016-05-21 12:01:21 -04:00
PJ Dietz 4fb7bf6050 Refractor Transmitter test 2016-05-21 11:51:12 -04:00
PJ Dietz b3dc82e744 Refactor router test 2016-05-21 11:05:35 -04:00
PJ Dietz 91249d885f MethodMap tests use Dispatcher and MiddlewareMock; rename NextMock 2016-05-21 10:16:22 -04:00
PJ Dietz f9ab311b79 MethodMap test uses ServerRequest and NextSpy 2016-05-21 09:48:44 -04:00
PJ Dietz 4eb0b2641e Use ServerRequest instance in MethodMap test 2016-05-21 08:46:13 -04:00
PJ Dietz 36bb00dc1a Refactor route tests 2016-05-21 08:45:55 -04:00
PJ Dietz 929f8ffd97 Refactor message tests 2016-05-20 20:08:48 -04:00
PJ Dietz d3e924485c Refactor dispatch tests 2016-05-20 19:25:30 -04:00
PJ Dietz f48b3c5fd1 Clean user files from .gitignore 2016-05-18 20:43:39 -04:00
PJ Dietz 344b4bb4b9 Update PHPUnit to v5 2016-05-18 20:32:55 -04:00
PJ Dietz e9a18ba224 Update Vagrant PHP to v5.6 2016-05-18 20:26:53 -04:00
PJ Dietz 0c61641376 Do not attempt to rewind unsociable streams when transmitting response 2016-02-22 14:45:16 -05:00
PJ Dietz db7aaa2688 ServerRequest parses request body when Content-type header includes expected values; allows for charset in header value. 2015-11-08 19:31:43 -05:00
PJ Dietz 977f89c50e Add docs for additional components 2015-06-13 18:43:36 -04:00
PJ Dietz fa6fb124ad Update documentation 2015-06-13 18:21:30 -04:00
PJ Dietz 45379ab241 Edit docs index page 2015-06-10 18:17:24 -04:00
PJ Dietz 180608ac1a Update extending docs 2015-06-07 13:56:15 -04:00
PJ Dietz e6205b7ee7 Update docs for dependency injection 2015-06-07 13:32:39 -04:00
PJ Dietz 2fe3575e69 Relax protection on some Server members to allow extension 2015-06-07 10:34:38 -04:00
PJ Dietz 0cbcd6cbfc Spellcheck and edit documentation 2015-06-07 10:24:31 -04:00
PJ Dietz acc5b48314 Update docs for messages to include section on response status codes. 2015-06-07 09:50:21 -04:00
PJ Dietz 1945f63ca1 Add docs for extending and customizing 2015-06-07 09:42:27 -04:00
PJ Dietz 6a1f0c2915 Spellcheck 2015-06-04 19:18:11 -04:00
PJ Dietz 6f33eab90b Update docblocks and fix typos 2015-06-02 19:45:14 -04:00
PJ Dietz 4429d4280b Stream opens php://temp for binary read-write 2015-06-01 19:07:08 -04:00
PJ Dietz 18d6e5fc6a Begin writing docs for Messages/Responses
Fix typo in Router docs
2015-06-01 19:06:34 -04:00
PJ Dietz a8b3ce9829 Add documentation for Messages 2015-05-30 19:06:53 -04:00
PJ Dietz 375ba819ef Merge pull request #14 from nthdesign/patch-2
Update middleware.rst
2015-05-30 14:51:38 -04:00
Nate Smith badf9cad95 Update middleware.rst
Added a missing parenthesis.
2015-05-30 14:00:03 -04:00
PJ Dietz eaff062895 Merge pull request #13 from nthdesign/patch-1
Update uri-templates-advanced.rst
2015-05-26 13:07:27 -04:00
Nate Smith aa8c7b2afe Update uri-templates-advanced.rst
This may or may not be correct... I believe that the {/path*} template, when used to mach /any/number/of/parts.jpg, will return :path = ["any", "number", "of", "parts.jpg"]. The only change here is the addition of the .jpg after parts. If this is incorrect, please disregard.
2015-05-26 10:46:49 -04:00
PJ Dietz 6abc4044f1 Update README docs badge to link to documentation page 2015-05-25 11:20:26 -04:00
PJ Dietz d08b1cda63 Add documentation for URI Templates (Advanced) 2015-05-25 11:17:46 -04:00
PJ Dietz 139e3c43da Template Routes do not match slash prefix variables that contain slashes as the non-first character 2015-05-25 10:17:42 -04:00
PJ Dietz 753e9ff33a Rewrite the documentation for URI Template (basic usage). 2015-05-24 14:34:05 -04:00
PJ Dietz 4ba6763126 URI Templates with slash prefix explosions do not match reserved characters 2015-05-24 13:53:56 -04:00
PJ Dietz 41336d9387 Uri does not percent encode reserved characters 2015-05-24 13:21:09 -04:00
PJ Dietz 559a08dc8d Update composer.lock 2015-05-24 13:20:46 -04:00
PJ Dietz 8db2babd44 Minor fix to provisioning script and composer.json 2015-05-24 10:51:29 -04:00
PJ Dietz 5dcd119952 Add public method Server::getDispatcher to make the dispatcher available. 2015-05-21 12:14:28 -04:00
PJ Dietz 6b3f2dded1 Update playground files for Vagrant 2015-05-21 09:33:18 -04:00
PJ Dietz 9dcb2502b7 Update Travis badge in README 2015-05-21 07:53:17 -04:00
PJ Dietz 5a56bdebbe Link to latest docs in README 2015-05-20 20:02:42 -04:00
PJ Dietz 9d9d5e3a1b Update README 2015-05-20 19:51:45 -04:00
PJ Dietz 97807d8735 Remove RTD custom theme 2015-05-20 19:23:43 -04:00
PJ Dietz 2179446433 Fixing Read the Docs custom theming 2015-05-20 19:04:16 -04:00
PJ Dietz 6dcd3251d4 Update Composer 2015-05-20 18:05:26 -04:00
PJ Dietz 6acd7c44a1 Rewrite documentation for version 3.0 2015-05-20 18:02:29 -04:00
PJ Dietz c8ddfaae37 Change Composer package name to wellrested/wellrested 2015-05-20 18:02:28 -04:00
PJ Dietz 19e72f7040 Update Composer to use psr/http-message ~1.0 2015-05-20 18:02:28 -04:00
PJ Dietz 4ec0694351 Revise docblocks for interfaces. 2015-05-20 14:42:57 -04:00
PJ Dietz ab05ca0b40 Propagate pathVariablesAttributeName from Server to Router 2015-05-19 21:21:58 -04:00
PJ Dietz dedec4ec4e Router stores path variables directly as attributes by default. 2015-05-19 21:06:50 -04:00
PJ Dietz 0387255676 Remove unused import. 2015-05-19 19:50:03 -04:00
PJ Dietz ac2ed4a24a Router stops propagating on 404, 405, and OPTIONS 2015-05-19 19:12:12 -04:00
PJ Dietz a825654336 Refactor MiddlewareInterface::dispatch to MiddlewareInterface::__invoke 2015-05-19 18:35:29 -04:00
PJ Dietz 474d8da61c Server accepts attributes array as first constructor parameter and sets attributes on server request 2015-05-17 16:56:30 -04:00
PJ Dietz b06abc0df2 Stream checks isSeeakable before calling rewind in __toString 2015-05-17 16:56:30 -04:00
PJ Dietz 15da2ab805 Add .gitattributes to remove non-essentials files from dist 2015-05-15 19:27:18 -04:00
PJ Dietz c8bbd6d2b8 Remove dead code from Transmitter 2015-05-15 19:11:54 -04:00
PJ Dietz 15602d8e97 Remove suppression operator from RegexRoute 2015-05-15 19:11:43 -04:00
PJ Dietz 74369f5b0b Update composer to tagged psr/http-message 2015-05-15 19:11:23 -04:00
PJ Dietz 6dda878dd7 Remove user files from .gitignore 2015-05-15 19:10:58 -04:00
PJ Dietz 1953acf25d Add integration tests to double check routing and dispatching functionality 2015-05-15 08:06:21 -04:00
PJ Dietz 3d4a263beb Server accepts all dependencies as arguments to either constructor or respond 2015-05-14 19:43:08 -04:00
PJ Dietz 3f5e2321d9 Transmitter provides Content-length header without external class. Transmitter no longer alters the body for HEAD requests. 2015-05-14 07:51:28 -04:00
PJ Dietz 1be4ff7691 Router uses only the request's path for routing 2015-05-13 21:53:33 -04:00
PJ Dietz 3b18d1dcdb Router reads path variables from route and adds them to request before dispatching route 2015-05-13 21:53:33 -04:00
PJ Dietz 6232f67b9c Update RouteInterface and routes 2015-05-13 21:53:33 -04:00
PJ Dietz 61fd0f3354 TemplateRoute more throughly implements URI Templates as defined in RFC 6570
Template support:
- Simple strings /{var}
- Reserved string /{+var}
- Multiple variables per expression /{hello,larry}
- Dot-prefixes /{.filename,extension}
- Slash-prefiex {/path,to,here}
- Explosion {/paths*}, /cats/{ids*} explode to list arrays
2015-05-13 21:53:27 -04:00
PJ Dietz 1bb93434b2 Store variables from URI as uriVariables attributes 2015-05-12 17:58:35 -04:00
PJ Dietz 22a17e42bb Update Route docblocks 2015-05-12 17:44:28 -04:00
PJ Dietz 297e985e84 DispatchStack calls $next only when the stack runs to the end. 2015-05-12 07:54:11 -04:00
PJ Dietz 26a6a25d3b Rename Server::makeRouter to Server::createRouter 2015-05-11 15:39:12 -04:00
PJ Dietz e3083609db Remove fork of psr/http-message from composer.json 2015-05-11 15:27:18 -04:00
PJ Dietz 75ddf6fa9c Remove extra assertions in Message tests 2015-05-10 20:55:14 -04:00
PJ Dietz 14a7a1bd17 Add @group message 2015-05-10 20:25:14 -04:00
PJ Dietz 9cc08bb875 Remove HttpExceptions from Composer 2015-05-10 20:24:35 -04:00
PJ Dietz 64eb5aecdd Rename Responding\Responder Transmission\Transmitter 2015-05-10 20:17:26 -04:00
PJ Dietz 0f9c5079f9 Add Server 2015-05-10 19:04:12 -04:00
PJ Dietz 67d562b3bc Responder::respond process responses for Content-length header and HEAD requests 2015-05-10 18:28:13 -04:00
PJ Dietz b198e83d55 Add Responder namespace
Move ContentLength and Head middleware to Resonder\Middleware
2015-05-10 16:59:50 -04:00
PJ Dietz 7874484c53 Remove HttpExceptions
This will become its own package.
2015-05-10 16:50:21 -04:00
PJ Dietz f849a6ff89 Router optionally takes a DispatcherInterface on construction 2015-05-10 14:32:43 -04:00
PJ Dietz 3811b9085f Dispatcher creates DispatchStack for array 2015-05-10 13:53:15 -04:00
PJ Dietz 3786cfaade Passing array to Router::register as middleware creates a DispatchStack 2015-05-10 12:15:39 -04:00
PJ Dietz 6507028dd3 Pass DispatchProvider to Router on construction 2015-05-10 12:04:36 -04:00
PJ Dietz 37af085ec5 Pass DispatcherInterface to RouteFactory on construction 2015-05-10 11:46:51 -04:00
PJ Dietz 87caa09b61 Pass DispatcherInterface into MethodMap on construction 2015-05-10 11:41:02 -04:00
PJ Dietz 94d6cc23b2 Add DispatchProviderInterface 2015-05-10 11:31:19 -04:00
PJ Dietz ec091b34c4 Fix paths and namespaces for Dispatching tests 2015-05-10 11:25:02 -04:00
PJ Dietz 8071b0b5db Move MiddlewareInterface to the root namespace. 2015-05-10 11:21:55 -04:00
PJ Dietz bbb138996a Add Dispatching namesapce 2015-05-10 11:02:59 -04:00
PJ Dietz 560b1e8ff0 Add DispatchStack 2015-05-10 10:30:22 -04:00
PJ Dietz 2adcbd8636 Remove Router and rename RouteMap to Router
Remove Router
Remove RouterInterface
Rename RouteMapInterface to RouterInterface
Rename RouteMap to Router
Rename add() to register()
Make register fluid
2015-05-10 09:05:05 -04:00
PJ Dietz b0db3cbcdd MethodMap::dispatch calls $next even on failure 2015-05-10 09:05:05 -04:00
PJ Dietz 9470f90ee2 RouteMap::dispatch calls $next even on failure 2015-05-10 09:05:05 -04:00
PJ Dietz c1a104af4f Update HeadHook 2015-05-10 09:05:05 -04:00
PJ Dietz 06f694154c Update ContentLengthHook 2015-05-10 09:05:05 -04:00
PJ Dietz 5a01d20f8e Update RouteMap to match updated MiddlewareInterface 2015-05-10 09:05:05 -04:00
PJ Dietz 36263ba3de Update routes to match new MiddlewareInterface 2015-05-10 09:05:05 -04:00
PJ Dietz 72767b74e8 Rename MethodMap::setMethod to ::register 2015-05-10 09:05:05 -04:00
PJ Dietz d8352e71d9 Update MethodMap to match new MiddlewareInterface 2015-05-10 09:05:05 -04:00
PJ Dietz a0e4ace6a5 Update Dispatcher 2015-05-10 09:05:00 -04:00
PJ Dietz 8874827524 Change MiddlewareInterface signature 2015-05-09 17:38:21 -04:00
PJ Dietz 1d30fcbbba Remove RouteTable, revise RouteFactory, 2015-05-08 01:03:07 -04:00
PJ Dietz 09ea17d349 Update TemplateRoute 2015-05-08 00:25:15 -04:00
PJ Dietz 8f4165cdb6 Revise RegexRoute 2015-05-07 23:56:47 -04:00
PJ Dietz cfcc3b9690 Revise PrefixRoute 2015-05-07 23:30:42 -04:00
PJ Dietz 86d36e8c15 Revise StaticRoute 2015-05-07 23:25:08 -04:00
PJ Dietz 58b5107289 Revise Route 2015-05-07 23:14:48 -04:00
PJ Dietz 7a53a02c5f RouteMap: Remove check for captures (push this into regex route's dispatch) 2015-05-07 22:49:39 -04:00
PJ Dietz 1a49a4ac6c RouteMap routes patterns 2015-05-07 22:01:11 -04:00
PJ Dietz d5eb044169 Begin RouteMap 2015-05-07 21:36:54 -04:00
PJ Dietz 66319218cb Test Router's default finalization hooks and sequence in which router dispatches middleware 2015-05-07 19:36:33 -04:00
PJ Dietz 9915dffcfc Update finalization hooks. 2015-05-07 18:23:44 -04:00
PJ Dietz ccbe8bb2e0 Rework MethodMap 2015-05-07 18:02:13 -04:00
PJ Dietz 7cbbe6d7c5 Revise DispatchterTest 2015-05-07 07:42:46 -04:00
PJ Dietz ec7dceac98 Rework Router 2015-05-07 07:42:39 -04:00
PJ Dietz 9083f2a444 Rewrite RouterTest 2015-05-03 21:05:21 -04:00
PJ Dietz 121b8be044 Add response preparation hooks to Router 2015-05-03 19:38:04 -04:00
PJ Dietz b523f2e79d Remove copy-paste comment from ServerRequestTest 2015-05-03 19:37:25 -04:00
PJ Dietz 147ddd0539 Add ContentLengthPrep 2015-05-03 18:11:23 -04:00
PJ Dietz 559044a82f Add HeadPrep 2015-05-03 16:54:03 -04:00
PJ Dietz e1058a4132 Remove sublcass from ServerRequestTest 2015-05-02 16:07:15 -04:00
PJ Dietz 8462e2effc Style, documentation, and test name updates 2015-05-02 15:15:13 -04:00
PJ Dietz f98ee59e4a Refactor validation for withUploadedFiles 2015-05-02 13:46:32 -04:00
PJ Dietz 81055c3bd9 Update ServerRequest:: addUploadedFilesToBranch to allow associate array at last level of $_FILES 2015-05-02 13:27:38 -04:00
PJ Dietz a93b37a548 Update UploadedFile's tests for SAPI and use of *_uploaded_file functions 2015-05-02 10:11:08 -04:00
PJ Dietz 257f2b7610 Update uploaded file functionality in ServerRequest 2015-04-30 22:10:40 -04:00
PJ Dietz b76883c9e9 Update reading $_FILES to UploadedFiles to conform to updates to PSR-7 2015-04-30 20:45:26 -04:00
PJ Dietz adf8def961 Update docblocks for Message namespace 2015-04-29 21:09:05 -04:00
PJ Dietz 086b09db4f Changes to match updates to PSR-7 in progress 2015-04-29 18:32:13 -04:00
PJ Dietz 2e7783d19d Message::getHeaderLine returns an empty string when the header is not set. 2015-04-29 07:42:40 -04:00
PJ Dietz 5eb30ccafb Rename UploadedFile::move to moveTo 2015-04-29 07:21:56 -04:00
PJ Dietz dc2aecf3ff Add Composer alias for psr/http-message fork. 2015-04-29 07:21:29 -04:00
PJ Dietz af9fbf9c50 Refactor constructor parameters for headers and body to Message 2015-04-27 11:46:36 -04:00
PJ Dietz cee55cada0 Add parameters for Request::__construct 2015-04-26 22:55:48 -04:00
PJ Dietz 4d5430e589 Add parameters to Response::__construct 2015-04-26 22:55:27 -04:00
PJ Dietz 1b0fccfe0e ServerRequest::getServerRequest builds URI 2015-04-26 22:13:33 -04:00
PJ Dietz 4f667f1dda Streams throw exceptions instead of returning false. 2015-04-26 20:49:59 -04:00
PJ Dietz dce4bdf572 ServerRequest provides proper defaults and throws exception for bad parsed body 2015-04-26 20:08:13 -04:00
PJ Dietz 534bd43d9b Request::getUri always returns a URI 2015-04-26 19:41:23 -04:00
PJ Dietz 212bb6871e Message::withHeader and withAddedHeader accept string or string[] as the second parameter. 2015-04-26 19:01:59 -04:00
PJ Dietz f706d47c6d Update Request to implement changes to PSR-7 PR #523 2015-04-26 16:05:46 -04:00
PJ Dietz 43c050ec2e Implement ServerRequest::withUploadedFiles 2015-04-26 15:32:33 -04:00
PJ Dietz 3686e3b1b2 ServerRequest parses uploaded files 2015-04-26 12:20:24 -04:00
PJ Dietz a254c69607 Reorganize ServerRequest and test 2015-04-26 11:21:55 -04:00
PJ Dietz 26d71bd792 Add UploadedFile 2015-04-23 21:53:54 -04:00
PJ Dietz 2e2b9d57c0 Uri parses string on construction 2015-04-21 15:15:53 -04:00
PJ Dietz 0fabbc5cb1 Add Uri::__toString 2015-04-19 20:59:49 -04:00
PJ Dietz 7dfa3facc1 Add Uri (Partially complete) 2015-04-19 19:07:25 -04:00
PJ Dietz 8c4b59c525 Update composer, phpunit configuration, and remove apache_request_headers 2015-04-16 19:02:43 -04:00
PJ Dietz 408d82fb73 Move Stream classes into Message namespace 2015-04-15 19:39:53 -04:00
PJ Dietz 6b20d1ea96 Update based on changes to PSR-7 2015-04-15 19:34:33 -04:00
PJ Dietz 4a75f4e3a6 Update tests to to avoid passing a reveal() return value by reference. 2015-04-13 19:46:03 -04:00
PJ Dietz b14641d2f4 Add bootstrap and set error reporting to E_STRICT 2015-04-13 19:17:08 -04:00
PJ Dietz 9e1c049c38 Include Psr\Http\Message manually while under revision. 2015-04-12 18:42:01 -04:00
PJ Dietz 5ef74f8b89 Remove old files 2015-04-12 18:40:25 -04:00
PJ Dietz 4dba068f3d Update to match revisions to PSR-7 2015-04-12 18:13:17 -04:00
PJ Dietz 79be20c826 ServerRequest::getServerRequest reads method, request target, and protocol version from request. 2015-04-12 14:17:12 -04:00
PJ Dietz 963e1acd58 Add pre- and post-route hooks to Router 2015-04-12 13:51:49 -04:00
PJ Dietz b0a0f5262e Remove old files 2015-04-12 13:51:16 -04:00
PJ Dietz 4096295421 Stream can be created with a string as well as resource handle. 2015-04-12 13:10:40 -04:00
PJ Dietz 6e83b6b050 Add Router::respond 2015-04-12 11:49:48 -04:00
PJ Dietz 90b9503c72 Add ResponderInterface and add setter for chunk site to Responder 2015-04-12 10:07:01 -04:00
PJ Dietz df8e274f26 Add Responder 2015-04-10 00:15:35 -04:00
PJ Dietz 15ddaa1dd2 Allow null reason phrase 2015-04-10 00:13:54 -04:00
PJ Dietz 9c768793db Test apache_request_headers in ServerRequestTest 2015-04-09 20:57:20 -04:00
PJ Dietz dea577fdb4 Add NullStream 2015-04-07 21:35:02 -04:00
PJ Dietz cbeadbda53 Use fstat to read the size of the resource in Stream 2015-04-07 21:34:57 -04:00
PJ Dietz 5cc259944e Extract DispatcherInterface 2015-04-06 20:59:34 -04:00
PJ Dietz d269970210 Router creates Dispatcher instance in overridable method 2015-04-06 20:27:44 -04:00
PJ Dietz 45b13691a2 Add MethodMapInterface 2015-04-06 20:24:59 -04:00
PJ Dietz cb87660548 Add RouteFactoryInterface 2015-04-06 20:24:40 -04:00
PJ Dietz 6d9adfc7ee Update Template Route to accept one parameter for the default variable pattern or map of patterns 2015-04-06 19:55:04 -04:00
PJ Dietz d66ba80ec9 Allow Router to assign middleware to MethodMap 2015-04-06 19:12:59 -04:00
PJ Dietz 0d204d9279 Add MethodMap
MethodMap::add adds each comma-separated method for one middleware

Fix name for MethodMapTest
2015-04-06 19:12:57 -04:00
PJ Dietz decf712354 Add Router 2015-04-03 06:35:47 -04:00
PJ Dietz 57271fa19f Add RouteFactory 2015-04-02 22:15:07 -04:00
PJ Dietz f788d9a2f3 Add RouteTableInterface 2015-04-02 21:56:12 -04:00
PJ Dietz 918e33bd0a Add RouteTable 2015-04-02 21:49:01 -04:00
PJ Dietz e4ef1a8cb3 Add TemplateRoute 2015-04-02 20:53:54 -04:00
PJ Dietz c82acfa380 Add RegexRoute 2015-04-02 20:21:49 -04:00
PJ Dietz d367f1de79 Add Static- and PrefixRoutes 2015-04-02 20:10:13 -04:00
PJ Dietz 506c37ffdd Add MiddlewareInterface and Dispatcher 2015-04-02 20:09:42 -04:00
PJ Dietz bd5902415a Rename StreamStream Stream 2015-04-02 18:45:11 -04:00
PJ Dietz 4502df5c1c ServerRequest creates stream wrapping php://input for body on creation 2015-04-02 18:42:05 -04:00
PJ Dietz a5cb481d79 Add StringStream 2015-03-25 21:52:52 -04:00
PJ Dietz a6b8a11cde Add StreamStream 2015-03-25 21:40:52 -04:00
PJ Dietz 64e5786537 Remove old ApacheRequestHeadersTest 2015-03-24 20:34:34 -04:00
PJ Dietz f3e5cddf4a Remove RouteBuilder and ParseException 2015-03-24 20:33:34 -04:00
PJ Dietz d95498bcae Move HttpExceptions to new namespace. 2015-03-24 20:32:30 -04:00
PJ Dietz 51e1be92fd Allow passing attributes into ServerRequest::getServerRequest
Remove other methods relating to the server request.
2015-03-24 20:29:14 -04:00
PJ Dietz 7cb6304037 Refactor Request::getRequestTarget to get PHPUnit to provide accurate coverage. 2015-03-24 19:38:59 -04:00
PJ Dietz d696727cb1 Code style update 2015-03-24 19:38:18 -04:00
PJ Dietz 166fc66117 Assign $_POST to parsedBody on creation if form content header is set. 2015-03-24 19:37:36 -04:00
PJ Dietz fcbdd1ebfb Read headers in ServerRequest
- Add withServerRequest
- Add updateWithServerRequest
- Add getServerRequestHeaders
2015-03-22 21:10:54 -04:00
PJ Dietz fe93ab13c1 Change Message\Message::headers from private to protected 2015-03-22 21:09:31 -04:00
PJ Dietz 51f057b300 Minor fix to Message\Request 2015-03-22 21:09:06 -04:00
PJ Dietz 513db2def1 Add Message\ServerRequest 2015-03-22 20:42:09 -04:00
PJ Dietz 734c87188f Style fixes for Message and MessageTest 2015-03-22 18:03:21 -04:00
PJ Dietz 60a0913daf Remove unused import from Response 2015-03-22 18:02:56 -04:00
PJ Dietz 292e213c0a Add Message\Request 2015-03-22 18:02:36 -04:00
PJ Dietz 9da0780875 Move old integration tests 2015-03-22 14:57:12 -04:00
PJ Dietz 197ea3000a Add Message\Response 2015-03-22 14:56:08 -04:00
PJ Dietz 60b309a3d1 Add Message\Message 2015-03-22 14:03:31 -04:00
PJ Dietz b6df67afd0 Add Iterator to HeaderCollection 2015-03-22 14:03:18 -04:00
PJ Dietz 2575bc743e Refactor HeaderCollection to store headers as string[] instead of Header[]
Remove Header
Move Header out of own namespace to Message
2015-03-22 12:05:48 -04:00
PJ Dietz a2ae6fff7d Cloning a HeaderCollection yields deep copies of the Headers. 2015-03-22 11:36:06 -04:00
PJ Dietz 5f676cb79f Run tests in strict coverage mode by default 2015-03-22 11:35:23 -04:00
PJ Dietz 465425f01f Move old tests from test/old 2015-03-22 11:35:04 -04:00
PJ Dietz de9d75fdfc Fix namespace on Message\Header\HeaderCollection test 2015-03-22 10:15:24 -04:00
PJ Dietz 9d7030faa0 Fix namespace on Message\Header test 2015-03-22 10:14:59 -04:00
PJ Dietz 3ab7c55257 Add Message\Header\HeaderCollection 2015-03-22 09:38:18 -04:00
PJ Dietz ba26379fdc Add Message\Header\Header 2015-03-22 09:16:17 -04:00
PJ Dietz 16ed00a841 Update minimum PHP in composer to 5.4 2015-03-22 09:10:46 -04:00
PJ Dietz f39e820287 Add psr/http-message to composer 2015-03-18 19:31:03 -04:00
PJ Dietz b318e26076 Update README.md with link to Read the Docs. 2015-03-16 20:32:08 -04:00
PJ Dietz fcc5474114 Add reStructuredText documentation 2015-03-16 20:09:53 -04:00
PJ Dietz a2f6bc1f22 Add home page to test site. Add autoload sandbox directory. 2015-03-11 21:34:20 -04:00
PJ Dietz 381310a88a Exclude integration tests from code coverage 2015-03-11 21:33:59 -04:00
PJ Dietz d505512ee3 Ignore and stop tracking .idea directory 2015-03-11 21:33:59 -04:00
PJ Dietz fb68febe93 Add Nginx site in Vagrant provisioning script 2015-03-11 15:36:49 -04:00
PJ Dietz 961916cca5 Add placeholder files to preserve track empty directories under docs/source 2015-03-11 15:36:49 -04:00
PJ Dietz 750425b143 Read Vagrant host port from environment variable. 2015-03-11 15:36:49 -04:00
PJ Dietz 7c00fb8342 Run unit tests and build documentation on Vagrant provisioning 2015-03-10 20:14:16 -04:00
PJ Dietz a0116166f7 Update ignore rules for documentation 2015-03-10 18:04:46 -04:00
PJ Dietz 3ed2b03ad2 Create initial Vagrant files. 2015-03-10 17:06:27 -04:00
PJ Dietz b1f8b076a7 PhpDoc cleanup 2015-03-08 17:36:36 -04:00
PJ Dietz f08691fff1 Allow template variables to be named as alpha followed by alphanumeric and underscore. 2015-03-08 14:59:32 -04:00
PJ Dietz 81ce6dae9d Add additional HttpExceptions 2015-03-04 20:59:52 -05:00
PJ Dietz 2aadfe74b3 Code cleanup 2015-03-04 19:06:16 -05:00
PJ Dietz 42c0b87285 Updates to README. 2015-02-22 18:39:47 -05:00
PJ Dietz fdeff57a79 Prevent Router from trying to call respond on non responses. 2015-02-22 17:17:56 -05:00
PJ Dietz 13e683225d Fix PhpDoc errors 2015-02-22 14:44:20 -05:00
PJ Dietz a5c180dace Code inspection 2015-02-22 14:10:15 -05:00
PJ Dietz 1a5712a417 Split tests into unit and integration test suites 2015-02-22 14:05:05 -05:00
PJ Dietz 812012bdbf Propagate $request and $args to route and errorHandler callables 2015-02-22 12:01:21 -05:00
PJ Dietz d785e21fee Propagate arguments to callable in HandlerUnpacker::unpack 2015-02-22 09:08:15 -05:00
PJ Dietz bc966f5924 New README in progress 2015-02-21 18:11:54 -05:00
PJ Dietz 64ef9cc4e7 Update copyright and rename a couple tests 2015-02-21 16:01:06 -05:00
PJ Dietz b582fcf546 Deprecate RouteBuilder 2015-02-21 15:59:27 -05:00
PJ Dietz 614da5f2ff Update tests for messages. 2015-02-21 15:59:08 -05:00
PJ Dietz d4ad282abc Update tests for Client and Handler 2015-02-21 14:51:42 -05:00
PJ Dietz 5dacb232ec Updates to Router
- Add Router::add method
- Refactor Router to contain one RouteTable
2015-02-21 14:11:53 -05:00
PJ Dietz b350693aca Rearrange Route 2015-02-21 09:52:23 -05:00
PJ Dietz 259dd0baa1 Remove RouteTable::addRoutes 2015-02-21 08:14:38 -05:00
PJ Dietz 1c82908eeb Refactor Router to use RouteTables 2015-02-21 08:10:58 -05:00
PJ Dietz 14195355e3 Add RouteTable 2015-02-21 07:13:09 -05:00
PJ Dietz 56cf56c6c5 Fix typo 2015-02-20 07:45:48 -05:00
PJ Dietz 3d68a0af86 Add RouteFactory 2015-02-20 07:45:39 -05:00
PJ Dietz 04561076d5 Update Router to work with updated Routes and ErrorHandlers
Deprecate:
	- Router::setStaticRoute
	- Router::setPrefixRoute
2015-02-19 22:04:34 -05:00
PJ Dietz 4deac492dd Update BaseRoute to use HandlerUnpacker 2015-02-19 19:59:33 -05:00
PJ Dietz 5dc5cdab06 Add HandlerUnpacker 2015-02-19 19:51:42 -05:00
PJ Dietz d34607a0d9 Update TemplateRoute test 2015-02-18 21:54:58 -05:00
PJ Dietz deff504942 Update BaseRoute test 2015-02-18 21:54:25 -05:00
PJ Dietz c38659a310 Update TemplateRoute tests 2015-02-18 21:39:07 -05:00
PJ Dietz 6859bd9707 Update RegexRoute tests 2015-02-18 21:21:56 -05:00
PJ Dietz 1b17ef5d0a Revise BaseRoute tests 2015-02-18 20:48:12 -05:00
PJ Dietz 63fd00fff0 Update PrefixRoute tests 2015-02-18 20:47:45 -05:00
PJ Dietz 38aaf26943 Update StaticRoute tests to use Prophecy 2015-02-18 20:47:00 -05:00
PJ Dietz 9498542f30 Allow Route target to be a callable, string, or instance 2015-02-18 20:17:09 -05:00
PJ Dietz 38639d9ee4 Update TemplateRouter to better match templates with variables in more complicated paths 2015-02-02 17:08:25 -05:00
PJ Dietz bcaa0ee7b7 Use random ports for Client test to reduce false errors on Travis 2015-01-21 13:49:17 -05:00
PJ Dietz e256610680 Update README. Add PHP 5.6 to Travis. 2015-01-21 12:56:55 -05:00
PJ Dietz 84044d5057 Re-add converting HttpExceptions to responses in Handler 2015-01-21 11:03:01 -05:00
PJ Dietz 1a88e0273d Use registered 404 error handler when no route matches in Router::respond 2015-01-21 09:51:14 -05:00
PJ Dietz ca2c8625ec Store PrefixRoutes to a separate array.
Prioritize routes in the order static, prefix, everything else.
2015-01-02 13:13:08 -05:00
PJ Dietz caef817535 Do not allow routing to continue after a dispatched StaticRoute returns null 2015-01-02 12:31:02 -05:00
PJ Dietz 78fe57d736 Store StaticRoutes to separate hash array in Router
Add StaticRouteInterface
2015-01-02 12:00:30 -05:00
Phil 6ae85398db Just making silly mistakes now... 2015-01-01 20:43:07 +00:00
Phil b6ec262d0e Forgot to catch exceptions in static routes 2015-01-01 20:38:57 +00:00
Phil 451a1c0576 Trying to stop not set error... 2015-01-01 20:27:45 +00:00
Phil aaaf644118 Initialized response to null to be safe 2015-01-01 20:21:17 +00:00
Phil bb052625af Made sure not to iterate over StaticRoutes again because the key they map to is a specific child Handler 2015-01-01 20:17:48 +00:00
Phil 3aca197d7a Updated readme to note optimized static routes 2015-01-01 19:47:05 +00:00
Phil b0c1330a26 Optimized for static routes 2015-01-01 19:44:33 +00:00
130 changed files with 12499 additions and 5532 deletions

15
.gitattributes vendored Normal file
View File

@ -0,0 +1,15 @@
/.env export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.php_cs* export-ignore
/.travis.yml export-ignore
/composer.lock export-ignore
/coverage export-ignore
/docker export-ignore
/docker-compose* export-ignore
/docs export-ignore
/tests export-ignore
/phpunit.xml* export-ignore
/psalm.xml export-ignore
/public export-ignore
/vendor export-ignore

25
.gitignore vendored
View File

@ -1,17 +1,30 @@
# Composer
vendor/
# Generated documentation
docs/
# PhpDocumentor
phpdoc/
# Code coverage report
coverage/
report/
# Cache
.php_cs.cache
# Sphinx Documentation
docs/build
# Previewing the README.md
README.html
preview
# OSX
.DS_Store
.AppleDouble
# PhpStorm
workspace.xml
# Local scratch files
notes
# Local overrides
.env
docker-compose.override.yml
phpunit.xml

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
</project>

View File

@ -1,11 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0" is_locked="false">
<option name="myName" value="Project Default" />
<option name="myLocal" value="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

View File

@ -1,7 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="Project Default" />
<option name="USE_PROJECT_PROFILE" value="true" />
<version value="1.0" />
</settings>
</component>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" />
</project>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/wellrested.iml" filepath="$PROJECT_DIR$/.idea/wellrested.iml" />
</modules>
</component>
</project>

View File

@ -1,3 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4" />

View File

@ -1,5 +0,0 @@
<component name="DependencyValidationManager">
<state>
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
</state>
</component>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -1,11 +0,0 @@
language: php
php:
- "5.5"
- "5.4"
before_script:
- composer selfupdate
- composer install --prefer-source
script:
- vendor/bin/phpunit

243
README.md
View File

@ -1,199 +1,128 @@
WellRESTed
==========
[![Build Status](https://travis-ci.org/pjdietz/wellrested.svg?branch=master)](https://travis-ci.org/pjdietz/wellrested)
[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D7.2-blue)](https://php.net/)
[![Documentation Status](https://readthedocs.org/projects/wellrested/badge/?version=latest)](http://wellrested.readthedocs.org/en/latest/)
WellRESTed is a micro-framework for creating RESTful APIs in PHP. It provides a lightweight yet powerful routing system and classes to make working with HTTP requests and responses clean and easy.
WellRESTed is a library for creating RESTful APIs and websites in PHP that provides abstraction for HTTP messages, a powerful handler and middleware system, and a flexible router.
Version 2
---------
This fork (basemaster/wellrested) is back to php 7.2 release.
It's more RESTed than ever!
Version 2 brings a lot improvements over 1.x, but it is **not backwards compatible**. See [Changes from Version 1](https://github.com/pjdietz/wellrested/wiki/Changes-from-Version-1) if you are migrating from a previous 1.x version of WellRESTed.
Requirements
------------
- PHP 5.3
- [PHP cURL](http://php.net/manual/en/book.curl.php) for making requests with the [`Client`](src/pjdietz/WellRESTed/Client.php) class (Optional)
### Features
- Uses [PSR-7](https://www.php-fig.org/psr/psr-7/) interfaces for requests, responses, and streams. This lets you use other PSR-7 compatable libraries seamlessly with WellRESTed.
- Uses [PSR-15](https://www.php-fig.org/psr/psr-15/) interfaces for handlers and middleware to allow sharing and reusing code
- Router allows you to match paths with variables such as `/foo/{bar}/{baz}`.
- Middleware system provides a way to compose your application from discrete, modular components.
- Lazy-loaded handlers and middleware don't instantiate unless they're needed.
Install
-------
Add an entry for "pjdietz/wellrested" to your composer.json file's `require` property. If you are not already using Composer, create a file in your project called "composer.json" with the following content:
Add an entry for "wellrested/wellrested" to your composer.json file's `require` property.
```json
{
"require": {
"pjdietz/wellrested": "2.*"
"wellrested/wellrested": "^5"
}
}
```
Use Composer to download and install WellRESTed. Run these commands from the directory containing the **composer.json** file.
Documentation
-------------
See [the documentation](https://wellrested.readthedocs.org/en/latest/) to get started.
Example
-------
```php
<?php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use WellRESTed\Message\Response;
use WellRESTed\Message\Stream;
use WellRESTed\Server;
// Create a handler using the PSR-15 RequestHandlerInterface
class HomePageHandler implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
// Create and return new Response object to return with status code,
// headers, and body.
$response = (new Response(200))
->withHeader('Content-type', 'text/html')
->withBody(new Stream('<h1>Hello, world!</h1>'));
return $response;
}
}
// -----------------------------------------------------------------------------
// Create a new Server instance.
$server = new Server();
// Add a router to the server to map methods and endpoints to handlers.
$router = $server->createRouter();
// Register the route GET / with an anonymous function that provides a handler.
$router->register("GET", "/", function () { return new HomePageHandler(); });
// Add the router to the server.
$server->add($router);
// Read the request from the client, dispatch a handler, and output.
$server->respond();
```
Development
-----------
Use Docker to run unit tests, manage Composer dependencies, and render a preview of the documentation site.
To get started, run:
```bash
$ curl -s https://getcomposer.org/installer | php
$ php composer.phar install
docker-compose build
docker-compose run --rm php composer install
```
You can now use WellRESTed by including the `vendor/autoload.php` file generated by Composer.
To run PHPUnit tests, use the `php` service:
Examples
--------
### Routing
WellRESTed's primary goal is to facilitate mapping of URIs to classes that will provide or accept representations. To do this, create a [`Router`](src/pjdietz/WellRESTed/Router.php) instance and load it up with some routes. Each route is simply a mapping of a path or path pattern to a class name. The class name represents the "handler" (any class implementing [`HandlerInterface`](src/pjdietz/WellRESTed/Interfaces/HandlerInterface.php) ) which the router will dispatch when it receives a request for the given URI. **The handlers are never instantiated or loaded unless they are needed.**
```php
// Build the router.
$myRouter = new Router();
$myRouter->addRoutes(array(
new StaticRoute("/", "\\myapi\\Handlers\\RootHandler"),
new StaticRoute("/cats/", "\\myapi\\Handlers\\CatCollectionHandler"),
new TemplateRoute("/cats/{id}/", "\\myapi\\Handlers\\CatItemHandler")
));
$myRouter->respond();
```bash
docker-compose run --rm php phpunit
```
See [Routes](https://github.com/pjdietz/wellrested/wiki/Routes) to learn about the various route classes.
To run Psalm for static analysis:
### Handlers
Any class that implements [`HandlerInterface`](src/pjdietz/WellRESTed/Interfaces/HandlerInterface.php) may be the handler for a route. This could be a class that builds the actual response, or it could be another [`Router`](src/pjdietz/WellRESTed/Router.php).
For most cases, you'll want to use a subclass of the [`Handler`](src/pjdietz/WellRESTed/Handler.php) class, which provides methods for responding based on HTTP method. When you create your [`Handler`](src/pjdietz/WellRESTed/Handler.php) subclass, you will implement a method for each HTTP verb you would like the endpoint to support. For example, if `/cats/` should support `GET`, you would override the `get()` method. For `POST`, `post()`, etc.
Here's a simple Handler that allows `GET` and `POST`.
```php
class CatsCollectionHandler extends \pjdietz\WellRESTed\Handler
{
protected function get()
{
// Read some cats from the database, cache, whatever.
// ...read these an array as the variable $cats.
// Set the values for the instance's response member. This is what the
// Router will eventually output to the client.
$this->response->setStatusCode(200);
$this->response->setHeader("Content-Type", "application/json");
$this->response->setBody(json_encode($cats));
}
protected function post()
{
// Read from the instance's request member and store a new cat.
$cat = json_decode($this->request->getBody());
// ...store $cat to the database...
// Build a response to send to the client.
$this->response->setStatusCode(201);
$this->response->setBody(json_encode($cat));
}
}
```bash
docker-compose run --rm php psalm
```
See [Handlers](https://github.com/pjdietz/wellrested/wiki/Handlers) to learn about the subclassing the [`Handler`](src/pjdietz/WellRESTed/Handler.php) class.
See [HandlerInteface](https://github.com/pjdietz/wellrested/wiki/HandlerInterface) to learn about more ways build completely custom classes.
To run PHP Coding Standards Fixer:
### Responses
You've already seen a [`Response`](src/pjdietz/WellRESTed/Response.php) used inside a [`Handler`](src/pjdietz/WellRESTed/Handler.php) in the examples above. You can also create a [`Response`](src/pjdietz/WellRESTed/Response.php) outside of [`Handler`](src/pjdietz/WellRESTed/Handler.php). Let's take a look at creating a new [`Response`](src/pjdietz/WellRESTed/Response.php), setting a header, supplying the body, and outputting.
```php
$resp = new \pjdietz\WellRESTed\Response();
$resp->setStatusCode(200);
$resp->setHeader("Content-type", "text/plain");
$resp->setBody("Hello world!");
$resp->respond();
exit;
```bash
docker-compose run --rm php php-cs-fixer fix
```
This will output nice response, complete with status code, headers, body.
To generate documentation, use the `docs` service:
### Requests
From outside the context of a [`Handler`](src/pjdietz/WellRESTed/Handler.php), you can also use the [`Request`](src/pjdietz/WellRESTed/Request.php) class to read info for the request sent to the server by using the static method `Request::getRequest()`.
```php
// Call the static method Request::getRequest() to get the request made to the server.
$rqst = \pjdietz\WellRESTed\Request::getRequest();
if ($rqst->getMethod() === 'PUT') {
$obj = json_decode($rqst->getBody());
// Do something with the JSON sent as the message body.
// ...
}
```bash
# Generate
docker-compose run --rm docs
# Clean
docker-compose run --rm docs make clean -C docs
```
### HTTP Client
To run a local playground site, use:
The [`Client`](src/pjdietz/WellRESTed/Client.php) class allows you to make an HTTP request using cURL.
(This feature requires [PHP cURL](http://php.net/manual/en/book.curl.php).)
```php
// Prepare a request.
$rqst = new \pjdietz\WellRESTed\Request();
$rqst->setUri('http://my.api.local/resources/');
$rqst->setMethod('POST');
$rqst->setBody(json_encode($newResource));
// Use a Client to get a Response.
$client = new Client();
$resp = $client->request($rqst);
// Read the response.
if ($resp->getStatusCode() === 201) {
// The new resource was created.
$createdResource = json_decode($resp->getBody());
}
```bash
docker-compose up -d
```
### Building Routes with JSON
WellRESTed also provides a class to construct routes for you based on a JSON description. Here's an example:
```php
$json = <<<'JSON'
{
"handlerNamespace": "\\myapi\\Handlers",
"routes": [
{
"path": "/",
"handler": "RootHandler"
},
{
"path": "/cats/",
"handler": "CatCollectionHandler"
},
{
"tempalte": "/cats/{id}",
"handler": "CatItemHandler"
}
]
}
JSON;
$builder = new RouteBuilder();
$routes = $builder->buildRoutes($json);
$router = new Router();
$router->addRoutes($routes);
$router->respond();
```
When you build routes through JSON, you can provide a `handlerNamespace` to be affixed to the front of every handler.
The runs a site you can access at [http://localhost:8080](http://localhost:8080). You can use this site to browser the [documentation](http://localhost:8080/docs/) or [code coverage report](http://localhost:8080/coverage/).
Copyright and License
---------------------
Copyright © 2015 by PJ Dietz
Copyright © 2020 by PJ Dietz
Licensed under the [MIT license](http://opensource.org/licenses/MIT)

View File

@ -1,26 +1,29 @@
{
"name": "pjdietz/wellrested",
"description": "Simple PHP Library for RESTful APIs",
"keywords": ["rest", "restful", "api", "curl", "http"],
"homepage": "https://github.com/pjdietz/wellrested",
"name": "basemaster/wellrested",
"description": "Clone for Simple PHP Library for RESTful APIs (wellrested.org)",
"keywords": ["rest", "restful", "api", "http", "psr7", "psr-7", "psr15", "psr-15", "psr17", "psr-17"],
"license": "MIT",
"type": "library",
"authors": [
{
"name": "PJ Dietz",
"email": "pj@pjdietz.com"
"email": "pjdietz@gmail.com"
}
],
"require": {
"php": ">=5.3.0"
"php": ">=7.2",
"psr/http-factory": "~1.0",
"psr/http-message": "~1.0",
"psr/http-server-handler": "~1.0",
"psr/http-server-middleware": "~1.0"
},
"require-dev": {
"fzaninotto/faker": "1.5.*@dev",
"phpunit/phpunit": "4.4.*",
"pjdietz/shamserver": "dev-master"
"provide": {
"psr/http-message-implementation": "1.0",
"psr/http-factory-implementation": "1.0"
},
"autoload": {
"psr-0": { "pjdietz\\WellRESTed": "src/" },
"classmap": ["src/pjdietz/WellRESTed/Exceptions/HttpExceptions.php"]
"psr-4": {
"WellRESTed\\": "src/"
}
}
}

1002
composer.lock generated

File diff suppressed because it is too large Load Diff

177
docs/Makefile Normal file
View File

@ -0,0 +1,177 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/WellRESTed.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/WellRESTed.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/WellRESTed"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/WellRESTed"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

242
docs/make.bat Normal file
View File

@ -0,0 +1,242 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
set I18NSPHINXOPTS=%SPHINXOPTS% source
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. xml to make Docutils-native XML files
echo. pseudoxml to make pseudoxml-XML files for display purposes
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
%SPHINXBUILD% 2> nul
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\WellRESTed.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\WellRESTed.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdf" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf
cd %BUILDDIR%/..
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdfja" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf-ja
cd %BUILDDIR%/..
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
if "%1" == "xml" (
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The XML files are in %BUILDDIR%/xml.
goto end
)
if "%1" == "pseudoxml" (
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
goto end
)
:end

View File

View File

View File

@ -0,0 +1,22 @@
Additional Components
=====================
The core WellRESTed library is designed to be very small and limited in scope. It should do only what's needed, and no more. One of WellRESTed's main goals is to stay small, and not force anything on consumers.
That being said, there are a number or situations that come up that warrant solutions. For that, WellRESTed also provides a (growing) number of companion packages that you may find useful, depending on the project.
`HTTP Exceptions`_
A collection of Exception classes that correspond to common HTTP error status codes.
`Error Handling`_
Classes to facilitate error handling including
`Test Components`_
Test cases and doubles for use with WellRESTed
Or, see WellRESTed_ on GitHub.
.. _HTTP Exceptions: https://github.com/wellrestedphp/http-exceptions
.. _Error Handling: https://github.com/wellrestedphp/error-handling
.. _Test Components: https://github.com/wellrestedphp/test
.. _WellRESTed: https://github.com/wellrestedphp

195
docs/source/conf.py Normal file
View File

@ -0,0 +1,195 @@
# -*- coding: utf-8 -*-
import sys
import os
from sphinx.highlighting import lexers
from pygments.lexers.web import PhpLexer
lexers['php'] = PhpLexer(startinline=True, linenos=1)
lexers['php-annotations'] = PhpLexer(startinline=True, linenos=1)
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'WellRESTed'
copyright = u'2021, PJ Dietz'
version = '5.0.0'
release = '5.0.0'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = []
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'WellRESTeddoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'WellRESTed.tex', u'WellRESTed Documentation',
u'PJ Dietz', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'wellrested', u'WellRESTed Documentation',
[u'PJ Dietz'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'WellRESTed', u'WellRESTed Documentation',
u'PJ Dietz', 'WellRESTed', 'One line description of project.',
'Miscellaneous'),
]
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if not on_rtd: # only import and set the theme if we're building docs locally
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]

View File

@ -0,0 +1,63 @@
Dependency Injection
====================
WellRESTed strives to play nicely with other code and not force developers into using any specific libraries or frameworks. As such, WellRESTed does not provide a dependency injection container, nor does it require you to use a specific container (or any).
This section describes the recommended way of using WellRESTed with Pimple_, a common dependency injection container for PHP.
Imaging we have a ``FooHandler`` that depends on a ``BarInterface``, and ``BazInterface``. Our handler looks something like this:
.. code-block:: php
class FooHandler implements RequestHandlerInterface
{
private $bar;
private $baz;
public function __construct(BarInterface $bar, BazInterface $baz)
{
$this->bar = $bar;
$this->baz = $baz;
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
// Do something with the bar and baz and return a response...
// ...
}
}
We can register the handler and these dependencies in a Pimple_ service provider.
.. code-block:: php
class MyServiceProvider implements ServiceProviderInterface
{
public function register(Container $c)
{
// Register the Bar and Baz as services.
$c['bar'] = function ($c) {
return new Bar();
};
$c['baz'] = function ($c) {
return new Baz();
};
// Register the Handler as a protected function. When you use
// protect, Pimple returns the function itself instead of the return
// value of the function.
$c['fooHandler'] = $c->protect(function () use ($c) {
return new FooHandler($c['bar'], $c['baz']);
});
}
}
To register this handler with a router, we can pass the service:
.. code-block:: php
$router->register('GET', '/foo', $c['fooHandler']);
By "protecting" the ``fooHandler`` service, we are delaying the instantiation of the ``FooHandler``, the ``Bar``, and the ``Baz`` until the handler needs to be dispatched. This works because we're not passing instance of ``FooHandler`` when we register this with a router, we're passing a function to it that does the instantiation on demand.
.. _Pimple: https://pimple.symfony.com/

102
docs/source/extending.rst Normal file
View File

@ -0,0 +1,102 @@
Extending and Customizing
=========================
WellRESTed is designed with customization in mind. This section describes some common scenarios for customization, starting with using a handler that implements a different interface.
Custom Handlers and Middleware
------------------------------
Imagine you found a handler class from a third party that does exactly what you need. The only problem is that it implements a different interface.
Here's the interface for the third-party handler:
.. code-block:: php
interface OtherHandlerInterface
{
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function run(ResponseInterface $response);
}
Wrapping
^^^^^^^^
One solution is to wrap an instance of this handler inside of a ``Psr\Http\Server\RequestHandlerInterface`` instance.
.. code-block:: php
/**
* Wraps an instance of OtherHandlerInterface
*/
class OtherHandlerWrapper implements RequestHandlerInterface
{
private $handler;
public function __construct(OtherHandlerInterface $handler)
{
$this->handler = $handler;
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->handler->run($request);
}
}
Custom Dispatcher
^^^^^^^^^^^^^^^^^
Wrapping works well when you have one or two handlers implementing a third-party interface. If you want to integrate a lot of classes that implement a given third-party interface, you're might consider customizing the dispatcher.
The dispatcher is an instance that unpacks your handlers and middleware and sends the request and response through it. A default dispatcher is created for you when you use your ``WellRESTed\Server``.
If you need the ability to dispatch other types of middleware, you can create your own by implementing ``WellRESTed\Dispatching\DispatcherInterface``. The easiest way to do this is to subclass ``WellRESTed\Dispatching\Dispatcher``. Here's an example that extends ``Dispatcher`` and adds support for ``OtherHandlerInterface``:
.. code-block:: php
/**
* Dispatcher with support for OtherHandlerInterface
*/
class CustomDispatcher extends \WellRESTed\Dispatching\Dispatcher
{
public function dispatch(
$dispatchable,
ServerRequestInterface $request,
ResponseInterface $response,
$next
) {
try {
// Use the dispatch method in the parent class first.
$response = parent::dispatch($dispatchable, $request, $response, $next);
} catch (\WellRESTed\Dispatching\DispatchException $e) {
// If there's a problem, check if the handler or middleware
// (the "dispatchable") implements OtherHandlerInterface.
// Dispatch it if it does.
if ($dispatchable instanceof OtherHandlerInterface) {
$response = $dispatchable->run($request);
} else {
// Otherwise, re-throw the exception.
throw $e;
}
}
return $response;
}
}
To use this dispatcher, create an instance implementing ``WellRESTed\Dispatching\DispatcherInterface`` and pass it to the server's ``setDispatcher`` method.
.. code-block:: php
$server = new WellRESTed\Server();
$server->setDispatcher(new MyApi\CustomDispatcher());
.. warning::
When you supply a custom Dispatcher, be sure to call ``Server::setDispatcher`` before you create any routers with ``Server::createRouter`` to allow the ``Server`` to pass you customer ``Dispatcher`` on to the newly created ``Router``.
.. _PSR-7: https://www.php-fig.org/psr/psr-7/
.. _Handlers and Middleware: handlers-and-middleware.html
.. _Request Attributes: messages.html#attributes

View File

@ -0,0 +1,169 @@
Getting Started
===============
This page provides a brief introduction to WellRESTed. We'll take a tour of some of the features of WellRESTed without getting into too much depth.
To start, we'll make a "Hello, world!" to demonstrate the concepts of handlers and routing and show how to read variables from the request path.
Hello, World!
^^^^^^^^^^^^^
Let's start with a very basic "Hello, world!" Here, we will create a server. A ``WellRESTed\Server`` reads the incoming request from the client, dispatches a handler, and transmits a response back to the client.
Our handler will create and return a response with the status code set to ``200`` and the body set to "Hello, world!".
.. _`Example 1`:
.. rubric:: Example 1: Simple "Hello, world!"
.. code-block:: php
<?php
use Psr\Http\Server\RequestHandlerInterface;
use WellRESTed\Message\Response;
use WellRESTed\Message\Stream;
use WellRESTed\Server;
require_once 'vendor/autoload.php';
// Define a handler implementing the PSR-15 RequestHandlerInterface interface.
class HelloHandler implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$response = (new Response(200))
->withHeader('Content-type', 'text/plain')
->withBody(new Stream('Hello, world!'));
return $response;
}
}
// Create a new server.
$server = new Server();
// Add this handler to the server.
$server->add(new HelloHandler());
// Read the request sent to the server and use it to output a response.
$server->respond();
.. note::
The handler in this example provides a ``Stream`` as the body instead of a string. This is a feature or PSR-7 where HTTP message bodies are always represented by streams. This allows you to work with very large bodies without having to store the entire contents in memory.
WellRESTed provides ``Stream`` and ``NullStream``, but you can use any implementation of ``Psr\Http\Message\StreamInterface``.
Routing by Path
^^^^^^^^^^^^^^^
This is a good start, but it provides the same response to every request. Let's provide this response only when a client sends a request to ``/hello``.
For this, we need a router_. A router_ examines the request and sends the request through to the handler that matches the request's HTTP method and path.
.. _`Example 2`:
.. rubric:: Example 2: Routed "Hello, world!"
.. code-block:: php
// Create a new server.
$server = new Server();
// Create a router to map methods and endpoints to handlers.
$router = $server->createRouter();
$router->register('GET', '/hello', new HelloHandler());
$server->add($router);
// Read the request sent to the server and use it to output a response.
$server->respond();
Reading Path Variables
^^^^^^^^^^^^^^^^^^^^^^
Routes can be static (like the one above that matches only ``/hello``), or they can be dynamic. Here's an example that uses a dynamic route to read a portion from the path to use as the greeting. For example, a request to ``/hello/Molly`` will respond "Hello, Molly", while a request to ``/hello/Oscar`` will respond "Hello, Oscar!"
.. _`Example 3`:
.. rubric:: Example 3: Personalized "Hello, world!"
.. code-block:: php
class HelloHandler implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
// Check for a "name" attribute which may have been provided as a
// path variable. Use "world" as a default.
$name = $request->getAttribute("name", "world");
// Set the response body to the greeting and the status code to 200 OK.
$response = (new Response(200))
->withHeader("Content-type", "text/plain")
->withBody(new Stream("Hello, $name!"));
// Return the response.
return $response;
}
}
// Create the server and router.
$server = new Server();
$router = $server->createRouter();
// Register the middleware for an exact match to /hello
$router->register("GET", "/hello", $hello);
// Register to match a pattern with a variable.
$router->register("GET", "/hello/{name}", $hello);
$server->add($router);
$server->respond();
Middleware
^^^^^^^^^^
In addition to handlers, which provide responses directly, WellRESTed also supports middleware to act on the requests and then pass them on for other middleware or handlers to work with.
Middleware allows you to compose your application in multiple pieces. In the example, we'll use middleware to add a header to every response, regardless of which handler is called.
.. code-block:: php
// This middleware will add a custom header to every response.
class CustomHeaderMiddleware implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
// Delegate to the next handler in the chain to obtain a response.
$response = $handler->handle($request);
// Add the header to the response we got back from upstream.
$response = $response->withHeader("X-example", "hello world");
// Return the altered response.
return $response;
}
}
// Create a server
$server = new Server();
// Add the header-adding middleware to the server first so that it will
// forward requests on to the router.
$server->add(new CustomHeaderMiddleware());
// Create a router to map methods and endpoints to handlers.
$router = $server->createRouter();
$handler = new HelloHandler();
// Register a route to the handler without a variable in the path.
$router->register('GET', '/hello', $handler);
// Register a route that reads a "name" from the path.
// This will make the "name" request attribute available to the handler.
$router->register('GET', '/hello/{name}', $handler);
$server->add($router);
// Read the request from the client, dispatch, and output.
$server->respond();
.. _middleware: middleware.html
.. _router: router.html

View File

@ -0,0 +1,236 @@
Handlers and Middleware
=======================
WellRESTed allows you to define and use your handlers and middleware in a number of ways.
Defining Handlers and Middleware
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
PSR-15 Interfaces
-----------------
The preferred method is to use the interfaces standardized by PSR-15_. This standard includes two interfaces, ``Psr\Http\Server\RequestHandlerInterface`` and ``Psr\Http\Server\MiddlewareInterface``.
Use ``RequestHandlerInterface`` for individual components that generate and return responses.
.. code-block:: php
class HelloHandler implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
// Check for a "name" attribute which may have been provided as a
// path variable. Use "world" as a default.
$name = $request->getAttribute("name", "world");
// Set the response body to the greeting and the status code to 200 OK.
$response = (new Response(200))
->withHeader("Content-type", "text/plain")
->withBody(new Stream("Hello, $name!"));
// Return the response.
return $response;
}
}
Use ``MiddlewareInterface`` for classes that interact with other middleware and handlers. For example, you may have middleware that attempts to retrieve a cached response and delegates to other handlers on a cache miss.
.. code-block:: php
class CacheMiddleware implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface
{
// Inspect the request to see if there is a representation on hand.
$representation = $this->getCachedRepresentation($request);
if ($representation !== null) {
// There is already a cached representation.
// Return it without delegating to the next handler.
return (new Response())
->withStatus(200)
->withBody($representation);
}
// No representation exists. Delegate to the next handler.
$response = $handler->handle($request);
// Attempt to store the response to the cache.
$this->storeRepresentationToCache($response);
return $response
}
private function getCachedRepresentation(ServerRequestInterface $request)
{
// Look for a cached representation. Return null if not found.
// ...
}
private function storeRepresentationToCache(ResponseInterface $response)
{
// Ensure the response contains a success code, a valid body,
// headers that allow caching, etc. and store the representation.
// ...
}
}
Legacy Middleware Interface
---------------------------
Prior to PSR-15, WellRESTed's recommended handler interface was ``WellRESTed\MiddlewareInterface``. This interface is still supported for backwards compatibility.
This interface serves for both handlers and middleware. It differs from the ``Psr\Http\Server\MiddlewareInterface`` in that is expects an incoming ``$response`` parameter which you may use to generate the returned response. It also expected a ``$next`` parameter which is a ``callable`` with this signature:
.. code-block:: php
function next($request, $response): ResponseInterface
Call ``$next`` and pass ``$request`` and ``$response`` to forward the request to the next handler. ``$next`` will return the response from the handler. Here's the cache example above as a ``WellRESTed\MiddlewareInterface``.
.. code-block:: php
class CacheMiddleware implements WellRESTed\MiddlewareInterface
{
public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response,
$next
) {
// Inspect the request to see if there is a representation on hand.
$representation = $this->getCachedRepresentation($request);
if ($representation !== null) {
// There is already a cached representation.
// Return it without delegating to the next handler.
return $response
->withStatus(200)
->withBody($representation);
}
// No representation exists. Delegate to the next handler.
$response = $next($request, $response);
// Attempt to store the response to the cache.
$this->storeRepresentationToCache($response);
return $response
}
private function getCachedRepresentation(ServerRequestInterface $request)
{
// Look for a cached representation. Return null if not found.
// ...
}
private function storeRepresentationToCache(ResponseInterface $response)
{
// Ensure the response contains a success code, a valid body,
// headers that allow caching, etc. and store the representation.
// ...
}
}
Callables
---------
You may also use a ``callable`` similar to the legacy ``WellRESTed\MiddlewareInterface``. The signature of the callable matches the signature of ``WellRESTed\MiddlewareInterface::__invoke``.
.. code-block:: php
$handler = function ($request, $response, $next) {
// Delegate to the next handler.
$response = $next($request, $response);
return $response
->withHeader("Content-type", "text/plain")
->withBody(new Stream("Hello, $name!"));
}
Using Handlers and Middleware
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Methods that accept handlers and middleware (e.g., ``Server::add``, ``Router::register``) allow you to provide them in a number of ways. For example, you can provide an instance, a ``callable`` that returns an instance, or an ``array`` of middleware to use in sequence. The following examples will demonstrate all of the ways you can register handlers and middleware.
Factory Functions
-----------------
The best method is to use a function that returns an instance of your handler. The main benefit of this approach is that no handlers are instantiated until they are needed.
.. code-block:: php
$router->register("GET,PUT,DELETE", "/widgets/{id}",
function () { return new App\WidgetHandler() }
);
If you're using ``Pimple``, a popular `dependency injection`_ container for PHP, you may have code that looks like this:
.. code-block:: php
// Create a DI container.
$c = new Container();
// Store a function to the container that will create and return the handler.
$c['widgetHandler'] = $c->protect(function () use ($c) {
return new App\WidgetHandler();
});
$router->register("GET,PUT,DELETE", "/widgets/{id}", $c['widgetHandler']);
Instance
--------
WellRESTed also allows you to pass an instance of a handler directly. This may be useful for smaller handlers that don't require many dependencies, although the factory function approach is better in most cases.
.. code-block:: php
$widgetHandler = new App\WidgetHandler();
$router->register("GET,PUT,DELETE", "/widgets/{id}", $widgetHandler);
.. warning::
This is simple, but has a significant disadvantage over the other options because each middleware used this way will be loaded and instantiated, even if it's not needed for a given request-response cycle. You may find this approach useful for testing, but avoid if for production code.
Fully Qualified Class Name (FQCN)
---------------------------------
For handlers that do not require any arguments passed to the constructor, you may pass the fully qualified class name of your handler as a ``string``. You can do that like this:
.. code-block:: php
$router->register('GET,PUT,DELETE', '/widgets/{id}', App\WidgetHandler::class);
// ... or ...
$router->register('GET,PUT,DELETE', '/widgets/{id}', 'App\\WidgetHandler');
The class is not loaded, and no instances are created, until the route is matched and dispatched. However, the drawback to this approach is the there is no way to pass any arguments to the constructor.
Array
-----
The final approach is to provide a sequence of middleware and a handler as an ``array``.
For example, imagine if we had a Pimple_ container with these services:
.. code-block:: php
$c['authMiddleware'] // Ensures the user is logged in
$c['cacheMiddleware'] // Provides a cached response if able
$c['widgetHandler'] // Provides a widget representation
We could provide these as a sequence by using an ``array``.
.. code-block:: php
$router->register('GET', '/widgets/{id}', [
$c['authMiddleware'],
$c['cacheMiddleware'],
$c['widgetHandler']
]);
.. _Dependency Injection: dependency-injection.html
.. _Pimple: https://pimple.symfony.com/
.. _PSR-15: https://www.php-fig.org/psr/psr-15/

147
docs/source/index.rst Normal file
View File

@ -0,0 +1,147 @@
WellRESTed
==========
WellRESTed is a library for creating RESTful APIs and websites in PHP that provides abstraction for HTTP messages, a powerful handler and middleware system, and a flexible router.
Features
--------
PSR-7 HTTP Messages
^^^^^^^^^^^^^^^^^^^
Request and response messages are built to the interfaces standardized by PSR-7_ making it easy to share code and use components from other libraries and frameworks.
The message abstractions facilitate working with message headers, status codes, variables extracted from the path, message bodies, and all the other aspects of requests and responses.
PSR-15 Handler Interfaces
^^^^^^^^^^^^^^^^^^^^^^^^^
WellRESTed can use handlers and middleware using the interfaces defined by the PSR-15_ standard.
Router
^^^^^^
The router_ allows you to define your endpoints using `URI Templates`_ like ``/foo/{bar}/{baz}`` that match patterns of paths and provide captured variables. You can also match exact paths for extra speed or regular expressions for extra flexibility.
WellRESTed's router automates responding to ``OPTIONS`` requests for each endpoint based on the methods you assign. ``405 Method Not Allowed`` responses come free of charge as well for any methods you have not implemented on a given endpoint.
Middleware
^^^^^^^^^^
The middleware_ system allows you to build your Web service out of discrete, modular pieces. These pieces can be run in sequences where each has an opportunity to work with the request before handing it off to the next. For example, an authenticator can validate a request and forward it to a cache; the cache can check for a stored representation and forward to another middleware if no cached representation is found, etc. All of this happens without any one middleware needing to know anything about where it is in the chain or which middleware comes before or after.
Lazy Loading
^^^^^^^^^^^^
Handlers and middleware can be registered using `factory functions`_ so that they are only instantiated if needed. This way, a Web service with hundreds of handlers and middleware only creates instances required for the current request-response cycle.
Extensible
^^^^^^^^^^
Most classes are coded to interfaces to allow you to provide your own implementations and use them in place of the built-in classes. For example, if your Web service needs to be able to dispatch middleware that implements a third-party interface, you can provide your own custom ``DispatcherInterface`` implementation.
Example
-------
Here's a customary "Hello, world!" example. This site will respond to requests for ``GET /hello`` with "Hello, world!" and provide custom responses for other paths (e.g., ``GET /hello/Molly`` will respond "Hello, Molly!").
The site will also provide an ``X-example: hello world`` using dedicated middleware, just to illustrate how middleware propagates.
.. code-block:: php
<?php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use WellRESTed\Message\Response;
use WellRESTed\Message\Stream;
use WellRESTed\Server;
require_once 'vendor/autoload.php';
// Create a handler that will construct and return a response. We'll
// register this handler with a server and router below.
class HelloHandler implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
// Check for a "name" attribute which may have been provided as a
// path variable. Use "world" as a default.
$name = $request->getAttribute("name", "world");
// Set the response body to the greeting and the status code to 200 OK.
$response = (new Response(200))
->withHeader("Content-type", "text/plain")
->withBody(new Stream("Hello, $name!"));
// Return the response.
return $response;
}
}
// Create middleware that will add a custom header to every response.
class CustomerHeaderMiddleware implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
// Delegate to the next handler in the chain to obtain a response.
$response = $handler->handle($request);
// Add the header.
$response = $response->withHeader("X-example", "hello world");
// Return the altered response.
return $response;
}
}
// Create a server
$server = new Server();
// Add the header-adding middleware to the server first so that it will
// forward requests on to the router.
$server->add(new CustomerHeaderMiddleware());
// Create a router to map methods and endpoints to handlers.
$router = $server->createRouter();
$handler = new HelloHandler();
// Register a route to the handler without a variable in the path.
$router->register('GET', '/hello', $handler);
// Register a route that reads a "name" from the path.
// This will make the "name" request attribute available to the handler.
$router->register('GET', '/hello/{name}', $handler);
$server->add($router);
// Read the request from the client, dispatch, and output.
$server->respond();
Contents
--------
.. toctree::
:maxdepth: 4
overview
getting-started
messages
handlers-and-middleware
router
uri-templates
uri-templates-advanced
extending
dependency-injection
additional
web-server-configuration
.. _PSR-7: https://www.php-fig.org/psr/psr-7/
.. _PSR-15: https://www.php-fig.org/psr/psr-15/
.. _factory functions: handlers-and-middleware.html#factory-functions
.. _middleware: handlers-and-middleware.html
.. _router: router.html
.. _URI Templates: uri-templates.html

443
docs/source/messages.rst Normal file
View File

@ -0,0 +1,443 @@
Messages and PSR-7
==================
WellRESTed uses the PSR-7_ interfaces for HTTP messages. This section provides an introduction to working with these interfaces and the implementations provided with WellRESTed. For more information, please read about PSR-7_.
Requests
--------
The ``$request`` variable passed to handlers and middleware represents the request message sent by the client. You can inspect this variable to read information such as the request path, method, query, headers, and body.
Let's start with a very simple GET request to the path ``/cats/?color=orange``.
.. code-block:: http
GET /cats/?color=orange HTTP/1.1
Host: example.com
Cache-control: no-cache
You can read information from the request in your handler like this:
.. code-block:: php
class MyHandler implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$path = $request->getRequestTarget();
// "/cats/?color=orange"
$method = $request->getMethod();
// "GET"
$query = $request->getQueryParams();
/*
Array
(
[color] => orange
)
*/
}
}
This example shows that you can use:
- ``getRequestTarget()`` to read the path and query string for the request
- ``getMethod()`` to read the HTTP verb (e.g., GET, POST, OPTIONS, DELETE)
- ``getQueryParams()`` to read the query as an associative array
Headers
^^^^^^^
The request above also included a ``Cache-control: no-cache`` header. You can read this header a number of ways. The simplest way is with the ``getHeaderLine($name)`` method.
Call ``getHeaderLine($name)`` and pass the case-insensitive name of a header. The method will return the value for the header, or an empty string if the header is not present.
.. code-block:: php
class MyHandler implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
// This message contains a "Cache-control: no-cache" header.
$cacheControl = $request->getHeaderLine("cache-control");
// "no-cache"
// This message does not contain any authorization headers.
$authorization = $request->getHeaderLine("authorization");
// ""
}
}
.. note::
All methods relating to headers treat header field names case insensitively.
Because HTTP messages may contain multiple headers with the same field name, ``getHeaderLine($name)`` has one other feature: If multiple headers with the same field name are present in the message, ``getHeaderLine($name)`` returns a string containing all of the values for that field, concatenated by commas. This is more common with responses, particularly with the ``Set-cookie`` header, but is still possible for requests.
You may also use ``hasHeader($name)`` to test if a header exists, ``getHeader($name)`` to receive an array of values for this field name, and ``getHeaders()`` to receive an associative array of headers where each key is a field name and each value is an array of field values.
Body
^^^^
PSR-7_ provides access to the body of the request as a stream and—when possible—as a parsed object or array. Let's start by looking at a request with form fields made available as an array.
Parsed Body
~~~~~~~~~~~
For POST requests for forms (i.e., the ``Content-type`` header is either ``application/x-www-form-urlencoded`` or ``multipart/form-data``), the request makes the form fields available via the ``getParsedBody`` method. This provides access to the fields without needing to rely on the ``$_POST`` superglobal.
Given this request:
.. code-block:: http
POST /cats/ HTTP/1.1
Host: example.com
Content-type: application/x-www-form-urlencoded
Content-length: 23
name=Molly&color=Calico
We can read the parsed body like this:
.. code-block:: php
class MyHandler implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$cat = $request->getParsedBody();
/*
Array
(
[name] => Molly
[color] => calico
)
*/
}
}
Body Stream
~~~~~~~~~~~
For other content types, use the ``getBody`` method to get a stream containing the contents of request entity body.
Using a JSON representation of our cat, we can make a request like this:
.. code-block:: http
POST /cats/ HTTP/1.1
Host: example.com
Content-type: application/json
Content-length: 46
{
"name": "Molly",
"color": "Calico"
}
We can read and parse the JSON body, and even provide it as the parsedBody for later middleware or handlers like this:
.. code-block:: php
class JsonParser implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface
{
// Parse the body.
$cat = json_decode((string) $request->getBody());
/*
stdClass Object
(
[name] => Molly
[color] => calico
)
*/
// Add the parsed JSON to the request.
$request = $request->withParsedBody($cat);
// Send the request to the next handler.
return $handler->handle($request);
}
}
Because the entity body of a request or response can be very large, PSR-7_ represents bodies as streams using the ``Psr\Http\Message\StreamInterface`` (see `PSR-7 Section 1.3`_).
The JSON example casts the stream to a string, but we can also do things like copy the stream to a local file:
.. code-block:: php
// Store the body to a temp file.
$chunkSize = 2048; // Number of bytes to read at once.
$localPath = tempnam(sys_get_temp_dir(), "body");
$h = fopen($localPath, "wb");
$body = $request->getBody();
while (!$body->eof()) {
fwrite($h, $body->read($chunkSize));
}
fclose($h);
Parameters
^^^^^^^^^^
PSR-7_ eliminates the need to read from many of the superglobals. We already saw how ``getParsedBody`` takes the place of reading directly from ``$_POST`` and ``getQueryParams`` replaces reading from ``$_GET``. Here are some other ``ServerRequestInterface`` methods with brief descriptions. Please see PSR-7_ for full details, particularly for ``getUploadedFiles``.
.. list-table::
:header-rows: 1
* - Method
- Replaces
- Note
* - getServerParams
- $_SERVER
- Data related to the request environment
* - getCookieParams
- $_COOKIE
- Compatible with the structure of $_COOKIE
* - getQueryParams
- $_GET
- Deserialized query string arguments, if any
* - getParsedBody
- $_POST
- Request body as an object or array
* - getUploadedFiles
- $_FILES
- Normalized tree of file upload data
Attributes
^^^^^^^^^^
``ServerRequestInterface`` provides another useful feature called "attributes". Attributes are key-value pairs associated with the request that can be, well, pretty much anything.
The primary use for attributes in WellRESTed is to provide access to path variables when using `template routes`_ or `regex routes`_.
For example, the template route ``/cats/{name}`` matches routes such as ``/cats/Molly`` and ``/cats/Oscar``. When the route is dispatched, the router takes the portion of the actual request path matched by ``{name}`` and provides it as an attribute.
For a request to ``/cats/Rufus``:
.. code-block:: php
$name = $request->getAttribute("name");
// "Rufus"
When calling ``getAttribute``, you can optionally provide a default value as the second argument. The value of this argument will be returned if the request has no attribute with that name.
.. code-block:: php
// Request has no attribute "dog"
$name = $request->getAttribute("dog", "Bear");
// "Bear"
Middleware can also use attributes as a way to provide extra information to subsequent handlers. For example, an authorization middleware could obtain an object representing a user and store is as the "user" attribute which later middleware could read.
.. code-block:: php
class AuthorizationMiddleware implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface
try {
$user = $this->readUserFromCredentials($request);
} catch (NoCredentialsSupplied $e) {
return $response->withStatus(401);
} catch (UserNotAllowedHere $e) {
return $response->withStatus(403);
}
// Store this as an attribute.
$request = $request->withAttribute("user", $user);
// Delegate to the handler, passing the request with the "user" attribute.
return $handler->handle($request);
}
};
class SecureHandler implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
// Read the "user" attribute added by a previous middleware.
$user = $request->getAttribute("user");
// Do something with $user ...
}
}
$server = new \WellRESTed\Server();
$server->add(new AuthorizationMiddleware());
$server->add(new SecureHandler()); // Must be added AFTER authorization to get "user"
$server->respond();
Responses
---------
PSR-7_ messages are immutable, so you will not be able to alter values of response properties. Instead, ``with*`` methods provide ways to get a copy of the current message with updated properties. For example, ``ResponseInterface::withStatus`` returns a copy of the original response with the status changed.
.. code-block:: php
// The original response has a 500 status code.
$response->getStatusCode();
// 500
// Replace this instance with a new instance with the status updated.
$response = $response->withStatus(200);
$response->getStatusCode();
// 200
.. note::
PSR-7_ requests are immutable as well, and we used ``withAttribute`` and ``withParsedBody`` in a few of the examples in the Requests section.
Chain multiple ``with`` methods together fluently:
.. code-block:: php
// Get a new response with updated status, headers, and body.
$response = (new Response())
->withStatus(200)
->withHeader("Content-type", "text/plain")
->withBody(new \WellRESTed\Message\Stream("Hello, world!);
Status
^^^^^^
Provide the status code for your response with the ``withStatus`` method. When you pass a standard status code to this method, the WellRESTed response implementation will provide an appropriate reason phrase for you. For a list of reason phrases provided by WellRESTed, see the IANA `HTTP Status Code Registry`_.
.. note::
The "reason phrase" is the text description of the status that appears in the status line of the response. The "status line" is the very first line in the response that appears before the first header.
Although the PSR-7_ ``ResponseInterface::withStatus`` method accepts the reason phrase as an optional second parameter, you generally shouldn't pass anything unless you are using a non-standard status code. (And you probably shouldn't be using a non-standard status code.)
.. code-block:: php
// Set the status and view the reason phrase provided.
$response = $response->withStatus(200);
$response->getReasonPhrase();
// "OK"
$response = $response->withStatus(404);
$response->getReasonPhrase();
// "Not Found"
Headers
^^^^^^^
Use the ``withHeader`` method to add a header to a response. ``withHeader`` will add the header if not already set, or replace the value of an existing header with the same name.
.. code-block:: php
// Add a "Content-type" header.
$response = $response->withHeader("Content-type", "text/plain");
$response->getHeaderLine("Content-type");
// "text/plain"
// Calling withHeader a second time updates the value.
$response = $response->withHeader("Content-type", "text/html");
$response->getHeaderLine("Content-type");
// "text/html"
To set multiple values for a given header field name (e.g., for ``Set-cookie`` headers), call ``withAddedHeader``. ``withAddedHeader`` adds the new header without altering existing headers with the same name.
.. code-block:: php
$response = $response
->withHeader("Set-cookie", "cat=Molly; Path=/cats; Expires=Wed, 13 Jan 2021 22:23:01 GMT;")
->withAddedHeader("Set-cookie", "dog=Bear; Domain=.foo.com; Path=/; Expires=Wed, 13 Jan 2021 22:23:01 GMT;")
->withAddedHeader("Set-cookie", "hamster=Fizzgig; Domain=.foo.com; Path=/; Expires=Wed, 13 Jan 2021 22:23:01 GMT;");
To check if a header exists or to remove a header, use ``hasHeader`` and ``withoutHeader``.
.. code-block:: php
// Check if a header exists.
$response->hasHeader("Content-type");
// true
// Clone this response without the "Content-type" header.
$response = $response->withoutHeader("Content-type");
// Check if a header exists.
$response->hasHeader("Content-type");
// false
Body
^^^^
To set the body for the response, pass an instance implementing ``Psr\Http\Message\Stream`` to the ``withBody`` method.
.. code-block:: php
$stream = new \WellRESTed\Message\Stream("Hello, world!");
$response = $response->withBody($stream);
WellRESTed provides two ``Psr\Http\Message\Stream`` implementations. You can use these, or any other implementation.
Stream
~~~~~~
``WellRESTed\Message\Stream`` wraps a file pointer resource and is useful for responding with a string or file.
When you pass a string to the constructor, the Stream instance uses `php://temp`_ as the file pointer resource. The string passed to the constructor is automatically stored to ``php://temp``, and you can write more content to it using the ``StreamInterface::write`` method.
.. note::
``php://temp`` stores the contents to memory, but switches to a temporary file once the amount of data stored hits a predefined limit (the default is 2 MB).
.. code-block:: php
// Pass the beginning of the contents to the constructor as a string.
$body = new \WellRESTed\Message\Stream("Hello ");
// Append more contents.
$body->write("world!");
// Set the body and status code.
$response = (new Response())
->withStatus(200)
->withBody($body);
To respond with the contents of an existing file, use ``fopen`` to open the file with read access and pass the pointer to the constructor.
.. code-block:: php
// Open the file with read access.
$resource = fopen("/home/user/some/file", "rb");
// Pass the file pointer resource to the constructor.
$body = new \WellRESTed\Message\Stream($resource);
// Set the body and status code.
$response = (new Response())
->withStatus(200)
->withBody($body);
NullStream
~~~~~~~~~~
Each PSR-7_ message MUST have a body, so there's no ``withoutBody`` method. You also cannot pass ``null`` to ``withBody``. Instead, use a ``WellRESTed\Messages\NullStream`` to provide a very simple, zero-length, no-content body.
.. code-block:: php
$response = (new Response())
->withStatus(200)
->withBody(new \WellRESTed\Message\NullStream());
.. _HTTP Status Code Registry: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
.. _PSR-7: http://www.php-fig.org/psr/psr-7/
.. _PSR-7 Section 1.3: https://www.php-fig.org/psr/psr-7/#13-streams
.. _Getting Started: getting-started.html
.. _Middleware: middleware.html
.. _template routes: router.html#template-routes
.. _regex routes: router.html#regex-routes
.. _dependency injection: dependency-injection.html
.. _`php://temp`: https://php.net/manual/ro/wrappers.php.php

49
docs/source/overview.rst Normal file
View File

@ -0,0 +1,49 @@
Overview
========
Installation
^^^^^^^^^^^^
The recommended method for installing WellRESTed is to use Composer_. Add an entry for WellRESTed in your project's ``composer.json`` file.
.. code-block:: js
{
"require": {
"wellrested/wellrested": "^5"
}
}
Requirements
^^^^^^^^^^^^
- PHP 7.3
License
^^^^^^^
Licensed using the `MIT license <http://opensource.org/licenses/MIT>`_.
The MIT License (MIT)
Copyright (c) 2021 PJ Dietz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
.. _Composer: https://getcomposer.org/

358
docs/source/router.rst Normal file
View File

@ -0,0 +1,358 @@
Router
======
A router is a type of middleware that organizes the components of a site by associating HTTP methods and paths with handlers and middleware. When the router receives a request, it examines the path components of the request's URI, determines which "route" matches, and dispatches the associated handler. The dispatched handler is then responsible for reacting to the request and providing a response.
Basic Usage
^^^^^^^^^^^
Typically, you will want to use the ``WellRESTed\Server::createRouter`` method to create a ``Router``.
.. code-block:: php
$server = new WellRESTed\Server();
$router = $server->createRouter();
Suppose ``$catHandler`` is a handler that you want to dispatch whenever a client makes a ``GET`` request to the path ``/cats/``. Use the ``register`` method map it to that path and method.
.. code-block:: php
$router->register("GET", "/cats/", $catHandler);
The ``register`` method is fluent, so you can add multiple routes in either of these styles:
.. code-block:: php
$router->register("GET", "/cats/", $catReader);
$router->register("POST", "/cats/", $catWriter);
$router->register("GET", "/cats/{id}", $catItemReader);
$router->register("PUT,DELETE", "/cats/{id}", $catItemWriter);
...Or...
.. code-block:: php
$router
->register("GET", "/cats/", $catReader)
->register("POST", "/cats/", $catWriter)
->register("GET", "/cats/{id}", $catItemReader)
->register("PUT,DELETE", "/cats/{id}", $catItemWriter);
Paths
^^^^^
A router can map a handler to an exact path, or to a pattern of paths.
Static Routes
-------------
The simplest type of route is called a "static route". It maps a handler to an exact path.
.. code-block:: php
$router->register("GET", "/cats/", $catHandler);
This route will map a request to ``/cats/`` and only ``/cats/``. It will **not** match requests to ``/cats`` or ``/cats/molly``.
Prefix Routes
-------------
The next simplest type of route is a "prefix route". A prefix route matches requests by the beginning of the path.
To create a "prefix handler", include ``*`` at the end of the path. For example, this route will match any request that begins with ``/cats/``.
.. code-block:: php
$router->register("GET", "/cats/*", $catHandler);
Template Routes
---------------
Template routes allow you to provide patterns for paths with one or more variables (sections surrounded by curly braces) that will be extracted.
For example, this template will match requests to ``/cats/12``, ``/cats/molly``, etc.,
.. code-block:: php
$router->register("GET", "/cats/{cat}", $catHandler);
When the router dispatches a route matched by a template route, it provides the extracted variables as request attributes. To access a variable, call the request object's ``getAttribute`` method and pass the variable's name.
For a request to ``/cats/molly``:
.. code-block:: php
$name = $request->getAttribute("cat");
// "molly"
Template routes are very powerful, and this only scratches the surface. See `URI Templates`_ for a full explanation of the syntax supported.
Regex Routes
------------
You can also use regular expressions to describe route paths.
.. code-block:: php
$router->register("GET", "~cats/(?<name>[a-z]+)-(?<number>[0-9]+)~", $catHandler);
When using regular expression routes, the attributes will contain the captures from preg_match_.
For a request to ``/cats/molly-90``:
.. code-block:: php
$vars = $request->getAttributes();
/*
Array
(
[0] => cats/molly-12
[name] => molly
[1] => molly
[number] => 12
[2] => 12
)
*/
Route Priority
--------------
A router will often contain many routes, and sometimes more than one route will match for a given request. When the router looks for a matching route, it performs these checks in order.
#. If there is a static route with exact match to path, dispatch it.
#. If one prefix route matches the beginning of the path, dispatch it.
#. If multiple prefix routes match, dispatch the longest matching prefix route.
#. Inspect each pattern route (template and regular expression) in the order in which they were added to the router. Dispatch the first route that matches.
#. If no pattern routes match, return a response with a ``404 Not Found`` status. (**Note:** This is the default behavior. To configure a router to delegate to the next middleware when no route matches, call the router's ``continueOnNotFound()`` method.)
Static vs. Prefix
~~~~~~~~~~~~~~~~~
Consider these routes:
.. code-block:: php
$router
->register("GET", "/cats/", $static);
->register("GET", "/cats/*", $prefix);
The router will dispatch a request for ``/cats/`` to ``$static`` because the static route ``/cats/`` has priority over the prefix route ``/cats/*``.
The router will dispatch a request to ``/cats/maine-coon`` to ``$prefix`` because it is not an exact match for ``/cats/``, but it does begin with ``/cats/``.
Prefix vs. Prefix
~~~~~~~~~~~~~~~~~
Given these routes:
.. code-block:: php
$router
->register("GET", "/dogs/*", $short);
->register("GET", "/dogs/sporting/*", $long);
A request to ``/dogs/herding/australian-shepherd`` will be dispatched to ``$short`` because it matches ``/dogs/*``, but does not match ``/dogs/sporting/*``
A request to ``/dogs/sporing/flat-coated-retriever`` will be dispatched to ``$long`` because it matches both routes, but ``/dogs/sporting`` is longer.
Prefix vs. Pattern
~~~~~~~~~~~~~~~~~~
Given these routes:
.. code-block:: php
$router
->register("GET", "/dogs/*", $prefix);
->register("GET", "/dogs/{group}/{breed}", $pattern);
``$pattern`` will **never** be dispatched because any route that matches ``/dogs/{group}/{breed}`` also matches ``/dogs/*``, and prefix routes have priority over pattern routes.
Pattern vs. Pattern
~~~~~~~~~~~~~~~~~~~
When multiple pattern routes match a path, the first one that was added to the router will be the one dispatched. **Be careful to add the specific routes before the general routes.** For example, say you want to send traffic to two similar looking URIs to different handlers based whether the variables were supplied as numbers or letters—``/dogs/102/132`` should be dispatched to ``$numbers``, while ``/dogs/herding/australian-shepherd`` should be dispatched to ``$letters``.
This will work:
.. code-block:: php
// Matches only when the variables are digits.
$router->register("GET", "~/dogs/([0-9]+)/([0-9]+)", $numbers);
// Matches variables with any unreserved characters.
$router->register("GET", "/dogs/{group}/{breed}", $letters);
This will **NOT** work:
.. code-block:: php
// Matches variables with any unreserved characters.
$router->register("GET", "/dogs/{group}/{breed}", $letters);
// Matches only when the variables are digits.
$router->register("GET", "~/dogs/([0-9]+)/([0-9]+)", $numbers);
This is because ``/dogs/{group}/{breed}`` will match both ``/dogs/102/132`` **and** ``/dogs/herding/australian-shepherd``. If it is added to the router before the route for ``$numbers``, it will be dispatched before the route for ``$numbers`` is ever evaluated.
Methods
^^^^^^^
When you register a route, you can provide a specific method, a list of methods, or a wildcard to indicate any method.
Registering by Method
---------------------
Specify a specific handler for a path and method by including the method as the first parameter.
.. code-block:: php
// Dispatch $dogCollectionReader for GET requests to /dogs/
$router->register("GET", "/dogs/", $dogCollectionReader);
// Dispatch $dogCollectionWriter for POST requests to /dogs/
$router->register("POST", "/dogs/", $dogCollectionWriter);
Registering by Method List
--------------------------
Specify the same handler for multiple methods for a given path by proving a comma-separated list of methods as the first parameter.
.. code-block:: php
// Dispatch $catCollectionHandler for GET and POST requests to /cats/
$router->register("GET,POST", "/cats/", $catCollectionHandler);
// Dispatch $catItemReader for GET requests to /cats/12, /cats/12, etc.
$router->register("GET", "/cats/{id}", $catItemReader);
// Dispatch $catItemWriter for PUT, and DELETE requests to /cats/12, /cats/12, etc.
$router->register("PUT,DELETE", "/cats/{id}", $catItemWriter);
Registering by Wildcard
-----------------------
Specify a handler for all methods for a given path by proving a ``*`` wildcard.
.. code-block:: php
// Dispatch $guineaPigHandler for all requests to /guinea-pigs/, regardless of method.
$router->register("*", "/guinea-pigs/", $guineaPigHandler);
// Use $hamstersHandler by default for requests to /hamsters/
$router->register("*", "/hamsters/", $hamstersHandler);
// Provide a specific handler for POST /hamsters/
$router->register("POST", "/hamsters/", $hamstersPostOnly);
.. note::
The wildcard ``*`` can be useful, but be aware that the associated middleware will need to manage ``HEAD`` and ``OPTIONS`` requests, whereas this is done automatically for non-wildcard routes.
HEAD
----
Any route that supports ``GET`` requests will automatically support ``HEAD``. You don't need to provide any specific middleware for ``HEAD``, and you usually shouldn't. (Although you can if you want.)
For most cases, just implement ``GET``, and the webserver will manage suppressing the response body for you.
OPTIONS, 405 Responses, and Allow Headers
-----------------------------------------
When you add routes to a router by method, the router automatically provides responses for ``OPTIONS`` requests. For example, given this route:
.. code-block:: php
// Dispatch $catItemReader for GET requests to /cats/12, /cats/12, etc.
$router->register("GET", "/cats/{id}", $catItemReader);
// Dispatch $catItemWriter for PUT, and DELETE requests to /cats/12, /cats/12, etc.
$router->register("PUT,DELETE", "/cats/{id}", $catItemWriter);
An ``OPTIONS`` request to ``/cats/12`` will provide a response like:
.. code-block:: http
HTTP/1.1 200 OK
Allow: GET,PUT,DELETE,HEAD,OPTIONS
Likewise, a request to an unsupported method will return a ``405 Method Not Allowed`` response with a descriptive ``Allow`` header.
A ``POST`` request to ``/cats/12`` will provide:
.. code-block:: http
HTTP/1.1 405 Method Not Allowed
Allow: GET,PUT,DELETE,HEAD,OPTIONS
Error Responses
^^^^^^^^^^^^^^^
Then a router is able to locate a route that matches the path, but that route doesn't support the request's method, the router will respond ``405 Method Not Allowed``.
When a router is unable to match the route, it will delegate to the next middleware.
.. note::
When no route matches, the Router will delegate to the next middleware in the server. This is a change from previous versions of WellRESTed where there Router would return a 404 Not Found response. This new behaviour allows a servers to have multiple routers.
Router-specific Middleware
^^^^^^^^^^^^^^^^^^^^^^^^^^
WellRESTed allows a Router to have a set of middleware to dispatch whenever it finds a route that matches. This middleware runs before the handler for the matched route, and only when a route matches.
This feature allows you to build a site where some sections use certain middleware and other do not. For example, suppose your site has a public section that does not require authentication and a private section that does. We can use a different router for each section, and provide authentication middleware on only the router for the private area.
.. code-block:: php
$server = new Server();
// Add the "public" router.
$public = $server->createRouter();
$public->register('GET', '/', $homeHandler);
$public->register('GET', '/about', $homeHandler);
// Set the router call the next middleware when no route matches.
$public->continueOnNotFound();
$server->add($public);
// Add the "private" router.
$private = $server->createRouter();
// Authorization middleware checks for an Authorization header and
// responds 401 when the header is missing or invalid.
$private->add($authorizationMiddleware);
$private->register('GET', '/secret', $secretHandler);
$private->register('GET', '/members-only', $otherHandler);
$server->add($private);
$server->respond();
Nested Routers
^^^^^^^^^^^^^^
For large Web services with large numbers of endpoints, a single, monolithic router may not to optimal. To avoid having each request test every pattern-based route, you can break up a router into a hierarchy of routers.
Here's an example where all of the traffic beginning with ``/cats/`` is sent to one router, and all the traffic for endpoints beginning with ``/dogs/`` is sent to another.
.. code-block:: php
$server = new Server();
$catRouter = $server->createRouter()
->register("GET", "/cats/", $catReader)
->register("POST", "/cats/", $catWriter)
// ... many more endpoints starting with /cats/
->register("POST", "/cats/{cat}/photo/{gallery}/{width}x{height}.{extension}", $catImageHandler);
$dogRouter = $server->createRouter()
->register("GET,POST", "/dogs/", $dogHandler)
// ... many more endpoints starting with /dogs/
->register("POST", "/dogs/{dog}/photo/{gallery}/{width}x{height}.{extension}", $dogImageHandler);
$server->add($server->createRouter()
->register("*", "/cats/*", $catRouter)
->register("*", "/dogs/*", $dogRouter)
);
$server->respond();
.. _preg_match: https://php.net/manual/en/function.preg-match.php
.. _URI Template: `URI Templates`_s
.. _URI Templates: uri-templates.html

View File

@ -0,0 +1,144 @@
URI Templates (Advanced)
========================
In `URI Templates`_, we looked at the most common ways to use URI Templates. Here, we'll look at some of the extended syntaxes that URI Templates provide.
Path Components
^^^^^^^^^^^^^^^
To match a path component, include a slash ``/`` at the beginning of the variable expression. This instructs the template to match the variable if it:
- Begins with ``/``
- Contains only unreserved and percent-encoded characters
You may also use the explode (``*``) modifier to match a variable number of path components and provide them as an array. When using the explode (``*``) modifier to match paths components, the ``/`` character serves as the delimiter instead of a comma.
.. list-table:: Matching path components
:header-rows: 1
* - Template
- Path
- Match?
- Attributes
* - {/path}
- /hello.html
- Yes
- :path: ``"hello.html"``
* - {/path}
- /too/many/parts.jpg
- No
-
* - {/one}{/two}{/three}
- /just/enough/parts.jpg
- Yes
- :one: ``"just"``
:two: ``"enough"``
:three: ``"parts.jpg"``
* - {/path*}
- /any/number/of/parts.jpg
- Yes
- :path: ``["any", "number", "of", "parts.jpg"]``
* - /image{/image*}.jpg
- /image/with/any/path.jpg
- Yes
- :image: ``["with", "any", "path"]``
.. note::
The template ``{/path}`` fails to match the path ``/too/many/parts.jpg``. Although the path does begin with a slash, the subsequent slashes are reserved characters, and therefore the match fails. To match a variable number of path components, use the explode ``*`` modifier (e.g, ``{/paths*}``), or use the reserved (``+``) operator (e.g., ``/{+paths}``).
Dot Prefixes
^^^^^^^^^^^^
Dot prefixes work similarly to matching path components, but a dot ``.`` is the prefix character in place of a slash. This may be useful for file extensions, etc.
Including a dot ``.`` at the beginning of the variable expression instructs the template to match the variable if it:
- Begins with ``.``
- Contains only unreserved (including ``.``) and percent-encoded characters
You may also use the explode (``*``) modifier to match a variable number of dot-prefixed segments and store them to an array. When using the explode (``*``) modifier to match paths components, the ``.`` character serves as the delimiter instead of a comma.
.. list-table:: Matching dot prefixes
:header-rows: 1
* - Template
- Path
- Match?
- Attributes
* - /file{.ext}
- /file.jpg
- Yes
- :ext: ``"jpg"``
* - /file{.ext}
- /file.tar.gz
- Yes
- :ext: ``"tar.gz"``
* - /file{.ext1}{.ext2}
- /file.tar.gz
- Yes
- :ext1: ``"tar"``
:ext2: ``"gz"``
* - /file{.ext*}
- /file.tar.gz
- Yes
- :ext: ``["tar", "gz"]``
.. note::
Because ``.`` is an unreserved character, the template ``/file{.ext}`` matches the path ``/file.tar.gz`` and provides the value ``"tar.gz"``. This is different from the behavior of the slash prefix, where an unexpected slash causes the match to fail.
Multiple-variable Expressions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
An expression in a URI template may contain more than one variable. For example, the template ``/aliases/{one},{two},{three}`` can be written as ``/aliases/{one,two,three}``.
The delimiter between the matched variables is the same as when matching with the explode (``*``) modifier:
.. list-table::
:header-rows: 1
* - Type
- Delimiter
* - Simple String
- Comma ``,``
* - Reserved
- Comma ``,``
* - Path Components
- Slash ``/``
* - Dot Prefix
- Dot ``.``
.. list-table:: Multiple-variable expressions
:header-rows: 1
* - Template
- Path
- Attributes
* - /{one,two,three}
- /fry,leela,bender
- :one: ``"fry"``
:two: ``"leela"``
:three: ``"bender"``
* - /{one,two,three}
- /fry,leela,Nixon%27s%20head
- :one: ``"fry"``
:two: ``"leela"``
:three: ``"Nixon's head"``
* - /{+one,two,three}
- /fry,leela,Nixon's+head
- :one: ``"fry"``
:two: ``"leela"``
:three: ``"Nixon's head"``
* - /{/one,two,three}
- /fry/leela/bender
- :one: ``"fry"``
:two: ``"leela"``
:three: ``"bender"``
* - /file{.one,two,three}
- /file.fry.leela.bender
- :one: ``"fry"``
:two: ``"leela"``
:three: ``"bender"``
.. _URI Templates: uri-templates.html

View File

@ -0,0 +1,139 @@
URI Templates
=============
WellRESTed allows you to register handlers with a router using URI Templates, based on the URI Templates defined in `RFC 6570`_. These templates include variables (enclosed in curly braces) which are extracted and made available to the dispatched middleware.
Reading Variables
^^^^^^^^^^^^^^^^^
Basic Usage
-----------
Register a handler with a URI Template by providing a path that include at least one section enclosed in curly braces. The curly braces define variables for the template.
.. code-block:: php
$router->register("GET", "/widgets/{id}", $widgetHandler);
The router will match requests for paths like ``/widgets/12`` and ``/widgets/mega-widget`` and dispatch ``$widgetHandler`` with the extracted variables made available as request attributes.
To read a path variable, router inspects the request attribute named ``"id"``, since ``id`` is what appears inside curly braces in the URI template.
.. code-block:: php
// For a request to /widgets/12
$id = $request->getAttribute("id");
// "12"
// For a request to /widgets/mega-widget
$id = $request->getAttribute("id");
// "mega-widget"
.. note::
Request attributes are a feature of the ``ServerRequestInterface`` provided by PSR-7_.
Multiple Variables
------------------
The example above included one variable, but URI Templates may include multiple. Each variable will be provided as a request attribute, so be sure to give your variables unique names.
Here's an example with a handful of variables. Suppose we have a template describing the path for a user's avatar image. The image is identified by a username and the image dimensions.
.. code-block:: php
$router->register("GET", "/avatars/{username}-{width}x{height}.jpg", $avatarHandlers);
A request for ``GET /avatars/zoidberg-100x150.jpg`` will provide these request attributes:
.. code-block:: php
// Read the variables extracted form the path.
$username = $request->getAttribute("username");
// "zoidberg"
$width = $request->getAttribute("width");
// "100"
$height = $request->getAttribute("height");
// "150"
Arrays
------
You may also match a comma-separated series of values as an array using a URI Template by providing a ``*`` at the end of the variable name.
.. code-block:: php
$router->register("GET", "/favorite-colors/{colors*}", $colorsHandler);
A request for ``GET /favorite-colors/red,green,blue`` will provide an array as the value for the ``"colors"`` request attribute.
.. code-block:: php
$colorsHandler = function ($request, $response, $next) {
// Read the variable extracted form the path.
$colorsList = $request->getAttribute("colors");
/* Array
(
[0] => red
[1] => green
[2] => blue
)
*/
};
Matching Characters
^^^^^^^^^^^^^^^^^^^
Unreserved Characters
---------------------
By default, URI Template variables will match only "unreserved" characters. `RFC 3968 Section 2.3`_ defines unreserved characters as alphanumeric characters, ``-``, ``.``, ``_``, and ``~``. All other characters must be percent encoded to be matched by a default template variable.
.. note::
Percent-encoded characters matched by template variables are automatically decoded when provided as request attributes.
Given the template ``/users/{user}``, the following paths provide these values for ``getAttribute("user")``:
.. list-table:: Paths and Values for the Template ``/users/{user}``
:header-rows: 1
* - Path
- Value
* - /users/123
- "123"
* - /users/zoidberg
- "zoidberg"
* - /users/zoidberg%40planetexpress.com
- "zoidberg@planetexpress.com"
A request for ``GET /uses/zoidberg@planetexpress.com`` will **not** match this template, because ``@`` is a reserved character and is not percent encoded.
Reserved Characters
-------------------
If you need to match a non-percent-encoded reserved character like ``@`` or ``/``, use the ``+`` operator at the beginning of the variable name.
Using the template ``/users/{+user}``, we can match all of the paths above, plus ``/users/zoidberg@planetexpress.com``.
Reserved matching also allows matching unencoded slashes (``/``). For example, given this template:
.. code-block:: php
$router->register("GET", "/my-favorite-path{+path}", $pathHandler);
The router will dispatch ``$pathHandler`` with for a request to ``GET /my-favorite-path/has/a/few/slashes.jpg``
.. code-block:: php
$path = $request->getAttribute("path");
// "/has/a/few/slashes.jpg"
.. note::
Combine the ``+`` operator and ``*`` modifier to match reserved characters as an array. For example, the template ``/{+vars*}`` will match the path ``/c@t,d*g``, providing the array ``["c@t", "d*g"]``.
.. _RFC 3968 Section 2.3: https://tools.ietf.org/html/rfc3986#section-2.3
.. _PSR-7: https://www.php-fig.org/psr/psr-7/
.. _RFC 6570: https://tools.ietf.org/html/rfc6570
.. _RFC 6570 Section 3.2.7: https://tools.ietf.org/html/rfc6570#section-3.2.7

View File

@ -0,0 +1,45 @@
Web Server Configuration
========================
You will typically want to have all traffic on your site directed to a single script that creates a ``WellRESTed\Server`` and calls ``respond``. Here are basic setups for doing this in Nginx_ and Apache_.
Nginx
^^^^^
.. code-block:: nginx
server {
listen 80;
server_name your.hostname.here;
root /your/sites/document/root;
index index.php index.html;
charset utf-8;
# Attempt to serve actual files first.
# If no file exists, send to /index.php
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}
}
Apache
^^^^^^
.. code-block:: apache
RewriteEngine on
RewriteBase /
# Send all requests to non-regular files and directories to index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.+$ index.php [L,QSA]

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<phpdoc>
<title>WellRESTed</title>
<parser>
<target>docs</target>
<markers>
<item>TODO</item>
<item>FIXME</item>
</markers>
</parser>
<transformer>
<target>docs</target>
</transformer>
<transformations>
<template name="responsive" />
</transformations>
<files>
<directory>src</directory>
<ignore>test/*</ignore>
</files>
</phpdoc>

View File

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
verbose="true"
>
<php>
<!-- To override, "export PORT=8081" -->
<env name="PORT" value="8080" />
</php>
<testsuites>
<testsuite name="WellRESTed Test Suite">
<directory>./test/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">./src/</directory>
</whitelist>
</filter>
<logging>
<log type="coverage-html" target="./report" charset="UTF-8" hightlight="true" />
</logging>
</phpunit>

59
public/index.php Normal file
View File

@ -0,0 +1,59 @@
<?php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use WellRESTed\Message\Response;
use WellRESTed\Message\Stream;
use WellRESTed\Server;
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../vendor/autoload.php';
// Create a handler using the PSR-15 RequestHandlerInterface
class HomePageHandler implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$view = <<<HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WellRESTed Development Site</title>
</head>
<body>
<h1>WellRESTed Development Site</h1>
<p>To run unit tests, run:</p>
<code>docker-compose run --rm php phpunit</code>
<p>View the <a href="/coverage/">code coverage report</a>.</p>
<p>To generate documentation, run:</p>
<code>docker-compose run --rm docs</code>
<p>View <a href="/docs/"> documentation</a>.</p>
</body>
</html>
HTML;
return (new Response(200))
->withHeader('Content-type', 'text/html')
->withBody(new Stream($view));
}
}
// -----------------------------------------------------------------------------
// Create a new Server instance.
$server = new Server();
// Add a router to the server to map methods and endpoints to handlers.
$router = $server->createRouter();
// Register the route GET / with an anonymous function that provides a handler.
$router->register("GET", "/", function () { return new HomePageHandler(); });
// Add the router to the server.
$server->add($router);
// Read the request from the client, dispatch a handler, and output.
$server->respond();

View File

@ -0,0 +1,9 @@
<?php
namespace WellRESTed\Dispatching;
use InvalidArgumentException;
class DispatchException extends InvalidArgumentException
{
}

View File

@ -0,0 +1,103 @@
<?php
namespace WellRESTed\Dispatching;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Dispatches an ordered sequence of middleware.
*/
class DispatchStack implements DispatchStackInterface
{
/** @var mixed[] */
private $stack;
/** @var DispatcherInterface */
private $dispatcher;
public function __construct(DispatcherInterface $dispatcher)
{
$this->dispatcher = $dispatcher;
$this->stack = [];
}
/**
* Push a new middleware onto the stack.
*
* @param mixed $middleware Middleware to dispatch in sequence
* @return static
*/
public function add($middleware)
{
$this->stack[] = $middleware;
return $this;
}
/**
* Dispatch the contained middleware in the order in which they were added.
*
* The first middleware that was added is dispatched first.
*
* Each middleware, when dispatched, receives a $next callable that, when
* called, will dispatch the following middleware in the sequence.
*
* When the stack is dispatched empty, or when all middleware in the stack
* call the $next argument they were passed, this method will call the
* $next it received.
*
* When any middleware in the stack returns a response without calling its
* $next, the stack will not call the $next it received.
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param callable $next
* @return ResponseInterface
*/
public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response,
$next
) {
$dispatcher = $this->dispatcher;
// This flag will be set to true when the last middleware calls $next.
$stackCompleted = false;
// The final middleware's $next returns $response unchanged and sets
// the $stackCompleted flag to indicate the stack has completed.
$chain = function (
ServerRequestInterface $request,
ResponseInterface $response
) use (&$stackCompleted): ResponseInterface {
$stackCompleted = true;
return $response;
};
// Create a chain of callables.
//
// Each callable will take $request and $response parameters, and will
// contain a dispatcher, the associated middleware, and a $next function
// that serves as the link to the next middleware in the chain.
foreach (array_reverse($this->stack) as $middleware) {
$chain = function (
ServerRequestInterface $request,
ResponseInterface $response
) use ($dispatcher, $middleware, $chain): ResponseInterface {
return $dispatcher->dispatch(
$middleware,
$request,
$response,
$chain
);
};
}
$response = $chain($request, $response);
if ($stackCompleted) {
return $next($request, $response);
} else {
return $response;
}
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace WellRESTed\Dispatching;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use WellRESTed\MiddlewareInterface;
/**
* Dispatches an ordered sequence of middleware
*/
interface DispatchStackInterface extends MiddlewareInterface
{
/**
* Push a new middleware onto the stack.
*
* This method MUST preserve the order in which middleware are added.
*
* @param mixed $middleware Middleware to dispatch in sequence
* @return static
*/
public function add($middleware);
/**
* Dispatch the contained middleware in the order in which they were added.
*
* The first middleware added to the stack MUST be dispatched first.
*
* Each middleware, when dispatched, MUST receive a $next callable that
* dispatches the middleware that follows it, unless it is the last
* middleware. The last middleware MUST receive a $next callable that
* returns the response unchanged.
*
* When any middleware returns a response without calling the $next
* argument it received, the stack instance MUST stop propagating and MUST
* return a response without calling the $next argument passed to __invoke.
*
* This method MUST call the passed $next argument when:
* - The stack is empty (i.e., there is no middleware to dispatch)
* - Each middleware called the $next that it received.
*
* This method MUST NOT call the passed $next argument when the stack is
* not empty and any middleware returns a response without calling the
* $next it received.
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param callable $next
* @return ResponseInterface
*/
public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response,
$next
);
}

View File

@ -0,0 +1,82 @@
<?php
namespace WellRESTed\Dispatching;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Runs a handler or middleware with a request and return the response.
*/
class Dispatcher implements DispatcherInterface
{
/**
* Run a handler or middleware with a request and return the response.
*
* Dispatcher can dispatch any of the following:
* - An instance implementing one of these interfaces:
* - Psr\Http\Server\RequestHandlerInterface
* - Psr\Http\Server\MiddlewareInterface
* - WellRESTed\MiddlewareInterface
* - Psr\Http\Message\ResponseInterface
* - A string containing the fully qualified class name of a class
* implementing one of the interfaces listed above.
* - A callable that returns an instance implementing one of the
* interfaces listed above.
* - A callable with a signature matching the signature of
* WellRESTed\MiddlewareInterface::__invoke
* - An array containing any of the items in this list.
*
* When Dispatcher receives a $dispatchable that is not of a type it
* can dispatch, it throws a DispatchException.
*
* @param mixed $dispatchable
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param callable $next
* @return ResponseInterface
* @throws DispatchException Unable to dispatch $middleware
*/
public function dispatch(
$dispatchable,
ServerRequestInterface $request,
ResponseInterface $response,
$next
) {
if (is_callable($dispatchable)) {
$dispatchable = $dispatchable($request, $response, $next);
} elseif (is_string($dispatchable)) {
$dispatchable = new $dispatchable();
} elseif (is_array($dispatchable)) {
$dispatchable = $this->getDispatchStack($dispatchable);
}
if (is_callable($dispatchable)) {
return $dispatchable($request, $response, $next);
} elseif ($dispatchable instanceof RequestHandlerInterface) {
return $dispatchable->handle($request);
} elseif ($dispatchable instanceof MiddlewareInterface) {
$delegate = new DispatcherDelegate($response, $next);
return $dispatchable->process($request, $delegate);
} elseif ($dispatchable instanceof ResponseInterface) {
return $dispatchable;
} else {
throw new DispatchException('Unable to dispatch handler.');
}
}
/**
* @param mixed[] $dispatchables
* @return DispatchStack
*/
private function getDispatchStack($dispatchables)
{
$stack = new DispatchStack($this);
foreach ($dispatchables as $dispatchable) {
$stack->add($dispatchable);
}
return $stack;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace WellRESTed\Dispatching;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Adapter to allow use of PSR-15 Middleware with double pass implementations.
*/
class DispatcherDelegate implements RequestHandlerInterface
{
/** @var ResponseInterface */
private $response;
/** @var callable */
private $next;
public function __construct(ResponseInterface $response, callable $next)
{
$this->response = $response;
$this->next = $next;
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return call_user_func($this->next, $request, $this->response);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace WellRESTed\Dispatching;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Runs a handler or middleware with a request and return the response.
*/
interface DispatcherInterface
{
/**
* Run a handler or middleware with a request and return the response.
*
* Dispatchables (middleware and handlers) comes in a number of varieties
* (e.g., instance, string, callable). DispatcherInterface interface unpacks
* the dispatchable and dispatches it.
*
* Implementations MUST be able to dispatch the following:
* - An instance implementing one of these interfaces:
* - Psr\Http\Server\RequestHandlerInterface
* - Psr\Http\Server\MiddlewareInterface
* - WellRESTed\MiddlewareInterface
* - Psr\Http\Message\ResponseInterface
* - A string containing the fully qualified class name of a class
* implementing one of the interfaces listed above.
* - A callable that returns an instance implementing one of the
* interfaces listed above.
* - A callable with a signature matching the signature of
* WellRESTed\MiddlewareInterface::__invoke
* - An array containing any of the items in this list.
*
* Implementation MAY dispatch other types of middleware.
*
* When an implementation receives a $dispatchable that is not of a type it
* can dispatch, it MUST throw a DispatchException.
*
* @param mixed $dispatchable
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param callable $next
* @return ResponseInterface
* @throws DispatchException Unable to dispatch $middleware
*/
public function dispatch(
$dispatchable,
ServerRequestInterface $request,
ResponseInterface $response,
$next
);
}

View File

@ -0,0 +1,137 @@
<?php
namespace WellRESTed\Message;
use ArrayAccess;
use Iterator;
/**
* HeaderCollection provides case-insensitive access to lists of header values.
*
* HeaderCollection preserves the cases of keys as they are set, but treats key
* access case insensitively.
*
* Any values added to HeaderCollection are added to list arrays. Subsequent
* calls to add a value for a given key will append the new value to the list
* array of values for that key.
*
* @internal This class is an internal class used by Message and is not intended
* for direct use by consumers.
*/
class HeaderCollection implements ArrayAccess, Iterator
{
/**
* Hash array mapping lowercase header names to original case header names.
*
* @var array<string, string>
*/
private $fields = [];
/**
* Hash array mapping lowercase header names to values as string[]
*
* @var array<string, string[]>
*/
private $values = [];
/**
* List array of lowercase header names.
*
* @var string[]
*/
private $keys = [];
/** @var int */
private $position = 0;
// -------------------------------------------------------------------------
// ArrayAccess
/**
* @param string $offset
* @return bool
*/
public function offsetExists($offset): bool
{
return isset($this->values[strtolower($offset)]);
}
/**
* @param mixed $offset
* @return string[]
*/
public function offsetGet($offset): array
{
return $this->values[strtolower($offset)];
}
/**
* @param string $offset
* @param string $value
*/
public function offsetSet($offset, $value): void
{
$normalized = strtolower($offset);
// Add the normalized key to the list of keys, if not already set.
if (!in_array($normalized, $this->keys)) {
$this->keys[] = $normalized;
}
// Add or update the preserved case key.
$this->fields[$normalized] = $offset;
// Store the value.
if (isset($this->values[$normalized])) {
$this->values[$normalized][] = $value;
} else {
$this->values[$normalized] = [$value];
}
}
/**
* @param string $offset
*/
public function offsetUnset($offset): void
{
$normalized = strtolower($offset);
unset($this->fields[$normalized]);
unset($this->values[$normalized]);
// Remove and normalize the list of keys.
if (($key = array_search($normalized, $this->keys)) !== false) {
unset($this->keys[$key]);
$this->keys = array_values($this->keys);
}
}
// -------------------------------------------------------------------------
// Iterator
/**
* @return string[]
*/
public function current(): array
{
return $this->values[$this->keys[$this->position]];
}
public function next(): void
{
++$this->position;
}
public function key(): string
{
return $this->fields[$this->keys[$this->position]];
}
public function valid(): bool
{
return isset($this->keys[$this->position]);
}
public function rewind(): void
{
$this->position = 0;
}
}

297
src/Message/Message.php Normal file
View File

@ -0,0 +1,297 @@
<?php
namespace WellRESTed\Message;
use InvalidArgumentException;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\StreamInterface;
/**
* Message defines core functionality for classes that represent HTTP messages.
*/
abstract class Message implements MessageInterface
{
/** @var HeaderCollection */
protected $headers;
/** @var StreamInterface */
protected $body;
/** @var string */
protected $protocolVersion = '1.1';
/**
* Create a new Message, optionally with headers and a body.
*
* $headers is an optional associative array with header field names as
* string keys and values as either string or string[].
*
* If no StreamInterface is provided for $body, the instance will create
* a NullStream instance for the message body.
*
* @param array $headers Associative array with header field names as
* keys and values as string|string[]
* @param StreamInterface|null $body A stream representation of the message
* entity body
*/
public function __construct(
array $headers = [],
?StreamInterface $body = null
) {
$this->headers = new HeaderCollection();
foreach ($headers as $name => $values) {
if (is_string($values)) {
$values = [$values];
}
foreach ($values as $value) {
$this->headers[$name] = $value;
}
}
$this->body = $body ?? new Stream('');
}
public function __clone()
{
$this->headers = clone $this->headers;
}
// -------------------------------------------------------------------------
// Psr\Http\Message\MessageInterface
/**
* Retrieves the HTTP protocol version as a string.
*
* @return string HTTP protocol version.
*/
public function getProtocolVersion()
{
return $this->protocolVersion;
}
/**
* Create a new instance with the specified HTTP protocol version.
*
* @param string $version HTTP protocol version
* @return static
*/
public function withProtocolVersion($version)
{
$message = clone $this;
$message->protocolVersion = $version;
return $message;
}
/**
* Retrieve all message headers.
*
* The keys represent the header name as it will be sent over the wire, and
* each value is an array of strings associated with the header.
*
* // Represent the headers as a string
* foreach ($message->getHeaders() as $name => $values) {
* echo $name . ': ' . implode(', ', $values);
* }
*
* // Emit headers iteratively:
* foreach ($message->getHeaders() as $name => $values) {
* foreach ($values as $value) {
* header(sprintf('%s: %s', $name, $value), false);
* }
* }
*
* While header names are not case-sensitive, getHeaders() will preserve the
* exact case in which headers were originally specified.
*
* @return string[][] Returns an associative array of the message's headers.
*/
public function getHeaders()
{
$headers = [];
foreach ($this->headers as $key => $value) {
$headers[$key] = $value;
}
return $headers;
}
/**
* Checks if a header exists by the given case-insensitive name.
*
* @param string $name Case-insensitive header field name.
* @return bool Returns true if any header names match the given header
* name using a case-insensitive string comparison. Returns false if
* no matching header name is found in the message.
*/
public function hasHeader($name)
{
return isset($this->headers[$name]);
}
/**
* Retrieves a message header value by the given case-insensitive name.
*
* This method returns an array of all the header values of the given
* case-insensitive header name.
*
* If the header does not appear in the message, this method returns an
* empty array.
*
* @param string $name Case-insensitive header field name.
* @return string[] An array of string values as provided for the given
* header. If the header does not appear in the message, this method
* returns an empty array.
*/
public function getHeader($name)
{
if (isset($this->headers[$name])) {
return $this->headers[$name];
} else {
return [];
}
}
/**
* Retrieves the line for a single header, with the header values as a
* comma-separated string.
*
* This method returns all of the header values of the given
* case-insensitive header name as a string concatenated together using
* a comma.
*
* NOTE: Not all header values may be appropriately represented using
* comma concatenation. For such headers, use getHeader() instead
* and supply your own delimiter when concatenating.
*
* If the header does not appear in the message, this method returns an
* empty string.
*
* @param string $name Case-insensitive header field name.
* @return string A string of values as provided for the given header
* concatenated together using a comma. If the header does not appear in
* the message, this method returns an empty string.
*/
public function getHeaderLine($name)
{
if (isset($this->headers[$name])) {
return join(', ', $this->headers[$name]);
} else {
return '';
}
}
/**
* Create a new instance with the provided header, replacing any existing
* values of any headers with the same case-insensitive name.
*
* While header names are case-insensitive, the casing of the header will
* be preserved by this function, and returned from getHeaders().
*
* @param string $name Case-insensitive header field name.
* @param string|string[] $value Header value(s).
* @return static
* @throws InvalidArgumentException for invalid header names or values.
*/
public function withHeader($name, $value)
{
$values = $this->getValidatedHeaders($name, $value);
$message = clone $this;
unset($message->headers[$name]);
foreach ($values as $value) {
$message->headers[$name] = (string) $value;
}
return $message;
}
/**
* Creates a new instance, with the specified header appended with the
* given value.
*
* Existing values for the specified header will be maintained. The new
* value(s) will be appended to the existing list. If the header did not
* exist previously, it will be added.
*
* @param string $name Case-insensitive header field name to add.
* @param string|string[] $value Header value(s).
* @return static
* @throws InvalidArgumentException for invalid header names or values.
*/
public function withAddedHeader($name, $value)
{
$values = $this->getValidatedHeaders($name, $value);
$message = clone $this;
foreach ($values as $value) {
$message->headers[$name] = (string) $value;
}
return $message;
}
/**
* Creates a new instance, without the specified header.
*
* @param string $name Case-insensitive header field name to remove.
* @return static
*/
public function withoutHeader($name)
{
$message = clone $this;
unset($message->headers[$name]);
return $message;
}
/**
* Gets the body of the message.
*
* @return StreamInterface Returns the body as a stream.
*/
public function getBody()
{
return $this->body;
}
/**
* Create a new instance, with the specified message body.
*
* The body MUST be a StreamInterface object.
*
* @param StreamInterface $body Body.
* @return static
* @throws InvalidArgumentException When the body is not valid.
*/
public function withBody(StreamInterface $body)
{
$message = clone $this;
$message->body = $body;
return $message;
}
// -------------------------------------------------------------------------
/**
* @param mixed $name
* @param mixed|mixed[] $values
* @return string[]
* @throws InvalidArgumentException Name is not a string or value is not
* a string or array of strings
*/
private function getValidatedHeaders($name, $values)
{
if (!is_string($name)) {
throw new InvalidArgumentException('Header name must be a string');
}
if (!is_array($values)) {
$values = [$values];
}
$isNotStringOrNumber = function ($item): bool {
return !(is_string($item) || is_numeric($item));
};
$invalid = array_filter($values, $isNotStringOrNumber);
if ($invalid) {
throw new InvalidArgumentException('Header values must be a string or string[]');
}
return array_map('strval', $values);
}
}

188
src/Message/NullStream.php Normal file
View File

@ -0,0 +1,188 @@
<?php
namespace WellRESTed\Message;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
/**
* NullStream is a minimal, always-empty, non-writable stream.
*
* Use this for messages with no body.
*/
class NullStream implements StreamInterface
{
/**
* Returns an empty string
*
* @return string
*/
public function __toString()
{
return '';
}
/**
* No-op
*
* @return void
*/
public function close()
{
}
/**
* No-op
*
* @return resource|null Underlying PHP stream, if any
*/
public function detach()
{
return null;
}
/**
* Returns 0
*
* @return int|null Returns the size in bytes if known, or null if unknown.
*/
public function getSize()
{
return 0;
}
/**
* Returns the current position of the file read/write pointer
*
* @return int Position of the file pointer
* @throws RuntimeException on error.
*/
public function tell()
{
return 0;
}
/**
* Returns true
*
* @return bool
*/
public function eof()
{
return true;
}
/**
* Returns false
*
* @return bool
*/
public function isSeekable()
{
return false;
}
/**
* Always throws exception
*
* @link http://www.php.net/manual/en/function.fseek.php
* @param int $offset Stream offset
* @param int $whence Specifies how the cursor position will be calculated
* based on the seek offset. Valid values are identical to the built-in
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
* offset bytes SEEK_CUR: Set position to current location plus offset
* SEEK_END: Set position to end-of-stream plus offset.
* @return void
* @throws RuntimeException on failure.
*/
public function seek($offset, $whence = SEEK_SET)
{
throw new RuntimeException('Unable to seek to position.');
}
/**
* Always throws exception
*
* @see seek()
* @link http://www.php.net/manual/en/function.fseek.php
* @return void
* @throws RuntimeException on failure.
*/
public function rewind()
{
throw new RuntimeException('Unable to rewind stream.');
}
/**
* Returns false.
*
* @return bool
*/
public function isWritable()
{
return false;
}
/**
* Always throws exception
*
* @param string $string The string that is to be written.
* @return int Returns the number of bytes written to the stream.
* @throws RuntimeException on failure.
*/
public function write($string)
{
throw new RuntimeException('Unable to write to stream.');
}
/**
* Returns true
*
* @return bool
*/
public function isReadable()
{
return true;
}
/**
* Returns an empty string
*
* @param int $length Read up to $length bytes from the object and return
* them. Fewer than $length bytes may be returned if underlying stream
* call returns fewer bytes.
* @return string Returns the data read from the stream, or an empty string
* if no bytes are available.
* @throws RuntimeException if an error occurs.
*/
public function read($length)
{
return '';
}
/**
* Returns the remaining contents in a string
*
* @return string
* @throws RuntimeException if unable to read or an error occurs while
* reading.
*/
public function getContents()
{
return '';
}
/**
* Returns null
*
* @link http://php.net/manual/en/function.stream-get-meta-data.php
* @param string $key Specific metadata to retrieve.
* @return array|mixed|null Returns an associative array if no key is
* provided. Returns a specific key value if a key is provided and the
* value is found, or null if the key is not found.
*/
public function getMetadata($key = null)
{
return null;
}
}

235
src/Message/Request.php Normal file
View File

@ -0,0 +1,235 @@
<?php
namespace WellRESTed\Message;
use InvalidArgumentException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
/**
* Representation of an outgoing, client-side request.
*
* Per the HTTP specification, this interface includes properties for
* each of the following:
*
* - Protocol version
* - HTTP method
* - URI
* - Headers
* - Message body
*/
class Request extends Message implements RequestInterface
{
/** @var string */
protected $method;
/** @var string|null */
protected $requestTarget;
/** @var UriInterface */
protected $uri;
// -------------------------------------------------------------------------
/**
* Create a new Request.
*
* $headers is an optional associative array with header field names as
* string keys and values as either string or string[].
*
* If no StreamInterface is provided for $body, the instance will create
* a NullStream instance for the message body.
*
* @param string $method
* @param string|UriInterface $uri
* @param array $headers Associative array with header field names as
* keys and values as string|string[]
* @param StreamInterface|null $body A stream representation of the message
* entity body
*/
public function __construct(
string $method = 'GET',
$uri = '',
array $headers = [],
?StreamInterface $body = null
) {
parent::__construct($headers, $body);
$this->method = $method;
if (!($uri instanceof UriInterface)) {
$uri = new Uri($uri);
}
$this->uri = $uri;
$this->requestTarget = null;
}
public function __clone()
{
$this->uri = clone $this->uri;
parent::__clone();
}
// -------------------------------------------------------------------------
// Psr\Http\Message\RequestInterface
/**
* Retrieves the message's request target.
*
* Retrieves the message's request-target either as it will appear (for
* clients), as it appeared at request (for servers), or as it was
* specified for the instance (see withRequestTarget()).
*
* In most cases, this will be the origin-form of the composed URI,
* unless a value was provided to the concrete implementation (see
* withRequestTarget() below).
*
* If no URI is available, and no request-target has been specifically
* provided, this method will return the string "/".
*
* @return string
*/
public function getRequestTarget()
{
// Use the explicitly set request target first.
if ($this->requestTarget !== null) {
return $this->requestTarget;
}
// Build the origin form from the composed URI.
$target = $this->uri->getPath();
$query = $this->uri->getQuery();
if ($query) {
$target .= '?' . $query;
}
// Return "/" if the origin form is empty.
return $target ?: '/';
}
/**
* Create a new instance with a specific request-target.
*
* If the request needs a non-origin-form request-target e.g., for
* specifying an absolute-form, authority-form, or asterisk-form
* this method may be used to create an instance with the specified
* request-target, verbatim.
*
* @link http://tools.ietf.org/html/rfc7230#section-2.7 (for the various
* request-target forms allowed in request messages)
* @param mixed $requestTarget
* @return static
*/
public function withRequestTarget($requestTarget)
{
$request = clone $this;
$request->requestTarget = $requestTarget;
return $request;
}
/**
* Retrieves the HTTP method of the request.
*
* @return string Returns the request method.
*/
public function getMethod()
{
return $this->method;
}
/**
* Create a new instance with the provided HTTP method.
*
* While HTTP method names are typically all uppercase characters, HTTP
* method names are case-sensitive. Therefore, this method will not
* modify the given string.
*
* @param string $method Case-insensitive method.
* @return static
* @throws InvalidArgumentException for invalid HTTP methods.
*/
public function withMethod($method)
{
$request = clone $this;
$request->method = $this->getValidatedMethod($method);
return $request;
}
/**
* Retrieves the URI instance.
*
* @link http://tools.ietf.org/html/rfc3986#section-4.3
* @return UriInterface Returns a UriInterface instance
* representing the URI of the request.
*/
public function getUri()
{
return $this->uri;
}
/**
* Returns an instance with the provided URI.
*
* This method updates the Host header of the returned request by
* default if the URI contains a host component. If the URI does not
* contain a host component, any pre-existing Host header will be carried
* over to the returned request.
*
* You can opt-in to preserving the original state of the Host header by
* setting `$preserveHost` to `true`. When `$preserveHost` is set to
* `true`, this method interacts with the Host header in the following ways:
*
* - If the the Host header is missing or empty, and the new URI contains
* a host component, this method will update the Host header in the returned
* request.
* - If the Host header is missing or empty, and the new URI does not contain a
* host component, this method will not update the Host header in the returned
* request.
* - If a Host header is present and non-empty, this method will not update
* the Host header in the returned request.
*
* @link http://tools.ietf.org/html/rfc3986#section-4.3
* @param UriInterface $uri New request URI to use.
* @param bool $preserveHost Preserve the original state of the Host header.
* @return static
*/
public function withUri(UriInterface $uri, $preserveHost = false)
{
$request = clone $this;
$newHost = $uri->getHost();
$oldHost = $request->headers['Host'] ?? '';
if ($preserveHost === false) {
// Update Host
if ($newHost && $newHost !== $oldHost) {
unset($request->headers['Host']);
$request->headers['Host'] = $newHost;
}
} else {
// Preserve Host
if (!$oldHost && $newHost) {
$request->headers['Host'] = $newHost;
}
}
$request->uri = $uri;
return $request;
}
// -------------------------------------------------------------------------
/**
* @param mixed $method
* @return string
* @throws InvalidArgumentException
*/
private function getValidatedMethod($method)
{
if (!is_string($method)) {
throw new InvalidArgumentException('Method must be a string.');
}
$method = trim($method);
if (strpos($method, ' ') !== false) {
throw new InvalidArgumentException('Method cannot contain spaces.');
}
return $method;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace WellRESTed\Message;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
class RequestFactory implements RequestFactoryInterface
{
/**
* Create a new request.
*
* @param string $method The HTTP method associated with the request.
* @param UriInterface|string $uri The URI associated with the request. If
* the value is a string, the factory MUST create a UriInterface
* instance based on it.
*
* @return RequestInterface
*/
public function createRequest(string $method, $uri): RequestInterface
{
return new Request($method, $uri);
}
}

177
src/Message/Response.php Normal file
View File

@ -0,0 +1,177 @@
<?php
namespace WellRESTed\Message;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* Representation of an outgoing, server-side response.
*
* Per the HTTP specification, this interface includes properties for
* each of the following:
*
* - Protocol version
* - Status code and reason phrase
* - Headers
* - Message body
*/
class Response extends Message implements ResponseInterface
{
/** @var string Text explanation of the HTTP Status Code. */
private $reasonPhrase;
/** @var int HTTP status code */
private $statusCode;
/**
* Create a new Response, optionally with status code, headers, and a body.
*
* If provided, $headers MUST by an associative array with header field
* names as (string) keys and lists of header field values (string[])
* as values.
*
* If no StreamInterface is provided for $body, the instance will create
* a NullStream instance for the message body.
*
* @see \WellRESTed\Message\Message
*
* @param int $statusCode
* @param array $headers
* @param StreamInterface|null $body
*/
public function __construct(
int $statusCode = 500,
array $headers = [],
?StreamInterface $body = null
) {
parent::__construct($headers, $body);
$this->statusCode = $statusCode;
$this->reasonPhrase = $this->getDefaultReasonPhraseForStatusCode($statusCode);
}
// -------------------------------------------------------------------------
// Psr\Http\Message\ResponseInterface
/**
* Gets the response status code.
*
* The status code is a 3-digit integer result code of the server's attempt
* to understand and satisfy the request.
*
* @return int Status code.
*/
public function getStatusCode()
{
return $this->statusCode;
}
/**
* Create a new instance with the specified status code, and optionally
* reason phrase, for the response.
*
* If no reason phrase is specified, this method will provide a standard
* reason phrase, if possible.
*
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
* @param int $code The 3-digit integer result code to set.
* @param string $reasonPhrase The reason phrase to use with the
* provided status code; if none is provided, implementations MAY
* use the defaults as suggested in the HTTP specification.
* @return static
* @throws InvalidArgumentException For invalid status code arguments.
*/
public function withStatus($code, $reasonPhrase = '')
{
$response = clone $this;
$response->statusCode = $code;
if (!$reasonPhrase) {
$reasonPhrase = $this->getDefaultReasonPhraseForStatusCode($code);
}
$response->reasonPhrase = $reasonPhrase;
return $response;
}
/**
* Gets the response reason phrase, a short textual description of the status code.
*
* The reason phrase is not required and may be an empty string.
*
* @link http://tools.ietf.org/html/rfc7231#section-6
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
* @return string Reason phrase, or an empty string if unknown.
*/
public function getReasonPhrase()
{
return $this->reasonPhrase;
}
/**
* @param int $statusCode
* @return string
*/
private function getDefaultReasonPhraseForStatusCode($statusCode)
{
$reasonPhraseLookup = [
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
208 => 'Already Reported',
226 => 'IM Used',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
307 => 'Temporary Redirect',
308 => 'Permanent Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Payload Too Large',
414 => 'URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Range Not Satisfiable',
417 => 'Expectation Failed',
421 => 'Misdirected Request',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
426 => 'Upgrade Required',
428 => 'Precondition Required',
429 => 'Too Many Requests',
431 => 'Request Header Fields Too Large',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates',
507 => 'Insufficient Storage',
508 => 'Loop Detected',
510 => 'Not Extended',
511 => 'Network Authentication Required'
];
return $reasonPhraseLookup[$statusCode] ?? '';
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace WellRESTed\Message;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
class ResponseFactory implements ResponseFactoryInterface
{
/**
* Create a new response.
*
* @param int $code HTTP status code; defaults to 200
* @param string $reasonPhrase Reason phrase to associate with status code
* in generated response; if none is provided implementations MAY use
* the defaults as suggested in the HTTP specification.
*
* @return ResponseInterface
*/
public function createResponse(
int $code = 200,
string $reasonPhrase = ''
): ResponseInterface {
return (new Response())
->withStatus($code, $reasonPhrase);
}
}

View File

@ -0,0 +1,400 @@
<?php
namespace WellRESTed\Message;
use InvalidArgumentException;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Message\UriInterface;
/**
* Representation of an incoming, server-side HTTP request.
*
* Per the HTTP specification, this interface includes properties for
* each of the following:
*
* - Protocol version
* - HTTP method
* - URI
* - Headers
* - Message body
*
* Additionally, it encapsulates all data as it has arrived to the
* application from the CGI and/or PHP environment, including:
*
* - The values represented in $_SERVER.
* - Any cookies provided (generally via $_COOKIE)
* - Query string arguments (generally via $_GET, or as parsed via parse_str())
* - Upload files, if any (as represented by $_FILES)
* - Deserialized body parameters (generally from $_POST)
*
* $_SERVER values MUST be treated as immutable, as they represent application
* state at the time of request; as such, no methods are provided to allow
* modification of those values. The other values provide such methods, as they
* can be restored from $_SERVER or the request body, and may need treatment
* during the application (e.g., body parameters may be deserialized based on
* content type).
*/
class ServerRequest extends Request implements ServerRequestInterface
{
/** @var array */
private $attributes;
/** @var array */
private $cookieParams;
/** @var mixed */
private $parsedBody;
/** @var array */
private $queryParams;
/** @var array */
private $serverParams;
/** @var array */
private $uploadedFiles;
// -------------------------------------------------------------------------
/**
* Create a new ServerRequest.
*
* $headers is an optional associative array with header field names as
* string keys and values as either string or string[].
*
* If no StreamInterface is provided for $body, the instance will create
* a NullStream instance for the message body.
*
* @param string $method
* @param string|UriInterface $uri
* @param array $headers Associative array with header field names as
* keys and values as string|string[]
* @param StreamInterface|null $body A stream representation of the message
* entity body
* @param array $serverParams An array of Server API (SAPI) parameters
*/
public function __construct(
string $method = 'GET',
$uri = '',
array $headers = [],
?StreamInterface $body = null,
array $serverParams = []
) {
parent::__construct($method, $uri, $headers, $body);
$this->serverParams = $serverParams;
$this->cookieParams = [];
$this->queryParams = [];
$this->attributes = [];
$this->uploadedFiles = [];
}
public function __clone()
{
if (is_object($this->parsedBody)) {
$this->parsedBody = clone $this->parsedBody;
}
parent::__clone();
}
// -------------------------------------------------------------------------
// Psr\Http\Message\ServerRequestInterface
/**
* Retrieve server parameters.
*
* Retrieves data related to the incoming request environment,
* typically derived from PHP's $_SERVER superglobal. The data IS NOT
* REQUIRED to originate from $_SERVER.
*
* @return array
*/
public function getServerParams()
{
return $this->serverParams;
}
/**
* Retrieve cookies.
*
* Retrieves cookies sent by the client to the server.
*
* @return array
*/
public function getCookieParams()
{
return $this->cookieParams;
}
/**
* Create a new instance with the specified cookies.
*
* The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
* be compatible with the structure of $_COOKIE. Typically, this data will
* be injected at instantiation.
*
* @param array $cookies Array of key/value pairs representing cookies.
* @return static
*/
public function withCookieParams(array $cookies)
{
$request = clone $this;
$request->cookieParams = $cookies;
return $request;
}
/**
* Retrieve query string arguments.
*
* Retrieves the deserialized query string arguments, if any.
*
* Note: the query params might not be in sync with the URI or server
* params. If you need to ensure you are only getting the original
* values, you may need to parse the query string from `getUri()->getQuery()`
* or from the `QUERY_STRING` server param.
*
* @return array
*/
public function getQueryParams()
{
return $this->queryParams;
}
/**
* Create a new instance with the specified query string arguments.
*
* These values SHOULD remain immutable over the course of the incoming
* request. They MAY be injected during instantiation, such as from PHP's
* $_GET superglobal, or MAY be derived from some other value such as the
* URI. In cases where the arguments are parsed from the URI, the data
* MUST be compatible with what PHP's parse_str() would return for
* purposes of how duplicate query parameters are handled, and how nested
* sets are handled.
*
* Setting query string arguments MUST NOT change the URL stored by the
* request, nor the values in the server params.
*
* @param array $query Array of query string arguments, typically from
* $_GET.
* @return static
*/
public function withQueryParams(array $query)
{
$request = clone $this;
$request->queryParams = $query;
return $request;
}
/**
* Retrieve normalized file upload data.
*
* This method returns upload metadata in a normalized tree, with each leaf
* an instance of Psr\Http\Message\UploadedFileInterface.
*
* These values MAY be prepared from $_FILES or the message body during
* instantiation, or MAY be injected via withUploadedFiles().
*
* @return array An array tree of UploadedFileInterface instances; an empty
* array will be returned if no data is present.
*/
public function getUploadedFiles()
{
return $this->uploadedFiles;
}
/**
* Create a new instance with the specified uploaded files.
*
* @param array $uploadedFiles An array tree of UploadedFileInterface instances.
* @return static
* @throws InvalidArgumentException if an invalid structure is provided.
*/
public function withUploadedFiles(array $uploadedFiles)
{
if (!$this->isValidUploadedFilesTree($uploadedFiles)) {
throw new InvalidArgumentException(
'withUploadedFiles expects an array tree with UploadedFileInterface leaves.'
);
}
$request = clone $this;
$request->uploadedFiles = $uploadedFiles;
return $request;
}
/**
* Retrieve any parameters provided in the request body.
*
* If the request Content-Type is either application/x-www-form-urlencoded
* or multipart/form-data, and the request method is POST, this method will
* return the contents of $_POST.
*
* Otherwise, this method may return any results of deserializing
* the request body content; as parsing returns structured content, the
* potential types MUST be arrays or objects only. A null value indicates
* the absence of body content.
*
* @return null|array|object The deserialized body parameters, if any.
* These will typically be an array or object.
*/
public function getParsedBody()
{
return $this->parsedBody;
}
/**
* Create a new instance with the specified body parameters.
*
* These MAY be injected during instantiation.
*
* If the request Content-Type is either application/x-www-form-urlencoded
* or multipart/form-data, and the request method is POST, use this method
* ONLY to inject the contents of $_POST.
*
* The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
* deserializing the request body content. Deserialization/parsing returns
* structured data, and, as such, this method ONLY accepts arrays or objects,
* or a null value if nothing was available to parse.
*
* As an example, if content negotiation determines that the request data
* is a JSON payload, this method could be used to create a request
* instance with the deserialized parameters.
*
* @param null|array|object $data The deserialized body data. This will
* typically be in an array or object.
* @return static
*/
public function withParsedBody($data)
{
if (!(is_null($data) || is_array($data) || is_object($data))) {
throw new InvalidArgumentException('Parsed body must be null, array, or object.');
}
$request = clone $this;
$request->parsedBody = $data;
return $request;
}
/**
* Retrieve attributes derived from the request.
*
* The request "attributes" may be used to allow injection of any
* parameters derived from the request: e.g., the results of path
* match operations; the results of decrypting cookies; the results of
* deserializing non-form-encoded message bodies; etc. Attributes
* will be application and request specific, and CAN be mutable.
*
* @return array Attributes derived from the request.
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* Retrieve a single derived request attribute.
*
* Retrieves a single derived request attribute as described in
* getAttributes(). If the attribute has not been previously set, returns
* the default value as provided.
*
* This method obviates the need for a hasAttribute() method, as it allows
* specifying a default value to return if the attribute is not found.
*
* @see getAttributes()
* @param string $name The attribute name.
* @param mixed $default Default value to return if the attribute does not exist.
* @return mixed
*/
public function getAttribute($name, $default = null)
{
if (isset($this->attributes[$name])) {
return $this->attributes[$name];
}
return $default;
}
/**
* Create a new instance with the specified derived request attribute.
*
* This method allows setting a single derived request attribute as
* described in getAttributes().
*
* @see getAttributes()
* @param string $name The attribute name.
* @param mixed $value The value of the attribute.
* @return static
*/
public function withAttribute($name, $value)
{
$request = clone $this;
$request->attributes[$name] = $value;
return $request;
}
/**
* Create a new instance that removes the specified derived request
* attribute.
*
* This method allows removing a single derived request attribute as
* described in getAttributes().
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return a new instance that removes
* the attribute.
*
* @see getAttributes()
* @param string $name The attribute name.
* @return static
*/
public function withoutAttribute($name)
{
$request = clone $this;
unset($request->attributes[$name]);
return $request;
}
// -------------------------------------------------------------------------
/**
* @param array $root
* @return bool
*/
private function isValidUploadedFilesTree(array $root)
{
// Allow empty array.
if (count($root) === 0) {
return true;
}
// If not empty, the array MUST have all string keys.
$keys = array_keys($root);
if (count($keys) !== count(array_filter($keys, 'is_string'))) {
return false;
}
// Valid if each child branch is valid.
foreach ($root as $branch) {
if (!$this->isValidUploadedFilesBranch($branch)) {
return false;
}
}
return true;
}
/**
* @param UploadedFileInterface|array $branch
* @return bool
*/
private function isValidUploadedFilesBranch($branch): bool
{
if (is_array($branch)) {
// Branch.
foreach ($branch as $child) {
if (!$this->isValidUploadedFilesBranch($child)) {
return false;
}
}
return true;
} else {
// Leaf. Valid only if this is an UploadedFileInterface.
return $branch instanceof UploadedFileInterface;
}
}
}

View File

@ -0,0 +1,191 @@
<?php
namespace WellRESTed\Message;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
class ServerRequestMarshaller
{
/**
* Read the request as sent from the client and construct a ServerRequest
* representation.
*
* @return ServerRequestInterface
* @internal
*/
public function getServerRequest(): ServerRequestInterface
{
$method = self::parseMethod($_SERVER);
$uri = self::readUri($_SERVER);
$headers = self::parseHeaders($_SERVER);
$body = self::readBody();
$request = (new ServerRequest($method, $uri, $headers, $body, $_SERVER))
->withProtocolVersion(self::parseProtocolVersion($_SERVER))
->withUploadedFiles(self::readUploadedFiles($_FILES))
->withCookieParams($_COOKIE)
->withQueryParams(self::parseQuery($_SERVER));
if (self::isForm($request)) {
$request = $request->withParsedBody($_POST);
}
return $request;
}
private static function parseQuery(array $serverParams): array
{
$queryParams = [];
if (isset($serverParams['QUERY_STRING'])) {
parse_str($serverParams['QUERY_STRING'], $queryParams);
}
return $queryParams;
}
private static function parseProtocolVersion(array $serverParams): string
{
if (isset($serverParams['SERVER_PROTOCOL'])
&& $serverParams['SERVER_PROTOCOL'] === 'HTTP/1.0') {
return '1.0';
}
return '1.1';
}
private static function parseHeaders(array $serverParams): array
{
// http://www.php.net/manual/en/function.getallheaders.php#84262
$headers = [];
foreach ($serverParams as $name => $value) {
if (substr($name, 0, 5) === 'HTTP_') {
$name = self::normalizeHeaderName(substr($name, 5));
$headers[$name] = trim($value);
} elseif (self::isContentHeader($name) && !empty(trim($value))) {
$name = self::normalizeHeaderName($name);
$headers[$name] = trim($value);
}
}
return $headers;
}
private static function normalizeHeaderName(string $name): string
{
$name = ucwords(strtolower(str_replace('_', ' ', $name)));
return str_replace(' ', '-', $name);
}
private static function isContentHeader(string $name): bool
{
return $name === 'CONTENT_LENGTH' || $name === 'CONTENT_TYPE';
}
private static function parseMethod(array $serverParams): string
{
return $serverParams['REQUEST_METHOD'] ?? 'GET';
}
private static function readBody(): StreamInterface
{
$input = fopen('php://input', 'rb');
$temp = fopen('php://temp', 'wb+');
stream_copy_to_stream($input, $temp);
rewind($temp);
return new Stream($temp);
}
private static function readUri(array $serverParams): UriInterface
{
$uri = '';
$scheme = 'http';
if (isset($serverParams['HTTPS']) && $serverParams['HTTPS'] && $serverParams['HTTPS'] !== 'off') {
$scheme = 'https';
}
if (isset($serverParams['HTTP_HOST'])) {
$authority = $serverParams['HTTP_HOST'];
$uri .= "$scheme://$authority";
}
// Path and query string
if (isset($serverParams['REQUEST_URI'])) {
$uri .= $serverParams['REQUEST_URI'];
}
return new Uri($uri);
}
private static function isForm(ServerRequestInterface $request): bool
{
$contentType = $request->getHeaderLine('Content-type');
return (strpos($contentType, 'application/x-www-form-urlencoded') !== false)
|| (strpos($contentType, 'multipart/form-data') !== false);
}
private static function readUploadedFiles(array $input): array
{
$uploadedFiles = [];
foreach ($input as $name => $value) {
self::addUploadedFilesToBranch($uploadedFiles, $name, $value);
}
return $uploadedFiles;
}
private static function addUploadedFilesToBranch(
array &$branch,
string $name,
array $value
): void {
if (self::isUploadedFile($value)) {
if (self::isUploadedFileList($value)) {
$files = [];
$keys = array_keys($value['name']);
foreach ($keys as $key) {
$files[$key] = new UploadedFile(
$value['name'][$key],
$value['type'][$key],
$value['size'][$key],
$value['tmp_name'][$key],
$value['error'][$key]
);
}
$branch[$name] = $files;
} else {
// Single uploaded file
$uploadedFile = new UploadedFile(
$value['name'],
$value['type'],
$value['size'],
$value['tmp_name'],
$value['error']
);
$branch[$name] = $uploadedFile;
}
} else {
// Add another branch
$nextBranch = [];
foreach ($value as $nextName => $nextValue) {
self::addUploadedFilesToBranch($nextBranch, $nextName, $nextValue);
}
$branch[$name] = $nextBranch;
}
}
private static function isUploadedFile(array $value): bool
{
// Check for each of the expected keys. If all are present, this is a
// a file. It may be a single file, or a list of files.
return isset($value['name'], $value['type'], $value['tmp_name'], $value['error'], $value['size']);
}
private static function isUploadedFileList(array $value): bool
{
// When each item is an array, this is a list of uploaded files.
return is_array($value['name'])
&& is_array($value['type'])
&& is_array($value['tmp_name'])
&& is_array($value['error'])
&& is_array($value['size']);
}
}

353
src/Message/Stream.php Normal file
View File

@ -0,0 +1,353 @@
<?php
namespace WellRESTed\Message;
use Exception;
use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
class Stream implements StreamInterface
{
private const READABLE_MODES = ['r', 'r+', 'w+', 'a+', 'x+', 'c+'];
private const WRITABLE_MODES = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'];
/** @var resource|null */
private $resource;
/**
* Create a new Stream by passing either a stream resource handle (e.g.,
* from fopen) or a string.
*
* If $resource is a string, the Stream will open a php://temp stream,
* write the string to the stream, and use that temp resource.
*
* @param resource|string $resource A file system pointer resource or
* string
*/
public function __construct($resource = '')
{
if (is_resource($resource) && get_resource_type($resource) === 'stream') {
$this->resource = $resource;
} elseif (is_string($resource)) {
$this->resource = fopen('php://temp', 'wb+');
if ($resource !== '') {
$this->write($resource);
}
} else {
throw new InvalidArgumentException('Expected resource or string.');
}
}
/**
* Reads all data from the stream into a string, from the beginning to end.
*
* This method will attempt to seek to the beginning of the stream before
* reading data and read the stream until the end is reached.
*
* Warning: This could attempt to load a large amount of data into memory.
*
* @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
* @return string
*/
public function __toString()
{
try {
if ($this->isSeekable()) {
$this->rewind();
}
return $this->getContents();
} catch (Exception $e) {
// Silence exceptions in order to conform with PHP's string casting
// operations.
return '';
}
}
/**
* Closes the stream and any underlying resources.
*
* @return void
*/
public function close()
{
if ($this->resource === null) {
return;
}
$resource = $this->resource;
fclose($resource);
$this->resource = null;
}
/**
* Separates any underlying resources from the stream.
*
* After the stream has been detached, the stream is in an unusable state.
*
* @return resource|null Underlying file-pointer handler
*/
public function detach()
{
$resource = $this->resource;
$this->resource = null;
return $resource;
}
/**
* Get the size of the stream if known
*
* @return int|null Returns the size in bytes if known, or null if unknown.
*/
public function getSize()
{
if ($this->resource === null) {
return null;
}
$statistics = fstat($this->resource);
if ($statistics && $statistics['size']) {
return $statistics['size'];
}
return null;
}
/**
* Returns the current position of the file read/write pointer
*
* @return int Position of the file pointer
* @throws RuntimeException on error.
*/
public function tell()
{
if ($this->resource === null) {
throw new RuntimeException('Unable to retrieve current position of detached stream.');
}
$position = ftell($this->resource);
if ($position === false) {
throw new RuntimeException('Unable to retrieve current position of file pointer.');
}
return $position;
}
/**
* Returns true if the stream is at the end of the stream.
*
* @return bool
*/
public function eof()
{
if ($this->resource === null) {
return true;
}
return feof($this->resource);
}
/**
* Returns whether or not the stream is seekable.
*
* @return bool
*/
public function isSeekable()
{
if ($this->resource === null) {
return false;
}
return $this->getMetadata('seekable') == 1;
}
/**
* Seek to a position in the stream.
*
* @link http://www.php.net/manual/en/function.fseek.php
* @param int $offset Stream offset
* @param int $whence Specifies how the cursor position will be calculated
* based on the seek offset. Valid values are identical to the built-in
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
* offset bytes SEEK_CUR: Set position to current location plus offset
* SEEK_END: Set position to end-of-stream plus offset.
* @return void
* @throws RuntimeException on failure.
*/
public function seek($offset, $whence = SEEK_SET)
{
if ($this->resource === null) {
throw new RuntimeException('Unable to seek detached stream.');
}
$result = -1;
if ($this->isSeekable()) {
$result = fseek($this->resource, $offset, $whence);
}
if ($result === -1) {
throw new RuntimeException('Unable to seek to position.');
}
}
/**
* Seek to the beginning of the stream.
*
* If the stream is not seekable, this method will raise an exception;
* otherwise, it will perform a seek(0).
*
* @see seek()
* @link http://www.php.net/manual/en/function.fseek.php
* @return void
* @throws RuntimeException on failure.
*/
public function rewind()
{
if ($this->resource === null) {
throw new RuntimeException('Unable to seek detached stream.');
}
$result = false;
if ($this->isSeekable()) {
$result = rewind($this->resource);
}
if ($result === false) {
throw new RuntimeException('Unable to rewind.');
}
}
/**
* Returns whether or not the stream is writable.
*
* @return bool
*/
public function isWritable()
{
if ($this->resource === null) {
return false;
}
$mode = $this->getBasicMode();
return in_array($mode, self::WRITABLE_MODES);
}
/**
* Write data to the stream.
*
* @param string $string The string that is to be written.
* @return int Returns the number of bytes written to the stream.
* @throws RuntimeException on failure.
*/
public function write($string)
{
if ($this->resource === null) {
throw new RuntimeException('Unable to write to detached stream.');
}
$result = false;
if ($this->isWritable()) {
$result = fwrite($this->resource, $string);
}
if ($result === false) {
throw new RuntimeException('Unable to write to stream.');
}
return $result;
}
/**
* Returns whether or not the stream is readable.
*
* @return bool
*/
public function isReadable()
{
if ($this->resource === null) {
return false;
}
$mode = $this->getBasicMode();
return in_array($mode, self::READABLE_MODES);
}
/**
* Read data from the stream.
*
* @param int $length Read up to $length bytes from the object and return
* them. Fewer than $length bytes may be returned if underlying stream
* call returns fewer bytes.
* @return string Returns the data read from the stream, or an empty string
* if no bytes are available.
* @throws RuntimeException if an error occurs.
*/
public function read($length)
{
if ($this->resource === null) {
throw new RuntimeException('Unable to read to detached stream.');
}
$result = false;
if ($this->isReadable()) {
$result = fread($this->resource, $length);
}
if ($result === false) {
throw new RuntimeException('Unable to read from stream.');
}
return $result;
}
/**
* Returns the remaining contents in a string
*
* @return string
* @throws RuntimeException if unable to read or an error occurs while
* reading.
*/
public function getContents()
{
if ($this->resource === null) {
throw new RuntimeException('Unable to read to detached stream.');
}
$result = false;
if ($this->isReadable()) {
$result = stream_get_contents($this->resource);
}
if ($result === false) {
throw new RuntimeException('Unable to read from stream.');
}
return $result;
}
/**
* Get stream metadata as an associative array or retrieve a specific key.
*
* The keys returned are identical to the keys returned from PHP's
* stream_get_meta_data() function.
*
* @link http://php.net/manual/en/function.stream-get-meta-data.php
* @param string|null $key Specific metadata to retrieve.
* @return array|mixed|null Returns an associative array if no key is
* provided. Returns a specific key value if a key is provided and the
* value is found, or null if the key is not found.
*/
public function getMetadata($key = null)
{
if ($this->resource === null) {
return null;
}
$metadata = stream_get_meta_data($this->resource);
if ($key === null) {
return $metadata;
} else {
return $metadata[$key];
}
}
/**
* @return string Mode for the resource reduced to only the characters
* r, w, a, x, c, and + needed to determine readable and writeable status.
*/
private function getBasicMode()
{
$mode = $this->getMetadata('mode') ?? '';
return preg_replace('/[^rwaxc+]/', '', $mode);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace WellRESTed\Message;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
class StreamFactory implements StreamFactoryInterface
{
/**
* Create a new stream from a string.
*
* @param string $content String content with which to populate the stream.
* @return StreamInterface
*/
public function createStream(string $content = ''): StreamInterface
{
return new Stream($content);
}
/**
* Create a stream from an existing file.
*
* @param string $filename Filename or stream URI to use as basis of stream.
* @param string $mode Mode with which to open the underlying file/stream.
*
* @return StreamInterface
* @throws RuntimeException If the file cannot be opened.
*/
public function createStreamFromFile(
string $filename,
string $mode = 'r'
): StreamInterface {
$f = fopen($filename, $mode);
if ($f === false) {
throw new RuntimeException();
}
return new Stream($f);
}
/**
* Create a new stream from an existing resource.
*
* @param resource $resource PHP resource to use as basis of stream.
*
* @return StreamInterface
*/
public function createStreamFromResource($resource): StreamInterface
{
return new Stream($resource);
}
}

View File

@ -0,0 +1,194 @@
<?php
namespace WellRESTed\Message;
use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;
use RuntimeException;
/**
* Value object representing a file uploaded through an HTTP request.
*/
class UploadedFile implements UploadedFileInterface
{
/** @var string */
private $clientFilename;
/** @var string */
private $clientMediaType;
/** @var int */
private $error;
/** @var bool */
private $moved = false;
/** @var int */
private $size;
/** @var StreamInterface */
private $stream;
/** @var string|null */
private $tmpName;
/**
* Create a new UploadedFile. The arguments correspond with keys from arrays
* provided by $_FILES. For example, given this structure for $_FILES:
*
* array(
* 'avatar' => array(
* 'name' => 'my-avatar.png',
* 'type' => 'image/png',
* 'size' => 90996,
* 'tmp_name' => 'phpUxcOty',
* 'error' => 0
* )
* )
*
* ...use this call:
*
* new UploadedFile(
* $_FILES['avatar']['name'],
* $_FILES['avatar']['type'],
* $_FILES['avatar']['size'],
* $_FILES['avatar']['tmp_name'],
* $_FILES['avatar']['error']
* );
*
* @param string $name Name of the file; provided by the client
* @param string $type Media type of the file; provided by the client
* @param int $size The file size in bytes.
* @param string $tmpName Local filesystem name of the file
* @param int $error One of PHP's UPLOAD_ERR_XXX constants.
* @see http://php.net/manual/en/features.file-upload.errors.php
*/
public function __construct($name, $type, $size, $tmpName, $error)
{
$this->clientFilename = $name;
$this->error = $error;
$this->clientMediaType = $type;
$this->size = $size;
if (file_exists($tmpName)) {
$this->stream = new Stream(fopen($tmpName, 'rb'));
$this->tmpName = $tmpName;
} else {
$this->stream = new NullStream();
$this->tmpName = null;
}
}
/**
* Retrieve a stream representing the uploaded file.
*
* This method returns a StreamInterface instance, representing the
* uploaded file. The purpose of this method is to allow using native PHP
* stream functionality to manipulate the file upload, such as
* stream_copy_to_stream() (though the result will need to be decorated in
* a native PHP stream wrapper to work with such functions).
*
* If the moveTo() method has been called previously, this method will
* raise an exception.
*
* @return StreamInterface Stream representation of the uploaded file.
* @throws RuntimeException in cases when no stream is available or can
* be created.
*/
public function getStream()
{
if ($this->tmpName === null) {
throw new RuntimeException('Unable to read uploaded file.');
}
if ($this->moved) {
throw new RuntimeException('File has already been moved.');
}
if (php_sapi_name() !== 'cli' && !is_uploaded_file($this->tmpName)) {
throw new RuntimeException('File is not an uploaded file.');
}
return $this->stream;
}
/**
* Move the uploaded file to a new location.
*
* Use this method as an alternative to move_uploaded_file(). This method
* is guaranteed to work in both SAPI and non-SAPI environments.
*
* The original file or stream will be removed on completion.
*
* If this method is called more than once, any subsequent calls will raise
* an exception.
*
* @see http://php.net/is_uploaded_file
* @see http://php.net/move_uploaded_file
* @param string $targetPath Path to which to move the uploaded file.
* @return void
* @throws InvalidArgumentException if the $path specified is invalid.
* @throws RuntimeException on any error during the move operation, or on
* the second or subsequent call to the method.
*/
public function moveTo($targetPath)
{
if ($this->tmpName === null || !file_exists($this->tmpName)) {
throw new RuntimeException("File {$this->tmpName} does not exist.");
}
if (php_sapi_name() === 'cli') {
rename($this->tmpName, $targetPath);
} else {
move_uploaded_file($this->tmpName, $targetPath);
}
$this->moved = true;
}
/**
* Retrieve the file size.
*
* @return int|null The file size in bytes or null if unknown.
*/
public function getSize()
{
return $this->size;
}
/**
* Retrieve the error associated with the uploaded file.
*
* The return value will be one of PHP's UPLOAD_ERR_XXX constants.
*
* If the file was uploaded successfully, this method will return
* UPLOAD_ERR_OK.
*
* @see http://php.net/manual/en/features.file-upload.errors.php
* @return int One of PHP's UPLOAD_ERR_XXX constants.
*/
public function getError()
{
return $this->error;
}
/**
* Retrieve the filename sent by the client.
*
* Do not trust the value returned by this method. A client could send
* a malicious filename with the intention to corrupt or hack your
* application.
*
* @return string|null The filename sent by the client or null if none
* was provided.
*/
public function getClientFilename()
{
return $this->clientFilename;
}
/**
* Retrieve the media type sent by the client.
*
* Do not trust the value returned by this method. A client could send
* a malicious media type with the intention to corrupt or hack your
* application.
*
* @return string|null The media type sent by the client or null if none
* was provided.
*/
public function getClientMediaType()
{
return $this->clientMediaType;
}
}

533
src/Message/Uri.php Normal file
View File

@ -0,0 +1,533 @@
<?php
namespace WellRESTed\Message;
use InvalidArgumentException;
use Psr\Http\Message\UriInterface;
/**
* Value object representing a URI.
*
* This interface is meant to represent URIs according to RFC 3986 and to
* provide methods for most common operations. Additional functionality for
* working with URIs can be provided on top of the interface or externally.
* Its primary use is for HTTP requests, but may also be used in other
* contexts.
*
* @link http://tools.ietf.org/html/rfc3986 (the URI specification)
*/
class Uri implements UriInterface
{
private const MIN_PORT = 0;
private const MAX_PORT = 65535;
/** @var string */
private $scheme;
/** @var string */
private $user;
/** @var string */
private $password;
/** @var string */
private $host;
/** @var int|null */
private $port;
/** @var string */
private $path;
/** @var string */
private $query;
/** @var string */
private $fragment;
/**
* @param string $uri A string representation of a URI.
*/
public function __construct(string $uri = '')
{
$parsed = parse_url($uri);
$this->scheme = $parsed['scheme'] ?? '';
$this->user = $parsed['user'] ?? '';
$this->password = $parsed['pass'] ?? '';
$this->host = strtolower($parsed['host'] ?? '');
$this->port = $parsed['port'] ?? null;
$this->path = $parsed['path'] ?? '';
$this->query = $parsed['query'] ?? '';
$this->fragment = $parsed['fragment'] ?? '';
}
/**
* Retrieve the scheme component of the URI.
*
* If no scheme is present, this method will return an empty string.
*
* The value returned will be normalized to lowercase, per RFC 3986
* Section 3.1.
*
* The trailing ":" character is not part of the scheme and will not be
* added.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.1
* @return string The URI scheme.
*/
public function getScheme()
{
return $this->scheme;
}
/**
* Retrieve the authority component of the URI.
*
* If no authority information is present, this method will return an empty
* string.
*
* The authority syntax of the URI is:
*
* <pre>
* [user-info@]host[:port]
* </pre>
*
* If the port component is not set or is the standard port for the current
* scheme, it will not be included.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2
* @return string The URI authority, in "[user-info@]host[:port]" format.
*/
public function getAuthority()
{
$host = $this->getHost();
if (!$host) {
return '';
}
$authority = '';
// User Info
$userInfo = $this->getUserInfo();
if ($userInfo) {
$authority .= $userInfo . '@';
}
// Host
$authority .= $host;
// Port: Include only if non-standard
if ($this->nonStandardPort()) {
$authority .= ':' . $this->getPort();
}
return $authority;
}
private function nonStandardPort(): bool
{
$port = $this->getPort();
$scheme = $this->getScheme();
return $scheme === 'http' && $port !== 80
|| $scheme === 'https' && $port !== 443;
}
/**
* Retrieve the user information component of the URI.
*
* If no user information is present, this method will return an empty
* string.
*
* If a user is present in the URI, this will return that value;
* additionally, if the password is also present, it will be appended to the
* user value, with a colon (":") separating the values.
*
* The trailing "@" character is not part of the user information and will
* not be added.
*
* @return string The URI user information, in "username[:password]" format.
*/
public function getUserInfo()
{
$userInfo = $this->user;
if ($userInfo && $this->password) {
$userInfo .= ':' . $this->password;
}
return $userInfo;
}
/**
* Retrieve the host component of the URI.
*
* If no host is present, this method will return an empty string.
*
* The value returned will be normalized to lowercase, per RFC 3986
* Section 3.2.2.
*
* @see http://tools.ietf.org/html/rfc3986#section-3.2.2
* @return string The URI host.
*/
public function getHost()
{
return $this->host;
}
/**
* Retrieve the port component of the URI.
*
* If a port is present, and it is non-standard for the current scheme,
* this method MUST return it as an integer. If the port is the standard port
* used with the current scheme, this method SHOULD return null.
*
* If no port is present, and no scheme is present, this method MUST return
* a null value.
*
* If no port is present, but a scheme is present, this method MAY return
* the standard port for that scheme, but SHOULD return null.
*
* @return null|int The URI port.
*/
public function getPort()
{
if ($this->port === null) {
switch ($this->scheme) {
case 'http':
return 80;
case 'https':
return 443;
default:
return null;
}
}
return $this->port;
}
/**
* Retrieve the path component of the URI.
*
* The path can either be empty or absolute (starting with a slash) or
* rootless (not starting with a slash). Implementations MUST support all
* three syntaxes.
*
* Normally, the empty path "" and absolute path "/" are considered equal as
* defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
* do this normalization because in contexts with a trimmed base path, e.g.
* the front controller, this difference becomes significant. It's the task
* of the user to handle both "" and "/".
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.3.
*
* As an example, if the value should include a slash ("/") not intended as
* delimiter between path segments, that value MUST be passed in encoded
* form (e.g., "%2F") to the instance.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.3
* @return string The URI path.
*/
public function getPath()
{
if ($this->path === '*') {
return $this->path;
}
return $this->percentEncode($this->path);
}
/**
* Retrieve the query string of the URI.
*
* If no query string is present, this method MUST return an empty string.
*
* The leading "?" character is not part of the query and MUST NOT be
* added.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.4.
*
* As an example, if a value in a key/value pair of the query string should
* include an ampersand ("&") not intended as a delimiter between values,
* that value MUST be passed in encoded form (e.g., "%26") to the instance.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.4
* @return string The URI query string.
*/
public function getQuery()
{
return $this->percentEncode($this->query);
}
/**
* Retrieve the fragment component of the URI.
*
* If no fragment is present, this method MUST return an empty string.
*
* The leading "#" character is not part of the fragment and MUST NOT be
* added.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.5.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.5
* @return string The URI fragment.
*/
public function getFragment()
{
return $this->percentEncode($this->fragment);
}
/**
* Return an instance with the specified scheme.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified scheme.
*
* Implementations MUST support the schemes "http" and "https" case
* insensitively, and MAY accommodate other schemes if required.
*
* An empty scheme is equivalent to removing the scheme.
*
* @param string $scheme The scheme to use with the new instance.
* @return static A new instance with the specified scheme.
* @throws InvalidArgumentException for invalid or unsupported schemes.
*/
public function withScheme($scheme)
{
$scheme = strtolower($scheme ?? '');
if (!in_array($scheme, ['', 'http', 'https'])) {
throw new InvalidArgumentException('Scheme must be http, https, or empty.');
}
$uri = clone $this;
$uri->scheme = $scheme;
return $uri;
}
/**
* Return an instance with the specified user information.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified user information.
*
* Password is optional, but the user information MUST include the
* user; an empty string for the user is equivalent to removing user
* information.
*
* @param string $user The user name to use for authority.
* @param null|string $password The password associated with $user.
* @return static A new instance with the specified user information.
*/
public function withUserInfo($user, $password = null)
{
$uri = clone $this;
$uri->user = $user;
$uri->password = $password ?? '';
return $uri;
}
/**
* Return an instance with the specified host.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified host.
*
* An empty host value is equivalent to removing the host.
*
* @param string $host The hostname to use with the new instance.
* @return static A new instance with the specified host.
* @throws InvalidArgumentException for invalid hostnames.
*/
public function withHost($host)
{
if (!is_string($host)) {
throw new InvalidArgumentException('Host must be a string.');
}
$uri = clone $this;
$uri->host = strtolower($host);
return $uri;
}
/**
* Return an instance with the specified port.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified port.
*
* Implementations MUST raise an exception for ports outside the
* established TCP and UDP port ranges.
*
* A null value provided for the port is equivalent to removing the port
* information.
*
* @param null|int $port The port to use with the new instance; a null value
* removes the port information.
* @return static A new instance with the specified port.
* @throws InvalidArgumentException for invalid ports.
*/
public function withPort($port)
{
if (is_numeric($port)) {
if ($port < self::MIN_PORT || $port > self::MAX_PORT) {
$message = sprintf('Port must be between %s and %s.', self::MIN_PORT, self::MAX_PORT);
throw new InvalidArgumentException($message);
}
$port = (int) $port;
} elseif ($port !== null) {
throw new InvalidArgumentException('Port must be an int or null.');
}
$uri = clone $this;
$uri->port = $port;
return $uri;
}
/**
* Return an instance with the specified path.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified path.
*
* The path can either be empty or absolute (starting with a slash) or
* rootless (not starting with a slash). Implementations MUST support all
* three syntaxes.
*
* Users can provide both encoded and decoded path characters.
* Implementations ensure the correct encoding as outlined in getPath().
*
* @param string $path The path to use with the new instance.
* @return static A new instance with the specified path.
* @throws InvalidArgumentException for invalid paths.
*/
public function withPath($path)
{
if (!is_string($path)) {
throw new InvalidArgumentException('Path must be a string');
}
$uri = clone $this;
$uri->path = $path;
return $uri;
}
/**
* Return an instance with the specified query string.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified query string.
*
* Users can provide both encoded and decoded query characters.
* Implementations ensure the correct encoding as outlined in getQuery().
*
* An empty query string value is equivalent to removing the query string.
*
* @param string $query The query string to use with the new instance.
* @return static A new instance with the specified query string.
* @throws InvalidArgumentException for invalid query strings.
*/
public function withQuery($query)
{
$uri = clone $this;
$uri->query = $query;
return $uri;
}
/**
* Return an instance with the specified URI fragment.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified URI fragment.
*
* Users can provide both encoded and decoded fragment characters.
* Implementations ensure the correct encoding as outlined in getFragment().
*
* An empty fragment value is equivalent to removing the fragment.
*
* @param string $fragment The fragment to use with the new instance.
* @return static A new instance with the specified fragment.
*/
public function withFragment($fragment)
{
$uri = clone $this;
$uri->fragment = $fragment ?? '';
return $uri;
}
/**
* Return the string representation as a URI reference.
*
* Depending on which components of the URI are present, the resulting
* string is either a full URI or relative reference according to RFC 3985,
* Section 4.1. The method concatenates the various components of the URI,
* using the appropriate delimiters:
*
* - If a scheme is present, it MUST be suffixed by ":".
* - If an authority is present, it MUST be prefixed by "//".
* - The path can be concatenated without delimiters. But there are two
* cases where the path has to be adjusted to make the URI reference
* valid as PHP does not allow to throw an exception in __toString():
* - If the path is rootless and an authority is present, the path MUST
* be prefixed by "/".
* - If the path is starting with more than one "/" and no authority is
* present, the starting slashes MUST be reduced to one.
* - If a query is present, it MUST be prefixed by "?".
* - If a fragment is present, it MUST be prefixed by "#".
*
* @see http://tools.ietf.org/html/rfc3986#section-4.1
* @return string
*/
public function __toString()
{
$string = '';
$authority = $this->getAuthority();
if ($authority !== '') {
$scheme = $this->getScheme();
if ($scheme !== '') {
$string = $scheme . ':';
}
$string .= "//$authority";
}
$path = $this->getPath();
if ($path !== '') {
$string .= $path;
}
$query = $this->getQuery();
if ($query !== '') {
$string .= "?$query";
}
$fragment = $this->getFragment();
if ($fragment !== '') {
$string .= "#$fragment";
}
return $string;
}
/**
* Return a percent-encoded string.
*
* This method encode each character that is not:
* - A percent sign ("%") that is followed by a hex character (0-9, a-f, A-F)
* - An "unreserved character" per RFC 3986 (see below)
* - A "reserved character" per RFC 3986 (see below)
*
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* reserved = gen-delims / sub-delims
* gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
*
* @param string $subject
* @return string
*/
private function percentEncode(string $subject)
{
$reserved = ':/?#[]@!$&\'()*+,;=';
$reserved = preg_quote($reserved);
$pattern = '~(?:%(?![a-fA-F0-9]{2}))|(?:[^%a-zA-Z0-9\-\.\_\~' . $reserved . ']{1})~';
$callback = function (array $matches): string {
return urlencode($matches[0]);
};
return preg_replace_callback($pattern, $callback, $subject);
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace WellRESTed;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
interface MiddlewareInterface
{
/**
* Accepts a request and response and returns a modified response.
*
* $request represents the request issued by the client.
*
* $response represents the current state of the response.
*
* $next is a callable that expects a request and response as parameters
* and returns a response. Calling $next forwards a request and response
* to the next middleware in the sequence (if any) and continues
* propagation; returning a response without calling $next halts
* propagation and prevents subsequent middleware from running.
*
* Implementations SHOULD call $next to allow subsequent middleware to act
* on the request and response. Implementations MAY further alter the
* response returned by $next before returning it.
*
* Implementations MAY return a response without calling $next to prevent
* propagation (e.g., for error conditions).
*
* Implementations SHOULD NOT call $next and disregard the response by
* returning an entirely unrelated response.
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param callable $next
* @return ResponseInterface
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next);
}

View File

@ -0,0 +1,134 @@
<?php
namespace WellRESTed\Routing\Route;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use WellRESTed\Dispatching\DispatcherInterface;
use WellRESTed\MiddlewareInterface;
/**
* @internal
*/
class MethodMap implements MiddlewareInterface
{
/** @var DispatcherInterface */
private $dispatcher;
/** @var array */
private $map;
// -------------------------------------------------------------------------
public function __construct(DispatcherInterface $dispatcher)
{
$this->map = [];
$this->dispatcher = $dispatcher;
}
/**
* Register a dispatchable (e.g.m handler or middleware) with a method.
*
* $method may be:
* - A single verb ("GET"),
* - A comma-separated list of verbs ("GET,PUT,DELETE")
* - "*" to indicate any method.
*
* $dispatchable may be anything a Dispatcher can dispatch.
* @see DispatcherInterface::dispatch
*
* $dispatchable may also be null, in which case any previously set
* handlers and middle for that method or methods will be unset.
*
* @param string $method
* @param mixed $dispatchable
*/
public function register(string $method, $dispatchable): void
{
$methods = explode(',', $method);
$methods = array_map('trim', $methods);
foreach ($methods as $method) {
$this->map[$method] = $dispatchable;
}
}
// -------------------------------------------------------------------------
// MiddlewareInterface
/**
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param callable $next
* @return ResponseInterface
*/
public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response,
$next
) {
$method = $request->getMethod();
// Dispatch middleware registered with the explicitly matching method.
if (isset($this->map[$method])) {
$middleware = $this->map[$method];
return $this->dispatchMiddleware($middleware, $request, $response, $next);
}
// For HEAD, dispatch GET by default.
if ($method === 'HEAD' && isset($this->map['GET'])) {
$middleware = $this->map['GET'];
return $this->dispatchMiddleware($middleware, $request, $response, $next);
}
// Dispatch * middleware, if registered.
if (isset($this->map['*'])) {
$middleware = $this->map['*'];
return $this->dispatchMiddleware($middleware, $request, $response, $next);
}
// Respond describing the allowed methods, either as a 405 response or
// in response to an OPTIONS request.
if ($method === 'OPTIONS') {
$response = $response->withStatus(200);
} else {
$response = $response->withStatus(405);
}
return $this->addAllowHeader($response);
}
// -------------------------------------------------------------------------
private function addAllowHeader(ResponseInterface $response): ResponseInterface
{
$methods = join(',', $this->getAllowedMethods());
return $response->withHeader('Allow', $methods);
}
/**
* @return string[]
*/
private function getAllowedMethods(): array
{
$methods = array_keys($this->map);
// Add HEAD if GET is allowed and HEAD is not present.
if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
$methods[] = 'HEAD';
}
// Add OPTIONS if not already present.
if (!in_array('OPTIONS', $methods)) {
$methods[] = 'OPTIONS';
}
return $methods;
}
/**
* @param mixed $middleware
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param callable $next
* @return ResponseInterface
*/
private function dispatchMiddleware(
$middleware,
ServerRequestInterface $request,
ResponseInterface $response,
$next
) {
return $this->dispatcher->dispatch($middleware, $request, $response, $next);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace WellRESTed\Routing\Route;
/**
* @internal
*/
class PrefixRoute extends Route
{
public function __construct(string $target, MethodMap $methodMap)
{
parent::__construct(rtrim($target, '*'), $methodMap);
}
public function getType(): int
{
return Route::TYPE_PREFIX;
}
/**
* Examines a request target to see if it is a match for the route.
*
* @param string $requestTarget
* @return bool
*/
public function matchesRequestTarget(string $requestTarget): bool
{
return strrpos($requestTarget, $this->target, -strlen($requestTarget)) !== false;
}
/**
* Always returns an empty array.
*/
public function getPathVariables(): array
{
return [];
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace WellRESTed\Routing\Route;
use RuntimeException;
/**
* @internal
*/
class RegexRoute extends Route
{
/** @var array */
private $captures = [];
public function getType(): int
{
return Route::TYPE_PATTERN;
}
/**
* Examines a request target to see if it is a match for the route.
*
* @param string $requestTarget
* @return bool
*/
public function matchesRequestTarget(string $requestTarget): bool
{
$this->captures = [];
$matched = preg_match($this->getTarget(), $requestTarget, $captures);
if ($matched) {
$this->captures = $captures;
return true;
} elseif ($matched === false) {
throw new RuntimeException('Invalid regular expression: ' . $this->getTarget());
}
return false;
}
/**
* Returns an array of matches from the last call to matchesRequestTarget.
*
* @see \preg_match
* @return array
*/
public function getPathVariables(): array
{
return $this->captures;
}
}

112
src/Routing/Route/Route.php Normal file
View File

@ -0,0 +1,112 @@
<?php
namespace WellRESTed\Routing\Route;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use RuntimeException;
use WellRESTed\MiddlewareInterface;
/**
* @internal
*/
abstract class Route implements MiddlewareInterface
{
/** Matches when request path is an exact match to entire target */
public const TYPE_STATIC = 0;
/** Matches when request path is an exact match to start of target */
public const TYPE_PREFIX = 1;
/** Matches by request path by pattern and may extract matched varialbes */
public const TYPE_PATTERN = 2;
/** @var string */
protected $target;
/** @var MethodMap */
protected $methodMap;
public function __construct(string $target, MethodMap $methodMap)
{
$this->target = $target;
$this->methodMap = $methodMap;
}
/**
* Return the Route::TYPE_ constants that identifies the type.
*
* TYPE_STATIC indicates the route MUST match only when the path is an
* exact match to the route's entire target. This route type SHOULD NOT
* provide path variables.
*
* TYPE_PREFIX indicates the route MUST match when the route's target
* appears in its entirety at the beginning of the path.
*
* TYPE_PATTERN indicates that matchesRequestTarget MUST be used
* to determine a match against a given path. This route type SHOULD
* provide path variables.
*
* @return int One of the Route::TYPE_ constants.
*/
abstract public function getType(): int;
/**
* Return an array of variables extracted from the path most recently
* passed to matchesRequestTarget.
*
* If the path does not contain variables, or if matchesRequestTarget
* has not yet been called, this method MUST return an empty array.
*
* @return array
*/
abstract public function getPathVariables(): array;
/**
* Examines a request target to see if it is a match for the route.
*
* @param string $requestTarget
* @return bool
* @throws RuntimeException Error occurred testing the target such as an
* invalid regular expression
*/
abstract public function matchesRequestTarget(string $requestTarget): bool;
/**
* Path, partial path, or pattern to match request paths against.
*
* @return string
*/
public function getTarget(): string
{
return $this->target;
}
/**
* Register a dispatchable (handler or middleware) with a method.
*
* $method may be:
* - A single verb ("GET"),
* - A comma-separated list of verbs ("GET,PUT,DELETE")
* - "*" to indicate any method.
*
* $dispatchable may be anything a Dispatcher can dispatch.
* @see DispatcherInterface::dispatch
*
* @param string $method
* @param mixed $dispatchable
*/
public function register(string $method, $dispatchable): void
{
$this->methodMap->register($method, $dispatchable);
}
/**
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param callable $next
* @return ResponseInterface
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
{
$map = $this->methodMap;
return $map($request, $response, $next);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace WellRESTed\Routing\Route;
use WellRESTed\Dispatching\DispatcherInterface;
/**
* @internal
*/
class RouteFactory
{
private $dispatcher;
public function __construct(DispatcherInterface $dispatcher)
{
$this->dispatcher = $dispatcher;
}
/**
* Creates a route for the given target.
*
* - Target with no special characters will create StaticRoutes
* - Target ending with * will create PrefixRoutes
* - Target containing URI variables (e.g., {id}) will create TemplateRoutes
* - Regular expressions will create RegexRoutes
*
* @param string $target Route target or target pattern
* @return Route
*/
public function create(string $target): Route
{
if ($target[0] === '/') {
// Possible static, prefix, or template
// PrefixRoutes end with *
if (substr($target, -1) === '*') {
return new PrefixRoute($target, new MethodMap($this->dispatcher));
}
// TemplateRoutes contain {variable}
if (preg_match(TemplateRoute::URI_TEMPLATE_EXPRESSION_RE, $target)) {
return new TemplateRoute($target, new MethodMap($this->dispatcher));
}
// StaticRoute
return new StaticRoute($target, new MethodMap($this->dispatcher));
}
// Regex
return new RegexRoute($target, new MethodMap($this->dispatcher));
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace WellRESTed\Routing\Route;
/**
* @internal
*/
class StaticRoute extends Route
{
public function getType(): int
{
return Route::TYPE_STATIC;
}
/**
* Examines a request target to see if it is a match for the route.
*
* @param string $requestTarget
* @return bool
*/
public function matchesRequestTarget(string $requestTarget): bool
{
return $requestTarget === $this->getTarget();
}
/**
* Always returns an empty array.
*/
public function getPathVariables(): array
{
return [];
}
}

View File

@ -0,0 +1,175 @@
<?php
namespace WellRESTed\Routing\Route;
/**
* @internal
*/
class TemplateRoute extends Route
{
/** Regular expression matching a URI template variable (e.g., {id}) */
public const URI_TEMPLATE_EXPRESSION_RE = '/{([+.\/]?[a-zA-Z0-9_,]+\*?)}/';
/**
* Regular expression matching 1 or more unreserved characters.
* ALPHA / DIGIT / "-" / "." / "_" / "~"
*/
private const RE_UNRESERVED = '[0-9a-zA-Z\-._\~%]*';
/** @var array */
private $pathVariables = [];
/** @var array */
private $explosions = [];
public function getType(): int
{
return Route::TYPE_PATTERN;
}
public function getPathVariables(): array
{
return $this->pathVariables;
}
/**
* Examines a request target to see if it is a match for the route.
*
* @param string $requestTarget
* @return bool
*/
public function matchesRequestTarget(string $requestTarget): bool
{
$this->pathVariables = [];
$this->explosions = [];
if (!$this->matchesStartOfRequestTarget($requestTarget)) {
return false;
}
$matchingPattern = $this->getMatchingPattern();
if (preg_match($matchingPattern, $requestTarget, $captures)) {
$this->pathVariables = $this->processMatches($captures);
return true;
}
return false;
}
private function matchesStartOfRequestTarget(string $requestTarget): bool
{
$firstVarPos = strpos($this->target, '{');
if ($firstVarPos === false) {
return $requestTarget === $this->target;
}
return substr($requestTarget, 0, $firstVarPos) === substr($this->target, 0, $firstVarPos);
}
private function processMatches(array $matches): array
{
$variables = [];
// Isolate the named captures.
$keys = array_filter(array_keys($matches), 'is_string');
// Store named captures to the variables.
foreach ($keys as $key) {
$value = $matches[$key];
if (isset($this->explosions[$key])) {
$values = explode($this->explosions[$key], $value);
$variables[$key] = array_map('urldecode', $values);
} else {
$value = urldecode($value);
$variables[$key] = $value;
}
}
return $variables;
}
private function getMatchingPattern(): string
{
// Convert the template into the pattern
$pattern = $this->target;
// Escape allowable characters with regex meaning.
$escape = [
'.' => '\\.',
'-' => '\\-',
'+' => '\\+',
'*' => '\\*'
];
$pattern = str_replace(array_keys($escape), array_values($escape), $pattern);
$unescape = [
'{\\+' => '{+',
'{\\.' => '{.',
'\\*}' => '*}'
];
$pattern = str_replace(array_keys($unescape), array_values($unescape), $pattern);
// Surround the pattern with delimiters.
$pattern = "~^{$pattern}$~";
$pattern = preg_replace_callback(
self::URI_TEMPLATE_EXPRESSION_RE,
[$this, 'uriVariableReplacementCallback'],
$pattern
);
return $pattern;
}
private function uriVariableReplacementCallback(array $matches): string
{
$name = $matches[1];
$pattern = self::RE_UNRESERVED;
$prefix = '';
$delimiter = ',';
$explodeDelimiter = ',';
// Read the first character as an operator. This determines which
// characters to allow in the match.
$operator = $name[0];
// Read the last character as the modifier.
$explosion = (substr($name, -1, 1) === '*');
switch ($operator) {
case '+':
$name = substr($name, 1);
$pattern = '.*';
break;
case '.':
$name = substr($name, 1);
$prefix = '\\.';
$delimiter = '\\.';
$explodeDelimiter = '.';
break;
case '/':
$name = substr($name, 1);
$prefix = '\\/';
$delimiter = '\\/';
if ($explosion) {
$pattern = '[0-9a-zA-Z\-._\~%,\/]*'; // Unreserved + "," and "/"
$explodeDelimiter = '/';
}
break;
}
// Explosion
if ($explosion) {
$name = substr($name, 0, -1);
if ($pattern === self::RE_UNRESERVED) {
$pattern = '[0-9a-zA-Z\-._\~%,]*'; // Unreserved + ","
}
$this->explosions[$name] = $explodeDelimiter;
}
$names = explode(',', $name);
$results = [];
foreach ($names as $name) {
$results[] = "(?<{$name}>{$pattern})";
}
return $prefix . join($delimiter, $results);
}
}

290
src/Routing/Router.php Normal file
View File

@ -0,0 +1,290 @@
<?php
namespace WellRESTed\Routing;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use WellRESTed\Dispatching\Dispatcher;
use WellRESTed\Dispatching\DispatcherInterface;
use WellRESTed\MiddlewareInterface;
use WellRESTed\Routing\Route\Route;
use WellRESTed\Routing\Route\RouteFactory;
class Router implements MiddlewareInterface
{
/** @var string|null Attribute name for matched path variables */
private $pathVariablesAttributeName;
/** @var DispatcherInterface */
private $dispatcher;
/** @var RouteFactory */
private $factory;
/** @var Route[] Array of Route objects */
private $routes;
/** @var Route[] Hash array mapping exact paths to routes */
private $staticRoutes;
/** @var Route[] Hash array mapping path prefixes to routes */
private $prefixRoutes;
/** @var Route[] Hash array mapping path prefixes to routes */
private $patternRoutes;
/** @var mixed[] List array of middleware */
private $stack;
/** @var bool Call the next middleware when no route matches */
private $continueOnNotFound = false;
/**
* Create a new Router.
*
* By default, when a route containing path variables matches, the path
* variables are stored individually as attributes on the
* ServerRequestInterface.
*
* When $pathVariablesAttributeName is set, a single attribute will be
* stored with the name. The value will be an array containing all of the
* path variables.
*
* Use Server->createRouter to instantiate a new Router rather than calling
* this constructor manually.
*
* @param string|null $pathVariablesAttributeName
* Attribute name for matched path variables. A null value sets
* attributes directly.
* @param DispatcherInterface|null $dispatcher
* Instance to use for dispatching middleware and handlers.
* @param RouteFactory|null $routeFactory
*/
public function __construct(
?string $pathVariablesAttributeName = null,
?DispatcherInterface $dispatcher = null,
?RouteFactory $routeFactory = null
) {
$this->pathVariablesAttributeName = $pathVariablesAttributeName;
$this->dispatcher = $dispatcher ?? new Dispatcher();
$this->factory = $routeFactory ?? new RouteFactory($this->dispatcher);
$this->routes = [];
$this->staticRoutes = [];
$this->prefixRoutes = [];
$this->patternRoutes = [];
$this->stack = [];
}
/**
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param callable $next
* @return ResponseInterface
*/
public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response,
$next
): ResponseInterface {
$path = $this->getPath($request->getRequestTarget());
$route = $this->getStaticRoute($path);
if ($route) {
return $this->dispatch($route, $request, $response, $next);
}
$route = $this->getPrefixRoute($path);
if ($route) {
return $this->dispatch($route, $request, $response, $next);
}
// Try each of the routes.
foreach ($this->patternRoutes as $route) {
if ($route->matchesRequestTarget($path)) {
$pathVariables = $route->getPathVariables();
if ($this->pathVariablesAttributeName) {
$request = $request->withAttribute($this->pathVariablesAttributeName, $pathVariables);
} else {
foreach ($pathVariables as $name => $value) {
$request = $request->withAttribute($name, $value);
}
}
return $this->dispatch($route, $request, $response, $next);
}
}
if (!$this->continueOnNotFound) {
return $response->withStatus(404);
}
return $next($request, $response);
}
private function getPath(string $requestTarget): string
{
$queryStart = strpos($requestTarget, '?');
if ($queryStart === false) {
return $requestTarget;
}
return substr($requestTarget, 0, $queryStart);
}
private function dispatch(
callable $route,
ServerRequestInterface $request,
ResponseInterface $response,
callable $next
): ResponseInterface {
if (!$this->stack) {
return $route($request, $response, $next);
}
$stack = array_merge($this->stack, [$route]);
return $this->dispatcher->dispatch(
$stack,
$request,
$response,
$next
);
}
/**
* Register handlers and middleware with the router for a given path and
* method.
*
* $method may be:
* - A single verb ("GET"),
* - A comma-separated list of verbs ("GET,PUT,DELETE")
* - "*" to indicate any method.
*
* $target may be:
* - An exact path (e.g., "/path/")
* - A prefix path ending with "*"" ("/path/*"")
* - A URI template with variables enclosed in "{}" ("/path/{id}")
* - A regular expression ("~/cat/([0-9]+)~")
*
* $dispatchable may be:
* - An instance implementing one of these interfaces:
* - Psr\Http\Server\RequestHandlerInterface
* - Psr\Http\Server\MiddlewareInterface
* - WellRESTed\MiddlewareInterface
* - Psr\Http\Message\ResponseInterface
* - A string containing the fully qualified class name of a class
* implementing one of the interfaces listed above.
* - A callable that returns an instance implementing one of the
* interfaces listed above.
* - A callable with a signature matching the signature of
* WellRESTed\MiddlewareInterface::__invoke
* - An array containing any of the items in this list.
* @see DispatchedInterface::dispatch
*
* @param string $method HTTP method(s) to match
* @param string $target Request target or pattern to match
* @param mixed $dispatchable Handler or middleware to dispatch
* @return static
*/
public function register(string $method, string $target, $dispatchable): Router
{
$route = $this->getRouteForTarget($target);
$route->register($method, $dispatchable);
return $this;
}
/**
* Push a new middleware onto the stack.
*
* Middleware for a router runs before the middleware and handler for the
* matched route and runs only when a route matched.
*
* $middleware may be:
* - An instance implementing MiddlewareInterface
* - A string containing the fully qualified class name of a class
* implementing MiddlewareInterface
* - A callable that returns an instance implementing MiddleInterface
* - A callable matching the signature of MiddlewareInterface::dispatch
* @see DispatchedInterface::dispatch
*
* @param mixed $middleware Middleware to dispatch in sequence
* @return static
*/
public function add($middleware): Router
{
$this->stack[] = $middleware;
return $this;
}
/**
* Configure the instance to delegate to the next middleware when no route
* matches.
*
* @return static
*/
public function continueOnNotFound(): Router
{
$this->continueOnNotFound = true;
return $this;
}
private function getRouteForTarget(string $target): Route
{
if (isset($this->routes[$target])) {
$route = $this->routes[$target];
} else {
$route = $this->factory->create($target);
$this->registerRouteForTarget($route, $target);
}
return $route;
}
private function registerRouteForTarget(Route $route, string $target): void
{
// Store the route to the hash indexed by original target.
$this->routes[$target] = $route;
// Store the route to the array of routes for its type.
switch ($route->getType()) {
case Route::TYPE_STATIC:
$this->staticRoutes[$route->getTarget()] = $route;
break;
case Route::TYPE_PREFIX:
$this->prefixRoutes[rtrim($route->getTarget(), '*')] = $route;
break;
case Route::TYPE_PATTERN:
$this->patternRoutes[] = $route;
break;
}
}
private function getStaticRoute(string $requestTarget): ?Route
{
if (isset($this->staticRoutes[$requestTarget])) {
return $this->staticRoutes[$requestTarget];
}
return null;
}
private function getPrefixRoute(string $requestTarget): ?Route
{
// Find all prefixes that match the start of this path.
$prefixes = array_keys($this->prefixRoutes);
$matches = array_filter(
$prefixes,
function ($prefix) use ($requestTarget) {
return $this->startsWith($requestTarget, $prefix);
}
);
if (!$matches) {
return null;
}
// If there are multiple matches, sort them to find the one with the
// longest string length.
if (count($matches) > 1) {
$compareByLength = function (string $a, string $b): int {
return strlen($b) - strlen($a);
};
usort($matches, $compareByLength);
}
$bestMatch = array_values($matches)[0];
return $this->prefixRoutes[$bestMatch];
}
private function startsWith(string $haystack, string $needle): bool
{
$length = strlen($needle);
return substr($haystack, 0, $length) === $needle;
}
}

171
src/Server.php Normal file
View File

@ -0,0 +1,171 @@
<?php
namespace WellRESTed;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use WellRESTed\Dispatching\Dispatcher;
use WellRESTed\Dispatching\DispatcherInterface;
use WellRESTed\Message\Response;
use WellRESTed\Message\ServerRequestMarshaller;
use WellRESTed\Routing\Router;
use WellRESTed\Transmission\Transmitter;
use WellRESTed\Transmission\TransmitterInterface;
class Server
{
/** @var mixed[] */
private $attributes = [];
/** @var DispatcherInterface */
private $dispatcher;
/** @var string|null attribute name for matched path variables */
private $pathVariablesAttributeName = null;
/** @var ServerRequestInterface|null */
private $request = null;
/** @var ResponseInterface */
private $response;
/** @var TransmitterInterface */
private $transmitter;
/** @var mixed[] List array of middleware */
private $stack;
public function __construct()
{
$this->stack = [];
$this->response = new Response();
$this->dispatcher = new Dispatcher();
$this->transmitter = new Transmitter();
}
/**
* Push a new middleware onto the stack.
*
* @param mixed $middleware Middleware to dispatch in sequence
* @return Server
*/
public function add($middleware): Server
{
$this->stack[] = $middleware;
return $this;
}
/**
* Return a new Router that uses the server's configuration.
*
* @return Router
*/
public function createRouter(): Router
{
return new Router(
$this->pathVariablesAttributeName,
$this->dispatcher
);
}
/**
* Perform the request-response cycle.
*
* This method reads a server request, dispatches the request through the
* server's stack of middleware, and outputs the response via a Transmitter.
*/
public function respond(): void
{
$request = $this->getRequest();
foreach ($this->attributes as $name => $value) {
$request = $request->withAttribute($name, $value);
}
$next = function (
ServerRequestInterface $rqst,
ResponseInterface $resp
): ResponseInterface {
return $resp;
};
$response = $this->response;
$response = $this->dispatcher->dispatch(
$this->stack,
$request,
$response,
$next
);
$this->transmitter->transmit($request, $response);
}
// -------------------------------------------------------------------------
/* Configuration */
/**
* @param array $attributes
* @return Server
*/
public function setAttributes(array $attributes): Server
{
$this->attributes = $attributes;
return $this;
}
/**
* @param DispatcherInterface $dispatcher
* @return Server
*/
public function setDispatcher(DispatcherInterface $dispatcher): Server
{
$this->dispatcher = $dispatcher;
return $this;
}
/**
* @param string $name
* @return Server
*/
public function setPathVariablesAttributeName(string $name): Server
{
$this->pathVariablesAttributeName = $name;
return $this;
}
/**
* @param ServerRequestInterface $request
* @return Server
*/
public function setRequest(ServerRequestInterface $request): Server
{
$this->request = $request;
return $this;
}
/**
* @param ResponseInterface $response
* @return Server
*/
public function setResponse(ResponseInterface $response): Server
{
$this->response = $response;
return $this;
}
/**
* @param TransmitterInterface $transmitter
* @return Server
*/
public function setTransmitter(TransmitterInterface $transmitter): Server
{
$this->transmitter = $transmitter;
return $this;
}
// -------------------------------------------------------------------------
/* Defaults */
private function getRequest(): ServerRequestInterface
{
if (!$this->request) {
$marshaller = new ServerRequestMarshaller();
return $marshaller->getServerRequest();
}
return $this->request;
}
}

View File

@ -0,0 +1,104 @@
<?php
namespace WellRESTed\Transmission;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
class Transmitter implements TransmitterInterface
{
/** @var int */
private $chunkSize = 8192;
/**
* Outputs a response to the client.
*
* This method outputs the status line, headers, and body to the client.
*
* This method will also provide a Content-length header if:
* - Response does not have a Content-length header
* - Response does not have a Transfer-encoding: chunked header
* - Response body stream is readable and reports a non-null size
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response Response to output
*/
public function transmit(
ServerRequestInterface $request,
ResponseInterface $response
): void {
// Prepare the response for output.
$response = $this->prepareResponse($request, $response);
// Status Line
header($this->getStatusLine($response));
// Headers
foreach ($response->getHeaders() as $key => $headers) {
$replace = true;
foreach ($headers as $header) {
header("$key: $header", $replace);
$replace = false;
}
}
// Body
$body = $response->getBody();
if ($body->isReadable()) {
$this->outputBody($response->getBody());
}
}
public function setChunkSize(int $chunkSize): void
{
$this->chunkSize = $chunkSize;
}
private function prepareResponse(
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface {
// Add Content-length header to the response when all of these are true:
//
// - Response does not have a Content-length header
// - Response does not have a Transfer-encoding: chunked header
// - Response body stream is readable and reports a non-null size
//
$contentLengthMissing = !$response->hasHeader('Content-length');
$notChunked = strtolower($response->getHeaderLine('Transfer-encoding'))
!== 'chunked';
$size = $response->getBody()->getSize();
if ($contentLengthMissing && $notChunked && $size !== null) {
$response = $response->withHeader('Content-length', (string) $size);
}
return $response;
}
private function getStatusLine(ResponseInterface $response): string
{
$protocol = $response->getProtocolVersion();
$statusCode = $response->getStatusCode();
$reasonPhrase = $response->getReasonPhrase();
if ($reasonPhrase) {
return "HTTP/$protocol $statusCode $reasonPhrase";
} else {
return "HTTP/$protocol $statusCode";
}
}
private function outputBody(StreamInterface $body): void
{
if ($this->chunkSize > 0) {
if ($body->isSeekable()) {
$body->rewind();
}
while (!$body->eof()) {
print $body->read($this->chunkSize);
}
} else {
print (string) $body;
}
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace WellRESTed\Transmission;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
interface TransmitterInterface
{
/**
* Outputs a response to the client.
*
* This method MUST output the status line, headers, and body to the client.
*
* Implementations MAY add response headers to ensure expected headers are
* presents but MUST NOT alter existing headers.
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response Response to output
*/
public function transmit(ServerRequestInterface $request, ResponseInterface $response): void;
}

View File

@ -1,131 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\Client
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed;
use pjdietz\WellRESTed\Exceptions\CurlException;
use pjdietz\WellRESTed\Interfaces\RequestInterface;
use pjdietz\WellRESTed\Interfaces\ResponseInterface;
/**
* Class for making HTTP requests using cURL.
*/
class Client
{
/** @var array cURL options */
private $curlOpts;
/**
* Create a new client.
*
* You may optionally provide an array of cURL options to use by default.
* Options passed in the requset method will override these.
*
* @param array $curlOpts Optional array of cURL options
*/
public function __construct(array $curlOpts = null)
{
if (is_array($curlOpts)) {
$this->curlOpts = $curlOpts;
} else {
$this->curlOpts = array();
}
}
/**
* Make an HTTP request and return a Response.
*
* @param RequestInterface $rqst
* @param array $curlOpts Optional array of cURL options
* @throws \pjdietz\WellRESTed\Exceptions\CurlException
* @return ResponseInterface
*/
public function request(RequestInterface $rqst, $curlOpts = null)
{
$ch = curl_init();
$headers = array();
foreach ($rqst->getHeaders() as $field => $value) {
$headers[] = sprintf('%s: %s', $field, $value);
}
$options = $this->curlOpts;
$options[CURLOPT_URL] = $rqst->getUri();
$options[CURLOPT_PORT] = $rqst->getPort();
$options[CURLOPT_HEADER] = 1;
$options[CURLOPT_RETURNTRANSFER] = 1;
$options[CURLOPT_HTTPHEADER] = $headers;
// Set the method. Include the body, if needed.
switch ($rqst->getMethod()) {
case 'GET':
$options[CURLOPT_HTTPGET] = 1;
break;
case 'POST':
$options[CURLOPT_POST] = 1;
$options[CURLOPT_POSTFIELDS] = $rqst->getBody();
break;
case 'PUT':
$options[CURLOPT_CUSTOMREQUEST] = 'PUT';
$options[CURLOPT_POSTFIELDS] = $rqst->getBody();
break;
default:
$options[CURLOPT_CUSTOMREQUEST] = $rqst->getMethod();
$options[CURLOPT_POSTFIELDS] = $rqst->getBody();
break;
}
// Override cURL options with the user options passed in.
if ($curlOpts) {
foreach ($curlOpts as $optKey => $optValue) {
$options[$optKey] = $optValue;
}
}
// Set the cURL options.
curl_setopt_array($ch, $options);
// Make the cURL request.
$result = curl_exec($ch);
// Throw an exception in the event of a cURL error.
if ($result === false) {
$error = curl_error($ch);
$errno = curl_errno($ch);
curl_close($ch);
throw new CurlException($error, $errno);
}
// Make a reponse to populate and return with data obtained via cURL.
$resp = new Response();
$resp->setStatusCode(curl_getinfo($ch, CURLINFO_HTTP_CODE));
// Split the result into headers and body.
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = substr($result, 0, $headerSize);
$body = substr($result, $headerSize);
// Set the body. Do not auto-add the Content-length header.
$resp->setBody($body, false);
// Iterate over the headers line by line and add each one.
foreach (explode("\r\n", $headers) as $header) {
if (strpos($header, ':')) {
list ($headerName, $headerValue) = explode(':', $header, 2);
$resp->setHeader($headerName, ltrim($headerValue));
}
}
curl_close($ch);
return $resp;
}
}

View File

@ -1,19 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\Exceptions\CurlException
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed\Exceptions;
/**
* Exception related to a cURL operation. The message and code should correspond
* to the cURL error and error number that caused the excpetion.
*/
class CurlException extends WellRESTedException
{
}

View File

@ -1,86 +0,0 @@
<?php
/**
* HttpException and its subclasses provides exceptions that correspond to HTTP
* error status codes. The most common are included, but you may create
* additional subclasses if needed by subclassing HttpException.
*
* The HttpException classes are intended to be used with Handler subclasses
* (pjdietz\WellRESTed\Handler). Handler::getResponse() catches HttpException
* exceptions and converts them to responses using the exception's code as the
* HTTP status code and the exception's message as the response body.
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed\Exceptions\HttpExceptions;
use pjdietz\WellRESTed\Exceptions\WellRESTedException;
/**
* Base exception for HTTP-related errors. Also represents a 500 Internal Server error.
*/
class HttpException extends WellRESTedException
{
/** @var int HTTP Status Code */
protected $code = 500;
/** @var string Default description for the error */
protected $message = "500 Internal Server Error";
}
/**
* Represents a 400 Bad Request error.
*/
class BadRequestException extends HttpException
{
/** @var int HTTP Status Code */
protected $code = 400;
/** @var string Default description for the error */
protected $message = "400 Bad Request";
}
/**
* Represents a 401 Unauthorization error.
*/
class UnauthorizedException extends HttpException
{
/** @var int HTTP Status Code */
protected $code = 401;
/** @var string Default description for the error */
protected $message = "401 Unauthorized";
}
/**
* Represents a 403 Forbidden error.
*/
class ForbiddenException extends HttpException
{
/** @var int HTTP Status Code */
protected $code = 403;
/** @var string Default description for the error */
protected $message = "403 Forbidden";
}
/**
* Represents a 404 Not Found error.
*/
class NotFoundException extends HttpException
{
/** @var int HTTP Status Code */
protected $code = 404;
/** @var string Default description for the error */
protected $message = "404 Not Found";
}
/**
* Represents a 409 Conflict error.
*/
class ConflictException extends HttpException
{
/** @var int HTTP Status Code */
protected $code = 409;
/** @var string Default description for the error */
protected $message = "409 Conflict";
}

View File

@ -1,18 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\Exceptions\ParseException
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed\Exceptions;
/**
* Exception related to a parsing data.
*/
class ParseException extends WellRESTedException
{
}

View File

@ -1,20 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\Exceptions\WellRESTedException
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2013 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed\Exceptions;
use Exception;
/**
* Top level class for custom exceptions thrown by Well RESTed.
*/
class WellRESTedException extends Exception
{
}

View File

@ -1,202 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\Handler
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed;
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
use pjdietz\WellRESTed\Interfaces\RequestInterface;
use pjdietz\WellRESTed\Interfaces\ResponseInterface;
/**
* Responds to a request based on the HTTP method.
*
* To use Handler, create a subclass and implement the methods for any HTTP
* verbs you would like to support. (get() for GET, post() for POST, etc).
* <br /><br />
* - Access the request via the protected member $this->request<br />
* - Access a map of arguments via $this->args (e.g., URI path variables)<br />
* - Modify $this->response to provide the response the instance will return<br />
*/
abstract class Handler implements HandlerInterface
{
/** @var array Map of variables to supplement the request, usually path variables. */
protected $args;
/** @var RequestInterface The HTTP request to respond to. */
protected $request;
/** @var ResponseInterface The HTTP response to send based on the request. */
protected $response;
/**
* Return the handled response.
*
* @param RequestInterface $request
* @param array|null $args
* @return ResponseInterface
*/
public function getResponse(RequestInterface $request, array $args = null)
{
$this->request = $request;
$this->args = $args;
$this->response = new Response();
$this->buildResponse();
return $this->response;
}
/**
* Prepare the Response. Override this method if your subclass needs to
* repond to any non-standard HTTP methods. Otherwise, override the
* get, post, put, etc. methods.
*
* An uncaught HttpException (or subclass) will be converted to a response
* using the exception's code as the status code and the exceptios message
* as the body.
*/
protected function buildResponse()
{
switch ($this->request->getMethod()) {
case 'GET':
$this->get();
break;
case 'HEAD':
$this->head();
break;
case 'POST':
$this->post();
break;
case 'PUT':
$this->put();
break;
case 'DELETE':
$this->delete();
break;
case 'PATCH':
$this->patch();
break;
case 'OPTIONS':
$this->options();
break;
default:
$this->respondWithMethodNotAllowed();
}
}
/**
* Method for handling HTTP GET requests.
*
* This method should modify the instance's response member.
*/
protected function get()
{
$this->respondWithMethodNotAllowed();
}
/**
* Method for handling HTTP HEAD requests.
*
* This method should modify the instance's response member.
*/
protected function head()
{
// The default function calls the instance's get() method, then sets
// the resonse's body member to an empty string.
$this->get();
$this->response->setBody('', false);
}
/**
* Method for handling HTTP POST requests.
*
* This method should modify the instance's response member.
*/
protected function post()
{
$this->respondWithMethodNotAllowed();
}
/**
* Method for handling HTTP PUT requests.
*
* This method should modify the instance's response member.
*/
protected function put()
{
$this->respondWithMethodNotAllowed();
}
/**
* Method for handling HTTP DELETE requests.
*
* This method should modify the instance's response member.
*/
protected function delete()
{
$this->respondWithMethodNotAllowed();
}
/**
* Method for handling HTTP PATCH requests.
*
* This method should modify the instance's response member.
*/
protected function patch()
{
$this->respondWithMethodNotAllowed();
}
/**
* Method for handling HTTP OPTION requests.
*
* This method should modify the instance's response member.
*/
protected function options()
{
if ($this->addAllowHeader()) {
$this->response->setStatusCode(200);
} else {
$this->response->setStatusCode(405);
}
}
/**
* Provide a default response for unsupported methods.
*/
protected function respondWithMethodNotAllowed()
{
$this->response->setStatusCode(405);
$this->addAllowHeader();
}
/**
* Return an array of HTTP verbs this handler supports.
*
* For example, to support GET and POST, return array("GET","POST");
*
* @return array
*/
protected function getAllowedMethods()
{
}
/**
* Add an Allow: header using the methods returned by getAllowedMethods()
*
* @return bool The header was added.
*/
protected function addAllowHeader()
{
$methods = $this->getAllowedMethods();
if ($methods) {
$this->response->setHeader('Allow', join($methods, ', '));
return true;
}
return false;
}
}

View File

@ -1,26 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\Interfaces\HandlerInterface
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed\Interfaces;
/**
* Provides a mechanism for obtaining a response given a request.
*/
interface HandlerInterface
{
/**
* Return the handled response.
*
* @param RequestInterface $request The request to respond to.
* @param array|null $args Optional additional arguments.
* @return ResponseInterface The handled response.
*/
public function getResponse(RequestInterface $request, array $args = null);
}

View File

@ -1,84 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\Interfaces\RequestInterface
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed\Interfaces;
/**
* Interface for representing an HTTP request.
*/
interface RequestInterface
{
/**
* Return the HTTP verb (e.g., GET, POST, PUT).
*
* @return string Request verb
*/
public function getMethod();
/**
* Return the full URI for the request.
*
* @return string
*/
public function getUri();
/**
* Return the hostname portion of the URI
*
* @return string
*/
public function getHostname();
/**
* Return path component of the request URI.
*
* @return string Path component
*/
public function getPath();
/**
* Return the HTTP port
*
* @return int
*/
public function getPort();
/**
* Return an associative array of query paramters.
*
* @return array Query paramters
*/
public function getQuery();
/**
* Return the value for a given header.
*
* Per RFC 2616, HTTP headers are case-insensitive. Take care to conform to
* this when implementing.
*
* @param string $headerName Field name of the header
* @return string Header field value
*/
public function getHeader($headerName);
/**
* Return an associative array of headers.
*
* @return array
*/
public function getHeaders();
/**
* Return the body of the request.
*
* @return string Request body
*/
public function getBody();
}

View File

@ -1,70 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\Interfaces\ResponseInterface
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed\Interfaces;
/**
* Interface for representing an HTTP response.
*/
interface ResponseInterface
{
/**
* Return the HTTP status code
*
* @return int HTTP status code
*/
public function getStatusCode();
/**
* Set the status code for the response.
*
* @param int $statusCode HTTP status code
*/
public function setStatusCode($statusCode);
/**
* Return the value for a given header.
*
* Per RFC 2616, HTTP headers are case-insensitive. Take care to conform to
* this when implementing.
*
* @param string $headerName Field name of the header
* @return string Header field value
*/
public function getHeader($headerName);
/**
* Set the value for a given header.
*
* Per RFC 2616, HTTP headers are case-insensitive. Take care to conform to
* this when implementing.
*
* @param string $headerName Field name
* @param string $headerValue Field value
*/
public function setHeader($headerName, $headerValue);
/**
* Return the body of the response.
*
* @return string Response body
*/
public function getBody();
/**
* Set the body of the response.
*
* @param string $body Response body
*/
public function setBody($body);
/** Issue the reponse to the client. */
public function respond();
}

View File

@ -1,150 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\Message
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed;
/**
* Common base class for the Request and Response classes.
*/
abstract class Message
{
/** @var string Entity body of the message */
protected $body;
/** @var array Associative array of HTTP headers */
protected $headers;
/**
* Associative array of lowercase header field names as keys with
* corresponding case sensitive field names from the $headers array as
* values.
*
* @var array
*/
protected $headerLookup;
// -------------------------------------------------------------------------
/**
* Create a new HTTP message.
*/
public function __construct()
{
$this->headers = array();
$this->headerLookup = array();
}
// -------------------------------------------------------------------------
// Accessors
/**
* Return the body payload of the instance.
*
* @return string
*/
public function getBody()
{
return $this->body;
}
/**
* Set the body for the request.
*
* @param string $body
*/
public function setBody($body)
{
$this->body = $body;
}
/**
* Return an associative array of all set headers.
*
* @return array
*/
public function getHeaders()
{
return $this->headers;
}
/**
* Return the value of a given header, or false if it does not exist.
*
* @param string $name
* @return string|null
*/
public function getHeader($name)
{
$lowerName = strtolower($name);
if (isset($this->headerLookup[$lowerName])) {
$realName = $this->headerLookup[$lowerName];
return $this->headers[$realName];
}
return null;
}
/**
* Add or update a header to a given value
*
* @param string $name
* @param $value
* @param string $value
*/
public function setHeader($name, $value)
{
$lowerName = strtolower($name);
// Check if a mapping already exists for this header.
// Remove it, if needed.
if (isset($this->headerLookup[$lowerName])
&& $this->headerLookup[$lowerName] !== $name
) {
unset($this->headers[$this->headerLookup[$lowerName]]);
}
// Store the actual header.
$this->headers[$name] = $value;
// Store a mapping to the user's prefered case.
$this->headerLookup[$lowerName] = $name;
}
/**
* Return if the response contains a header with the given key.
*
* @param $name
* @return bool
*/
public function issetHeader($name)
{
$lowerName = strtolower($name);
return isset($this->headerLookup[$lowerName]);
}
/**
* Remove a header. This method does nothing if the header does not exist.
*
* @param string $name
*/
public function unsetHeader($name)
{
$lowerName = strtolower($name);
if (isset($this->headerLookup[$lowerName])) {
$realName = $this->headerLookup[$lowerName];
if (isset($this->headers[$realName])) {
unset($this->headers[$realName]);
}
unset($this->headerLookup[$lowerName]);
}
}
}

View File

@ -1,372 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\Request
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed;
use InvalidArgumentException;
use pjdietz\WellRESTed\Interfaces\RequestInterface;
use UnexpectedValueException;
/**
* A Request instance represents an HTTP request. This class has two main uses:
*
* First, you can access a singleton instance via the getRequest() method that
* represents the request sent to the server. The instance will contain the URI,
* headers, body, etc.
*
* Second, you can create a custom Request and use it to obtain a Response
* from a server through cURL.
*/
class Request extends Message implements RequestInterface
{
/**
* Singleton instance derived from reading info from Apache.
*
* @var Request
* @static
*/
static protected $theRequest;
/** @var string HTTP method or verb for the request */
private $method = "GET";
/** @var string Scheme for the request (Must be "http" or "https" */
private $scheme;
/** @var string The Hostname for the request (e.g., www.google.com) */
private $hostname = "localhost";
/** @var string Path component of the URI for the request */
private $path = "/";
/** @var array Array of fragments of the path, delimited by slashes */
private $pathParts;
/** @var int HTTP Port */
private $port = 80;
/** @var array Associative array of query parameters */
private $query;
// -------------------------------------------------------------------------
/**
* Create a new Request instance.
*
* @param string|null $uri
* @param string $method
*/
public function __construct($uri = null, $method = "GET")
{
parent::__construct();
if (!is_null($uri)) {
$this->setUri($uri);
}
$this->method = $method;
}
// -------------------------------------------------------------------------
/**
* Return a reference to the singleton instance of the Request derived
* from the server's information about the request sent to the script.
*
* @return Request
* @static
*/
public static function getRequest()
{
if (!isset(self::$theRequest)) {
$request = new Request();
$request->readHttpRequest();
self::$theRequest = $request;
}
return self::$theRequest;
}
/**
* Read and return all request headers from the request issued to the server.
*
* @return array Associative array of headers
*/
public static function getRequestHeaders()
{
// Prefer apache_request_headers is available.
if (function_exists('apache_request_headers')) {
return apache_request_headers();
}
// http://www.php.net/manual/en/function.getallheaders.php#84262
$headers = array();
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) === "HTTP_") {
$headers[str_replace(" ", "-", ucwords(strtolower(str_replace("_", " ", substr($name, 5)))))] = $value;
}
}
return $headers;
}
/**
* Set instance members based on the HTTP request sent to the server.
*/
public function readHttpRequest()
{
$this->setBody(file_get_contents("php://input"), false);
$this->headers = self::getRequestHeaders();
// Add case insensitive headers to the lookup table.
foreach ($this->headers as $key => $value) {
$this->headerLookup[strtolower($key)] = $key;
}
$this->setMethod($_SERVER["REQUEST_METHOD"]);
$this->setUri($_SERVER["REQUEST_URI"]);
$this->setHostname($_SERVER["HTTP_HOST"]);
}
/**
* Return the method (e.g., GET, POST, PUT, DELETE)
*
* @return string
*/
public function getMethod()
{
return $this->method;
}
/**
* Assign the method (e.g., GET, POST, PUT, DELETE)
*
* @param string $method
*/
public function setMethod($method)
{
$this->method = $method;
}
/**
* Return the full URI includeing protocol, hostname, path, and query.
*
* @return array
*/
public function getUri()
{
$uri = $this->scheme . "://" . $this->hostname;
if ($this->port !== $this->getDefaultPort()) {
$uri .= ":" . $this->port;
}
if ($this->path !== "/") {
$uri .= $this->path;
}
if ($this->query) {
$uri .= "?" . http_build_query($this->query);
}
return $uri;
}
/**
* Set the URI for the Request. This sets the other members: hostname,
* path, port, and query.
*
* @param string $uri
*/
public function setUri($uri)
{
// Provide http and localhost if missing.
if ($uri[0] === "/") {
$uri = "http://localhost" . $uri;
} elseif (strpos($uri, "://") === false) {
$uri = "http://" . $uri;
}
$parsed = parse_url($uri);
$scheme = isset($parsed["scheme"]) ? $parsed["scheme"] : "http";
$this->setScheme($scheme);
$host = isset($parsed["host"]) ? $parsed["host"] : "localhost";
$this->setHostname($host);
$port = isset($parsed["port"]) ? (int) $parsed["port"] : $this->getDefaultPort();
$this->setPort($port);
$path = isset($parsed["path"]) ? $parsed["path"] : "/";
$this->setPath($path);
$query = isset($parsed["query"]) ? $parsed["query"] : "";
$this->setQuery($query);
}
/**
* Return the hostname portion of the URI
*
* @return string
*/
public function getHostname()
{
return $this->hostname;
}
/**
* Assign the hostname portion of the URI
*
* @param string $hostname
*/
public function setHostname($hostname)
{
$this->hostname = $hostname;
}
/**
* Set the scheme for the request (either "http" or "https")
*
* @param string $scheme
* @throws \UnexpectedValueException
*/
public function setScheme($scheme)
{
$scheme = strtolower($scheme);
if (!in_array($scheme, array("http", "https"))) {
throw new UnexpectedValueException('Scheme must be "http" or "https".');
}
$this->scheme = $scheme;
}
/**
* Return the scheme for the request (either "http" or "https")
*
* @return string
*/
public function getScheme()
{
return $this->scheme;
}
/**
* Return the path part of the URI as a string.
*
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* Set the path and pathParts members.
*
* @param string $path
*/
public function setPath($path)
{
$this->path = $path;
if ($path !== "/") {
$this->pathParts = explode("/", substr($path, 1));
} else {
$this->pathParts = array();
}
}
/**
* Return an array of the sections of the path delimited by slashes.
*
* @return array
*/
public function getPathParts()
{
return $this->pathParts;
}
/**
* Return the HTTP port
*
* @return int
*/
public function getPort()
{
return $this->port;
}
/**
* Set the HTTP port
*
* @param int $port
*/
public function setPort($port = null)
{
if (is_null($port)) {
$port = $this->getDefaultPort();
}
$this->port = $port;
}
/**
* Return an associative array representing the query.
*
* @return array
*/
public function getQuery()
{
return $this->query;
}
/**
* Set the query. The value passed can be a query string of key-value pairs
* joined by ampersands or it can be an associative array.
*
* @param string|array $query
* @throws InvalidArgumentException
*/
public function setQuery($query)
{
if (is_string($query)) {
$qs = $query;
parse_str($qs, $query);
} elseif (is_object($query)) {
$query = (array) $query;
}
if (is_array($query)) {
ksort($query);
$this->query = $query;
} else {
throw new InvalidArgumentException("Unable to parse query string.");
}
}
/**
* Return the form fields for this request.
*
* @return array
*/
public function getFormFields()
{
parse_str($this->getBody(), $fields);
return $fields;
}
/**
* Set the body by supplying an associative array of form fields.
*
* In addition, add a "Content-type: application/x-www-form-urlencoded" header
*
* @param array $fields
*/
public function setFormFields(array $fields)
{
$this->setBody(http_build_query($fields));
$this->setHeader("Content-type", "application/x-www-form-urlencoded");
}
// -------------------------------------------------------------------------
/**
* Return the default port for the currently set scheme.
*
* @return int;
*/
protected function getDefaultPort()
{
return $this->scheme === "http" ? 80 : 443;
}
}

View File

@ -1,328 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\Response
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed;
use InvalidArgumentException;
use pjdietz\WellRESTed\Interfaces\ResponseInterface;
/**
* A Response instance allows you to build an HTTP response and send it when
* finished.
*/
class Response extends Message implements ResponseInterface
{
const CHUNK_SIZE = 1048576;
/** @var string Path to a file to read and output as the body. */
private $bodyFilePath;
/**
* Text explanation of the HTTP Status Code. You only need to set this if
* you are using nonstandard status codes. Otherwise, the instance will
* set the when you update the status code.
*
* @var string
*/
private $reasonPhrase;
/** @var int HTTP status code */
private $statusCode;
/** @var string HTTP protocol and version */
private $protocol = "HTTP/1.1";
// -------------------------------------------------------------------------
/**
* Create a new Response instance, optionally passing a status code, body,
* and headers.
*
* @param int $statusCode
* @param string $body
* @param array $headers
*/
public function __construct($statusCode = 500, $body = null, $headers = null)
{
parent::__construct();
$this->setStatusCode($statusCode);
if (is_array($headers)) {
foreach ($headers as $key => $value) {
$this->setHeader($key, $value);
}
}
if (!is_null($body)) {
$this->body = $body;
}
}
// -------------------------------------------------------------------------
// Accessors
/**
* Provide a new entity body for the respone.
* This method also updates the content-length header based on the length
* of the new body string.
*
* @param string $value
* @param bool $setContentLength Automatically add a Content-length header
*/
public function setBody($value, $setContentLength = true)
{
$this->body = $value;
if ($setContentLength === true) {
$this->setHeader('Content-Length', strlen($value));
}
}
/**
* Provide the path to a file to output as the response body.
*
* @param string $bodyFilePath Filepath
*/
public function setBodyFilePath($bodyFilePath)
{
$this->bodyFilePath = $bodyFilePath;
}
/**
* Return the path to the file to output as the response body.
*
* @return string Filepath
*/
public function getBodyFilePath()
{
return $this->bodyFilePath;
}
/**
* Return the portion of the status line explaining the status.
*
* @return string
*/
public function getReasonPhrase()
{
return $this->reasonPhrase;
}
/**
* Return true for status codes in the 1xx-3xx range.
*
* @return bool
*/
public function getSuccess()
{
return $this->statusCode < 400;
}
/**
* Return the HTTP status code for the response.
*
* @return int
*/
public function getStatusCode()
{
return $this->statusCode;
}
/**
* Return the HTTP status line, e.g. HTTP/1.1 200 OK.
*
* @return string
*/
public function getStatusLine()
{
return $this->protocol . " " . $this->statusCode . " " . $this->reasonPhrase;
}
/**
* Set the status code and optionally the reason phrase explaining it.
*
* @param int $statusCode
* @param string|null $reasonPhrase
* @throws InvalidArgumentException
*/
public function setStatusCode($statusCode, $reasonPhrase = null)
{
$this->statusCode = (int) $statusCode;
if (is_null($reasonPhrase)) {
switch ($this->statusCode) {
case 100:
$text = 'Continue';
break;
case 101:
$text = 'Switching Protocols';
break;
case 200:
$text = 'OK';
break;
case 201:
$text = 'Created';
break;
case 202:
$text = 'Accepted';
break;
case 203:
$text = 'Non-Authoritative Information';
break;
case 204:
$text = 'No Content';
break;
case 205:
$text = 'Reset Content';
break;
case 206:
$text = 'Partial Content';
break;
case 300:
$text = 'Multiple Choices';
break;
case 301:
$text = 'Moved Permanently';
break;
case 302:
$text = 'Found';
break;
case 303:
$text = 'See Other';
break;
case 304:
$text = 'Not Modified';
break;
case 305:
$text = 'Use Proxy';
break;
case 400:
$text = 'Bad Request';
break;
case 401:
$text = 'Unauthorized';
break;
case 402:
$text = 'Payment Required';
break;
case 403:
$text = 'Forbidden';
break;
case 404:
$text = 'Not Found';
break;
case 405:
$text = 'Method Not Allowed';
break;
case 406:
$text = 'Not Acceptable';
break;
case 407:
$text = 'Proxy Authentication Required';
break;
case 408:
$text = 'Request Timeout';
break;
case 409:
$text = 'Conflict';
break;
case 410:
$text = 'Gone';
break;
case 411:
$text = 'Length Required';
break;
case 412:
$text = 'Precondition Failed';
break;
case 413:
$text = 'Request Entity Too Large';
break;
case 414:
$text = 'Request-URI Too Long';
break;
case 415:
$text = 'Unsupported Media Type';
break;
case 500:
$text = 'Internal Server Error';
break;
case 501:
$text = 'Not Implemented';
break;
case 502:
$text = 'Bad Gateway';
break;
case 503:
$text = 'Service Unavailable';
break;
case 504:
$text = 'Gateway Timeout';
break;
case 505:
$text = 'HTTP Version Not Supported';
break;
default:
$text = 'Nonstandard';
break;
}
$this->reasonPhrase = $text;
} else {
if (is_string($reasonPhrase)) {
$this->reasonPhrase = $reasonPhrase;
} else {
throw new InvalidArgumentException('$reasonPhrase must be a string (or null to use standard HTTP Reason-Phrase');
}
}
}
/**
* Output the response to the client.
*
* @param bool $headersOnly Do not include the body, only the headers.
*/
public function respond($headersOnly = false)
{
// Output the HTTP status code.
header($this->getStatusLine());
// Output each header.
foreach ($this->headers as $header => $value) {
header($header . ': ' . $value);
}
// Output the entity body.
if (!$headersOnly) {
if (isset($this->bodyFilePath) && $this->bodyFilePath && file_exists($this->bodyFilePath)) {
$this->outputBodyFile();
} else {
print $this->body;
}
}
}
// -------------------------------------------------------------------------
/** Output the contents of a file */
private function outputBodyFile()
{
$handle = fopen($this->getBodyFilePath(), 'rb');
if ($handle !== false) {
while (!feof($handle)) {
$buffer = fread($handle, self::CHUNK_SIZE);
print $buffer;
flush();
}
}
}
}

View File

@ -1,259 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\RouteBuilder
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed;
use pjdietz\WellRESTed\Exceptions\ParseException;
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
use pjdietz\WellRESTed\Routes\RegexRoute;
use pjdietz\WellRESTed\Routes\StaticRoute;
use pjdietz\WellRESTed\Routes\TemplateRoute;
/**
* Class for facilitating constructing Routers.
*/
class RouteBuilder
{
/** @var string Regex pattern to use for URI template patters. */
private $defaultVariablePattern;
/** @var string Common prefix to affix to handler class names. */
private $handlerNamespace;
/** @var array Associative array of variable names and regex patterns. */
private $templateVariablePatterns;
/**
* Contruct and return an array of routes.
*
* If $data is a string, buildRoutes() will parse it as JSON with json_decode.
* <br /><br />
* If $data is an array, buildRoutes() assumes each item in the array is
* an object it can translate into a route.
* <br /><br />
* If $data is an object, buildRoutes() assumes it will have a "routes"
* property with an array value that is a collection of objects to
* translate into routes. Any other properties will be read with
* readConfiguration()
*
* @param string|array|object $data Description of routes to build.
* @return array List of routes to add to a router.
* @throws Exceptions\ParseException
*/
public function buildRoutes($data)
{
// If $data is a string, attempt to parse it as JSON.
if (is_string($data)) {
$data = json_decode($data);
if (is_null($data)) {
throw new ParseException("Unable to parse as JSON.");
}
}
// Locate the list of routes. This should be one of these:
// - If $data is an object, $data->routes
// - If $data is an array, $data
if (is_array($data)) {
$dataRoutes = $data;
} elseif (is_object($data) && isset($data->routes) && is_array($data->routes)) {
$dataRoutes = $data->routes;
$this->readConfiguration($data);
} else {
throw new ParseException("Unable to parse. Missing array of routes.");
}
// Build a route instance and append it to the list.
$routes = array();
foreach ($dataRoutes as $item) {
$routes[] = $this->buildRoute($item);
}
return $routes;
}
/**
* Parse an object and update the instances with the new configuration.
*
* ->handlerNamespace is passed to setHandlerNamesapce()
* <br /></br />
* ->variablePattern is passed to setDefaultVariablePattern()
* <br /><br />
* ->vars is passed to setTemplateVars()
*
* @param object
*/
public function readConfiguration($data)
{
if (isset($data->handlerNamespace)) {
$this->setHandlerNamespace($data->handlerNamespace);
}
if (isset($data->variablePattern)) {
$this->setDefaultVariablePattern($data->variablePattern);
}
if (isset($data->vars)) {
$this->setTemplateVars((array) $data->vars);
}
}
/**
* Return the string to prepend to handler class names.
*
* @return string
*/
public function getHandlerNamespace()
{
return $this->handlerNamespace;
}
/**
* Set the prefix to prepend to handler class names.
*
* @param mixed $handlerNamespace
*/
public function setHandlerNamespace($handlerNamespace = "")
{
$this->handlerNamespace = $handlerNamespace;
}
/**
* Return an associative array of variable names and regex patterns.
*
* @return mixed
*/
public function getTemplateVars()
{
return $this->templateVariablePatterns;
}
/**
* Set the array of template variable patterns.
*
* Keys are names of variables for use in URI template (do not include {}).
* Values are regex patterns or any of the following special names: SLUG,
* ALPHA, ALPHANUM, DIGIT, NUM.
* <br /><br />
* If you wish to use additional named patterns, subclass RouteBuilder and
* override getTemplateVariablePattern.
*
* @param array $vars Associative array of variable name => pattern
*/
public function setTemplateVars(array $vars)
{
foreach ($vars as $name => $var) {
$vars[$name] = $this->getTemplateVariablePattern($var);
}
$this->templateVariablePatterns = $vars;
}
/**
* Return the default regex pattern to use for URI template variables.
*
* @return string
*/
public function getDefaultVariablePattern()
{
return $this->defaultVariablePattern;
}
/**
* Set the default regex pattern to use for URI template variables.
*
* $defaultVariablePattern may be a regex pattern or one of the following:
* SLUG, ALPHA, ALPHANUM, DIGIT, NUM.
* <br /><br />
* If you wish to use additional named patterns, subclass RouteBuilder and
* override getTemplateVariablePattern.
*
* @param mixed $defaultVariablePattern
*/
public function setDefaultVariablePattern($defaultVariablePattern)
{
$this->defaultVariablePattern = $this->getTemplateVariablePattern($defaultVariablePattern);
}
/**
* Create and return an appropriate route given an object describing a route.
*
* $item must contain a "handler" property providing the classname for the
* HandlerInterface to call getResponse() on if the route matches. "handler"
* may be fully qualified and begin with "\". If it does not begin with "\",
* the instance's $handlerNamespace is affixed to the begining.
* <br /><br />
* $item must also contain a "path", "template", or "pattern" property to
* indicate how to create the StaticRoute, TemplateRoute, or RegexRoute.
*
* @param object|array $item
* @return HandlerInterface
* @throws Exceptions\ParseException
*/
protected function buildRoute($item)
{
// Determine the handler for this route.
if (isset($item->handler)) {
$handler = $item->handler;
if ($handler[0] != "\\") {
$handler = $this->getHandlerNamespace() . "\\" . $handler;
}
} else {
throw new ParseException("Unable to parse. Route is missing a handler.");
}
// Static Route
if (isset($item->path)) {
return new StaticRoute($item->path, $handler);
}
// Template Route
if (isset($item->template)) {
$vars = isset($item->vars) ? (array) $item->vars : array();
foreach ($vars as $name => $var) {
$vars[$name] = $this->getTemplateVariablePattern($var);
}
if ($this->templateVariablePatterns) {
$vars = array_merge($this->templateVariablePatterns, $vars);
}
return new TemplateRoute($item->template, $handler, $this->getDefaultVariablePattern(), $vars);
}
// Regex Route
if (isset($item->pattern)) {
return new RegexRoute($item->pattern, $handler);
}
return null;
}
/**
* Provide a regular expression pattern given a name.
*
* The names SLUG, ALPHA, ALPHANUM, DIGIT, NUM convert to regex patterns.
* Anything else passes through as is.
* <br /><br />
* If you wish to use additional named patterns, subclass RouteBuilder and
* override getTemplateVariablePattern.
*
* @param string $variable Regex pattern or name (SLUG, ALPHA, ALPHANUM, DIGIT, NUM
* @return string
*/
protected function getTemplateVariablePattern($variable)
{
switch ($variable) {
case "SLUG":
return TemplateRoute::RE_SLUG;
case "ALPHA":
return TemplateRoute::RE_ALPHA;
case "ALPHANUM":
return TemplateRoute::RE_ALPHANUM;
case "DIGIT":
case "NUM":
return TemplateRoute::RE_NUM;
default:
return $variable;
}
}
}

View File

@ -1,150 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\Router
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed;
use pjdietz\WellRESTed\Exceptions\HttpExceptions\HttpException;
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
use pjdietz\WellRESTed\Interfaces\RequestInterface;
use pjdietz\WellRESTed\Interfaces\ResponseInterface;
/**
* Router
*
* A Router uses a table of Routes to find the appropriate Handler for a request.
*/
class Router implements HandlerInterface
{
/** @var array Array of Route objects */
private $routes;
/** @var array Hash array of status code => qualified HandlerInterface names for error handling. */
private $errorHandlers;
/** Create a new Router. */
public function __construct()
{
$this->routes = array();
$this->errorHandlers = array();
}
/**
* Return the response built by the handler based on the request
*
* @param RequestInterface $request
* @param array|null $args
* @return ResponseInterface
*/
public function getResponse(RequestInterface $request, array $args = null)
{
foreach ($this->routes as $route) {
/** @var HandlerInterface $route */
try {
$response = $route->getResponse($request, $args);
} catch (HttpException $e) {
$response = new Response();
$response->setStatusCode($e->getCode());
$response->setBody($e->getMessage());
}
if ($response) {
// Check if the router has an error handler for this status code.
$status = $response->getStatusCode();
if (array_key_exists($status, $this->errorHandlers)) {
/** @var HandlerInterface $errorHandler */
$errorHandler = new $this->errorHandlers[$status]();
// Pass the response triggering this along to the error handler.
$errorArgs = array("response" => $response);
if ($args) {
$errorArgs = array_merge($args, $errorArgs);
}
return $errorHandler->getResponse($request, $errorArgs);
}
return $response;
}
}
return null;
}
/**
* Append a new route to the route route table.
*
* @param HandlerInterface $route
*/
public function addRoute(HandlerInterface $route)
{
$this->routes[] = $route;
}
/**
* Append a series of routes.
*
* @param array $routes List array of HandlerInterface instances
*/
public function addRoutes(array $routes)
{
foreach ($routes as $route) {
if ($route instanceof HandlerInterface) {
$this->addRoute($route);
}
}
}
/**
* Add a custom error handler.
*
* @param integer $statusCode The error status code.
* @param string $errorHandler Fully qualified name to an autoloadable handler class.
*/
public function setErrorHandler($statusCode, $errorHandler)
{
$this->errorHandlers[$statusCode] = $errorHandler;
}
/**
* Add custom error handlers.
*
* @param array $errorHandlers Array mapping integer error codes to qualified handler names.
*/
public function setErrorHandlers(array $errorHandlers)
{
foreach ($errorHandlers as $statusCode => $errorHandler) {
$this->setErrorHandler($statusCode, $errorHandler);
}
}
/**
* Dispatch the singleton Request through the router and output the response.
*
* Respond with a 404 Not Found if no route provides a response.
* @param array|null $args
*/
public function respond($args = null)
{
$request = Request::getRequest();
$response = $this->getResponse($request, $args);
if (!$response) {
$response = $this->getNoRouteResponse($request);
}
$response->respond();
}
/**
* Prepare a response indicating a 404 Not Found error
*
* @param RequestInterface $request
* @return ResponseInterface
*/
protected function getNoRouteResponse(RequestInterface $request)
{
$response = new Response(404);
$response->setBody('No resource at ' . $request->getPath());
return $response;
}
}

View File

@ -1,53 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\BaseRoute
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed\Routes;
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
/**
* Base class for Routes.
*/
abstract class BaseRoute implements HandlerInterface
{
/** @var string Fully qualified name for the interface for handlers */
const HANDLER_INTERFACE = '\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface';
/** @var string Fully qualified classname of the HandlerInterface to dispatch */
private $targetClassName;
/**
* Create a new route that will dispatch an instance of the given handelr class.
*
* @param string $targetClassName Fully qualified name to a handler class.
*/
public function __construct($targetClassName)
{
$this->targetClassName = $targetClassName;
}
/**
* Instantiate and return an instance of the assigned HandlerInterface
*
* @throws \UnexpectedValueException
* @return HandlerInterface
*/
protected function getTarget()
{
if (is_subclass_of($this->targetClassName, self::HANDLER_INTERFACE)) {
/** @var HandlerInterface $target */
$target = new $this->targetClassName();
return $target;
} else {
throw new \UnexpectedValueException("Target class must implement HandlerInterface");
}
}
}

View File

@ -1,43 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\PrefixRoute
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed\Routes;
use pjdietz\WellRESTed\Interfaces\RequestInterface;
/**
* Maps a list of static URI paths to a Handler
*/
class PrefixRoute extends StaticRoute
{
// ------------------------------------------------------------------------
/* HandlerInterface */
/**
* Return the response issued by the handler class or null.
*
* A null return value indicates that this route failed to match the request.
*
* @param RequestInterface $request
* @param array $args
* @return null|\pjdietz\WellRESTed\Interfaces\ResponseInterface
*/
public function getResponse(RequestInterface $request, array $args = null)
{
$requestPath = $request->getPath();
foreach ($this->paths as $path) {
if (substr($requestPath, 0, strlen($path)) === $path) {
$target = $this->getTarget();
return $target->getResponse($request, $args);
}
}
return null;
}
}

View File

@ -1,78 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\RegexRout
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed\Routes;
use pjdietz\WellRESTed\Exceptions\ParseException;
use pjdietz\WellRESTed\Interfaces\RequestInterface;
/**
* Maps a regular expression pattern for a URI path to a Handler
*/
class RegexRoute extends BaseRoute
{
/** @var string Regular expression pattern for the route. */
private $pattern;
/**
* Create a new route mapping a regex pattern to a handler class name.
*
* @param string $pattern Regular expression the path must match.
* @param string $targetClassName Fully qualified name to an autoloadable handler class.
*/
public function __construct($pattern, $targetClassName)
{
parent::__construct($targetClassName);
$this->pattern = $pattern;
}
// ------------------------------------------------------------------------
/* HandlerInterface */
/**
* Return the response issued by the handler class or null.
*
* A null return value indicates that this route failed to match the request.
*
* @param RequestInterface $request
* @param array $args
* @throws \pjdietz\WellRESTed\Exceptions\ParseException
* @return null|\pjdietz\WellRESTed\Interfaces\ResponseInterface
*/
public function getResponse(RequestInterface $request, array $args = null)
{
$matched = @preg_match($this->getPattern(), $request->getPath(), $matches);
if ($matched) {
$target = $this->getTarget();
if (is_null($args)) {
$args = array();
}
$args = array_merge($args, $matches);
return $target->getResponse($request, $args);
} elseif ($matched === false) {
throw new ParseException("Invalid regular expression: " . $this->getPattern());
}
return null;
}
// ------------------------------------------------------------------------
/**
* Return the regex pattern for the route.
*
* @return string Regex pattern
*/
protected function getPattern()
{
return $this->pattern;
}
}

View File

@ -1,67 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\StaticRoute
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed\Routes;
use InvalidArgumentException;
use pjdietz\WellRESTed\Interfaces\RequestInterface;
/**
* Maps a list of static URI paths to a Handler
*/
class StaticRoute extends BaseRoute
{
/** @var array List of static URI paths */
protected $paths;
/**
* Create a new StaticRoute for a given path or paths and a handler class.
*
* @param string|array $paths Path or list of paths the request must match
* @param string $targetClassName Fully qualified name to an autoloadable handler class.
* @throws \InvalidArgumentException
*/
public function __construct($paths, $targetClassName)
{
parent::__construct($targetClassName);
if (is_string($paths)) {
$this->paths = array($paths);
} elseif (is_array($paths)) {
$this->paths = $paths;
} else {
throw new InvalidArgumentException("$paths must be a string or array of string");
}
}
// ------------------------------------------------------------------------
/* HandlerInterface */
/**
* Return the response issued by the handler class or null.
*
* A null return value indicates that this route failed to match the request.
*
* @param RequestInterface $request
* @param array $args
* @return null|\pjdietz\WellRESTed\Interfaces\ResponseInterface
*/
public function getResponse(RequestInterface $request, array $args = null)
{
$requestPath = $request->getPath();
foreach ($this->paths as $path) {
if ($path === $requestPath) {
$target = $this->getTarget();
return $target->getResponse($request, $args);
}
}
return null;
}
}

View File

@ -1,124 +0,0 @@
<?php
/**
* pjdietz\WellRESTed\TemplateRoute
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed\Routes;
/**
* Maps a URI template to a Handler
*/
class TemplateRoute extends RegexRoute
{
/**
* Regular expression matching URL friendly characters (i.e., letters,
* digits, hyphen and underscore)
*/
const RE_SLUG = '[0-9a-zA-Z\-_]+';
/** Regular expression matching digitis */
const RE_NUM = '[0-9]+';
/** Regular expression matching letters */
const RE_ALPHA = '[a-zA-Z]+';
/** Regular expression matching letters and digits */
const RE_ALPHANUM = '[0-9a-zA-Z]+';
/** Regular expression matching a URI template variable (e.g., {id}) */
const URI_TEMPLATE_EXPRESSION_RE = '/{([a-zA-Z]+)}/';
/**
* Create a new route that matches a URI template to a Handler.
*
* Optionally provide patterns for the variables in the template.
*
* @param string $template URI template the path must match
* @param string $targetClassName Fully qualified name to an autoloadable handler class
* @param string $defaultPattern Regular expression for variables
* @param array|null $variablePatterns Map of variable names and regular expression
*/
public function __construct(
$template,
$targetClassName,
$defaultPattern = self::RE_SLUG,
$variablePatterns = null
) {
$pattern = $this->buildPattern($template, $defaultPattern, $variablePatterns);
parent::__construct($pattern, $targetClassName);
}
/**
* Translate the URI template into a regular expression.
*
* @param string $template URI template the path must match
* @param string $defaultPattern Regular expression for variables
* @param array $variablePatterns Map of variable names and regular expression
* @return string
*/
private function buildPattern($template, $defaultPattern, $variablePatterns)
{
if (is_null($variablePatterns)) {
$variablePatterns = array();
} elseif (is_object($variablePatterns)) {
$variablePatterns = (array) $variablePatterns;
}
if (!$defaultPattern) {
$defaultPattern = self::RE_SLUG;
}
$pattern = '';
// Explode the template into an array of path segments.
if ($template[0] === '/') {
$parts = explode('/', substr($template, 1));
} else {
$parts = explode('/', $template);
}
foreach ($parts as $part) {
$pattern .= '\/';
// Is this part an expression or a literal?
if (preg_match(self::URI_TEMPLATE_EXPRESSION_RE, $part, $matches)) {
// Locate the name for the variable from the template.
$variableName = $matches[1];
// If the caller passed an array with this variable name
// as a key, use its value for the pattern here.
// Otherwise, use the class's current default.
if (isset($variablePatterns[$variableName])) {
$variablePattern = $variablePatterns[$variableName];
} else {
$variablePattern = $defaultPattern;
}
$pattern .= sprintf(
'(?<%s>%s)',
$variableName,
$variablePattern
);
} else {
// This part is a literal.
$pattern .= $part;
}
}
$pattern = '/^' . $pattern;
if (substr($pattern, -1) === "*") {
// Allow path to include characters passed the pattern.
$pattern = rtrim($pattern, "*") . '/';
} else {
// Path must end at the end of the pattern.
$pattern .= "$/";
}
return $pattern;
}
}

View File

@ -1,24 +0,0 @@
<?php
// This file must be in the global namespace to add apache_request_headers
use pjdietz\WellRESTed\Request;
class ApacheRequestHeadersTest extends \PHPUnit_Framework_TestCase
{
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testReadApacheRequestHeaders()
{
if (!function_exists('apache_request_headers')) {
function apache_request_headers() {
return array();
}
}
$headers = Request::getRequestHeaders();
$this->assertNotNull($headers);
}
}

View File

@ -1,272 +0,0 @@
<?php
namespace pjdietz\WellRESTed\Test;
use Faker\Factory;
use pjdietz\ShamServer\ShamServer;
use pjdietz\WellRESTed\Client;
use pjdietz\WellRESTed\Request;
class ClientTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider httpMethodProvider
*/
public function testSendHttpMethod($method)
{
$host = "localhost";
$port = getenv("PORT");
$script = realpath(__DIR__ . "/sham-routers/method.php");
$server = new ShamServer($host, $port, $script);
$rqst = $this->getMockBuilder('pjdietz\WellRESTed\Interfaces\RequestInterface')->getMock();
$rqst->expects($this->any())
->method("getUri")
->will($this->returnValue("http://$host:$port"));
$rqst->expects($this->any())
->method("getMethod")
->will($this->returnValue($method));
$rqst->expects($this->any())
->method("getPort")
->will($this->returnValue($port));
$rqst->expects($this->any())
->method("getHeaders")
->will($this->returnValue(array()));
$client = new Client();
$resp = $client->request($rqst);
$body = trim($resp->getBody());
$this->assertEquals($method, $body);
$server->stop();
}
public function httpMethodProvider()
{
return [
["GET"],
["POST"],
["PUT"],
["DELETE"],
["PATCH"],
["OPTIONS"]
];
}
/**
* @dataProvider httpHeaderProvider
*/
public function testSendHttpHeaders($headerKey, $headerValue)
{
$host = "localhost";
$port = getenv("PORT");
$script = realpath(__DIR__ . "/sham-routers/headers.php");
$server = new ShamServer($host, $port, $script);
$rqst = $this->getMockBuilder('pjdietz\WellRESTed\Interfaces\RequestInterface')->getMock();
$rqst->expects($this->any())
->method("getUri")
->will($this->returnValue("http://$host:$port"));
$rqst->expects($this->any())
->method("getMethod")
->will($this->returnValue("GET"));
$rqst->expects($this->any())
->method("getPort")
->will($this->returnValue($port));
$rqst->expects($this->any())
->method("getHeaders")
->will($this->returnValue(array($headerKey => $headerValue)));
$client = new Client();
$resp = $client->request($rqst);
$headers = json_decode($resp->getBody());
$this->assertEquals($headerValue, $headers->{$headerKey});
$server->stop();
}
public function httpHeaderProvider()
{
return [
["Cache-Control", "max-age=0"],
["X-Custom-Header", "custom value"],
["Accept-Charset", "utf-8"]
];
}
/**
* @dataProvider bodyProvider
*/
public function testSendBody($body)
{
$host = "localhost";
$port = getenv("PORT");
$script = realpath(__DIR__ . "/sham-routers/body.php");
$server = new ShamServer($host, $port, $script);
$rqst = $this->getMockBuilder('pjdietz\WellRESTed\Interfaces\RequestInterface')->getMock();
$rqst->expects($this->any())
->method("getUri")
->will($this->returnValue("http://$host:$port"));
$rqst->expects($this->any())
->method("getMethod")
->will($this->returnValue("POST"));
$rqst->expects($this->any())
->method("getPort")
->will($this->returnValue($port));
$rqst->expects($this->any())
->method("getHeaders")
->will($this->returnValue(array()));
$rqst->expects($this->any())
->method("getBody")
->will($this->returnValue($body));
$client = new Client();
$resp = $client->request($rqst);
$this->assertEquals($body, $resp->getBody());
$server->stop();
}
public function bodyProvider()
{
$faker = Factory::create();
return [
[$faker->text()],
[$faker->text()],
[$faker->text()]
];
}
/**
* @dataProvider formProvider
*/
public function testSendForm($form)
{
$host = "localhost";
$port = getenv("PORT");
$script = realpath(__DIR__ . "/sham-routers/formFields.php");
$server = new ShamServer($host, $port, $script);
$rqst = new Request("http://$host:$port");
$rqst->setMethod("POST");
$rqst->setFormFields($form);
$client = new Client();
$resp = $client->request($rqst);
$body = json_decode($resp->getBody(), true);
$this->assertEquals($form, $body);
$server->stop();
}
public function formProvider()
{
$faker = Factory::create();
return [
[
[
"firstName" => $faker->firstName,
"lastName" => $faker->lastName,
"email" => $faker->email
]
],
];
}
public function testSetCustomCurlOptionsOnInstantiation()
{
$host = "localhost";
$port = getenv("PORT");
$script = realpath(__DIR__ . "/sham-routers/headers.php");
$server = new ShamServer($host, $port, $script);
$rqst = $this->getMockBuilder('pjdietz\WellRESTed\Interfaces\RequestInterface')->getMock();
$rqst->expects($this->any())
->method("getUri")
->will($this->returnValue("http://$host:$port"));
$rqst->expects($this->any())
->method("getMethod")
->will($this->returnValue("GET"));
$rqst->expects($this->any())
->method("getPort")
->will($this->returnValue($port));
$rqst->expects($this->any())
->method("getHeaders")
->will($this->returnValue(array()));
$cookieValue = "key=value";
$client = new Client([CURLOPT_COOKIE => $cookieValue]);
$resp = $client->request($rqst);
$headers = json_decode($resp->getBody());
$this->assertEquals($cookieValue, $headers->Cookie);
$server->stop();
}
public function testSetCustomCurlOptionsOnRequest()
{
$host = "localhost";
$port = getenv("PORT");
$script = realpath(__DIR__ . "/sham-routers/headers.php");
$server = new ShamServer($host, $port, $script);
$rqst = $this->getMockBuilder('pjdietz\WellRESTed\Interfaces\RequestInterface')->getMock();
$rqst->expects($this->any())
->method("getUri")
->will($this->returnValue("http://$host:$port"));
$rqst->expects($this->any())
->method("getMethod")
->will($this->returnValue("GET"));
$rqst->expects($this->any())
->method("getPort")
->will($this->returnValue($port));
$rqst->expects($this->any())
->method("getHeaders")
->will($this->returnValue(array()));
$cookieValue = "key=value";
$client = new Client();
$resp = $client->request($rqst, [CURLOPT_COOKIE => $cookieValue]);
$headers = json_decode($resp->getBody());
$this->assertEquals($cookieValue, $headers->Cookie);
$server->stop();
}
/**
* @dataProvider curlErrorProvider
* @expectedException \pjdietz\WellRESTed\Exceptions\CurlException
*/
public function testFailOnCurlError($uri, $opts)
{
$rqst = $this->getMockBuilder('pjdietz\WellRESTed\Interfaces\RequestInterface')->getMock();
$rqst->expects($this->any())
->method("getUri")
->will($this->returnValue($uri));
$rqst->expects($this->any())
->method("getMethod")
->will($this->returnValue("GET"));
$rqst->expects($this->any())
->method("getPort")
->will($this->returnValue(parse_url($uri, PHP_URL_PORT)));
$rqst->expects($this->any())
->method("getHeaders")
->will($this->returnValue(array()));
$client = new Client();
$client->request($rqst, $opts);
}
public function curlErrorProvider()
{
return [
["http://localhost:9991", [
CURLOPT_FAILONERROR, true,
CURLOPT_TIMEOUT_MS, 10
]],
];
}
}

View File

@ -1,67 +0,0 @@
<?php
namespace pjdietz\WellRESTed\Test;
use pjdietz\WellRESTed\Handler;
class HandlerTest extends \PHPUnit_Framework_TestCase
{
public function testGetResponse()
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockHandler = $this->getMockForAbstractClass('\pjdietz\WellRESTed\Handler');
/** @var \pjdietz\WellRESTed\Handler $mockHandler */
$this->assertNotNull($mockHandler->getResponse($mockRequest));
}
/**
* @dataProvider verbProvider
*/
public function testCallMethodForHttpVerb($verb)
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getMethod')
->will($this->returnValue($verb));
$mockHandler = $this->getMockForAbstractClass('\pjdietz\WellRESTed\Handler');
/** @var \pjdietz\WellRESTed\Handler $mockHandler */
$this->assertNotNull($mockHandler->getResponse($mockRequest));
}
public function verbProvider()
{
return [
["GET"],
["POST"],
["PUT"],
["DELETE"],
["HEAD"],
["PATCH"],
["OPTIONS"],
["NOTALLOWED"]
];
}
public function testReadAllowedMethods()
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getMethod')
->will($this->returnValue("OPTIONS"));
$handler = new OptionsHandler();
$resp = $handler->getResponse($mockRequest);
$this->assertEquals("GET, POST", $resp->getHeader("Allow"));
}
}
class OptionsHandler extends Handler
{
protected function getAllowedMethods()
{
return ["GET","POST"];
}
}

View File

@ -1,502 +0,0 @@
<?php
namespace pjdietz\WellRESTed\Test;
use Faker\Factory;
use pjdietz\WellRESTed\Request;
use pjdietz\WellRESTed\Test;
class RequestTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider methodProvider
*/
public function testSetMethod($method)
{
$rqst = new Request();
$rqst->setMethod($method);
$this->assertEquals($method, $rqst->getMethod());
}
public function methodProvider()
{
return array(
array("GET"),
array("POST"),
array("PUT"),
array("DELETE"),
array("OPTIONS"),
array("HEAD")
);
}
/**
* @dataProvider uriProvider
*/
public function testSetUri($uri, $data)
{
$rqst = new Request($uri);
$this->assertEquals($data->uri, $rqst->getUri());
}
/**
* @dataProvider uriProvider
*/
public function testParseSchemeFromUri($uri, $data)
{
$rqst = new Request($uri);
$this->assertEquals($data->scheme, $rqst->getScheme());
}
/**
* @dataProvider uriProvider
*/
public function testParseHostnameFromUri($uri, $data)
{
$rqst = new Request($uri);
$this->assertEquals($data->hostname, $rqst->getHostname());
}
/**
* @dataProvider uriProvider
*/
public function testParsePortFromUri($uri, $data)
{
$rqst = new Request($uri);
$this->assertEquals($data->port, $rqst->getPort());
}
/**
* @dataProvider uriProvider
*/
public function testParsePathFromUri($uri, $data)
{
$rqst = new Request($uri);
$this->assertEquals($data->path, $rqst->getPath());
}
/**
* @dataProvider uriProvider
*/
public function testParsePathPartsFromUri($uri, $data)
{
$rqst = new Request($uri);
$this->assertEquals($data->parts, $rqst->getPathParts());
}
/**
* @dataProvider uriProvider
*/
public function testParseQueryFromUri($uri, $data)
{
$rqst = new Request($uri);
$this->assertEquals($data->query, $rqst->getQuery());
}
public function uriProvider()
{
return array(
array(
"http://www.google.com",
(object) [
"uri" => "http://www.google.com",
"scheme" => "http",
"hostname" => "www.google.com",
"port" => 80,
"path" => "/",
"query" => [],
"parts" => []
]
),
array(
"https://www.google.com",
(object) [
"uri" => "https://www.google.com",
"scheme" => "https",
"hostname" => "www.google.com",
"port" => 443,
"path" => "/",
"query" => [],
"parts" => []
]
),
array(
"localhost:8080/my/path/with/parts",
(object) [
"uri" => "http://localhost:8080/my/path/with/parts",
"scheme" => "http",
"hostname" => "localhost",
"port" => 8080,
"path" => "/my/path/with/parts",
"query" => [],
"parts" => ["my", "path", "with", "parts"]
]
),
array(
"localhost?dog=bear&cat=molly",
(object) [
"uri" => "http://localhost?cat=molly&dog=bear",
"scheme" => "http",
"hostname" => "localhost",
"port" => 80,
"path" => "/",
"query" => [
"cat" => "molly",
"dog" => "bear"
],
"parts" => []
]
),
array(
"/my-page?id=2",
(object) [
"uri" => "http://localhost/my-page?id=2",
"scheme" => "http",
"hostname" => "localhost",
"port" => 80,
"path" => "/my-page",
"query" => [
"id" => "2"
],
"parts" => ["my-page"]
]
)
);
}
public function testSetBody()
{
$body = "This is the body";
$rqst = new Request();
$rqst->setBody($body);
$this->assertEquals($body, $rqst->getBody());
}
public function testBodyIsNullByDefault()
{
$rqst = new Request();
$this->assertNull($rqst->getBody());
}
/**
* @dataProvider formProvider
*/
public function testFormFieldsEncodeProperly($form)
{
$rqst = new Request();
$rqst->setFormFields($form);
$body = $rqst->getBody();
parse_str($body, $fields);
$this->assertEquals($form, $fields);
}
/**
* @dataProvider formProvider
*/
public function testFormFieldsDecodeProperly($form)
{
$rqst = new Request();
$rqst->setFormFields($form);
$fields = $rqst->getFormFields();
$this->assertEquals($form, $fields);
}
public function formProvider()
{
$faker = Factory::create();
return [
[
[
"firstName" => $faker->firstName,
"lastName" => $faker->lastName,
"username" => $faker->userName
]
]
];
}
/**
* @dataProvider headerProvider
*/
public function testSetHeader($headerKey, $headerValue, $badCapsKey)
{
$rqst = new Request();
$rqst->setHeader($headerKey, $headerValue);
$this->assertEquals($headerValue, $rqst->getHeader($badCapsKey));
}
/**
* @dataProvider headerProvider
*/
public function testUpdateHeader($headerKey, $headerValue, $testName)
{
$rqst = new Request();
$rqst->setHeader($headerKey, $headerValue);
$newValue = "newvalue";
$rqst->setHeader($testName, "newvalue");
$this->assertEquals($newValue, $rqst->getHeader($testName));
}
/**
* @dataProvider headerProvider
*/
public function testNonsetHeaderIsNull()
{
$rqst = new Request();
$this->assertNull($rqst->getHeader("no-header"));
}
/**
* @dataProvider headerProvider
*/
public function testUnsetHeaderIsNull($headerKey, $headerValue, $testName)
{
$rqst = new Request();
$rqst->setHeader($headerKey, $headerValue);
$rqst->unsetHeader($testName);
$this->assertNull($rqst->getHeader($headerKey));
}
/**
* @dataProvider headerProvider
*/
public function testCheckIfHeaderIsSet($headerKey, $headerValue, $testName)
{
$rqst = new Request();
$rqst->setHeader($headerKey, $headerValue);
$this->assertTrue($rqst->issetHeader($testName));
}
public function headerProvider()
{
return array(
array("Accept-Charset", "utf-8", "accept-charset"),
array("Accept-Encoding", "gzip, deflate", "ACCEPT-ENCODING"),
array("Cache-Control", "no-cache", "Cache-Control"),
);
}
public function testCountHeader()
{
$rqst = new Request();
$headers = $this->headerProvider();
foreach ($headers as $header) {
$rqst->setHeader($header[0], $header[1]);
}
$this->assertEquals(count($headers), count($rqst->getHeaders()));
}
/**
* @dataProvider queryProvider
*/
public function testSetQuery($input, $expected)
{
$rqst = new Request();
$rqst->setQuery($input);
$this->assertEquals($expected, $rqst->getQuery());
}
public function queryProvider()
{
return [
[
"cat=molly&dog=bear",
[
"cat" => "molly",
"dog" => "bear"
]
],
[
["id" => "1"],
["id" => "1"]
],
[
(object)["dog" => "bear"],
["dog" => "bear"]
],
["", []],
[[], []],
];
}
/**
* @dataProvider invalidQueryProvider
* @expectedException \InvalidArgumentException
*/
public function testFailOnInvalidQuery($query)
{
$rqst = new Request();
$rqst->setQuery($query);
}
public function invalidQueryProvider()
{
return [
[11],
[false],
[true],
[null]
];
}
/**
* @dataProvider invalidSchemeProvider
* @expectedException \UnexpectedValueException
*/
public function testFailOnInvalidScheme($scheme)
{
$rqst = new Request();
$rqst->setScheme($scheme);
}
public function invalidSchemeProvider()
{
return [
[""],
["ftp"],
["ssh"],
[null],
[0]
];
}
/**
* @dataProvider defaultPortProvider
*/
public function testSetDefaultPort($scheme, $port)
{
$rqst = new Request("http://localhost:9999");
$rqst->setScheme($scheme);
$rqst->setPort();
$this->assertEquals($port, $rqst->getPort());
}
public function defaultPortProvider()
{
return [
["http", 80],
["https", 443]
];
}
/**
* @dataProvider serverProvider
*/
public function testReadServerRequestMethod($serverVars, $expected)
{
$original = $_SERVER;
$_SERVER = array_merge($_SERVER, $serverVars);
$rqst = new Request();
$rqst->readHttpRequest();
$this->assertEquals($expected->method, $rqst->getMethod());
$_SERVER = $original;
}
/**
* @dataProvider serverProvider
*/
public function testReadServerRequestHost($serverVars, $expected)
{
$original = $_SERVER;
$_SERVER = array_merge($_SERVER, $serverVars);
$rqst = new Request();
$rqst->readHttpRequest();
$this->assertEquals($expected->host, $rqst->getHostname());
$_SERVER = $original;
}
/**
* @dataProvider serverProvider
*/
public function testReadServerRequestPath($serverVars, $expected)
{
$original = $_SERVER;
$_SERVER = array_merge($_SERVER, $serverVars);
$rqst = new Request();
$rqst->readHttpRequest();
$this->assertEquals($expected->path, $rqst->getPath());
$_SERVER = $original;
}
/**
* @dataProvider serverProvider
*/
public function testReadServerRequestHeaders($serverVars, $expected)
{
$original = $_SERVER;
$_SERVER = array_merge($_SERVER, $serverVars);
$rqst = new Request();
$rqst->readHttpRequest();
foreach ($expected->headers as $name => $value) {
$this->assertEquals($value, $rqst->getHeader($name));
}
$_SERVER = $original;
}
/**
* We can only test the static member once, so no need for dataProvider.
*/
public function testReadStaticRequest()
{
$data = $this->serverProvider();
$serverVars = $data[0][0];
$expected = $data[0][1];
$original = $_SERVER;
$_SERVER = array_merge($_SERVER, $serverVars);
$rqst = Request::getRequest();
$this->assertEquals($expected->host, $rqst->getHostname());
$_SERVER = $original;
return $rqst;
}
/**
* @depends testReadStaticRequest
*/
public function testReadStaticRequestAgain($previousRequest)
{
$rqst = Request::getRequest();
$this->assertSame($previousRequest, $rqst);
}
public function serverProvider()
{
return [
[
[
"REQUEST_METHOD" => "GET",
"REQUEST_URI" => "/",
"HTTP_ACCEPT_CHARSET" => "utf-8",
"HTTP_HOST" => "localhost"
],
(object) [
"method" => "GET",
"host" => "localhost",
"path" => "/",
"headers" => [
"Accept-charset" => "utf-8"
]
]
],
[
[
"REQUEST_METHOD" => "POST",
"REQUEST_URI" => "/my/page",
"HTTP_ACCEPT_CHARSET" => "utf-8",
"HTTP_HOST" => "mysite.com"
],
(object) [
"method" => "POST",
"host" => "mysite.com",
"path" => "/my/page",
"headers" => [
"Accept-charset" => "utf-8"
]
]
]
];
}
}

View File

@ -1,238 +0,0 @@
<?php
namespace pjdietz\WellRESTed\Test;
use Faker\Factory;
use pjdietz\WellRESTed\Response;
class ResponseTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider statusCodeProvider
*/
public function testSetStatusCodeInConstructor($statusCode, $reasonPhrase, $statusLine)
{
$resp = new Response($statusCode);
$this->assertEquals($statusCode, $resp->getStatusCode());
}
/**
* @dataProvider statusCodeProvider
*/
public function testReadStatusLine($statusCode, $reasonPhrase, $statusLine)
{
$resp = new Response();
$resp->setStatusCode($statusCode, $reasonPhrase);
$this->assertEquals($statusLine, $resp->getStatusLine());
}
/**
* @dataProvider statusCodeProvider
*/
public function testReadSuccess($statusCode, $reasonPhrase, $statusLine)
{
$resp = new Response();
$resp->setStatusCode($statusCode, $reasonPhrase);
if ($statusCode < 400) {
$this->assertTrue($resp->getSuccess());
} else {
$this->assertFalse($resp->getSuccess());
}
}
/**
* @dataProvider statusCodeProvider
*/
public function testReadReasonPhrase($statusCode, $reasonPhrase, $statusLine)
{
$resp = new Response();
$resp->setStatusCode($statusCode, $reasonPhrase);
$this->assertEquals(substr($statusLine, 13), $resp->getReasonPhrase());
}
public function statusCodeProvider()
{
return [
[100, null, "HTTP/1.1 100 Continue"],
[101, null, "HTTP/1.1 101 Switching Protocols"],
[200, null, "HTTP/1.1 200 OK"],
[201, null, "HTTP/1.1 201 Created"],
[202, null, "HTTP/1.1 202 Accepted"],
[203, null, "HTTP/1.1 203 Non-Authoritative Information"],
[204, null, "HTTP/1.1 204 No Content"],
[205, null, "HTTP/1.1 205 Reset Content"],
[206, null, "HTTP/1.1 206 Partial Content"],
[300, null, "HTTP/1.1 300 Multiple Choices"],
[301, null, "HTTP/1.1 301 Moved Permanently"],
[302, null, "HTTP/1.1 302 Found"],
[303, null, "HTTP/1.1 303 See Other"],
[304, null, "HTTP/1.1 304 Not Modified"],
[305, null, "HTTP/1.1 305 Use Proxy"],
[400, null, "HTTP/1.1 400 Bad Request"],
[401, null, "HTTP/1.1 401 Unauthorized"],
[402, null, "HTTP/1.1 402 Payment Required"],
[403, null, "HTTP/1.1 403 Forbidden"],
[404, null, "HTTP/1.1 404 Not Found"],
[405, null, "HTTP/1.1 405 Method Not Allowed"],
[406, null, "HTTP/1.1 406 Not Acceptable"],
[407, null, "HTTP/1.1 407 Proxy Authentication Required"],
[408, null, "HTTP/1.1 408 Request Timeout"],
[409, null, "HTTP/1.1 409 Conflict"],
[410, null, "HTTP/1.1 410 Gone"],
[411, null, "HTTP/1.1 411 Length Required"],
[412, null, "HTTP/1.1 412 Precondition Failed"],
[413, null, "HTTP/1.1 413 Request Entity Too Large"],
[414, null, "HTTP/1.1 414 Request-URI Too Long"],
[415, null, "HTTP/1.1 415 Unsupported Media Type"],
[500, null, "HTTP/1.1 500 Internal Server Error"],
[501, null, "HTTP/1.1 501 Not Implemented"],
[502, null, "HTTP/1.1 502 Bad Gateway"],
[503, null, "HTTP/1.1 503 Service Unavailable"],
[504, null, "HTTP/1.1 504 Gateway Timeout"],
[505, null, "HTTP/1.1 505 HTTP Version Not Supported"],
[598, null, "HTTP/1.1 598 Nonstandard"],
[599, "Smelly", "HTTP/1.1 599 Smelly"]
];
}
/**
* @dataProvider invalidReasonPhraseProvider
* @expectedException \InvalidArgumentException
*/
public function testFailOnInvalidReasonPhrase($statusCode, $reasonPhrase)
{
$resp = new Response();
$resp->setStatusCode($statusCode, $reasonPhrase);
}
public function invalidReasonPhraseProvider()
{
return [
[599, false],
["100", true],
["*", []]
];
}
public function testSetBody()
{
$faker = Factory::create();
$body = $faker->text();
$resp = new Response();
$resp->setBody($body);
$this->assertEquals($body, $resp->getBody());
}
public function testSetBodyInConstructor()
{
$faker = Factory::create();
$body = $faker->text();
$resp = new Response(200, $body);
$this->assertEquals($body, $resp->getBody());
}
public function testSetBodyFile()
{
$path = tempnam(sys_get_temp_dir(), "TST");
$resp = new Response();
$resp->setBodyFilePath($path);
$this->assertEquals($path, $resp->getBodyFilePath());
unlink($path);
}
/**
* @dataProvider headerProvider
*/
public function testSetHeaders($headerKey, $headerValue, $testName)
{
$resp = new Response();
$resp->setHeader($headerKey, $headerValue);
$this->assertEquals($headerValue, $resp->getHeader($testName));
}
/**
* @dataProvider headerProvider
*/
public function testSetHeadersInConstructor($headerKey, $headerValue, $testName)
{
$resp = new Response(200, "Body", array($headerKey => $headerValue));
$this->assertEquals($headerValue, $resp->getHeader($testName));
}
public function headerProvider()
{
return [
["Content-Encoding", "gzip", "CONTENT-ENCODING"],
["Content-Length", "2048", "content-length"],
["Content-Type", "text/plain", "Content-Type"]
];
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testOutputResponse()
{
$faker = Factory::create();
$body = $faker->text();
$resp = new Response(200, $body, ["Content-type" => "text/plain"]);
ob_start();
$resp->respond();
$captured = ob_get_contents();
ob_end_clean();
$this->assertEquals($body, $captured);
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testOutputResponseFromFile()
{
$path = tempnam(sys_get_temp_dir(), "TST");
$faker = Factory::create();
$body = $faker->text();
$f = fopen($path, "w");
fwrite($f, $body);
fclose($f);
$resp = new Response();
$resp->setStatusCode(200);
$resp->setBodyFilePath($path);
ob_start();
$resp->respond();
$captured = ob_get_contents();
ob_end_clean();
unlink($path);
$this->assertEquals($captured, $body);
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testMissingResponseFile()
{
$path = tempnam(sys_get_temp_dir(), "TST");
$resp = new Response();
$resp->setStatusCode(200);
$resp->setBodyFilePath($path);
unlink($path);
ob_start();
$resp->respond();
$captured = ob_get_contents();
ob_end_clean();
$this->assertEquals("", $captured);
}
}

View File

@ -1,211 +0,0 @@
<?php
namespace pjdietz\WellRESTed\Test;
use pjdietz\WellRESTed\RouteBuilder;
use pjdietz\WellRESTed\Routes\TemplateRoute;
use stdClass;
class RouteBuilderTest extends \PHPUnit_Framework_TestCase
{
/*
* Parse JSON and get the correct number of routes.
*/
public function testBuildRoutesFromJson()
{
$json = <<<'JSON'
{
"handlerNamespace": "\\myapi\\Handlers",
"routes": [
{
"path": "/",
"handler": "RootHandler"
},
{
"path": "/cats/",
"handler": "CatCollectionHandler"
},
{
"tempalte": "/cats/{id}",
"handler": "CatItemHandler"
}
]
}
JSON;
$builder = new RouteBuilder();
$routes = $builder->buildRoutes($json);
$this->assertEquals(3, count($routes));
}
/**
* Fail properly on malformed JSON
*
* @expectedException \pjdietz\WellRESTed\Exceptions\ParseException
* @expectedExceptionMessage Unable to parse as JSON.
*/
public function testFailBuildingRoutesFromInvalidJson()
{
$json = "jadhjaksd";
$builder = new RouteBuilder();
$builder->buildRoutes($json);
}
public function testSetNamesapce()
{
$namespace = "\\test\\Namespace";
$builder = new RouteBuilder();
$builder->setHandlerNamespace($namespace);
$this->assertEquals($namespace, $builder->getHandlerNamespace());
}
/**
* @dataProvider varProvider
*/
public function testSetDefaultVariablePatternThroughAccessor($name, $pattern, $expected)
{
$builder = new RouteBuilder();
$builder->setDefaultVariablePattern($pattern);
$this->assertEquals($builder->getDefaultVariablePattern(), $expected);
}
/**
* @dataProvider varProvider
*/
public function testSetDefaultVariablePatternThroughConfiguration($name, $pattern, $expected)
{
$builder = new RouteBuilder();
$conf = new stdClass();
$conf->variablePattern = $pattern;
$builder->readConfiguration($conf);
$this->assertEquals($builder->getDefaultVariablePattern(), $expected);
}
/**
* @dataProvider varProvider
*/
public function testSetTemplateVariablesThroughAccessor($name, $pattern, $expected)
{
$builder = new RouteBuilder();
$builder->setTemplateVars(array($name => $pattern));
$vars = $builder->getTemplateVars();
$this->assertEquals($vars[$name], $expected);
}
/**
* @dataProvider varProvider
*/
public function testSetTemplateVariablesThroughConfiguration($name, $pattern, $expected)
{
$builder = new RouteBuilder();
$conf = new stdClass();
$conf->vars = [$name => $pattern];
$builder->readConfiguration($conf);
$vars = $builder->getTemplateVars();
$this->assertEquals($vars[$name], $expected);
}
public function varProvider()
{
return [
["slug", "SLUG", TemplateRoute::RE_SLUG],
["name", "ALPHA", TemplateRoute::RE_ALPHA],
["name", "ALPHANUM", TemplateRoute::RE_ALPHANUM],
["id", "DIGIT", TemplateRoute::RE_NUM],
["id", "NUM", TemplateRoute::RE_NUM],
["custom", ".*", ".*"]
];
}
/**
* @dataProvider routeDescriptionProvider
*/
public function testBuildRoutesFromRoutesArray($key, $value, $expectedClass)
{
$mockHander = $this->getMock('\pjdietz\WellRESTed\Interfaces\HandlerInterface');
$routes = [
(object) [
$key => $value,
"handler" => get_class($mockHander)
]
];
$builder = new RouteBuilder();
$routes = $builder->buildRoutes($routes);
$route = $routes[0];
$this->assertInstanceOf($expectedClass, $route);
}
/**
* @dataProvider routeDescriptionProvider
*/
public function testBuildRoutesFromConfigurationObject($key, $value, $expectedClass)
{
$mockHander = $this->getMock('\pjdietz\WellRESTed\Interfaces\HandlerInterface');
$conf = (object) [
"routes" => [
(object) [
$key => $value,
"handler" => get_class($mockHander)
]
]
];
$builder = new RouteBuilder();
$routes = $builder->buildRoutes($conf);
$route = $routes[0];
$this->assertInstanceOf($expectedClass, $route);
}
public function routeDescriptionProvider()
{
return [
["path", "/", '\pjdietz\WellRESTed\Routes\StaticRoute'],
["pattern", "/cat/[0-9]+", '\pjdietz\WellRESTed\Routes\RegexRoute'],
["template", "/cat/{id}", '\pjdietz\WellRESTed\Routes\TemplateRoute'],
];
}
public function testBuildRoutesWithTemplateVariables()
{
$mock = $this->getMock('\pjdietz\WellRESTed\Interfaces\HandlerInterface');
$routes = [
(object) [
"template" => "/cats/{catId}",
"handler" => get_class($mock),
"vars" => [
"catId" => "SLUG"
]
]
];
$builder = new RouteBuilder();
$builder->setTemplateVars(["dogId" => "NUM"]);
$routes = $builder->buildRoutes($routes);
$route = $routes[0];
$this->assertInstanceOf('\pjdietz\WellRESTed\Routes\TemplateRoute', $route);
}
/**
* @expectedException \pjdietz\WellRESTed\Exceptions\ParseException
* @expectedExceptionMessage Unable to parse. Missing array of routes.
*/
public function testFailOnConfigurationObjectMissingRoutesArray()
{
$conf = new stdClass();
$builder = new RouteBuilder();
$builder->buildRoutes($conf);
}
/**
* @expectedException \pjdietz\WellRESTed\Exceptions\ParseException
* @expectedExceptionMessage Unable to parse. Route is missing a handler.
*/
public function testFailOnRouteMissingHandler()
{
$routes = [
(object) [
"path" => "/"
]
];
$builder = new RouteBuilder();
$builder->buildRoutes($routes);
}
}

View File

@ -1,350 +0,0 @@
<?php
namespace pjdietz\WellRESTed\Test;
use pjdietz\WellRESTed\Exceptions\HttpExceptions\ForbiddenException;
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
use pjdietz\WellRESTed\Interfaces\RequestInterface;
use pjdietz\WellRESTed\Interfaces\ResponseInterface;
use pjdietz\WellRESTed\Response;
use pjdietz\WellRESTed\Router;
use pjdietz\WellRESTed\Routes\StaticRoute;
use pjdietz\WellRESTed\Routes\TemplateRoute;
class RouterTest extends \PHPUnit_Framework_TestCase
{
public function testAddRoute()
{
$path = "/";
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue($path));
$route = new StaticRoute($path, __NAMESPACE__ . '\\RouterTestHandler');
$router = new Router();
$router->addRoute($route);
$resp = $router->getResponse($mockRequest);
$this->assertNotNull($resp);
}
public function testAddRoutes()
{
$path = "/";
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue($path));
$routes = array();
$routes[] = new StaticRoute("/", __NAMESPACE__ . '\\RouterTestHandler');
$routes[] = new StaticRoute("/another/", __NAMESPACE__ . '\\RouterTestHandler');
$router = new Router();
$router->addRoutes($routes);
$resp = $router->getResponse($mockRequest);
$this->assertEquals(200, $resp->getStatusCode());
}
public function testRespondWithDefaultErrorForException()
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue("/"));
$router = new Router();
$router->addRoute(new StaticRoute("/", __NAMESPACE__ . '\\ForbiddenExceptionHandler'));
$resp = $router->getResponse($mockRequest);
$this->assertEquals(403, $resp->getStatusCode());
}
public function testRespondWithErrorHandlerForException()
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue("/"));
$router = new Router();
$router->addRoute(new StaticRoute("/", __NAMESPACE__ . '\\ForbiddenExceptionHandler'));
$router->setErrorHandler(403, __NAMESPACE__ . '\\ForbiddenErrorHandler');
$resp = $router->getResponse($mockRequest);
$this->assertEquals("YOU SHALL NOT PASS!", $resp->getBody());
}
public function testRespondWithErrorHandlerForStatusCode()
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue("/"));
$router = new Router();
$router->addRoute(new StaticRoute("/", __NAMESPACE__ . '\\ForbiddenHandler'));
$router->setErrorHandler(403, __NAMESPACE__ . '\\ForbiddenErrorHandler');
$resp = $router->getResponse($mockRequest);
$this->assertEquals("YOU SHALL NOT PASS!", $resp->getBody());
}
public function testRespondWithErrorHandlerUsingOriginalResponse()
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue("/"));
$router = new Router();
$router->addRoute(new StaticRoute("/", __NAMESPACE__ . '\\MessageHandler'));
$router->setErrorHandlers([404 => __NAMESPACE__ . '\\MessageErrorHandler']);
$resp = $router->getResponse($mockRequest);
$this->assertEquals("<h1>Not Found</h1>", $resp->getBody());
}
public function testRespondWithErrorHandlerUsingInjection()
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue("/"));
$router = new Router();
$router->addRoute(new StaticRoute("/", __NAMESPACE__ . '\\ForbiddenHandler'));
$router->setErrorHandlers([403 => __NAMESPACE__ . '\\InjectionErrorHandler']);
$resp = $router->getResponse($mockRequest, ["message" => "Pass through"]);
$this->assertEquals("Pass through", $resp->getBody());
}
public function testReturnNullWhenNoRouteMatches()
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue("/dog/"));
$route = new StaticRoute("/cat/", __NAMESPACE__ . '\\RouterTestHandler');
$router = new Router();
$router->addRoute($route);
$resp = $router->getResponse($mockRequest);
$this->assertNull($resp);
}
public function testNestedRouters()
{
$path = "/cats/";
$router1 = new Router();
$router2 = new Router();
$router3 = new Router();
$router1->addRoute($router2);
$router2->addRoute($router3);
$router3->addRoute(new StaticRoute($path, __NAMESPACE__ . '\\RouterTestHandler'));
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue($path));
$resp = $router1->getResponse($mockRequest);
$this->assertNotNull($resp);
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testStaticRequestDoesNotMatchRouter()
{
$_SERVER["REQUEST_URI"] = "/cats/";
$_SERVER["HTTP_HOST"] = "localhost";
$_SERVER["REQUEST_METHOD"] = "GET";
$route = new StaticRoute("/dogs/", __NAMESPACE__ . '\\RouterTestHandler');
$router = new Router();
$router->addRoute($route);
ob_start();
$router->respond();
$captured = ob_get_contents();
ob_end_clean();
$this->assertEquals("No resource at /cats/", $captured);
}
/**
* @dataProvider nestedRouterRoutesProvider
*/
public function testNestedRouterFromWithRoutes($path, $expectedBody)
{
$router = new Router();
$router->addRoutes(array(
new TemplateRoute("/cats/*", __NAMESPACE__ . "\\CatRouter"),
new TemplateRoute("/dogs/*", __NAMESPACE__ . "\\DogRouter"),
new NotFoundHandler()
));
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue($path));
$resp = $router->getResponse($mockRequest);
$this->assertEquals($expectedBody, $resp->getBody());
}
public function nestedRouterRoutesProvider()
{
return [
["/cats/", "/cats/"],
["/cats/molly", "/cats/molly"],
["/dogs/", "/dogs/"],
["/birds/", "No resource found at /birds/"]
];
}
public function testInjection()
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue("/2/3"));
$dependencies = [
"add" => function ($a, $b) {
return $a + $b;
}
];
$router = new Router();
$router->addRoute(new TemplateRoute("/{a}/{b}", __NAMESPACE__ . "\\InjectionHandler"));
$resp = $router->getResponse($mockRequest, $dependencies);
$this->assertEquals("5", $resp->getBody());
}
}
/**
* Mini Handler class that allways returns a 200 status code Response.
*/
class RouterTestHandler implements HandlerInterface
{
public function getResponse(RequestInterface $request, array $args = null)
{
$resp = new Response();
$resp->setStatusCode(200);
$resp->setBody($request->getPath());
return $resp;
}
}
class CatRouter extends Router
{
public function __construct()
{
parent::__construct();
$this->addRoutes([
new StaticRoute("/cats/", __NAMESPACE__ . "\\RouterTestHandler"),
new StaticRoute("/cats/molly", __NAMESPACE__ . "\\RouterTestHandler"),
new StaticRoute("/cats/oscar", __NAMESPACE__ . "\\RouterTestHandler")
]);
}
}
class DogRouter extends Router
{
public function __construct()
{
parent::__construct();
$this->addRoutes([
new StaticRoute("/dogs/", __NAMESPACE__ . "\\RouterTestHandler"),
new StaticRoute("/dogs/bear", __NAMESPACE__ . "\\RouterTestHandler")
]);
}
}
class NotFoundHandler implements HandlerInterface
{
public function getResponse(RequestInterface $request, array $args = null)
{
$response = new Response(404);
$response->setBody("No resource found at " . $request->getPath());
return $response;
}
}
class ForbiddenHandler implements HandlerInterface
{
public function getResponse(RequestInterface $request, array $args = null)
{
$response = new Response(403);
$response->setBody("Forbidden");
return $response;
}
}
class ForbiddenExceptionHandler implements HandlerInterface
{
public function getResponse(RequestInterface $request, array $args = null)
{
throw new ForbiddenException();
}
}
class ForbiddenErrorHandler implements HandlerInterface
{
public function getResponse(RequestInterface $request, array $args = null)
{
$response = new Response(403);
$response->setBody("YOU SHALL NOT PASS!");
return $response;
}
}
class MessageHandler implements HandlerInterface
{
public function getResponse(RequestInterface $request, array $args = null)
{
$response = new Response(404);
$response->setBody("Not Found");
return $response;
}
}
class MessageErrorHandler implements HandlerInterface
{
public function getResponse(RequestInterface $request, array $args = null)
{
if (isset($args["response"])) {
/** @var ResponseInterface $response */
$response = $args["response"];
$message = "<h1>" . $response->getBody() . "</h1>";
$response->setBody($message);
return $response;
}
return null;
}
}
class InjectionErrorHandler implements HandlerInterface
{
public function getResponse(RequestInterface $request, array $args = null)
{
$response = new Response(403);
$response->setBody($args["message"]);
return $response;
}
}
class InjectionHandler implements HandlerInterface
{
public function getResponse(RequestInterface $request, array $args = null)
{
$response = new Response(200);
$body = $args["add"]($args["a"], $args["b"]);
$response->setBody($body);
return $response;
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace pjdietz\WellRESTed\Test;
use pjdietz\WellRESTed\Routes\StaticRoute;
class BaseRouteTest extends \PHPUnit_Framework_TestCase
{
/**
* Create a route that will match, but has an incorrect handler assigned.
* @expectedException \UnexpectedValueException
*/
public function testFailOnHandlerDoesNotImplementInterface()
{
$path = "/";
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue($path));
$route = new StaticRoute($path, __NAMESPACE__ . '\NotAHandler');
$route->getResponse($mockRequest);
}
}
class NotAHandler
{
}

View File

@ -1,95 +0,0 @@
<?php
namespace pjdietz\WellRESTed\Test;
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
use pjdietz\WellRESTed\Response;
use pjdietz\WellRESTed\Routes\PrefixRoute;
class PrefixRouteTest extends \PHPUnit_Framework_TestCase
{
public function testMatchSinglePathExactly()
{
$path = "/";
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue($path));
$route = new PrefixRoute($path, __NAMESPACE__ . '\PrefixRouteTestHandler');
$resp = $route->getResponse($mockRequest);
$this->assertNotNull($resp);
}
public function testMatchSinglePathWithPrefix()
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue("/cats/"));
$route = new PrefixRoute("/", __NAMESPACE__ . '\PrefixRouteTestHandler');
$resp = $route->getResponse($mockRequest);
$this->assertNotNull($resp);
}
public function testMatchPathInList()
{
$paths = array("/cats/", "/dogs/");
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue("/cats/"));
$route = new PrefixRoute($paths, __NAMESPACE__ . '\StaticRouteTestHandler');
$resp = $route->getResponse($mockRequest);
$this->assertEquals(200, $resp->getStatusCode());
}
public function testFailToMatchPath()
{
$path = "/cat/";
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue("/not-this-path/"));
$route = new PrefixRoute($path, 'NoClass');
$resp = $route->getResponse($mockRequest);
$this->assertNull($resp);
}
/**
* @dataProvider invalidPathsProvider
* @expectedException \InvalidArgumentException
*/
public function testFailOnInvalidPath($path)
{
new PrefixRoute($path, 'NoClass');
}
public function invalidPathsProvider()
{
return array(
array(false),
array(17),
array(null)
);
}
}
/**
* Mini Handler class that allways returns a 200 status code Response.
*/
class PrefixRouteTestHandler implements HandlerInterface
{
public function getResponse(\pjdietz\WellRESTed\Interfaces\RequestInterface $request, array $args = null)
{
$resp = new Response();
$resp->setStatusCode(200);
return $resp;
}
}

View File

@ -1,119 +0,0 @@
<?php
namespace pjdietz\WellRESTed\Test;
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
use pjdietz\WellRESTed\Interfaces\RequestInterface;
use pjdietz\WellRESTed\Response;
use pjdietz\WellRESTed\Routes\RegexRoute;
class RegexRouteTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider matchingRouteProvider
*/
public function testMatchPatternForRoute($pattern, $path, $captures)
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue($path));
$route = new RegexRoute($pattern, __NAMESPACE__ . '\RegexRouteTestHandler');
$resp = $route->getResponse($mockRequest);
$this->assertNotNull($resp);
}
/**
* @dataProvider matchingRouteProvider
*/
public function testExctractCapturesForRoute($pattern, $path, $captures)
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue($path));
$route = new RegexRoute($pattern, __NAMESPACE__ . '\RegexRouteTestHandler');
$resp = $route->getResponse($mockRequest);
$body = json_decode($resp->getBody(), true);
$this->assertEquals($captures, $body);
}
public function matchingRouteProvider()
{
return [
["~/cat/[0-9]+~", "/cat/2", [0 => "/cat/2"]],
["#/dog/.*#", "/dog/his-name-is-bear", [0 => "/dog/his-name-is-bear"]],
["~/cat/([0-9+])~", "/cat/2", [
0 => "/cat/2",
1 => "2"
]],
["~/dog/(?<id>[0-9+])~", "/dog/2", [
0 => "/dog/2",
1 => "2",
"id" => "2"
]]
];
}
/**
* @dataProvider mismatchingRouteProvider
*/
public function testSkipMismatchingPattern($pattern, $path)
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue($path));
$route = new RegexRoute($pattern, 'NoClass');
$resp = $route->getResponse($mockRequest);
$this->assertNull($resp);
}
public function mismatchingRouteProvider()
{
return [
["~/cat/[0-9]+~", "/cat/molly"],
["~/cat/[0-9]+~", "/dog/bear"],
["#/dog/.*#", "/dog"]
];
}
/**
* @dataProvider invalidRouteProvider
* @expectedException \pjdietz\WellRESTed\Exceptions\ParseException
*/
public function testFailOnInvalidPattern($pattern)
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$route = new RegexRoute($pattern, 'NoClass');
$resp = $route->getResponse($mockRequest);
$this->assertNull($resp);
}
public function invalidRouteProvider()
{
return [
["~/unterminated"],
["/nope"]
];
}
}
/**
* Mini Handler class that allways returns a 200 status code Response.
*/
class RegexRouteTestHandler implements HandlerInterface
{
public function getResponse(RequestInterface $request, array $args = null)
{
$resp = new Response();
$resp->setStatusCode(200);
$resp->setBody(json_encode($args));
return $resp;
}
}

View File

@ -1,85 +0,0 @@
<?php
namespace pjdietz\WellRESTed\Test;
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
use pjdietz\WellRESTed\Response;
use pjdietz\WellRESTed\Routes\StaticRoute;
class StaticRouteTest extends \PHPUnit_Framework_TestCase
{
public function testMatchSinglePath()
{
$path = "/";
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue($path));
$route = new StaticRoute($path, __NAMESPACE__ . '\StaticRouteTestHandler');
$resp = $route->getResponse($mockRequest);
$this->assertEquals(200, $resp->getStatusCode());
}
public function testMatchPathInList()
{
$path = "/";
$paths = array($path, "/cats/", "/dogs/");
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue($path));
$route = new StaticRoute($paths, __NAMESPACE__ . '\StaticRouteTestHandler');
$resp = $route->getResponse($mockRequest);
$this->assertEquals(200, $resp->getStatusCode());
}
public function testFailToMatchPath()
{
$path = "/";
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue("/not-this-path/"));
$route = new StaticRoute($path, 'NoClass');
$resp = $route->getResponse($mockRequest);
$this->assertNull($resp);
}
/**
* @dataProvider invalidPathsProvider
* @expectedException \InvalidArgumentException
*/
public function testFailOnInvalidPath($path)
{
new StaticRoute($path, 'NoClass');
}
public function invalidPathsProvider()
{
return array(
array(false),
array(17),
array(null)
);
}
}
/**
* Mini Handler class that allways returns a 200 status code Response.
*/
class StaticRouteTestHandler implements HandlerInterface
{
public function getResponse(\pjdietz\WellRESTed\Interfaces\RequestInterface $request, array $args = null)
{
$resp = new Response();
$resp->setStatusCode(200);
return $resp;
}
}

View File

@ -1,86 +0,0 @@
<?php
namespace pjdietz\WellRESTed\Test;
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
use pjdietz\WellRESTed\Interfaces\RequestInterface;
use pjdietz\WellRESTed\Response;
use pjdietz\WellRESTed\Routes\TemplateRoute;
class TemplateRouteTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider matchingTemplateProvider
*/
public function testMatchTemplate($template, $default, $vars, $path, $testName, $expected)
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue($path));
$route = new TemplateRoute($template, __NAMESPACE__ . '\TemplateRouteTestMockHandler', $default, $vars);
$resp = $route->getResponse($mockRequest);
$args = json_decode($resp->getBody(), true);
$this->assertEquals($expected, $args[$testName]);
}
public function matchingTemplateProvider()
{
return [
["/cat/{id}", TemplateRoute::RE_NUM, null, "/cat/12", "id", "12"],
["/cat/{catId}/{dogId}", TemplateRoute::RE_SLUG, null, "/cat/molly/bear", "dogId", "bear"],
["/cat/{catId}/{dogId}", TemplateRoute::RE_NUM, [
"catId" => TemplateRoute::RE_SLUG,
"dogId" => TemplateRoute::RE_SLUG],
"/cat/molly/bear", "dogId", "bear"],
["cat/{catId}/{dogId}", TemplateRoute::RE_NUM, (object) [
"catId" => TemplateRoute::RE_SLUG,
"dogId" => TemplateRoute::RE_SLUG],
"/cat/molly/bear", "dogId", "bear"],
["/cat/{id}/*", null, null, "/cat/12/molly", "id", "12"]
];
}
/**
* @dataProvider nonmatchingTemplateProvider
*/
public function testSkipNonmatchingTemplate($template, $default, $vars, $path)
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue($path));
$route = new TemplateRoute($template, "NoClass", $default, $vars);
$resp = $route->getResponse($mockRequest);
$this->assertNull($resp);
}
public function nonmatchingTemplateProvider()
{
return array(
array("/cat/{id}", TemplateRoute::RE_NUM, null, "/cat/molly"),
array("/cat/{catId}/{dogId}", TemplateRoute::RE_ALPHA, null, "/cat/12/13"),
array("/cat/{catId}/{dogId}", TemplateRoute::RE_NUM, array(
"catId" => TemplateRoute::RE_ALPHA,
"dogId" => TemplateRoute::RE_ALPHA),
"/cat/12/13")
);
}
}
/**
* Mini Handler class that allways returns a 200 status code Response.
*/
class TemplateRouteTestMockHandler implements HandlerInterface
{
public function getResponse(RequestInterface $request, array $args = null)
{
$resp = new Response();
$resp->setStatusCode(200);
$resp->setBody(json_encode($args));
return $resp;
}
}

View File

@ -1,4 +0,0 @@
<?php
print file_get_contents("php://input");
exit;

View File

@ -1,5 +0,0 @@
<?php
print(json_encode($_POST));
exit;

View File

@ -1,10 +0,0 @@
<?php
// http://www.php.net/manual/en/function.getallheaders.php#84262
$headers = [];
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) === 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
print json_encode($headers);

Some files were not shown because too many files have changed in this diff Show More