diff --git a/CHANGELOG.md b/CHANGELOG.md index 6436a34..ae759de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Change Log +### 3.0.4 +Reverted: Breaking changes which can be done more neatly with current request middleware + +### 3.0.3 +Added: Current Request middleware. Provides a singleton that can be used to get the current request, similar to rhubarb's `Request::current()` + +### 3.0.2 +Fixed: To/From pagination + +### 3.0.1 + +Fixed: Optional argument issue +Added: Most adapter methods now receive arguments + +### 3.0.0 + +Changed: Total Slim down - switched to [Slim framework](https://www.slimframework.com). +This module is now a wrapper and a bit of tooling around a Slim app. + ### 2.0.8 Changed: Support for including null values on rest resources. diff --git a/composer.json b/composer.json index fa52030..4a12a75 100644 --- a/composer.json +++ b/composer.json @@ -1,17 +1,16 @@ { "name": "rhubarbphp/module-restapi", "description": "An module for building ReSTful API services", - "license": "Apache 2.0", + "license": "Apache-2.0", "autoload": { "psr-4": { - "Rhubarb\\RestApi\\": "src/", - "Rhubarb\\RestApi\\Tests\\": "tests/unit/" + "Rhubarb\\RestApi\\": "src/" } }, "require": { "rhubarbphp/rhubarb": "^1.4.2", - "rhubarbphp/module-leaf": "^1.0.0", - "rhubarbphp/module-stem": "^1.5.1" + "rhubarbphp/module-stem": "^1.8", + "slim/slim": "^3.12" }, "require-dev": { "codeception/codeception": "^2.3" diff --git a/composer.lock b/composer.lock index 742c6ec..732d9d4 100644 --- a/composer.lock +++ b/composer.lock @@ -1,11 +1,42 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8d8f83b39152443c75ba9fd93ef031e9", + "content-hash": "974176868063ec9a75e70126c1acb8c3", "packages": [ + { + "name": "container-interop/container-interop", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/container-interop/container-interop.git", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "shasum": "" + }, + "require": { + "psr/container": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Interop\\Container\\": "src/Interop/Container/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", + "homepage": "https://github.com/container-interop/container-interop", + "time": "2017-02-14T19:40:03+00:00" + }, { "name": "firebase/php-jwt", "version": "v4.0.0", @@ -50,80 +81,81 @@ "time": "2016-07-18T04:51:16+00:00" }, { - "name": "phpdocumentor/reflection-docblock", - "version": "2.0.5", + "name": "nikic/fast-route", + "version": "v1.3.0", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "e6a969a640b00d8daa3c66518b0405fb41ae0c4b" + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e6a969a640b00d8daa3c66518b0405fb41ae0c4b", - "reference": "e6a969a640b00d8daa3c66518b0405fb41ae0c4b", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "suggest": { - "dflydev/markdown": "~1.0", - "erusev/parsedown": "~1.0" + "phpunit/phpunit": "^4.8.35|~5.7" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, "autoload": { - "psr-0": { - "phpDocumentor": [ - "src/" - ] - } + "psr-4": { + "FastRoute\\": "src/" + }, + "files": [ + "src/functions.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "mike.vanriel@naenius.com" + "name": "Nikita Popov", + "email": "nikic@php.net" } ], - "time": "2016-01-25T08:17:30+00:00" + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "time": "2018-02-13T20:26:39+00:00" }, { - "name": "psr/log", - "version": "1.0.2", + "name": "pimple/pimple", + "version": "v3.2.3", "source": { "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + "url": "https://github.com/silexphp/Pimple.git", + "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/9e403941ef9d65d20cba7d54e29fe906db42cf32", + "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=5.3.0", + "psr/container": "^1.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "3.2.x-dev" } }, "autoload": { - "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "psr-0": { + "Pimple": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -132,156 +164,133 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" } ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", + "description": "Pimple, a simple Dependency Injection Container", + "homepage": "http://pimple.sensiolabs.org", "keywords": [ - "log", - "psr", - "psr-3" + "container", + "dependency injection" ], - "time": "2016-10-10T12:19:37+00:00" + "time": "2018-01-21T07:42:36+00:00" }, { - "name": "rhubarbphp/custard", - "version": "1.0.11", + "name": "psr/container", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/RhubarbPHP/Custard.git", - "reference": "6dc357a82cda13d9ef9bb3e9aa2ed823bbcc3ee6" + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/RhubarbPHP/Custard/zipball/6dc357a82cda13d9ef9bb3e9aa2ed823bbcc3ee6", - "reference": "6dc357a82cda13d9ef9bb3e9aa2ed823bbcc3ee6", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", "shasum": "" }, "require": { - "phpdocumentor/reflection-docblock": "^2.0.4", - "symfony/console": ">=2.7.5" + "php": ">=5.3.0" }, - "bin": [ - "bin/custard", - "bin/vcustard" - ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { "psr-4": { - "Rhubarb\\Custard\\": "src/", - "Rhubarb\\Custard\\Tests\\": "tests/" + "Psr\\Container\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache-2.0" + "MIT" ], - "description": "Provides command line tools for Rhubarb projects.", - "homepage": "http://www.rhubarbphp.com/", - "keywords": [ - "cli", - "command-line", - "custard", - "framework", - "php", - "rhubarb", - "tools" + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } ], - "time": "2017-01-09T16:06:24+00:00" - }, - { - "name": "rhubarbphp/module-jsvalidation", - "version": "1.1.5", - "source": { - "type": "git", - "url": "https://github.com/RhubarbPHP/Module.JsValidation.git", - "reference": "5f873919968066a44da83bcdea562599a6d025ef" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/RhubarbPHP/Module.JsValidation/zipball/5f873919968066a44da83bcdea562599a6d025ef", - "reference": "5f873919968066a44da83bcdea562599a6d025ef", - "shasum": "" - }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache 2.0" + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" ], - "description": "Provides javascript validation patterns for HTML forms", - "time": "2017-08-17T14:34:15+00:00" + "time": "2017-02-14T16:28:37+00:00" }, { - "name": "rhubarbphp/module-leaf", - "version": "1.3.13", + "name": "psr/http-message", + "version": "1.0.1", "source": { "type": "git", - "url": "https://github.com/RhubarbPHP/Module.Leaf.git", - "reference": "0a14a51b4afa11b8d1babd4a06e2808195b6cca1" + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/RhubarbPHP/Module.Leaf/zipball/0a14a51b4afa11b8d1babd4a06e2808195b6cca1", - "reference": "0a14a51b4afa11b8d1babd4a06e2808195b6cca1", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", "shasum": "" }, "require": { - "rhubarbphp/custard": "^1.0.9", - "rhubarbphp/module-jsvalidation": "^1.0.0", - "rhubarbphp/rhubarb": "^1.1.0" - }, - "require-dev": { - "codeception/codeception": "^2.0.0", - "pdepend/pdepend": "^2.0.0", - "phploc/phploc": "^3.0.0", - "phpmd/phpmd": "^2.0.0", - "rhubarbphp/module-build-status-updater": "^1.0.5", - "sebastian/phpcpd": "^2.0.0", - "squizlabs/php_codesniffer": "^2.0.0", - "tan-tan-kanarek/github-php-client": "^1.0.0", - "theseer/phpdox": "*" + "php": ">=5.3.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { "psr-4": { - "Rhubarb\\Leaf\\": "src/", - "Rhubarb\\Leaf\\Tests\\": "tests/unit/" + "Psr\\Http\\Message\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache-2.0" + "MIT" ], - "description": "A nestable user interface building module for the Rhubarb PHP framework based upon the model-view-presenter pattern.", - "homepage": "http://www.rhubarbphp.com/", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", "keywords": [ - "framework", - "leaf", - "mvp", - "php", - "presenter", - "rhubarb" + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" ], - "time": "2017-10-24T13:50:44+00:00" + "time": "2016-08-06T14:39:51+00:00" }, { "name": "rhubarbphp/module-stem", - "version": "1.5.1", + "version": "1.8.1", "source": { "type": "git", "url": "https://github.com/RhubarbPHP/Module.Stem.git", - "reference": "14de4fef64322d9a87020db1b34d818abb2dece4" + "reference": "a17dab06a636f73e2b77798247c12c14b7c53c36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/RhubarbPHP/Module.Stem/zipball/14de4fef64322d9a87020db1b34d818abb2dece4", - "reference": "14de4fef64322d9a87020db1b34d818abb2dece4", + "url": "https://api.github.com/repos/RhubarbPHP/Module.Stem/zipball/a17dab06a636f73e2b77798247c12c14b7c53c36", + "reference": "a17dab06a636f73e2b77798247c12c14b7c53c36", "shasum": "" }, "require": { - "rhubarbphp/rhubarb": "^1.1.0" + "rhubarbphp/rhubarb": "^1.5.2" }, "require-dev": { "codeception/codeception": "^2.1.8", @@ -316,24 +325,24 @@ "orm", "php" ], - "time": "2017-11-15T15:14:27+00:00" + "time": "2019-01-23T22:31:16+00:00" }, { "name": "rhubarbphp/rhubarb", - "version": "1.4.2", + "version": "1.6.7", "source": { "type": "git", "url": "https://github.com/RhubarbPHP/Rhubarb.git", - "reference": "919a0dd671bd540c94b34f1b670ac846bada7f05" + "reference": "ee1a5a7aa5bf1fe3186ac781500478ebf704343d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/RhubarbPHP/Rhubarb/zipball/919a0dd671bd540c94b34f1b670ac846bada7f05", - "reference": "919a0dd671bd540c94b34f1b670ac846bada7f05", + "url": "https://api.github.com/repos/RhubarbPHP/Rhubarb/zipball/ee1a5a7aa5bf1fe3186ac781500478ebf704343d", + "reference": "ee1a5a7aa5bf1fe3186ac781500478ebf704343d", "shasum": "" }, "require": { - "firebase/php-jwt": "^4.0", + "firebase/php-jwt": "^4.0 || ^5.0", "php": ">=5.6.0" }, "require-dev": { @@ -364,113 +373,42 @@ "framework", "php" ], - "time": "2017-11-16T12:38:01+00:00" + "time": "2018-10-22T16:18:02+00:00" }, { - "name": "symfony/console", - "version": "v3.3.12", + "name": "slim/slim", + "version": "3.12.0", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "099302cc53e57cbb7414fd9f3ace40e5e2767c0b" + "url": "https://github.com/slimphp/Slim.git", + "reference": "f4947cc900b6e51cbfda58b9f1247bca2f76f9f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/099302cc53e57cbb7414fd9f3ace40e5e2767c0b", - "reference": "099302cc53e57cbb7414fd9f3ace40e5e2767c0b", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/f4947cc900b6e51cbfda58b9f1247bca2f76f9f0", + "reference": "f4947cc900b6e51cbfda58b9f1247bca2f76f9f0", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", - "symfony/debug": "~2.8|~3.0", - "symfony/polyfill-mbstring": "~1.0" + "container-interop/container-interop": "^1.2", + "nikic/fast-route": "^1.0", + "php": ">=5.5.0", + "pimple/pimple": "^3.0", + "psr/container": "^1.0", + "psr/http-message": "^1.0" }, - "conflict": { - "symfony/dependency-injection": "<3.3" + "provide": { + "psr/http-message-implementation": "1.0" }, "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~3.3", - "symfony/dependency-injection": "~3.3", - "symfony/event-dispatcher": "~2.8|~3.0", - "symfony/filesystem": "~2.8|~3.0", - "symfony/process": "~2.8|~3.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/filesystem": "", - "symfony/process": "" + "phpunit/phpunit": "^4.0", + "squizlabs/php_codesniffer": "^2.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, "autoload": { "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "Slim\\": "Slim" } - ], - "description": "Symfony Console Component", - "homepage": "https://symfony.com", - "time": "2017-11-12T16:53:41+00:00" - }, - { - "name": "symfony/debug", - "version": "v3.3.12", - "source": { - "type": "git", - "url": "https://github.com/symfony/debug.git", - "reference": "74557880e2846b5c84029faa96b834da37e29810" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/74557880e2846b5c84029faa96b834da37e29810", - "reference": "74557880e2846b5c84029faa96b834da37e29810", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8", - "psr/log": "~1.0" - }, - "conflict": { - "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" - }, - "require-dev": { - "symfony/http-kernel": "~2.8|~3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Debug\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -478,76 +416,35 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "http://akrabat.com" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Debug Component", - "homepage": "https://symfony.com", - "time": "2017-11-10T16:38:39+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.6.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296", - "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.6-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Gabriel Manricks", + "email": "gmanricks@me.com", + "homepage": "http://gabrielmanricks.com" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "http://silentworks.co.uk" } ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", + "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", + "homepage": "https://slimframework.com", "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" + "api", + "framework", + "micro", + "router" ], - "time": "2017-10-11T12:05:26+00:00" + "time": "2019-01-15T13:21:25+00:00" } ], "packages-dev": [ @@ -1141,6 +1038,158 @@ "description": "Library for handling version information and constraints", "time": "2017-03-05T17:38:23+00:00" }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2017-09-11T18:02:19+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "4.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "94fd0001232e47129dd3504189fa1c7225010d08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", + "reference": "94fd0001232e47129dd3504189fa1c7225010d08", + "shasum": "" + }, + "require": { + "php": "^7.0", + "phpdocumentor/reflection-common": "^1.0.0", + "phpdocumentor/type-resolver": "^0.4.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "doctrine/instantiator": "~1.0.5", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2017-11-30T07:14:17+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7.0", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2017-07-14T14:27:02+00:00" + }, { "name": "phpspec/prophecy", "version": "v1.7.2", @@ -1598,17 +1647,17 @@ "time": "2017-08-03T14:08:16+00:00" }, { - "name": "psr/http-message", - "version": "1.0.1", + "name": "psr/log", + "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "url": "https://github.com/php-fig/log.git", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", "shasum": "" }, "require": { @@ -1622,7 +1671,7 @@ }, "autoload": { "psr-4": { - "Psr\\Http\\Message\\": "src/" + "Psr\\Log\\": "Psr/Log/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1635,17 +1684,14 @@ "homepage": "http://www.php-fig.org/" } ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", "keywords": [ - "http", - "http-message", + "log", "psr", - "psr-7", - "request", - "response" + "psr-3" ], - "time": "2016-08-06T14:39:51+00:00" + "time": "2018-11-20T15:27:04+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -2308,6 +2354,78 @@ "homepage": "https://symfony.com", "time": "2017-11-07T14:12:55+00:00" }, + { + "name": "symfony/console", + "version": "v3.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "069bf3f0e8f871a2169a06e43d9f3f03f355e9be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/069bf3f0e8f871a2169a06e43d9f3f03f355e9be", + "reference": "069bf3f0e8f871a2169a06e43d9f3f03f355e9be", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/debug": "~2.8|~3.0|~4.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.3|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.3|~4.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2019-01-25T10:42:12+00:00" + }, { "name": "symfony/css-selector", "version": "v3.3.12", @@ -2361,6 +2479,62 @@ "homepage": "https://symfony.com", "time": "2017-11-05T15:47:03+00:00" }, + { + "name": "symfony/debug", + "version": "v4.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "cf9b2e33f757deb884ce474e06d2647c1c769b65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/cf9b2e33f757deb884ce474e06d2647c1c769b65", + "reference": "cf9b2e33f757deb884ce474e06d2647c1c769b65", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": "<3.4" + }, + "require-dev": { + "symfony/http-kernel": "~3.4|~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "time": "2019-01-25T14:35:16+00:00" + }, { "name": "symfony/dom-crawler", "version": "v3.3.12", @@ -2529,6 +2703,123 @@ "homepage": "https://symfony.com", "time": "2017-11-05T15:47:03+00:00" }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Gert de Pagter", + "email": "backendtea@gmail.com" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2018-08-06T14:22:27+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494", + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2018-09-21T13:07:52+00:00" + }, { "name": "symfony/process", "version": "v3.3.12", @@ -2672,6 +2963,57 @@ ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "time": "2017-04-07T12:08:54+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9", + "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2018-12-25T11:19:39+00:00" } ], "aliases": [], diff --git a/docs/advanced.md b/docs/advanced.md deleted file mode 100644 index 0d67ebc..0000000 --- a/docs/advanced.md +++ /dev/null @@ -1,92 +0,0 @@ -Advanced Concepts -================= - -## Canonical URLs - -A REST API is composed of many different resource objects. Each of those resources is accessed using URLs some of -which are contextual, for example the "Organisation" resource in this URL is contextual in that it depends which -contact you've selected as to which organisation you get. - -``` -/contacts/3/organisation -``` - -However some resources should have a URL by which it is permanently reachable regardless of the status of -other resources. In our example there is no reason to assume that the organisation for contact 3 is -automatically invalid if contact 3 itself is deleted and we would assume there will be other contacts attached -to the same organisation. - -In this case an Organisation should have a "canonical URL" - a URL that permanently represents the resource. In -our example the canonical URL might be: - -``` -/organisations/1 -``` - -When a resource has a canonical URL it will appear in the "_href" property of all item resources requested for -that resource type, including [nested and linked resources](model-bound#nested-resources). - -### Setting canonical URLs - -The direct way to set a canonical URL is to simply override the `getHref` function on your RestResource -object. However for combined collection/item resource objects (i.e. all ModelRestResource objects) you need -to inspect whether the current context is the collection or an item: - -``` php -class OrganisationResource extends ModelRestResource -{ - protected function getHref() - { - if ( $this->model ){ - return "/organisations/".$this->model->UniqueIdentifier; - } else { - return "/organisations"; - } - } -} -``` - -A more straightforward approach however is to use a `RestApiRootHandler` `UrlHandler` object as a parent -for all of your top level resource end points. Child urls of this end point are automatically recognised -as the canonical urls for those resources. - -## Child Resources - -Let's say you want to support a sub collection filtere by a parent resource, for example - -``` -/organisation/1/contacts -``` - -i.e. fetch all the contacts linked to organisation number 1 - -To support this you need to override the `getChildResource` function in the resource object that represents -`/organisations/1`. - -```php -class OrganisationResource extends ModelRestResource -{ - public function getChildResource($childUrlFragment) - { - switch( $childUrlFragment ){ - case "/contacts": - // Get the collection of contacts for the organisation - $organisation = $this->getModel(); - $collection = $organisation->getContacts(); - - // Create the contact resource object and assign the correct - // collection. - $contactResource = new ContactResource($this); - $contactResource->setModelCollection($collection); - - return $contactResource; - break; - } - - return parent::getChildResource($childUrlFragment); - } -} -``` - -This function will receive the remaining part of the URL that hasn't been processed from which you can -decide what type of child resource needs returned. \ No newline at end of file diff --git a/docs/basics.md b/docs/basics.md deleted file mode 100644 index 2b2d3bb..0000000 --- a/docs/basics.md +++ /dev/null @@ -1,189 +0,0 @@ -Basics of REST API design in Rhubarb -==================================== - -To create a REST resource you need to extend the `RestResource` class. A RestResource object is the most basic -type of REST object. It knows that it needs an href property and provides a pattern for dealing with -GET, POST, PUT, HEAD and DELETE verbs. - -When you extend RestResource you must create a getRelativeUrl() method which should return the relative URL for this -resource (depending on how this resource is served the full "href" property maybe different) - -``` php -class WeatherResource extends RestResource -{ - -} -``` - -Now we need to get our resource to respond to HTTP verbs. Let's implement GET: - -``` php -class WeatherResource extends RestResource -{ - public function get() - { - // Start with the 'skeleton'. This gives us a stdClass object with the href already populated. - $resource = $this->getSkeleton(); - - $resource->Outlook = "cloudy"; - $resource->MaxTemp = 22; - $resource->MinTemp = 7; - - return $resource; - } -} -``` - -REST resources are retrieved using URLs and so are made visible in Rhubarb using UrlHandler objects like -all other URLs. Edit your app.config.php and register the handler: - -``` php -$this->addUrlHandlers( -[ - "/weather" => new RestResourceHandler('\MyAPI\Resources\WeatherResource') -]); -``` - -Requesting the resource in the browser should now give you the following output: - -``` javascript -{ - _href: "/weather", - Outlook: "cloudy", - MaxTemp: 22, - MinTemp: 7 -} -``` - -## Item resources - -If you need to represent a resource that has a unique identifier, you should extend `ItemRestResource` instead. -This class adds one special property to the output "_id" which can be used in passing to other requests etc. - -``` php -class DayOfTheWeek extends ItemRestResource -{ - public function get() - { - // Start with the 'skeleton'. This gives us a stdClass object with the href already populated. - $resource = $this->getSkeleton(); - - // Silly example but just switch on the ID and return the correct day of the week. - $days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; - - // $this->id contains the identifier. - $resource->Day = $days[$this->id]; - - return $resource; - } -} -``` - -In order to serve an item resource you must, however, use a RestCollectionHandler. This handler knows to expect -an ID on the URL and passes it to the resource (thereby ending up in $this->id). - -``` php -$this->addUrlHandlers( -[ - "/day-of-the-week" => new RestCollectionHandler('\MyAPI\Resources\DayOfTheWeek') -]); -``` - -Requesting /day-of-the-week/1 in the browser should now give you the following output: - -``` javascript -{ - _href: "/day-of-the-week", - _id: "1", - Day: "Tue" -} -``` - -## Collection resources - -To present a collection of items in a single resource extend the CollectionRestResource. Collection resources -still get an href property, but instead of the key-value pairs of an item it has a sub-node called **"items"** -which is an array of the matching items. It also has a count property, and because it will normally limit the -collection to 100 items, a range property tells you which section of the full list you're currently viewing. - -Instead of implementing the `get()` function you implement the `getItems()` function instead. - -Here is the collection form of our days-of-the-week resource. - -``` php -class DaysOfTheWeek extends CollectionRestResource -{ - protected function getItems($from, $to, RhubarbDateTime $since = null) - { - // Ignoring $since as it has no bearing in this case. - $items = []; - - for ($x = max($from, 0); $x < min($to, 6); $x++) { - $dayOfTheWeekResource = $this->getItemResource($x); - $items[] = $dayOfTheWeekResource->get(); - } - - return [$items, count($items)]; - } - - public function createItemResource($resourceIdentifier) - { - return new DayOfTheWeek($resourceIdentifier); - } -} -``` - -We can now change our url handler to use our collection resource instead of the item resource. - -``` php -$this->addUrlHandlers( -[ - "/days-of-the-week" => new RestCollectionHandler('\MyAPI\Resources\DaysOfTheWeek') -] ); -``` - -Now we can request either `/days-of-the-week` to get a list of the days or `/days-of-the-week/1` to get a -specific one. Here's what the collection looks like: - -``` javascript - -{ - "_href": "\/days-of-the-week", - "items": [ - { - "_href": "\/days-of-the-week\/0", - "Day": "Mon" - }, - { - "_href": "\/days-of-the-week\/1", - "_id": 1, - "Day": "Tue" - }, - { - "_href": "\/days-of-the-week\/2", - "_id": 2, - "Day": "Wed" - }, - { - "_href": "\/days-of-the-week\/3", - "_id": 3, - "Day": "Thu" - }, - { - "_href": "\/days-of-the-week\/4", - "_id": 4, - "Day": "Fri" - }, - { - "_href": "\/days-of-the-week\/5", - "_id": 5, - "Day": "Sat" - } - ], - "count": 6, - "range": { - "from": 0, - "to": 5 - } -} -``` \ No newline at end of file diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 080fc62..0000000 --- a/docs/index.md +++ /dev/null @@ -1,103 +0,0 @@ -Building RESTful APIs -===================== - -An application program interface (API) is an interface which allows interactivity between software components. -In the case of web applications, APIs are often provided to allow other applications to access the functionality -of your web app. APIs are easier to work with if they are based on a common standard, and REST (REpresentational -State Transfer) is one of the most popular on the web. REST is increasingly popular not just because it is easy -to understand but also because it aligns closely with the nature of HTTP. - -Understanding the HTTP protocol is the key to designing great REST APIs because the two are so closely bound. A -very useful guide in getting started with REST API design is -[Janseen Geert's introduction](http://restful-api-design.readthedocs.org/en/latest/intro.html). - -HTTP is a protocol for getting, creating, updating and deleting resources - usually HTML documents and images, but -it can be used for any type of resource. The most important concept to embrace with REST API design is to translate -all of your objects and actions into the language of resources and collections. - -For many of the objects you want to expose in your API this is an easy exercise. For example if you have a Contact -model in your application you might want a Contact resource in your API. The same applies for SalesOrder, BlogPost, -Product, and Comment models. Each of these resources will be associated with URLs like: - -URL |Meaning ---------------|---------------------------------------- -/contacts |A collection resource listing contacts -/contacts/{id}|A single contact item resource -/posts |A collection resource listing blog posts -/posts/{id} |A single blog post item resource - -Some transactions with your API require a little bit of creative thinking in order to mould them into the -resource paradigm. Mostly these types of transactions are 'actions' you need to take on those more traditional -resources. - -Let's imagine you had a SalesOrder resource, and your API needs to support the ability to 'dispatch' the order -with a courier. We also want to be able to track the progress of that dispatch. Traditionally we might think of -the dispatching to be an action on the SalesOrder resource. Likewise the polling for status updates on the -dispatch would be seen as a completely different endpoint where you pass some sort of ID and get status data -back. It's no coincidence that we all think this way - it is very likely how the application works on the server. -In this mode of thinking you might take the sales order API URL and add an action end point: - -``` -POST/(PUT?) /sales-orders/123456/dispatch -``` - -And for polling you might have an end point like - -``` -GET /dispatch-status/5432212 -``` - -You might notice a problem - I can't decide whether to use POST or PUT for the dispatch end point. -That's because the approach of triggering an action on a resource has no parallel in HTTP. This is the sort -of problem symptomatic of building REST APIs using a traditional RPC mindset. - -In REST API design you have the opportunity to express this transaction as a resource. A more RESTful -API would handle this transaction like this: - -``` -POST { "SalesOrderID": 123456 } to /dispatches -this returns a "Dispatch" resource -{ - "_id": 5432212, - "_href": "/dispatches/5432212", - "SalesOrderID": 123456, - "Status": "Awaiting Courier" -} -``` - -To poll: - -``` -GET /dispatches/5432212 -this returns the same "Dispatch" resource -{ - "_id": 5432212, - "_href": "/dispatches/5432212", - "SalesOrderID": 123456, - "Status": "In Transit" -} -``` - -This example highlights another facet of great REST APIs - you don't need a manual in order to program against them. -This is a natural consequence of building your API with a resource based mindset as all resources should have an -"href" to allow them to be fetched again. After we POST to the dispatches end point it returns a Dispatch resource. -That resource contains the href to the dispatch. The developer now believes that this dispatch resource is a -permanent resource they can re-fetch simply by requesting that href again (as we do in our polling operation). - -Building REST APIs with the REST API module in Rhubarb ------------------------------------------------------- - -[Creating basic REST resources](basics) -: Learn how to create custom REST resources and respond to get, post, put and delete - -[Model bound REST resources](model-bound) -: If you are using Stem modelling you can get REST resources up in minutes. - -[Authentication](authentication) -: Learn how to handle authentication with APIs - -[Advanced Concepts](advanced) -: More advanced ideas relating to REST API design - -[Consuming REST APIs](clients) -: Consume other Rhubarb APIs using the REST client classes. diff --git a/docs/model-bound.md b/docs/model-bound.md deleted file mode 100644 index dd3b71f..0000000 --- a/docs/model-bound.md +++ /dev/null @@ -1,360 +0,0 @@ -Model Bound Resources -===================== - -Most real world APIs will have many REST resources that map directly to models. For resources like this -Rhubarb has a `ModelRestResource` which lets you create these resources very quickly. - -A `ModelRestResource` combines the features of a collection resource and an item resource which allows -both 'views' to be configured in one class. - -## Creating a `ModelRestResource` - -`ModelRestResource` is abstract and so you must extend the class. - -``` php -class ContactResource extends ModelRestResource -{ - public function getModelName() - { - // Return the name of the model to bind to. - return "Contact"; - } -} -``` - -This example is a legitimate resource object for exposing a "Contact" model as a REST resource. To use the -resource we must register a URL handler for it. Because ModelRestResource objects can handle both collection -and item URLs we must use the RestCollectionHandler to register it as it is the RestCollectionHandler which -understands URLs with and without a final identifier. - -``` php -// In app.config.php -$this->addUrlHandlers( -[ - "/contacts" => new RestCollectionHandler('\MyAPI\Resources\ContactResource') -]); -``` - -Assuming that the `Contact` model has a label column name defined in its schema, you should already have a -basic API for serving contacts: - -``` javascript -GET /contacts - -{ - "_href": "/contacts", - "items": [ - { - "_href": "/contacts/1", - "_id": 1, - "Name": "John Smith" - }, - { - "_href": "/contacts/2", - "_id": 2, - "Name": "Peter Salmon" - }, - { - "_href": "/contacts/3", - "_id": 3, - "Name": "Claire Blackwood" - } - ], - "count": 3, - "range": { - "from": 0, - "to": 2 - } -} - -GET /contacts/3 - -{ - "_href": "/contacts/3", - "_id": 3, - "Name": "Claire Blackwood" -} -``` - -## Customising the resource item content - -By default a `ModelRestResource` will extract the ID and, if configured in the model, the model's label column. -However it's rare that this is sufficient for an API and if you need to customise the list of properties -you should override `getColumns` and return a specific list of columns: - -``` php -class ContactResource extends ModelRestResource -{ - public function getColumns() - { - // Let's keep the ID and label - $columns = parent::getColumns(); - // Now add another property to our resource: - $columns[] = "DateOfBirth"; - return $columns; - } -} -``` - -``` javascript -GET /contacts - -{ - "_href": "\/contacts", - "items": [ - { - "_href": "\/contacts\/1", - "_id": 1, - "Name": "John Smith", - "DateOfBirth": "2015-07-15T00:00:00+0100" - }, - { - "_href": "\/contacts\/2", - "_id": 2, - "Name": "Peter Salmon", - "DateOfBirth": "2015-07-14T00:00:00+0100" - }, - { - "_href": "\/contacts\/3", - "_id": 3, - "Name": "Claire Blackwood", - "DateOfBirth": "2015-07-06T00:00:00+0100" - } - ], - "count": 3, - "range": { - "from": 0, - "to": 2 - } -} -``` - -Properties from the model can be renamed in the resource: - -``` php -class ContactResource extends ModelRestResource -{ - public function getColumns() - { - // Let's keep the ID and label - $columns = parent::getColumns(); - // Now add another property to our resource but call it DOB this time. - $columns["DOB"] = "DateOfBirth"; - return $columns; - } -} -``` - -> Try to avoid aliasing properties like this unless absolutely necessary, as it can cause confusion later -> when you expect resource field names to match their models. If possible fix a poor choice of column name -> in the model itself rather than aliasing it in the REST API. - -You can also use callbacks to calculate values that aren't based on columns: - -``` php -class ContactResource extends ModelRestResource -{ - public function getColumns() - { - // Let's keep the ID and label - $columns = parent::getColumns(); - // Calculate a value in a call back. - $columns["Age"] = function(){ - $tz = new DateTimeZone('Europe/London'); - return $this->model->DateOfBirth - ->diff(new DateTime('now', $tz)) - ->y; - }; - - return $columns; - } -} -``` - -> Just as models abstract logic from your user interface, REST APIs provide one other layer of abstraction -> and also one more layer for adding calculated values like this. Try however to be consistent with which -> level you add these computed properties to. In this case it's a valid question as to whether Age should -> be calculated in the Contact model, in which case it could be listed here as a normal 'Column'. - -## Nested resources - -You can include relationship properties in your `ModelRestResource` and you have three choices for how to do -this: - -1. Embed the full resource as a 'child' -2. Embed a summary of the full resource as a 'child'. Includes an *_href* property so that the full object - can be accessed later when needed -3. Embed a *'link'* node which just includes the *_href* property. - -All three choices first require that the 'child' resource is mapped to the model returned by the relationship. -To set up the mapping you need to call the following in your app.config.php: - -``` php -// Map the Organisation model to its default RestResource object. -ModelRestResource::registerModelToResourceMapping("Organisation", OrganisationResource::class); -``` - -> Note: The most common reason for nested resources not appearing is because the mapping is incorrect -> or has been omitted entirely. - -### Nesting a full resource - -This is the most simple way to nest a related model, however it means enlarging your resource object -even if the data wasn't required. Some nested models can be large so you should think carefully before -doing this. To nest the related model simply include the navigation property in your `getColumns()` function. - -``` php -class ContactResource extends ModelRestResource -{ - public function getColumns() - { - $columns = parent::getColumns(); - - // Include an embedded model - $columns[] = "Organisation"; - - return $columns; - } -} -``` - -This will simulate a full "get" request on our Organisation resource and embed the content under an -"Organisation" property. - -> With fully nested resources there is a danger of creating an infinite nesting loop. A common example -> would be Organisation nesting a collection of Contact resources which in turn nest their Organisation -> resource, which in turn nests a collection of Contact resources etc. -> -> Presently there are no safeguards to prevent this however in many cases the issue is solved -> by using summaries or links. This is often a better solution anyway for resources that are -> complicated enough to expose the issue. - -### Nesting a resource summary - -This operates just like the full resource however it requests a summary of the resource rather than -a the full resource. To switch to a summary simply change the column mapping like this: - -``` php -class ContactResource extends ModelRestResource -{ - public function getColumns() - { - $columns = parent::getColumns(); - - // Include an embedded model - $columns[] = "Organisation:summary"; - - return $columns; - } -} -``` - -To control the columns listed in a summary (again by default just the label and unique identifier) -simply override `getSummaryColumns()` in the relevant resource. This function mirrors the behaviour -of `getColumns()` as outlined above. - -The `_href` property is also included so should the user require the full resource the can make a -second GET request on that URL. - -### Nesting a link to a resource - -The third approach returns only the `_href` property. - -``` php -class ContactResource extends ModelRestResource -{ - public function getColumns() - { - $columns = parent::getColumns(); - - // Include an embedded model - $columns[] = "Organisation:link:/organisation"; - - return $columns; - } -} -``` - -If the nested resource is a collection or the resource doesn't have a canonical link then you must supply -a URL suffix to append to the URL of the current resource. In our example there is no canonical URL -for organisation resources so we instruct the link to use the current URL appended with "/organisation". -i.e. /contacts/3/**organisation** - -Of course you must also make sure that you have a URL handler configured to handle this URL. - -## Filtering Collections - -In all but the most basic of applications a collection of items will need filtered to those appropriate for -the authenticated user. This is no different in a REST API and security must be considered carefully when -designing your API. - -For custom RestResource objects you should handle security in the `get` methods manually. Throw a -`RestAuthenticationException` or similar if the user should not have access to the requested resource. - -Because ModelRestResource objects handle both the collection and item resources we can control access to the item -resources by carefully filtering the collections. When requesting an item Rhubarb checks to see if the -item is contained in the collection and if not an exception is thrown. - -There are four filtering methods you can override: - -`filterModelCollectionAsContainer(Collection $collection)` -: Override this method to filter the collection to the set of generally allowed items. For example a - resource to provide "In Progress" orders would apply the status filter to the orders collection in this - method. - -`filterModelCollectionForQueries(Collection $collection)` -: If your resource supports filtering the list based on HTTP query parameters this is where you should - do it. For example a Staff resource might support searching by adding "?name=alice" to the GET URL. - -`filterModelCollectionForSecurity(Collection $collection)` -: Apply filters here to remove items the currently authenticated used should not have access to. Normally this - looks at the default login provider and applies a relevant criteria. - -`filterModelCollectionForModifiedSince(Collection $collection, RhubarbDateTime $since)` -: In order to support the "If-Modified-Since" HTTP header you should apply the relevant filter based upon the - $since RhubarbDateTime argument passed to this function. - -> The four methods exist as a pattern to allow you to create parent classes without requiring -> extenders to call the parent implementations. For example you might have a parent class that -> implements some site wide security filters in `filterModelCollectionForSecurity` but in a -> child class you need to implement some query filters. By asking the developer to extend -> `filterModelCollectionForSecurity` you know that the developer can't accidentally stop -> the security filters being applied. - -Each of the methods work the same way - take the Collection object passed to it and apply -some model filters using the `filter()` method: - -``` php -class ContactResource extends ModelRestResource -{ - protected function filterModelCollectionForSecurity(Collection $collection) - { - parent::filterModelCollectionForSecurity($collection); - - // Get the logged in customer - $login = LoginProvider::getDefaultLoginProvider(); - $customer = $login->getModel(); - - // Make sure we can only see that customer's contacts - $collection->filter(new Equals("CustomerID", $customer->UniqueIdentifier)); - } -} -``` - -## Responding to PUT and POST - -PUT and POST are both handled by the ModelRestResource to transparently create new model items or update -existing ones. Sometimes however you need to cause specific functionality to happen during these events. -There are three methods you can override to handle this: - -beforeModelUpdated($model, $restResource) -: Called just before the model is actually updated. You have access to both the Model object and the - payload passed to the API - -afterModelUpdated($model, $restResource) -: Called just after the model is actually updated. You have access to both the Model object and the - payload passed to the API - -afterModelCreated($model, $restResource) -: Called just after a model is created. You have access to both the Model object and the payload passed - to the API \ No newline at end of file diff --git a/resources/boot.php b/resources/boot.php new file mode 100644 index 0000000..9dd5341 --- /dev/null +++ b/resources/boot.php @@ -0,0 +1,28 @@ +initialise() + ->run(); +} catch (\Error $error) { + error_log($error->getMessage() . ' ' . $error->getFile() . ':' . $error->getLine()); + http_response_code(500); +} + diff --git a/src/Adapters/BaseEntityAdapter.php b/src/Adapters/BaseEntityAdapter.php new file mode 100644 index 0000000..95190fe --- /dev/null +++ b/src/Adapters/BaseEntityAdapter.php @@ -0,0 +1,86 @@ +getQueryParam('offset', $request->getQueryParam('from', 1) - 1); + $pageSize = (int)$request->getQueryParam('pageSize', $request->getQueryParam('to', 10 - $offset)); + $sort = $request->getQueryParam('sort'); + + $list = $this->getEntityList( + $request, + $offset, + $pageSize, + $sort + ); + return $response + ->withJson(array_map( + function ($entity) { + return $this->getPayloadForEntity($entity, true); + }, + $list->results + )) + ->withAddedHeader('X-Total', $list->total) + ->withAddedHeader('X-Offset', $offset) + ->withAddedHeader('X-PageSize', $pageSize) + ->withAddedHeader('X-From', $offset + 1) + ->withAddedHeader('X-To', $offset + $pageSize); + } + + final public function get(Request $request, Response $response, $id): Response + { + return $response->withJson($this->getPayloadForEntity($this->getEntityForId($id))); + } + + final public function patch(Request $request, Response $response, $id): Response + { + $entity = $this->getEntityForId($id); + $this->updateEntityWithPayload($entity, $request->getParsedBody()); + $this->storeEntity($entity); + return $response->withStatus(204, 'No Content'); + } + + final public function put(Request $request, Response $response, $id): Response + { + $entity = $this->getEntityForPayload($request->getParsedBody(), $id); + $this->storeEntity($entity); + return $response->withJson($this->getPayloadForEntity($entity)); + } + + final public function post(Request $request, Response $response): Response + { + return $this->put($request, $response, null); + } + + final public function delete(Request $request, Response $response, $id): Response + { + $this->deleteEntity($this->getEntityForId($id)); + return $response; + } +} diff --git a/src/Adapters/DIEntityAdapter.php b/src/Adapters/DIEntityAdapter.php new file mode 100644 index 0000000..4ac1631 --- /dev/null +++ b/src/Adapters/DIEntityAdapter.php @@ -0,0 +1,43 @@ +entityAdapter = Container::instance(static::class); + } + + final public function list(Request $request, Response $response): Response + { + return $this->entityAdapter->list(...func_get_args()); + } + + final public function get(Request $request, Response $response, $id): Response + { + return $this->entityAdapter->get(...func_get_args()); + } + + final public function post(Request $request, Response $response): Response + { + return $this->entityAdapter->post(...func_get_args()); + } + + final public function put(Request $request, Response $response, $id): Response + { + return $this->entityAdapter->put(...func_get_args()); + } + + final public function delete(Request $request, Response $response, $id): Response + { + return $this->entityAdapter->delete(...func_get_args()); + } +} diff --git a/src/Adapters/EntityAdapter.php b/src/Adapters/EntityAdapter.php new file mode 100644 index 0000000..9241996 --- /dev/null +++ b/src/Adapters/EntityAdapter.php @@ -0,0 +1,37 @@ +getSearchCriteriaEntity($request, $offset, $pageSize, $sort)); + $this->performSearch($response); + return $response; + } +} diff --git a/src/Adapters/EntityAdapterInterface.php b/src/Adapters/EntityAdapterInterface.php new file mode 100644 index 0000000..67443bd --- /dev/null +++ b/src/Adapters/EntityAdapterInterface.php @@ -0,0 +1,19 @@ +get( + '/', + self::entityAdapterHandler($entityAdapter, 'list') + ); + $allowed & self::ITEM_POST && $app->post( + '/', + self::entityAdapterHandler($entityAdapter, 'post') + ); + $allowed & self::ITEM_GET && $app->get( + '/{id}/', + self::entityAdapterWithRouteIDHandler($entityAdapter, 'get') + ); + $allowed & self::ITEM_PUT && $app->put( + '/{id}/', + self::entityAdapterWithRouteIDHandler($entityAdapter, 'put') + ); + $allowed & self::ITEM_PATCH && $app->patch( + '/{id}/', + self::entityAdapterWithRouteIDHandler($entityAdapter, 'patch') + ); + $allowed & self::ITEM_DELETE && $app->delete( + '/{id}/', + self::entityAdapterWithRouteIDHandler($entityAdapter, 'delete') + ); + if ($additional !== null) { + $additional($app, $entityAdapter); + } + }; + } + + /** + * @param App $app + * @param string $entityAdapter + * @param callable|null $additional function(App $app, string $entityAdapter) If provided allows definition of additional routes for this base + * @return callable + */ + public static function readOnly(App $app, string $entityAdapter, callable $additional = null): callable + { + return self::crud($app, $entityAdapter, self::LIST | self::ITEM_GET, $additional); + } + + private static function entityAdapterWithRouteIDHandler(EntityAdapterInterface $entityAdapter, $adapterMethod) + { + return function ($request, $response, $routeVariables) use ($entityAdapter, $adapterMethod) { + return $entityAdapter->$adapterMethod($request, $response, $routeVariables['id']); + }; + } + + public static function entityAdapterHandler(EntityAdapterInterface $entityAdapter, $adapterMethod) + { + return function (...$params) use ($entityAdapter, $adapterMethod) { + return $entityAdapter->$adapterMethod(...$params); + }; + } +} + diff --git a/src/Adapters/Stem/LegacyStemEntityAdapter.php b/src/Adapters/Stem/LegacyStemEntityAdapter.php new file mode 100644 index 0000000..1845852 --- /dev/null +++ b/src/Adapters/Stem/LegacyStemEntityAdapter.php @@ -0,0 +1,120 @@ +getModelClass(); + return new $modelClass($id); + } catch (RecordNotFoundException $exception) { + throw new ResourceNotFoundException($exception->getMessage(), $exception); + } + } + + /** + * @param Model $entity + * @param bool $resultList + * @return array + */ + protected function getPayloadForEntity($entity, $resultList = false) + { + return $entity->exportPublicData(); + } + + protected function getEntityForPayload($payload, $id = null) + { + $modelClass = $this->getModelClass(); + /** @var Model $model */ + $model = new $modelClass($id); + $model->importData($payload); + return $model; + } + + /** + * @param Model $entity + * @param array $payload + */ + protected function updateEntityWithPayload($entity, $payload) + { + $entity->importData($payload); + } + + /** + * @param Model $entity + * @throws \Rhubarb\Stem\Exceptions\DeleteModelException + */ + final protected function deleteEntity($entity) + { + $entity->delete(); + } + + /** + * @param Request $request + * @return Filter[] + */ + protected function getListFilterForRequest(Request $request): array + { + return []; + } + + /** + * @param Request $request + * @param int $offset + * @param int $pageSize + * @param string|null $sort + * @return Collection + */ + final protected function getEntityList( + Request $request, + int $offset, + int $pageSize, + ?string $sort = null + ): SearchResponseEntity { + $criteria = new SearchCriteriaEntity($offset, $pageSize, $sort); + $response = new SearchResponseEntity($criteria); + /** @var Model $modelClass */ + $modelClass = $this->getModelClass(); + $collection = $modelClass::find(...$this->getListFilterForRequest($request))->setRange($offset, $pageSize); + $response->total = $collection->count(); + foreach ($collection as $model) { + $response->results[] = $model; + } + return $response; + } + + /** + * @param Model $entity + * @throws \Rhubarb\Stem\Exceptions\ModelConsistencyValidationException + * @throws \Rhubarb\Stem\Exceptions\ModelException + */ + protected function storeEntity($entity) + { + $entity->save(); + } +} diff --git a/src/Authentication/AuthenticationProvider.php b/src/Authentication/AuthenticationProvider.php deleted file mode 100644 index cad33db..0000000 --- a/src/Authentication/AuthenticationProvider.php +++ /dev/null @@ -1,30 +0,0 @@ -getLoginProviderClassName(); - - return Container::singleton($class); - } - - public function authenticate(Request $request) - { - if (!$request->header("Authorization")) { - Log::debug("Authorization header missing. If using fcgi be sure to instruct Apache to include this header", "RESTAPI"); - throw new ForceResponseException(new BasicAuthorisationRequiredResponse("API")); - } - - $authString = trim($request->header("Authorization")); - - if (stripos($authString, "basic") !== 0) { - throw new ForceResponseException(new BasicAuthorisationRequiredResponse("API")); - } - - $authString = substr($authString, 6); - // Colon character support per http://www.ietf.org/rfc/rfc2617.txt - $credentials = explode(":", base64_decode($authString), 2); - - $provider = $this->getLoginProvider(); - - try { - $provider->login($credentials[0], $credentials[1]); - return true; - } catch (CredentialsFailedException $er) { - throw new ForceResponseException(new BasicAuthorisationRequiredResponse("API")); - } - } -} diff --git a/src/Authentication/IpRestrictedAuthenticationProvider.php b/src/Authentication/IpRestrictedAuthenticationProvider.php deleted file mode 100644 index 95d69cf..0000000 --- a/src/Authentication/IpRestrictedAuthenticationProvider.php +++ /dev/null @@ -1,51 +0,0 @@ -ipList = $ipList; - } - - public function authenticate(Request $request) - { - if (!($request instanceof WebRequest)){ - throw new ForceResponseException(new NotAuthorisedResponse($this)); - } - - /** - * @var WebRequest $request - */ - $ip = $request->server("REMOTE_ADDR"); - - if (!in_array($ip, $this->ipList)) { - throw new ForceResponseException(new NotAuthorisedResponse($this)); - } - - return true; - } -} \ No newline at end of file diff --git a/src/Authentication/ModelLoginProviderAuthenticationProvider.php b/src/Authentication/ModelLoginProviderAuthenticationProvider.php deleted file mode 100644 index f9b555f..0000000 --- a/src/Authentication/ModelLoginProviderAuthenticationProvider.php +++ /dev/null @@ -1,42 +0,0 @@ -header("Authorization")) { - Log::debug("Authorization header missing. If using fcgi be sure to instruct Apache to include this header", "RESTAPI"); - throw new ForceResponseException(new TokenAuthorisationRequiredResponse()); - } - - $authString = trim($request->header("Authorization")); - - if (stripos($authString, "token") !== 0) { - throw new ForceResponseException(new TokenAuthorisationRequiredResponse()); - } - - if (!preg_match("/token=\"?([[:alnum:]]+)\"?/", $authString, $match)) { - throw new ForceResponseException(new TokenAuthorisationRequiredResponse()); - } - - $token = $match[1]; - - if (!$this->isTokenValid($token)) { - throw new ForceResponseException(new TokenAuthorisationRequiredResponse()); - } - - return true; - } -} diff --git a/src/Calls/FakeCall.php b/src/Calls/FakeCall.php new file mode 100644 index 0000000..cbe47a6 --- /dev/null +++ b/src/Calls/FakeCall.php @@ -0,0 +1,42 @@ +initialise(); + $app['request'] = $request; + $this->response = $app->run(true); + } + + public function response(): ResponseInterface + { + return $this->response; + } + + public static function createRequest(string $method, string $uri, array $environment = []): Request + { + return Request::createFromEnvironment(Environment::mock(array_merge( + [ + 'REQUEST_METHOD' => $method, + 'REQUEST_URI' => $uri, + ], + $environment + ))); + } +} diff --git a/src/Clients/BasicAuthenticatedRestClient.php b/src/Clients/BasicAuthenticatedRestClient.php deleted file mode 100644 index 5066d92..0000000 --- a/src/Clients/BasicAuthenticatedRestClient.php +++ /dev/null @@ -1,45 +0,0 @@ -username = $username; - $this->password = $password; - } - - protected function applyAuthenticationDetailsToRequest(HttpRequest $request) - { - $request->addHeader("Authorization", "Basic " . base64_encode($this->username . ":" . $this->password)); - } -} \ No newline at end of file diff --git a/src/Clients/RestClient.php b/src/Clients/RestClient.php deleted file mode 100644 index 121a0ee..0000000 --- a/src/Clients/RestClient.php +++ /dev/null @@ -1,119 +0,0 @@ -apiUrl = rtrim($apiUrl, "/"); - } - - protected function applyAuthenticationDetailsToRequest(HttpRequest $request) - { - - } - - public function getApiUrl() - { - return $this->apiUrl; - } - - public function makeRequest(RestHttpRequest $request) - { - Log::debug("Making ReST request to " . $request->getMethod() . ":" . $request->getUri(), "RESTCLIENT"); - - $request->setApiUrl($this->getApiUrl()); - // ToDo: refactor this into a JSONRestClient as this is all json specific - $request->addHeader("Accept", "application/json"); - - $this->applyAuthenticationDetailsToRequest($request); - - $httpClient = HttpClient::getDefaultHttpClient(); - $response = $httpClient->getResponse($request); - - Log::debug("ReST response received"); - Log::bulkData("ReST response data", "RESTCLIENT", $response->getResponseBody()); - - $this->checkResponse($response); - - $responseObject = $this->parseResponseBody($response->getResponseBody()); - - $this->checkResponseBody($responseObject, $response); - - return $responseObject; - } - - /** - * Parses a response for use as an object - * @param String $responseBody - * @return mixed - */ - protected function parseResponseBody($responseBody) - { - return json_decode($responseBody); - } - - /** - * @param HttpResponse $response - * @throws RestAuthenticationException - */ - protected function checkResponse(HttpResponse $response) - { - if ($response->getResponseCode() == 401) { - throw new RestAuthenticationException(); - } - } - - /** - * @param $responseObject - * @param HttpResponse $response - * @throws HttpResponseException - * @throws RestImplementationException - */ - protected function checkResponseBody($responseObject, HttpResponse $response) - { - if ($responseObject === null) { - Log::error("REST Request was returned with an invalid response", "RESTCLIENT", - $response->getResponseBody()); - throw new RestImplementationException("A REST Request was returned with an invalid response"); - } - - if ($this->requireSuccessfulResponse && !$response->isSuccess()) { - throw new HttpResponseException("A REST Request was returned with an error.", null, $response); - } - } -} diff --git a/src/Clients/RestHttpRequest.php b/src/Clients/RestHttpRequest.php deleted file mode 100644 index 9756dd5..0000000 --- a/src/Clients/RestHttpRequest.php +++ /dev/null @@ -1,92 +0,0 @@ -setUri($uri); - $this->setMethod($method); - $this->setPayload(json_encode($payload)); - - $this->addHeader("Content-Type", "application/json"); - - // Note we don't call the parent constructor as it will try and set the $_url property which isn't - // valid for RestHttpRequests - if ($method == "post" || $method == "put") { - $this->addHeader("Content-Length", strlen($this->getPayload())); - } - } - - /** - * @param mixed $apiUrl - */ - public function setApiUrl($apiUrl) - { - $this->apiUrl = $apiUrl; - } - - /** - * @return mixed - */ - public function getApiUrl() - { - return $this->apiUrl; - } - - /** - * @param mixed $uri - */ - public function setUri($uri) - { - $this->uri = $uri; - } - - /** - * @return mixed - */ - public function getUri() - { - return $this->uri; - } - - public function setUrl($url) - { - throw new ImplementationException("A RestHttpRequest does not support setting the Url directly. Set the Uri and ApiUrl properties separately."); - } - - public function getUrl() - { - $url = rtrim($this->apiUrl, '/') . "/" . ltrim($this->uri, '/'); - - return $url; - } -} \ No newline at end of file diff --git a/src/Clients/TokenAuthenticatedRestClient.php b/src/Clients/TokenAuthenticatedRestClient.php deleted file mode 100644 index 9e8e90d..0000000 --- a/src/Clients/TokenAuthenticatedRestClient.php +++ /dev/null @@ -1,125 +0,0 @@ -tokensUri = $tokensUri; - $this->token = $existingToken; - } - - /** - * For long duration API conversations the token can be persisted externally and set using this method. - * - * @param $token - */ - public function setToken($token) - { - $this->token = $token; - } - - protected function applyAuthenticationDetailsToRequest(HttpRequest $request) - { - if ($this->gettingToken) { - parent::applyAuthenticationDetailsToRequest($request); - return; - } - - if ($this->token == "") { - $this->getToken(); - } - - $this->applyTokenAuthorizationHeader($request); - } - - /** - * A placeholder to be overriden usually to store the token in a session or somewhere similar - * - * @param $token - */ - protected function onTokenReceived($token) - { - - } - - protected final function getToken() - { - $this->gettingToken = true; - - try { - $response = $this->makeRequest($this->getTokenRequest()); - } catch (RestImplementationException $er) { - $this->gettingToken = false; - throw new RestAuthenticationException("The api credentials were rejected."); - } - - $this->gettingToken = false; - $this->token = $this->extractTokenFromResponse($response); - - $this->onTokenReceived($this->token); - } - - /** - * @param mixed $response - * @return string - */ - protected function extractTokenFromResponse($response) - { - return $response->token; - } - - /** - * @return RestHttpRequest - */ - protected function getTokenRequest() - { - return new RestHttpRequest($this->tokensUri, "post", ""); - } - - /** - * @param HttpRequest $request - */ - protected function applyTokenAuthorizationHeader(HttpRequest $request) - { - $request->addHeader("Authorization", "Token token=\"" . $this->token . "\"/"); - } -} diff --git a/src/Entities/SearchCriteriaEntity.php b/src/Entities/SearchCriteriaEntity.php new file mode 100644 index 0000000..f33c2a6 --- /dev/null +++ b/src/Entities/SearchCriteriaEntity.php @@ -0,0 +1,26 @@ +offset = $offset; + $this->pageSize = $pageSize; + $this->sort = $sort; + } +} diff --git a/src/Entities/SearchResponseEntity.php b/src/Entities/SearchResponseEntity.php new file mode 100644 index 0000000..9bfe91a --- /dev/null +++ b/src/Entities/SearchResponseEntity.php @@ -0,0 +1,26 @@ +criteria = $criteria; + } +} diff --git a/src/ErrorHandlers/DefaultErrorHandler.php b/src/ErrorHandlers/DefaultErrorHandler.php new file mode 100644 index 0000000..afbf2e4 --- /dev/null +++ b/src/ErrorHandlers/DefaultErrorHandler.php @@ -0,0 +1,23 @@ +getCode(); + + if (!($code > 199 && $code < 600) || !$code) { + error_log($exception->getMessage() . ' ' . $exception->getFile() . ':' . $exception->getLine()); + $error = 'Something went wrong'; + $code = 500; + } else { + $error = $exception->getMessage(); + } + return $response->withStatus($code, $error); + } +} diff --git a/src/ErrorHandlers/NotFoundHandler.php b/src/ErrorHandlers/NotFoundHandler.php new file mode 100644 index 0000000..77738ea --- /dev/null +++ b/src/ErrorHandlers/NotFoundHandler.php @@ -0,0 +1,14 @@ +withStatus(404); + } +} diff --git a/src/Exceptions/ApiException.php b/src/Exceptions/ApiException.php new file mode 100644 index 0000000..62b7d65 --- /dev/null +++ b/src/Exceptions/ApiException.php @@ -0,0 +1,7 @@ +response = $response; - parent::__construct($message); - } - -} diff --git a/src/Exceptions/UpdateException.php b/src/Exceptions/UpdateException.php deleted file mode 100644 index b7c15dd..0000000 --- a/src/Exceptions/UpdateException.php +++ /dev/null @@ -1,26 +0,0 @@ -addColumn( - new DateTimeColumn("DateModified"), - new DateTimeColumn("DateCreated"), - new BooleanColumn("Deleted") - ); - - $schema->addIndex(new Index("DateModified", Index::INDEX)); - $schema->addIndex(new Index("Deleted", Index::INDEX)); - } - - /** - * Replaces the standard delete by flagging the entry deleted instead. - */ - public function delete() - { - if ($this->isNewRecord()) { - throw new DeleteModelException("New models can't be deleted."); - } - - $this->beforeDelete(); - $this->raiseEvent("BeforeDelete"); - - $this->Deleted = true; - $this->save(); - - $this->afterDelete(); - $this->raiseEvent("AfterDelete"); - } - - protected function beforeSave() - { - parent::beforeSave(); - - $this->DateModified = "now"; - - if ($this->isNewRecord()) { - $this->DateCreated = "now"; - } - } -} diff --git a/src/Presenters/TestHarnessPresenter.php b/src/Presenters/TestHarnessPresenter.php deleted file mode 100644 index 8a6785f..0000000 --- a/src/Presenters/TestHarnessPresenter.php +++ /dev/null @@ -1,65 +0,0 @@ -view->attachEventHandler("SubmitRequest", function () { - $client = new TokenAuthenticatedRestClient( - $this->model->ApiUrl, - $this->model->Username, - $this->model->Password, - "/tokens" - ); - - $request = new RestHttpRequest($this->model->Uri, $this->model->Method, $this->model->RequestPayload); - - if ($this->model->Since) { - $since = new RhubarbDateTime($this->model->Since); - - $request->addHeader("If-Modified-Since", $since->format("r")); - } - - $this->lastResponse = $client->makeRequest($request); - }); - } - - protected function createView() - { - return new TestHarnessView(); - } - - protected function applyModelToView() - { - parent::applyModelToView(); - - $this->view->setResponse($this->lastResponse); - } -} \ No newline at end of file diff --git a/src/Presenters/TestHarnessView.php b/src/Presenters/TestHarnessView.php deleted file mode 100644 index e3dbe81..0000000 --- a/src/Presenters/TestHarnessView.php +++ /dev/null @@ -1,91 +0,0 @@ -response = $response; - } - - protected function createSubLeaves() - { - parent::createPresenters(); - - $this->registerSubLeaf( - new TextBox("ApiUrl"), - new TextBox("Uri"), - new TextBox("Username"), - new TextBox("Password"), - $method = new DropDown("Method"), - new TextArea("RequestPayload", 5, 60), - new Date("Since"), - new Button("Submit", "Submit", function () { - $this->raiseEvent("SubmitRequest"); - }) - ); - - $method->setSelectionItems( - [ - ["get"], - ["post"], - ["put"], - ["head"], - ["delete"] - ] - ); - } - - protected function printViewContent() - { - parent::printViewContent(); - - $this->layoutItemsWithContainer( - "", - [ - "ApiUrl", - "Username", - "Password", - "Uri", - "Method", - "RequestPayload", - "Since", - "Submit" - ] - ); - - if ($this->response) { - print "

Response

"; - - $pretty = json_encode($this->response, JSON_PRETTY_PRINT); - - print "
" . $pretty . "
"; - } - } -} diff --git a/src/Resources/ApiDescriptionResource.php b/src/Resources/ApiDescriptionResource.php deleted file mode 100644 index 9a01287..0000000 --- a/src/Resources/ApiDescriptionResource.php +++ /dev/null @@ -1,25 +0,0 @@ -createItemResource($resourceIdentifier); - $resource->setUrlHandler($this->urlHandler); - - return $resource; - } - - /** - * Test to see if the given resource identifier exists in the collection of resources. - * - * @param $resourceIdentifier - * @return True if it exists, false if it does not. - */ - public function containsResourceIdentifier($resourceIdentifier) - { - // This will be very slow however the base implementation can do nothing else. - // Inheritors of this class should override this if they can do this faster! - $items = $this->getItems(0, false); - - foreach ($items[0] as $item) { - if ($item->_id = $resourceIdentifier) { - return true; - } - } - - return false; - } - - protected function getResourceName() - { - return str_replace("Resource", "", basename(str_replace("\\", "/", get_class($this)))); - } - - protected function listItems($asSummary = false) - { - Log::performance("Building GET response", "RESTAPI"); - - $request = Request::current(); - - $rangeHeader = $request->server("HTTP_RANGE"); - - $rangeStart = 0; - $rangeEnd = $this->maximumCollectionSize === false ? false : $this->maximumCollectionSize - 1; - - if ($rangeHeader) { - $rangeHeader = str_replace("resources=", "", $rangeHeader); - $rangeHeader = str_replace("bytes=", "", $rangeHeader); - - $parts = explode("-", $rangeHeader); - - $fromText = false; - $toText = false; - - if (sizeof($parts) > 0) { - $fromText = (int)$parts[0]; - } - - if (sizeof($parts) > 1) { - $toText = (int)$parts[1]; - } - - if ($fromText) { - $rangeStart = $fromText; - } - - if ($toText) { - if ($rangeEnd === false) { - $rangeEnd = $toText; - } else { - $rangeEnd = min($toText, $rangeStart + ($this->maximumCollectionSize - 1)); - } - } - } - - $since = null; - - if ($request->header("If-Modified-Since") != "") { - $since = new RhubarbDateTime($request->header("If-Modified-Since")); - } - - Log::performance("Getting items for collection", "RESTAPI"); - - list($items, $count) = $asSummary ? - $this->summarizeItems($rangeStart, $rangeEnd, $since) : - $this->getItems($rangeStart, $rangeEnd, $since); - - Log::performance("Wrapping GET response", "RESTAPI"); - - return $this->createCollectionResourceForItems($items, $rangeStart, min($rangeEnd, ($count <= 0 ? 0 : $count - 1 )), $count); - } - - /** - * Creates a valid collection response from a list of objects. - * - * @param Collection|\stdClass[] $items - * @param int $from - * @param int $to - * @param int $count - * @return \stdClass - */ - protected function createCollectionResourceForItems($items, $from, $to, $count) - { - $resource = parent::get(); - $resource->items = $items; - $resource->count = $count; - $resource->range = new \stdClass(); - $resource->range->from = $from; - $resource->range->to = $to; - - return $resource; - } - - public function summary() - { - return $this->listItems(true); - } - - public function get() - { - return $this->listItems(); - } - - /** - * Implement getItems to return the items for the collection. - * - * @param int $rangeStart First index of the items to be returned - * @param int|bool $rangeEnd Last index. False if the items should not be limited - * @param RhubarbDateTime $since Optionally a date and time to filter the items for those modified since. - * @return array Return a two item array, the first is the items within the range. The second is the overall - * number of items available - */ - protected function getItems($rangeStart, $rangeEnd, RhubarbDateTime $since = null) - { - return [[], 0]; - } - - /** - * Implement getItems to return the items as a summary for the collection. - * - * @param int $rangeStart First index of the items to be returned - * @param int|bool $rangeEnd Last index. False if the items should not be limited - * @param RhubarbDateTime $since Optionally a date and time to filter the items for those modified since. - * @return array Return a two item array, the first is the items within the range. The second is the overall - * number of items available - */ - protected function summarizeItems($rangeStart, $rangeEnd, RhubarbDateTime $since = null) - { - return [[], 0]; - } -} diff --git a/src/Resources/ItemRestResource.php b/src/Resources/ItemRestResource.php deleted file mode 100644 index 7e81576..0000000 --- a/src/Resources/ItemRestResource.php +++ /dev/null @@ -1,71 +0,0 @@ -setID($resourceIdentifier); - } - - protected function setID($id) - { - $this->id = $id; - } - - protected function getHref() - { - $handler = $this->urlHandler->getParentHandler(); - - // If we have a canonical URL due to a root registration we should give that - // in preference to the current URL. - if ($handler instanceof RestApiRootHandler) { - $href = $handler->getCanonicalUrlForResource($this); - - return $href . "/" . $this->id; - } - - if ($this->invokedByUrl) { - return parent::getHref() . "/" . $this->id; - } - - return false; - } - - protected function getSkeleton() - { - $skeleton = parent::getSkeleton(); - - if ($this->id) { - $skeleton->_id = $this->id; - } - - return $skeleton; - } -} \ No newline at end of file diff --git a/src/Resources/ModelRestResource.php b/src/Resources/ModelRestResource.php deleted file mode 100644 index 10e2062..0000000 --- a/src/Resources/ModelRestResource.php +++ /dev/null @@ -1,681 +0,0 @@ -getModel(); - - $extract = []; - - $relationships = null; - - foreach ($columns as $label => $column) { - - $columnModel = $model; - - $modifier = ""; - $urlSuffix = false; - - $apiLabel = (is_numeric($label)) ? $column : $label; - - if (is_callable($column)) { - $value = $column(); - } else { - if (stripos($column, ":") !== false) { - $parts = explode(":", $column); - $column = $parts[0]; - - if (is_numeric($label)) { - $apiLabel = $column; - } - - $modifier = strtolower($parts[1]); - - if (sizeof($parts) > 2) { - $urlSuffix = $parts[2]; - } - } - - if (stripos($column, ".") !== false) { - $parts = explode(".", $column, 2); - - $column = $parts[0]; - $columnModel = $columnModel->$column; - - $column = $parts[1]; - - if (is_numeric($label)) { - $apiLabel = $parts[1]; - } - } - - if ($columnModel) { - $value = $columnModel->$column; - } else { - $value = ""; - } - } - - if (is_object($value)) { - // We can't pass objects back through the API! Let's get a JSON friendly structure instead. - if (!($value instanceof Model) && !($value instanceof Collection)) { - // This seems strange however if we just used json_encode we'd be passing the encoded version - // back as a string. We decode to get the original structure back again. - $value = json_decode(json_encode($value)); - } else { - $navigationResource = false; - $navigationResourceIsCollection = false; - - if ($value instanceof Model) { - - $navigationResource = $this->getRestResourceForModel($value); - - if ($navigationResource === false) { - throw new RestImplementationException(print_r($value, true)); - continue; - } - } - - if ($value instanceof Collection) { - $navigationResource = $this->getRestResourceForModelName(SolutionSchema::getModelNameFromClass($value->getModelClassName())); - - if ($navigationResource === false) { - continue; - } - - $navigationResourceIsCollection = true; - $navigationResource->setModelCollection($value); - } - - if ($navigationResource) { - switch ($modifier) { - case "summary": - $value = $navigationResource->summary(); - break; - case "link": - $link = $navigationResource->link(); - - if (!isset($link->href) || $navigationResourceIsCollection) { - - if (!$urlSuffix) { - throw new RestImplementationException("No canonical URL for " . get_class($navigationResource) . " and no URL suffix supplied for property " . $apiLabel); - } - - $ourHref = $this->getHref(); - - // Override the href with this appendage instead. - $link->href = $ourHref . $urlSuffix; - } - - $value = $link; - - break; - default: - $value = $navigationResource->get(); - break; - } - } - } - } - - if ($value !== null || $this->includeNullItems) { - $extract[$apiLabel] = $value; - } - } - - return $extract; - } - - /** - * Override to control the columns returned in HEAD requests - * - * @return string[] - */ - protected function getSummaryColumns() - { - $columns = []; - - $model = $this->getSampleModel(); - $columnName = $model->getLabelColumnName(); - - if ($columnName != "") { - $columns[] = $columnName; - } - - return $columns; - } - - /** - * Override to control the columns exposed by the rest resource (output in GET requests, accepted via PUT/POST) - * - * @return string[] - */ - protected function getColumns() - { - return $this->getSummaryColumns(); - } - - public function summary() - { - $resource = $this->getSkeleton(); - - $data = $this->transformModelToArray($this->getSummaryColumns()); - - foreach ($data as $key => $value) { - $resource->$key = $value; - } - - return $resource; - } - - public function get() - { - if (!$this->model) { - return parent::get(); - } - - $resource = $this->getSkeleton(); - - $data = $this->transformModelToArray($this->getColumns()); - - foreach ($data as $key => $value) { - $resource->$key = $value; - } - - return $resource; - } - - public function head() - { - if (!$this->model) { - return parent::get(); - } - - $resource = $this->getSkeleton(); - - $data = $this->transformModelToArray($this->getSummaryColumns()); - - foreach ($data as $key => $value) { - $resource->resource->$key = $value; - } - - return $resource; - } - - /** - * Gets the Model object used to populate the REST resource - * - * This is public as it is sometimes required by child handlers to check security etc. - * - * @throws \Rhubarb\RestApi\Exceptions\RestImplementationException - * @return Model|null - */ - public function getModel() - { - if (!$this->model) { - throw new RestImplementationException("There is no matching resource for this url"); - } - - return $this->model; - } - - /** - * Sets the model that should be used for the operations of this resource. - * - * This is normally only used by collections for efficiency (to avoid constructing additional objects) - * - * @param Model $model - */ - public function setModel(Model $model) - { - $this->model = $model; - } - - /** - * Called by a parent resource to pass the child resource a direct list of items for the collection - * - * @param Collection $collection - */ - protected function setModelCollection(Collection $collection) - { - $this->collection = $collection; - } - - /** - * Override to response to the event of a model being updated through a PUT before the model is saved - * - * @param $model - * @param $restResource - */ - protected function beforeModelUpdated($model, $restResource) - { - - } - - /** - * Override to response to the event of a model being updated through a PUT after the model is saved - * - * @param $model - * @param $restResource - */ - protected function afterModelUpdated($model, $restResource) - { - - } - - protected function getSkeleton() - { - $skeleton = parent::getSkeleton(); - - if ($this->model) { - $skeleton->_id = $this->model->UniqueIdentifier; - } - - return $skeleton; - } - - public function put($restResource) - { - try { - $model = $this->getModel(); - $this->importModelData($model, $restResource); - - $this->beforeModelUpdated($model, $restResource); - - $model->save(); - - $this->afterModelUpdated($model, $restResource); - - return true; - } catch (RecordNotFoundException $er) { - throw new UpdateException("That record could not be found."); - } catch (\Exception $er) { - throw new UpdateException($er->getMessage()); - } - } - - public function delete() - { - try { - $model = $this->getModel(); - $model->delete(); - - return true; - } catch (\Exception $er) { - return false; - } - } - - public static function registerModelToResourceMapping($modelName, $resourceClassName) - { - self::$modelToResourceMapping[$modelName] = $resourceClassName; - } - - public function getRestResourceForModel(Model $model) - { - $modelName = $model->getModelName(); - - if (!isset(self::$modelToResourceMapping[$modelName])) { - throw new RestImplementationException("The model $modelName does not have an associated rest resource."); - } - - $class = self::$modelToResourceMapping[$modelName]; - - /** @var RestResource $resource */ - $resource = new $class(); - $resource->setUrlHandler($this->urlHandler); - - if ($resource instanceof ModelRestResource) { - /** @var ModelRestResource $resource */ - $resource = $resource->getItemResourceForModel($model); - } - - return $resource; - } - - /** - * @param string $modelName - * @return bool|ModelRestResource - */ - public function getRestResourceForModelName($modelName) - { - if (!isset(self::$modelToResourceMapping[$modelName])) { - return false; - } - - $class = self::$modelToResourceMapping[$modelName]; - - /** @var ModelRestResource $resource */ - $resource = new $class(); - $resource->setUrlHandler($this->urlHandler); - return $resource; - } - - public static function clearRestResourceMapping() - { - self::$modelToResourceMapping = []; - } - - protected function getSampleModel() - { - return SolutionSchema::getModel($this->getModelName()); - } - - /** - * @return \Rhubarb\Stem\Collections\Collection|null - */ - public function getModelCollection() - { - if ($this->collection) { - return $this->collection; - } - - $collection = $this->createModelCollection(); - - Log::performance("Filtering collection", "RESTAPI"); - - $this->filterModelCollectionAsContainer($collection); - $this->filterModelCollectionForSecurity($collection); - $this->filterModelCollectionForQueries($collection); - - if ($this->parentResource instanceof ModelRestResource) { - $this->parentResource->filterModelCollectionAsContainer($collection); - } - - return $collection; - } - - /** - * Override to filter a model collection to apply any necessary filters only when this is the specific resource being fetched - * - * @param Collection $collection - */ - public function filterModelCollectionForQueries(Collection $collection) - { - - } - - /** - * Override to filter a model collection to apply any necessary filters only when this is the REST collection of the specific resource being fetched - * - * @param Collection $collection - */ - public function filterModelCollectionAsContainer(Collection $collection) - { - } - - public function filterModelCollectionForModifiedSince(Collection $collection, RhubarbDateTime $since) - { - throw new RestImplementationException("A collection filtered by modified date was requested however this resource does not support it."); - } - - /** - * Override to filter a model collection generated by a ModelRestCollection - * - * Normally used by root collections to filter based on authentication permissions. - * - * @param Collection $collection - */ - public function filterModelCollectionForSecurity(Collection $collection) - { - - } - - /** - * Returns the name of the model to use for this resource. - * - * @return string - */ - public abstract function getModelName(); - - protected function createModelCollection() - { - return new RepositoryCollection($this->getModelName()); - } - - public function containsResourceIdentifier($resourceIdentifier) - { - $collection = clone $this->getModelCollection(); - - $this->filterModelCollectionAsContainer($collection); - - $collection->filter(new Equals($collection->getModelSchema()->uniqueIdentifierColumnName, $resourceIdentifier)); - - if (count($collection) > 0) { - return true; - } - - return false; - } - - protected function summarizeItems($rangeStart, $rangeEnd, RhubarbDateTime $since = null) - { - return $this->fetchItems($rangeStart, $rangeEnd, $since, true); - } - - protected function getItems($rangeStart, $rangeEnd, RhubarbDateTime $since = null) - { - return $this->fetchItems($rangeStart, $rangeEnd, $since); - } - - private function fetchItems($rangeStart, $rangeEnd, RhubarbDateTime $since = null, $asSummary = false) - { - $collection = $this->getModelCollection(); - - if ($since !== null) { - $this->filterModelCollectionForModifiedSince($collection, $since); - } - - $collectionSize = count($collection); - if ($rangeStart > 0 || $rangeEnd !== false) { - if ($rangeEnd === false) { - $pageSize = $collectionSize - $rangeStart; - } else { - $pageSize = ($rangeEnd - $rangeStart) + 1; - } - $collection->setRange($rangeStart, min($pageSize, $collectionSize)); - } - - $items = []; - - Log::performance("Starting collection iteration", "RESTAPI"); - - foreach ($collection as $model) { - $resource = $this->getItemResourceForModel($model); - - $modelStructure = ($asSummary) ? $resource->summary() : $resource->get(); - $items[] = $modelStructure; - } - - return [$items, $collectionSize]; - } - - protected function getItemResourceForModel($model) - { - $resource = clone $this; - $resource->parentResource = $this; - $resource->setModel($model); - - return $resource; - } - - protected function getHref() - { - $handler = $this->urlHandler->getParentHandler(); - - $root = false; - - // If we have a canonical URL due to a root registration we should give that - // in preference to the current URL. - if ($handler instanceof RestApiRootHandler) { - $root = $handler->getCanonicalUrlForResource($this); - } - - if (!$root && !$this->invokedByUrl) { - return false; - } - - $root = $this->urlHandler->getUrl(); - - if ($this->model) { - return $root . "/" . $this->model->UniqueIdentifier; - } - - return $root; - } - - public function post($restResource) - { - try { - $newModel = SolutionSchema::getModel($this->getModelName()); - - if (is_array($restResource)) { - $this->importModelData($newModel, $restResource); - } - $this->beforeModelCreated($newModel, $restResource); - - $newModel->save(); - $this->model = $newModel; - - $this->afterModelCreated($newModel, $restResource); - - return $this->getItemResourceForModel($newModel)->get(); - } catch (RecordNotFoundException $er) { - throw new InsertException("That record could not be found."); - } catch (\Exception $er) { - throw new InsertException($er->getMessage()); - } - } - - /** - * Override to respond to the event of a new model being created through a POST - * - * @param $model - * @param $restResource - */ - protected function afterModelCreated($model, $restResource) - { - - } - - /** - * Override to perform additional actions on a model before save, eg setup required relationships from parent resources. - * Called when data has been imported into $model from $restResource, but before the model is saved. - * - * @param Model $model - * @param $restResource - */ - protected function beforeModelCreated($model, $restResource) - { - - } - - /** - * @param Model $model - * @param array $modelData - */ - protected function importModelData($model, $modelData) - { - $columns = $this->getColumns(); - - foreach($modelData as $key => $value){ - if (in_array($key, $columns)){ - $model->$key = $value; - } - } - } - - /** - * Returns the ItemRestResource for the $resourceIdentifier contained in this collection. - * - * @param $resourceIdentifier - * @return ItemRestResource - * @throws RestImplementationException Thrown if the item could not be found. - */ - public function createItemResource($resourceIdentifier) - { - try { - $model = SolutionSchema::getModel($this->getModelName(), $resourceIdentifier); - } catch (RecordNotFoundException $er) { - throw new RestResourceNotFoundException(self::class, $resourceIdentifier); - } - - return $this->getItemResourceForModel($model); - } -} diff --git a/src/Resources/MultiPartFormDataPayloadTrait.php b/src/Resources/MultiPartFormDataPayloadTrait.php deleted file mode 100644 index 0700b1a..0000000 --- a/src/Resources/MultiPartFormDataPayloadTrait.php +++ /dev/null @@ -1,36 +0,0 @@ -parentResource = $parentResource; - } - - /** - * Set to true by a RestResourceHandler that is invoking this resource directly. - * - * @param $invokedByUrl - */ - public function setInvokedByUrl($invokedByUrl) - { - $this->invokedByUrl = $invokedByUrl; - } - - public function setUrlHandler(UrlHandler $handler) - { - $this->urlHandler = $handler; - } - - public function getResponseHeaders() - { - return $this->responseHeaders; - } - - protected function getResourceName() - { - return str_replace("Resource", "", basename(str_replace("\\", "/", get_class($this)))); - } - - /** - * @param string $url - */ - public function setHref($url) - { - $this->href = $url; - } - - public function summary() - { - return $this->getSkeleton(); - } - - protected function link() - { - $encapsulatedForm = new \stdClass(); - $encapsulatedForm->rel = $this->getResourceName(); - - $href = $this->getHref(); - - if ($href) { - $encapsulatedForm->href = $href; - } - - return $encapsulatedForm; - } - - protected function getHref() - { - $handler = $this->urlHandler->getParentHandler(); - - $root = false; - - // If we have a canonical URL due to a root registration we should give that - // in preference to the current URL. - if ($handler instanceof RestApiRootHandler) { - $root = $handler->getCanonicalUrlForResource($this); - } - - if (!$root && $this->invokedByUrl) { - $root = $this->urlHandler->getUrl(); - } - - return $root; - } - - /** - * Called when a resource can't be returned due to an error state. - * - * @param string $message - * @return \stdClass - */ - protected function buildErrorResponse($message = "") - { - $date = new RhubarbDateTime("now"); - - $response = new \stdClass(); - $response->result = new \stdClass(); - $response->result->status = false; - $response->result->timestamp = $date->format("c"); - $response->result->message = $message; - - return $response; - } - - protected function getSkeleton() - { - $encapsulatedForm = new \stdClass(); - - $href = $this->getHref(); - - if ($href) { - $encapsulatedForm->_href = $href; - } - - return $encapsulatedForm; - } - - public function get() - { - return $this->getSkeleton(); - } - - public function head() - { - // HEAD requests must behave the same as get - return $this->get(); - } - - public function delete() - { - throw new RestImplementationException(); - } - - public function put($restResource) - { - throw new RestImplementationException(); - } - - public function post($restResource) - { - throw new RestImplementationException(); - } - - /** - * Validate that the payload is valid for the request. - * - * This is not the only chance to validate the payload. Throwing an exception - * during the act of handling the request will cause an error response to be - * given, however it does provide a nice place to do it. - * - * If using ModelRestResource you don't need to validate properties which your - * model validation will handle anyway. - * - * Throw a RestPayloadValidationException if the validation should fail. - * - * The base implementation simply checks that there is an actual array payload for - * put and post operations. - * - * @param mixed $payload - * @param string $method - * @throws RestRequestPayloadValidationException - */ - public function validateRequestPayload($payload, $method) - { - switch ($method) { - case "post": - case "put": - if (!is_array($payload)) { - throw new RestRequestPayloadValidationException("POST and PUT options require a JSON encoded resource object in the body of the request."); - } - - break; - } - } - - /** - * To support child resource URLs that have a relationship with this parent you must override this method and - * take responsibility for creating the resource here. - * - * @param $childUrlFragment - * @return RestResource|bool - * @throws RestImplementationException - */ - public function getChildResource($childUrlFragment) - { - return false; - } -} diff --git a/src/Response/TokenAuthorisationRequiredResponse.php b/src/Response/TokenAuthorisationRequiredResponse.php deleted file mode 100644 index 281cc74..0000000 --- a/src/Response/TokenAuthorisationRequiredResponse.php +++ /dev/null @@ -1,31 +0,0 @@ -setHeader("WWW-authenticate", "Token \"API\""); - } -} \ No newline at end of file diff --git a/src/RhubarbApiModule.php b/src/RhubarbApiModule.php new file mode 100644 index 0000000..e898fbe --- /dev/null +++ b/src/RhubarbApiModule.php @@ -0,0 +1,14 @@ +app->getContainer(); + $container['errorHandler'] = $container['phpErrorHandler'] = function () { + return new DefaultErrorHandler(); + }; + $container['notFoundHandler'] = function () { + return new NotFoundHandler(); + }; + } + + protected function registerMiddleware() + { + $this->app->add(function (Request $request, Response $response, callable $next) { + $uri = $request->getUri(); + $path = $uri->getPath(); + // ensure all routes have a trailing slash for simplified router configuration + if ($path !== '/' && substr($path, -1) !== '/') { + $uri = $uri->withPath($path . '/'); + return $next($request->withUri($uri), $response); + } + + return $next($request, $response); + }); + } + + final protected function registerModule(RhubarbApiModule $module) + { + $module->registerErrorHandlers($this->app); + $module->registerMiddleware($this->app); + } + + /** + * @return RhubarbApiModule[] + */ + protected function registerModules() + { + return []; + } + + abstract protected function registerRoutes(); + + final public function initialise(): App + { + $this->app = new App(); + $this->registerErrorHandlers(); + $this->registerMiddleware(); + foreach ($this->registerModules() as $module) { + $this->registerModule($module); + } + $this->registerRoutes(); + return $this->app; + } +} diff --git a/src/UrlHandlers/BinaryRestResourceHandler.php b/src/UrlHandlers/BinaryRestResourceHandler.php deleted file mode 100644 index 42f6b2c..0000000 --- a/src/UrlHandlers/BinaryRestResourceHandler.php +++ /dev/null @@ -1,74 +0,0 @@ - $binaryResponse, - 'image/jpeg' => $binaryResponse, - 'image/png' => $binaryResponse, - 'image/gif' => $binaryResponse, - 'application/octet-stream' => $binaryResponse, - 'application/json' => new JsonResponse($this), - ]; - } - - protected function handleGet(WebRequest $request, Response $response) - { - Log::debug("GET " . Request::current()->urlPath, "RESTAPI"); - - try { - $resource = $this->getRestResource(); - $resource->setInvokedByUrl(true); - Log::performance("Got resource", "RESTAPI"); - $resourceOutput = $resource->get(); - Log::performance("Got response", "RESTAPI"); - - $fileName = ''; - if ($resource instanceof BinaryRestResource) { - $fileName = $resource->getFileName(); - $contentType = $resource->getContentType(); - } else { - $contentType = $request->getAcceptsRequestMimeType(); - } - - $response = new BinaryResponse($this, $resourceOutput, $contentType, $fileName); - } catch (RestResourceNotFoundException $er) { - $response = new JsonResponse($this); - $response->setResponseCode(HttpHeaders::HTTP_STATUS_CLIENT_ERROR_NOT_FOUND); - $response->setResponseMessage("Resource not found"); - $response->setContent($this->buildErrorResponse("The resource could not be found.")); - } catch (RestImplementationException $er) { - $response = new JsonResponse($this); - $response->setResponseCode(HttpHeaders::HTTP_STATUS_SERVER_ERROR_GENERIC); - $response->setContent($this->buildErrorResponse($er->getPublicMessage())); - } - - Log::bulkData("Api response", "RESTAPI", $response->getContent()); - - return $response; - } -} diff --git a/src/UrlHandlers/PostOnlyRestCollectionHandler.php b/src/UrlHandlers/PostOnlyRestCollectionHandler.php deleted file mode 100644 index 51681b7..0000000 --- a/src/UrlHandlers/PostOnlyRestCollectionHandler.php +++ /dev/null @@ -1,29 +0,0 @@ -childUrlHandlers as $childHandler) { - if ($childHandler instanceof RestCollectionHandler || $childHandler instanceof RestResourceHandler) { - - // Register this handler to make sure it's url is known - $this->roots[ltrim($childHandler->getRestResourceClassName(), '\\')] = $url . $childHandler->getUrl(); - } - } - } - - public function getCanonicalUrlForResource(RestResource $resource) - { - $class = ltrim(get_class($resource), '\\'); - - if (isset($this->roots[$class])) { - return $this->roots[$class]; - } - - return false; - } -} \ No newline at end of file diff --git a/src/UrlHandlers/RestCollectionHandler.php b/src/UrlHandlers/RestCollectionHandler.php deleted file mode 100644 index 3d41851..0000000 --- a/src/UrlHandlers/RestCollectionHandler.php +++ /dev/null @@ -1,131 +0,0 @@ -supportedHttpMethods = ["get", "post", "put", "head", "delete"]; - - parent::__construct($collectionClassName, $childUrlHandlers, $supportedHttpMethods); - } - - protected function getMatchingUrlFragment(Request $request, $currentUrlFragment = "") - { - // Overrides the version of this function supplied by CollectionUrlHandling to remove the preference - // for gobbling up trailing slashes in parent url handlers. - - $uri = $currentUrlFragment; - - $this->matchedUrl = $this->url; - - if (preg_match("|^" . $this->url . "/?([^/]+)/?|", $uri, $match)) { - - $childUrls = []; - - foreach ($this->childUrlHandlers as $child) { - $childUrls[] = $child->getUrl(); - } - - // Check the matched item is not actually a child handler - let's not extract this as a resource - // identifier if it is. - if (!in_array($match[1], $childUrls) && - !in_array("/" . $match[1], $childUrls) - ) { - $this->resourceIdentifier = $match[1]; - $this->isCollection = false; - - $this->matchedUrl = rtrim($match[0], "/"); - } - } - - return $this->matchedUrl; - } - - protected function getRestResource() - { - $parentResource = $this->getParentResource(); - - if ($parentResource !== null) { - $childResource = $parentResource->getChildResource($this->matchingUrl); - if ($childResource) { - $childResource->setUrlHandler($this); - return $childResource; - } - } - - // We will either be returning a resource or a collection. - // However even if returning a resource, we first need to instantiate the collection - // to verify the resource is one of the items in the collection, in case it has been - // filtered for security reasons. - $class = $this->apiResourceClassName; - - /** - * @var CollectionRestResource $resource - */ - $resource = new $class($this->getParentResource()); - $resource->setInvokedByUrl(true); - $resource->setUrlHandler($this); - - if ($this->isCollection()) { - return $resource; - } else { - if (!$this->resourceIdentifier) { - throw new RestImplementationException("The resource identifier for was invalid."); - } - - try { - // The api resource attached to a collection url handler can be either an ItemRestResource or - // a CollectionRestResource. At this point we need an ItemRestResource so if we have a collection - // we need to ask it for the item. - if ($resource instanceof CollectionRestResource) { - $itemResource = $resource->getItemResource($this->resourceIdentifier); - } else { - $itemResource = new $class($this->resourceIdentifier); - } - - $itemResource->setUrlHandler($this); - $itemResource->setInvokedByUrl(true); - return $itemResource; - } catch (RestResourceNotFoundException $er) { - throw $er; - } catch (RestImplementationException $er) { - throw new RestImplementationException("That resource identifier does not exist in the collection."); - } - } - } -} diff --git a/src/UrlHandlers/RestHandler.php b/src/UrlHandlers/RestHandler.php deleted file mode 100644 index 36a26c3..0000000 --- a/src/UrlHandlers/RestHandler.php +++ /dev/null @@ -1,248 +0,0 @@ - new HtmlResponse($this)]; - } - - /** - * Returns an array of the HTTP methods this handler supports. - * - * @return array - */ - protected function getSupportedHttpMethods() - { - return ["get"]; - } - - /** - * If you require an authenticated user to handle the request, you can return the name of an authentication provider class - * - * Alternatively if a default authentication provider class name has been set this will be used instead. - * - * @see RestAuthenticationProvider::setDefaultAuthenticationProviderClassName() - * @return null - */ - protected function createAuthenticationProvider() - { - return null; - } - - protected final function getAuthenticationProvider() - { - $provider = $this->createAuthenticationProvider(); - - // Allow the handler to return false to indicate the url should be publicly accessible. - if ($provider === false) { - return null; - } - - if ($provider != null) { - return $provider; - } - - try { - return AuthenticationProvider::getProvider(); - } catch( ClassMappingException $er ){} - - return null; - } - - protected function authenticate(Request $request) - { - $authenticationProvider = $this->getAuthenticationProvider(); - - if ($authenticationProvider != null) { - $response = $authenticationProvider->authenticate($request); - - if ($response instanceof Response) { - throw new ForceResponseException($response); - } - - if ($response) { - Log::debug("Authentication Succeeded", "RESTAPI"); - return true; - } - - Log::warning("Authentication Failed", "RESTAPI"); - - return false; - } - - return true; - } - - protected function generateResponseForRequest($request = null, $currentUrlFragment = "") - { - if (!($request instanceof WebRequest)) { - throw new RestImplementationException("Rest handlers can only process Web Requests"); - } - - try { - if (!$this->authenticate($request)) { - return new NotAuthorisedResponse(); - } - } catch (ForceResponseException $ex) { - Log::warning("Authentication Failed: Forcing 401 Response", "RESTAPI"); - return $ex->getResponse(); - } - - $types = $this->getSupportedMimeTypes(); - $methods = $this->getSupportedHttpMethods(); - - $typeString = $request->getAcceptsRequestMimeType(); - - $type = false; - - $method = strtolower($request->server("REQUEST_METHOD")); - - if ($method == "") { - $method = "get"; - } - - foreach ($types as $possibleType => $match) { - if (stripos($typeString, $possibleType) !== false) { - $type = $possibleType; - // First match wins - break; - } - } - - if (!$type) { - return false; - } - - if (!isset($types[$type])) { - Log::warning("Rest url doesn't support " . $type, "RESTAPI"); - return false; - } - - // If GET is allowed then HEAD must also be allowed. - if ($method == "head" && !in_array($method, $methods) && in_array("get", $methods)) { - $methods[] = "head"; - } - - if (!in_array($method, $methods)) { - Log::warning("Rest url doesn't support " . $method, "RESTAPI"); - - $this->handleInvalidMethod($method); - } - - $correctMethodName = 'handle' . ucfirst($method); - - if (!method_exists($this, $correctMethodName)) { - throw new RestImplementationException("The REST end point `" . $correctMethodName . "` could not be found in handler `" . get_class($this) . "`"); - } - - return call_user_func([$this, $correctMethodName], $request, $this->createResponseObject()); - } - - /** - * Override to handle the case where an HTTP method is unsupported. - * - * This should throw a ForceResponseException - * - * @param $method - * @throws \Rhubarb\Crown\Exceptions\ForceResponseException - */ - protected function handleInvalidMethod($method) - { - $emptyResponse = new Response(); - $emptyResponse->setHeader("HTTP/1.1 405 Method $method Not Allowed", false); - $emptyResponse->setHeader("Allow", implode(", ", $this->getSupportedHttpMethods())); - - throw new ForceResponseException($emptyResponse); - } - - public function generateResponseForException(RhubarbException $er) - { - $date = new RhubarbDateTime("now"); - - $response = new \stdClass(); - $response->result = new \stdClass(); - $response->result->status = false; - $response->result->timestamp = $date->format("c"); - $response->result->message = $er->getPrivateMessage(); - - $response = $this->createResponseObject(); - $response->setContent($response); - $response->setResponseCode(Response::HTTP_STATUS_SERVER_ERROR_GENERIC); - - return $response; - } - - protected function createResponseObject() - { - /** @var WebRequest $request */ - $request = Request::current(); - $accepts = $request->getAcceptsRequestMimeType(); - $types = $this->getSupportedMimeTypes(); - if (isset($types[$accepts])) { - return clone $types[$accepts]; - } else { - return new JsonResponse(); - } - } -} diff --git a/src/UrlHandlers/RestResourceHandler.php b/src/UrlHandlers/RestResourceHandler.php deleted file mode 100644 index 4805539..0000000 --- a/src/UrlHandlers/RestResourceHandler.php +++ /dev/null @@ -1,308 +0,0 @@ -apiResourceClassName = $resourceClassName; - - if ($supportedHttpMethods != null) { - $this->supportedHttpMethods = $supportedHttpMethods; - } - - parent::__construct($childUrlHandlers); - } - - /** - * @return array|string - */ - public function getRestResourceClassName() - { - return $this->apiResourceClassName; - } - - /** - * Gets the RestResource object - * - * @return RestResource - */ - protected function getRestResource() - { - $parentResource = $this->getParentResource(); - - if ($parentResource !== null) { - $childResource = $parentResource->getChildResource($this->matchingUrl); - if ($childResource) { - $childResource->setUrlHandler($this); - return $childResource; - } - } - - $className = $this->apiResourceClassName; - /** @var RestResource $resource */ - $resource = new $className($this->getParentResource()); - $resource->setUrlHandler($this); - - return $resource; - } - - protected function getSupportedHttpMethods() - { - return $this->supportedHttpMethods; - } - - protected function getSupportedMimeTypes() - { - $jsonResponse = new JsonResponse($this); - $xmlResponse = new XmlResponse($this); - return [ - 'text/html' => $jsonResponse, - 'application/json' => $jsonResponse, - 'text/xml' => $xmlResponse, - 'application/xml' => $xmlResponse, - ]; - } - - protected function getRequestPayload() - { - $request = Request::current(); - $payload = $request->getPayload(); - - if ($payload instanceof \stdClass) { - $payload = get_object_vars($payload); - } - - Log::bulkData("Payload received", "RESTAPI", $payload); - - return $payload; - } - - protected function handleInvalidMethod($method) - { - $response = $this->createResponseObject(); - $response->setResponseCode(Response::HTTP_STATUS_CLIENT_ERROR_METHOD_NOT_ALLOWED); - $response->setContent( - $this->buildErrorResponse("This API resource does not support the `$method` HTTP method. Supported methods: " . - implode(", ", $this->getSupportedHttpMethods())) - ); - $response->setHeader("HTTP/1.1 405 Method $method Not Allowed", false); - $response->setHeader("Allow", implode(", ", $this->getSupportedHttpMethods())); - - throw new ForceResponseException($response); - } - - protected function handleGet(WebRequest $request, Response $response) - { - Log::debug("GET " . $request->urlPath, "RESTAPI"); - - try { - $resource = $this->getRestResource(); - $resource->setInvokedByUrl(true); - $resource->validateRequestPayload($this->getRequestPayload(), 'get'); - Log::performance("Got resource", "RESTAPI"); - $resourceOutput = $resource->get(); - Log::performance("Got response", "RESTAPI"); - $response->setContent($resourceOutput); - $response->addHeaders($resource->getResponseHeaders()); - } catch (RestResourceNotFoundException $er) { - $response->setResponseCode(Response::HTTP_STATUS_CLIENT_ERROR_NOT_FOUND); - $response->setResponseMessage("Resource not found"); - $response->setContent($this->buildErrorResponse("The resource could not be found.")); - } catch (RestImplementationException $er) { - $response->setResponseCode(Response::HTTP_STATUS_SERVER_ERROR_GENERIC); - $response->setContent($this->buildErrorResponse($er->getPublicMessage())); - } - - Log::bulkData("Api response", "RESTAPI", $response->getContent()); - - return $response; - } - - protected function handleHead(WebRequest $request, Response $response) - { - Log::debug("HEAD " . Request::current()->urlPath, "RESTAPI"); - - // HEAD requests must be identical in their consequences to a GET so we have to incur - // the overhead of actually doing a GET transaction. - $response = $this->handleGet($request, $response); - $response->setContent(''); - - return $response; - } - - protected function handlePut(WebRequest $request, Response $response) - { - Log::debug("PUT " . Request::current()->urlPath, "RESTAPI"); - - try { - $resource = $this->getRestResource(); - $payload = $this->getRequestPayload(); - $resource->validateRequestPayload($payload, "put"); - - $responseContent = $resource->put($payload, $this); - if ($responseContent) { - if ($responseContent === true) { - $response->setContent($this->buildSuccessResponse("The PUT operation completed successfully")); - } else { - $response->setContent($responseContent); - } - - $response->addHeaders($resource->getResponseHeaders()); - } else { - $response->setResponseCode(Response::HTTP_STATUS_SERVER_ERROR_GENERIC); - $response->setContent($this->buildErrorResponse("An unknown error occurred during the operation.")); - } - } catch (RestImplementationException $er) { - $response->setResponseCode(Response::HTTP_STATUS_SERVER_ERROR_GENERIC); - $response->setContent($this->buildErrorResponse($er->getMessage())); - } - - Log::bulkData("Api response", "RESTAPI", $response->getContent()); - - return $response; - } - - protected function handlePost(WebRequest $request, Response $response) - { - Log::debug("POST " . $request->urlPath . "RESTAPI"); - - try { - $resource = $this->getRestResource(); - $payload = $this->getRequestPayload(); - - $resource->validateRequestPayload($payload, "post"); - $newItem = $resource->post($payload, $this); - - if ($newItem || is_array($newItem)) { - $response->setContent($newItem); - $response->setHeader("HTTP/1.1 201 Created", false); - - if (isset($newItem->_href)) { - $response->setHeader("Location", $newItem->_href); - } - - $response->addHeaders($resource->getResponseHeaders()); - } else { - $response->setResponseCode(Response::HTTP_STATUS_SERVER_ERROR_GENERIC); - $response->setContent($this->buildErrorResponse("An unknown error occurred during the operation.")); - } - } catch (RestImplementationException $er) { - $response->setResponseCode(Response::HTTP_STATUS_SERVER_ERROR_GENERIC); - $response->setContent($this->buildErrorResponse($er->getMessage())); - } - - Log::bulkData("Api response", "RESTAPI", $response->getContent()); - - return $response; - } - - protected function handleDelete(WebRequest $request, Response $response) - { - Log::debug("DELETE " . Request::current()->urlPath, "RESTAPI"); - - $resource = $this->getRestResource(); - - if ($resource->delete($this)) { - try { - $response->setContent($this->buildSuccessResponse("The DELETE operation completed successfully")); - $response->addHeaders($resource->getResponseHeaders()); - return $response; - } catch (\Exception $er) { - } - } - - $response->setResponseCode(Response::HTTP_STATUS_CLIENT_ERROR_FORBIDDEN); - $response->setContent($this->buildErrorResponse("The resource could not be deleted.")); - - Log::bulkData("Api response", "RESTAPI", $response->getContent()); - - return $response; - } - - protected function buildSuccessResponse($message = "") - { - $date = new RhubarbDateTime("now"); - - $response = new \stdClass(); - $response->result = new \stdClass(); - $response->result->status = true; - $response->result->timestamp = $date->format("c"); - $response->result->message = $message; - - return $response; - } - - protected function buildErrorResponse($message = "") - { - $date = new RhubarbDateTime("now"); - - $response = new \stdClass(); - $response->result = new \stdClass(); - $response->result->status = false; - $response->result->timestamp = $date->format("c"); - $response->result->message = $message; - - return $response; - } - - /** - * get's the resource for the parent handler. - * - * Sometimes a resource needs the context of it's parent to check permissions or apply - * filters. - * - * @return null|RestResource - */ - public function getParentResource() - { - $parentHandler = $this->getParentHandler(); - - if ($parentHandler instanceof RestResourceHandler) { - return $parentHandler->getRestResource(); - } - - return null; - } -} diff --git a/src/UrlHandlers/UnauthenticatedRestCollectionHandler.php b/src/UrlHandlers/UnauthenticatedRestCollectionHandler.php deleted file mode 100644 index f4c91d2..0000000 --- a/src/UrlHandlers/UnauthenticatedRestCollectionHandler.php +++ /dev/null @@ -1,34 +0,0 @@ -Username = "bob"; - $user->Password = "smith"; - $user->Active = 1; - $user->save(); - } - - protected function tearDown() - { - parent::tearDown(); - -// AuthenticationProvider::setProviderClassName(""); - } - - public function testAuthenticationProviderWorks() - { - $request = new WebRequest(); - $request->serverData["HTTP_ACCEPT"] = "application/json"; - $request->serverData["REQUEST_METHOD"] = "get"; - $request->urlPath = "/contacts/"; - - $rest = new RestResourceHandler(RestAuthenticationTestResource::class); - $rest->setUrl("/contacts/"); - - $response = $rest->generateResponse($request); - $headers = $response->getHeaders(); - - $this->assertArrayHasKey("WWW-authenticate", $headers); - - $this->assertContains("Basic", $headers["WWW-authenticate"]); - $this->assertContains("realm=\"API\"", $headers["WWW-authenticate"]); - - // Supply the credentials - // Passing lowercase Authorization header to match the logic ran inside the WebRequest object - $request->headerData["authorization"] = "Basic " . base64_encode("bob:smith"); - - $response = $rest->generateResponse($request); - $headers = $response->getHeaders(); - - $this->assertArrayNotHasKey("WWW-authenticate", $headers); - $content = $response->getContent(); - - $this->assertTrue($content->authenticated); - - // Incorrect credentials. - $request->headerData["authorization"] = "Basic " . base64_encode("terry:smith"); - - $response = $rest->generateResponse($request); - $headers = $response->getHeaders(); - - $this->assertArrayHasKey("WWW-authenticate", $headers); - } - - public function testExpiredUserWithAuthenticationProvider() - { - AuthenticationProvider::setProviderClassName(UnitTestExpiredLoginProviderRestAuthenticationProvider::class); - - $user = new TestExpiredUser(); - $user->Username = "expireduser"; - $user->Password = "password"; - $user->Active = 1; - $user->save(); - - $request = new WebRequest(); - $request->serverData["HTTP_ACCEPT"] = "application/json"; - $request->serverData["REQUEST_METHOD"] = "get"; - $request->urlPath = "/contacts/"; - - $rest = new RestResourceHandler(RestAuthenticationTestResource::class); - $rest->setUrl("/contacts/"); - - // Supply the credentials - // Passing lowercase Authorization header to match the logic ran inside the WebRequest object - $request->headerData["authorization"] = "Basic " . base64_encode("expireduser:password"); - - $response = $rest->generateResponse($request); - $headers = $response->getHeaders(); - - $this->assertArrayHasKey("WWW-authenticate", $headers); - - $this->assertContains("Basic", $headers["WWW-authenticate"]); - $this->assertContains("realm=\"API\"", $headers["WWW-authenticate"]); - - $this->assertEquals($response->getResponseCode(), Response::HTTP_STATUS_CLIENT_ERROR_FORBIDDEN); - } -} - -class UnitTestLoginProviderRestAuthenticationProvider extends ModelLoginProviderAuthenticationProvider -{ - protected function getLoginProviderClassName() - { - return RestAuthenticationTestLoginProvider::class; - } -} - -class RestAuthenticationTestResource extends ItemRestResource -{ - public function get(RestHandler $handler = null) - { - $response = parent::get($handler); - $response->authenticated = true; - - return $response; - } -} - -class RestAuthenticationTestLoginProvider extends ModelLoginProvider -{ - public function __construct() - { - parent::__construct( - User::class, - "Username", - "Password", - "Active" - ); - } -} - -class UnitTestExpiredLoginProviderRestAuthenticationProvider extends ModelLoginProviderAuthenticationProvider -{ - protected function getLoginProviderClassName() - { - return RestAuthenticationExpiredTestLoginProvider::class; - } -} - -class RestAuthenticationExpiredTestLoginProvider extends ModelLoginProvider -{ - public function __construct() - { - parent::__construct( - TestExpiredUser::class, - "Username", - "Password", - "Active" - ); - } -} diff --git a/tests/unit/Authentication/TokenAuthenticationProviderBaseTest.php b/tests/unit/Authentication/TokenAuthenticationProviderBaseTest.php deleted file mode 100644 index 658b35b..0000000 --- a/tests/unit/Authentication/TokenAuthenticationProviderBaseTest.php +++ /dev/null @@ -1,89 +0,0 @@ -serverData["HTTP_ACCEPT"] = "application/json"; - $request->serverData["REQUEST_METHOD"] = "get"; - $request->urlPath = "/contacts/"; - - $rest = new RestResourceHandler(TokenAuthenticationTestResource::class); - $rest->setUrl("/contacts/"); - - $response = $rest->generateResponse($request); - $headers = $response->getHeaders(); - - $this->assertArrayHasKey("WWW-authenticate", $headers); - - $request->headerData["authorization"] = "Token token=\"abc123\""; - - $response = $rest->generateResponse($request); - $headers = $response->getHeaders(); - - $this->assertArrayNotHasKey("WWW-authenticate", $headers); - } - - protected function tearDown() - { - parent::tearDown(); - - AuthenticationProvider::setProviderClassName(""); - } -} - -class TokenAuthenticationTestAuthenticationProvider extends TokenAuthenticationProviderBase -{ - /** - * Returns true if the token is valid. - * - * @param $tokenString - * @return mixed - */ - protected function isTokenValid($tokenString) - { - return true; - } -} - -class TokenAuthenticationTestResource extends ItemRestResource -{ - -} diff --git a/tests/unit/Fixtures/UnitTestingConstructedRestResource.php b/tests/unit/Fixtures/UnitTestingConstructedRestResource.php deleted file mode 100644 index e974ee0..0000000 --- a/tests/unit/Fixtures/UnitTestingConstructedRestResource.php +++ /dev/null @@ -1,24 +0,0 @@ -resourceBody = $resourceBody; - - parent::__construct($resourceBody->_id, $parentResource); - } - - public function get(RestHandler $handler = null) - { - return $this->resourceBody; - } -} \ No newline at end of file diff --git a/tests/unit/Fixtures/UnitTestingRestHandler.php b/tests/unit/Fixtures/UnitTestingRestHandler.php deleted file mode 100644 index 580ab5e..0000000 --- a/tests/unit/Fixtures/UnitTestingRestHandler.php +++ /dev/null @@ -1,88 +0,0 @@ - "html", "application/json" => "json"]; - } - - protected function handleGet() - { - return $this->getHtml = true; - } - - protected function handlePost() - { - return $this->postHtml = true; - } - - protected function handlePut() - { - $this->putHtml = true; - } - -// protected function getJson() -// { -// $this->getJson = true; -// } -// -// protected function postJson() -// { -// $this->postJson = true; -// } - - - /** - * Should be implemented to return a true or false as to whether this handler supports the given request. - * - * Normally this involves testing the request URI. - * - * @param Request $request - * @param string $currentUrlFragment - * @return bool - */ - protected function getMatchingUrlFragment(Request $request, $currentUrlFragment = "") - { - return true; - } -} diff --git a/tests/unit/Fixtures/UnitTestingRestResource.php b/tests/unit/Fixtures/UnitTestingRestResource.php deleted file mode 100644 index d016647..0000000 --- a/tests/unit/Fixtures/UnitTestingRestResource.php +++ /dev/null @@ -1,51 +0,0 @@ -value = "collection"; - - return $resource; - } - - /** - * Returns the ItemRestResource for the $resourceIdentifier contained in this collection. - * - * @param $resourceIdentifier - * @return ItemRestResource - * @throws RestImplementationException Thrown if the item could not be found. - */ - public function createItemResource($resourceIdentifier) - { - $resource = new \stdClass(); - $resource->_id = 1; - $resource->value = "constructed"; - - return new UnitTestingConstructedRestResource($resource); - } -} \ No newline at end of file diff --git a/tests/unit/Resources/ModelRestResourceTest.php b/tests/unit/Resources/ModelRestResourceTest.php deleted file mode 100644 index 9c5aac0..0000000 --- a/tests/unit/Resources/ModelRestResourceTest.php +++ /dev/null @@ -1,487 +0,0 @@ -CompanyName = "Big Widgets"; - $company->save(); - - $example = new User(); - $example->Forename = "Andrew"; - $example->Surname = "Grasswisperer"; - $example->CompanyID = $company->UniqueIdentifier; - $example->save(); - - $example = new User(); - $example->Forename = "Billy"; - $example->Surname = "Bob"; - $example->CompanyID = $company->UniqueIdentifier + 1; - $example->save(); - - $example = new User(); - $example->Forename = "Mary"; - $example->Surname = "Smith"; - $example->CompanyID = $company->UniqueIdentifier + 1; - $example->save(); - } - - public function testResourceIncludesModel() - { - $request = new WebRequest(); - $request->serverData["HTTP_ACCEPT"] = "application/json"; - $request->serverData["REQUEST_METHOD"] = "get"; - $request->urlPath = "/contacts/1"; - - $rest = new RestCollectionHandler(__NAMESPACE__ . "\UnitTestExampleRestResource"); - $rest->setUrl("/contacts/"); - - $response = $rest->generateResponse($request); - $content = $response->getContent(); - - $this->assertEquals("Andrew", $content->Forename, "The rest handler is not loading the model"); - $this->assertEquals(1, $content->_id, "The rest handler is not loading the model"); - } - - public function testCollectionIsModelCollection() - { - $request = new WebRequest(); - $request->serverData["HTTP_ACCEPT"] = "application/json"; - $request->serverData["REQUEST_METHOD"] = "get"; - $request->urlPath = "/contacts/"; - - $rest = new RestCollectionHandler(__NAMESPACE__ . "\UnitTestExampleRestResource"); - $rest->setUrl("/contacts/"); - - $response = $rest->generateResponse($request); - $content = $response->getContent(); - - $this->assertEquals("Andrew", $content->items[0]->Forename, "The rest handler is not loading the collection"); - $this->assertEquals(1, $content->items[0]->_id, "The rest handler is not loading the collection"); - } - - public function testCollectionCountAndRanging() - { - Company::clearObjectCache(); - - for ($x = 0; $x < 110; $x++) { - $example = new User(); - $example->Forename = $x; - $example->Surname = $x; - $example->save(); - } - - $request = new WebRequest(); - $request->serverData["HTTP_ACCEPT"] = "application/json"; - $request->serverData["REQUEST_METHOD"] = "get"; - $request->urlPath = "/contacts/"; - - $application = Application::current(); - $application->setCurrentRequest($request); - $context = $application->context(); - -// $context = new Context(); -// $context->Request = $request; - - $rest = new RestCollectionHandler(UnitTestExampleRestResource::class); - $rest->setUrl("/contacts/"); - - $response = $rest->generateResponse($request); - $content = $response->getContent(); - - $this->assertEquals(110, $content->count, "The rest collection count is invalid"); - $this->assertEquals(0, $content->range->from, "The rest collection range is invalid"); - $this->assertEquals(99, $content->range->to, "The rest collection range is invalid"); - $this->assertCount(100, $content->items, "The rest collection range is invalid"); - $this->assertEquals(42, $content->items[42]->Forename, "The rest collection range is invalid"); - $this->assertEquals(48, $content->items[48]->Forename, "The rest collection range is invalid"); - - $request->server("HTTP_RANGE", "40-49"); - - $response = $rest->generateResponse($request); - $content = $response->getContent(); - - $this->assertEquals(110, $content->count, "The rest collection count is invalid"); - $this->assertEquals(40, $content->range->from, "The rest collection range is invalid"); - $this->assertEquals(49, $content->range->to, "The rest collection range is invalid"); - $this->assertEquals(42, $content->items[2]->Forename, "The rest collection range is invalid"); - $this->assertEquals(48, $content->items[8]->Forename, "The rest collection range is invalid"); - - $request->server("HTTP_RANGE", ""); - } - - public function testResourceCanBeUpdated() - { - $changes = ["Forename" => "Johnny"]; - -// $context = new Context(); -// $context->SimulatedRequestBody = json_encode($changes); - -// $request = new WebRequest(); -// $request->serverData["HTTP_ACCEPT"] = "application/json"; -// $request->serverData["REQUEST_METHOD"] = "get"; -// $request->urlPath = "/contacts/"; - - - $request = new JsonRequest(); - $request->server("HTTP_ACCEPT", "application/json"); - $request->server("REQUEST_METHOD", "put"); - - $application = Application::current(); - $application->setCurrentRequest($request); - $context = $application->context(); - $context->simulatedRequestBody = $changes; - -// $context->Request = $request; - $request->urlPath = "/contacts/1"; - - $rest = new RestCollectionHandler(UnitTestExampleRestResource::class); - $rest->setUrl("/contacts/"); - - $response = $rest->generateResponse($request); - $content = $response->getContent()->result; - - $example = User::findFirst(); - - $this->assertEquals("Johnny", $example->Forename, "The put operation didn't update the model"); - $this->assertTrue($content->status); - $this->assertContains("The PUT operation completed successfully", $content->message); - $this->assertEquals(date("c"), $content->timestamp); - } - - public function testResourceCanBeInserted() - { - $changes = ["Forename" => "Bobby", "Surname" => "Smith"]; - - $context = new Context(); - $context->SimulatedRequestBody = json_encode($changes); - - $request = new JsonRequest(); - $request->server("HTTP_ACCEPT", "application/json"); - $request->server("REQUEST_METHOD", "post"); - - $context->Request = $request; - $request->UrlPath = "/contacts/"; - - $rest = new RestCollectionHandler(UnitTestExampleRestResource::class); - $rest->setUrl("/contacts/"); - - Example::clearObjectCache(); - - $response = $rest->generateResponse($request); - $content = $response->getContent(); - - $example = Example::findFirst(); - - $this->assertEquals("Bobby", $example->Forename, "The post operation didn't update the model"); - $this->assertEquals("Smith", $example->Surname, "The post operation didn't update the model"); - $this->assertEquals("Bobby", $content->Forename); - } - - public function testResourceCanBeDeleted() - { - Example::clearObjectCache(); - - $example = new Example(); - $example->Forename = "Jerry"; - $example->Surname = "Maguire"; - $example->save(); - - $example = new Example(); - $example->Forename = "Jolly"; - $example->Surname = "Bob"; - $example->save(); - - $context = new Context(); - - $request = new JsonRequest(); - $request->server("HTTP_ACCEPT", "application/json"); - $request->server("REQUEST_METHOD", "delete"); - - $context->Request = $request; - $request->UrlPath = "/contacts/" . $example->UniqueIdentifier; - - $rest = new RestCollectionHandler(UnitTestExampleRestResource::class); - $rest->setUrl("/contacts/"); - - $response = $rest->generateResponse($request); - $content = $response->getContent(); - - $this->assertCount(1, Example::find()); - $this->assertEquals("Jerry", Example::findFirst()->Forename); - $this->assertTrue($content->result->status); - - $this->assertContains("The DELETE operation completed successfully", $content->result->message); - } - - public function testCustomColumns() - { - $context = new Context(); - - $request = new JsonRequest(); - $request->server("HTTP_ACCEPT", "application/json"); - $request->server("REQUEST_METHOD", "get"); - - $context->Request = $request; - $request->UrlPath = "/contacts/1"; - - $rest = new RestCollectionHandler(UnitTestExampleRestResource::class); - $rest->setUrl("/contacts/"); - - $response = $rest->generateResponse($request); - $content = $response->getContent(); - - $this->assertEquals("Andrew", $content->Forename); - $this->assertEquals("Grasswisperer", $content->Surname); - - $this->assertTrue(isset($content->Company)); - $this->assertNotInstanceOf(\Rhubarb\Stem\Models\Model::class, $content->Company); - $this->assertEquals("Big Widgets", $content->Company->CompanyName); - } - - public function testHeadLinks() - { - $context = new Context(); - - $request = new JsonRequest(); - $request->server("HTTP_ACCEPT", "application/json"); - $request->server("REQUEST_METHOD", "get"); - - $context->Request = $request; - $request->UrlPath = "/contacts/1"; - - $companyRest = new RestCollectionHandler(UnitTestCompanyRestResource::class); - $companyRest->setUrl("/companies/"); - - $rest = new RestCollectionHandler(UnitTestExampleRestResourceWithCompanyHeader::class); - $rest->setUrl("/contacts/"); - - $response = $rest->generateResponse($request); - $content = $response->getContent(); - - $this->assertTrue(isset($content->Company)); - $this->assertFalse(isset($content->Company->Balance)); - - $request = new JsonRequest(); - $request->server("HTTP_ACCEPT", "application/json"); - $request->server("REQUEST_METHOD", "get"); - - $context->Request = $request; - $request->UrlPath = "/companies/1"; - - $response = $companyRest->generateResponse($request); - $company = $response->getContent(); - - $this->assertEquals("Big Widgets", $company->CompanyName); - $this->assertTrue(isset($company->Contacts)); - } - - public function testUrlsAreSet() - { - $request = new WebRequest(); - $request->header("HTTP_ACCEPT", "application/json"); - $request->server("REQUEST_METHOD", "get"); - $request->server("SERVER_PORT", 80); - $request->server("HTTP_HOST", "cli"); - $request->UrlPath = "/contacts/1"; - - $rest = new RestCollectionHandler(UnitTestExampleRestResource::class); - - $api = new RestApiRootHandler(UnitTestDummyResource::class, - [ - "contacts" => $rest - ]); - - $api->setUrl("/"); - - $context = new Context(); - $context->Request = $request; - - $response = $api->generateResponse($request); - $content = $response->getContent(); - - $this->assertEquals("/contacts/1", $content->_href); - } - - public function testCollectionIsFiltered() - { - $request = new WebRequest(); - $request->headerData["Accept"] = "application/json"; - $request->serverData["REQUEST_METHOD"] = "get"; - $request->serverData["SERVER_PORT"] = 80; - $request->serverData["HTTP_HOST"] = "cli"; - $request->urlPath = "/companies/1/contacts"; - - new UnitTestRestModule(); - Application::current()->initialiseModules(); - -// Module::clearModules(); -// Module::registerModule(new UnitTestRestModule()); -// Module::initialiseModules(); - - $context = new Context(); - $context->Request = $request; - - $response = Module::generateResponseForRequest($request); - - $content = $response->getContent(); - - $this->assertCount(1, $content->items); - } -} - -class UnitTestRestModule extends Module -{ - public function __construct() - { - parent::__construct(); - - $this->namespace = __NAMESPACE__; - } - - protected function initialise() - { - parent::initialise(); - - $this->addUrlHandlers( - [ - "/companies" => new RestCollectionHandler(UnitTestCompanyRestResource::class, - [ - "contacts" => new RestCollectionHandler(UnitTestExampleRestResource::class) - ]) - ]); - } -} - -class UnitTestDummyResource extends ItemRestResource -{ - -} - -class UnitTestExampleRestResourceCustomisedColumns extends ModelRestResource -{ - protected function getColumns() - { - return ["Forename", "Company"]; - } - - - /** - * Returns the name of the model to use for this resource. - * - * @return string - */ - public function getModelName() - { - return "Example"; - } -} - -class UnitTestExampleRestResourceWithCompanyHeader extends ModelRestResource -{ - protected function getColumns() - { - return ["Forename", "Company:summary"]; - } - - /** - * Returns the name of the model to use for this resource. - * - * @return string - */ - public function getModelName() - { - return "Example"; - } -} - -class UnitTestExampleRestResource extends ModelRestResource -{ - /** - * Returns the name of the model to use for this resource. - * - * @return string - */ - public function getModelName() - { - return "UnitTestUser"; - } - - protected function getColumns() - { - $columns = parent::getColumns(); - $columns[] = "Forename"; - $columns[] = "Surname"; - $columns[] = "Company"; - - return $columns; - } -} - -class UnitTestCompanyRestResource extends ModelRestResource -{ - protected function getColumns() - { - return ["CompanyName", "Contacts"]; - } - - protected function getSummary() - { - return ["CompanyName"]; - } - - /** - * Returns the name of the model to use for this resource. - * - * @return string - */ - public function getModelName() - { - return "Company"; - } -} diff --git a/tests/unit/Resources/RestResourceTest.php b/tests/unit/Resources/RestResourceTest.php deleted file mode 100644 index 786aa22..0000000 --- a/tests/unit/Resources/RestResourceTest.php +++ /dev/null @@ -1,62 +0,0 @@ -serverData['HTTP_ACCEPT'] = "application/json"; - $request->serverData['REQUEST_METHOD'] = "post"; - $request->urlPath = "/contacts/"; - - $application = Application::current(); - $application->setCurrentRequest($request); - $context = $application->context(); - $context->simulatedRequestBody = null; - - $rest = new RestCollectionHandler(UnitTestExampleRestResource::class); - $rest->setUrl("/contacts/"); - - $response = $rest->generateResponse($request); - $content = $response->getContent(); - - $this->assertFalse($content->result->status, "POST requests with no payload should fail"); - - $stdClass = new \stdClass(); - $stdClass->a = "b"; - - $context->simulatedRequestBody = json_encode($stdClass); - - $response = $rest->generateResponse($request); - $content = $response->getContent(); - - $this->assertEquals("", $content->Forename, "Posting to this collection should return the new resource."); - - $context->simulatedRequestBody = ""; - } -} diff --git a/tests/unit/UrlHandlers/RestCollectionHandlerTest.php b/tests/unit/UrlHandlers/RestCollectionHandlerTest.php deleted file mode 100644 index b68aa88..0000000 --- a/tests/unit/UrlHandlers/RestCollectionHandlerTest.php +++ /dev/null @@ -1,59 +0,0 @@ -serverData["HTTP_ACCEPT"] = "application/json"; - $request->serverData["REQUEST_METHOD"] = "get"; - - $rest = new UnitTestRestCollectionHandler(); - $rest->setUrl("/users/"); - - $request->urlPath = "/users/"; - - $response = $rest->generateResponse($request); - $content = $response->getContent(); - - $this->assertEquals("collection", $content->value, "The rest handler is not recognising the collection"); - - $request->urlPath = "/users/1/"; - - $response = $rest->generateResponse($request); - $content = $response->getContent(); - - $this->assertEquals("constructed", $content->value, "The rest handler is not instantiating the resource"); - } -} - -class UnitTestRestCollectionHandler extends RestCollectionHandler -{ - public function __construct($childUrlHandlers = []) - { - parent::__construct(UnitTestingRestResource::class, $childUrlHandlers); - } -} diff --git a/tests/unit/UrlHandlers/RestHandlerTest.php b/tests/unit/UrlHandlers/RestHandlerTest.php deleted file mode 100644 index b59a871..0000000 --- a/tests/unit/UrlHandlers/RestHandlerTest.php +++ /dev/null @@ -1,142 +0,0 @@ -initialiseModules(); - - $this->unitTestRestHandler = new UnitTestingRestHandler(); - } - - public function testMethodsCalledCorrectly() - { - $request = new WebRequest(); - - $request->headerData["accept"] = "image/jpeg"; - $response = $this->unitTestRestHandler->generateResponse($request); - $this->assertFalse($response, "image/jpeg should not be handled by this handler"); - - $request->headerData["accept"] = "text/html"; - $request->serverData["REQUEST_METHOD"] = "options"; - - try { - $this->unitTestRestHandler->generateResponse($request); - $this->fail("HTTP OPTIONS should not be handled by this handler"); - } catch (ForceResponseException $er) { - } - - // Check that */* is treated as text/html - $request->headerData["accept"] = "*/*"; - $request->serverData["REQUEST_METHOD"] = "get"; - - $this->unitTestRestHandler->generateResponse($request); - $this->assertTrue($this->unitTestRestHandler->getHtml); - - $request->headerData["accept"] = "text/html"; - $request->serverData["REQUEST_METHOD"] = "get"; - - $this->unitTestRestHandler->generateResponse($request); - $this->assertTrue($this->unitTestRestHandler->getHtml); - - $request->serverData["REQUEST_METHOD"] = "post"; - - $this->unitTestRestHandler->generateResponse($request); - $this->assertTrue($this->unitTestRestHandler->postHtml); - - $request->serverData["REQUEST_METHOD"] = "put"; - - $this->unitTestRestHandler->generateResponse($request); - $this->assertTrue($this->unitTestRestHandler->putHtml); - -// $request->headerData["accept"] = "application/json"; -// $request->serverData["REQUEST_METHOD"] = "get"; -// -// $this->unitTestRestHandler->generateResponse($request); -// $this->assertTrue($this->unitTestRestHandler->getJson); - - $request->serverData["REQUEST_METHOD"] = "post"; - - $this->unitTestRestHandler->generateResponse($request); - $this->assertTrue($this->unitTestRestHandler->postHtml); - -// $request->serverData["REQUEST_METHOD"] = "put"; -// -// $this->setExpectedException(RestImplementationException::class); -// -// $this->unitTestRestHandler->generateResponse($request); - } - - public function testRestHandlerFormatsExceptionsCorrectly() - { - $request = new WebRequest(); - $request->urlPath = "/rest-test/"; - - $response = Application::current()->generateResponseForRequest($request); - - $this->assertInstanceOf(JsonResponse::class, $response); - - $this->assertEquals("Sorry, something went wrong and we couldn't complete your request. The developers have been notified.", - str_replace(["\r\n", "\n"], " ", $response->getContent()->result->message)); - } -} - -class UnitTestRestModule extends Application -{ - protected function registerUrlHandlers() - { - $this->addUrlHandlers( - [ - "/rest-test/" => $url = new RestResourceHandler(UnitTestRestExceptionResource::class) - ] - ); - - $url->setPriority(100); - } -} - -class UnitTestRestExceptionResource extends ItemRestResource -{ - public function get(RestHandler $handler = null) - { - throw new RestImplementationException("Something's crashed"); - } -} diff --git a/tests/unit/UrlHandlers/RestResourceHandlerTest.php b/tests/unit/UrlHandlers/RestResourceHandlerTest.php deleted file mode 100644 index 7cea114..0000000 --- a/tests/unit/UrlHandlers/RestResourceHandlerTest.php +++ /dev/null @@ -1,84 +0,0 @@ -serverData["accept"] = "application/json"; - $request->serverData["REQUEST_METHOD"] = "get"; - $request->urlPath = "/anything/test"; - - $restHandler->setUrl("/anything/test"); - - $response = $restHandler->generateResponse($request); - $content = $response->getContent(); - - $this->assertEquals("collection", $content->value, "The rest handler is not instantiating the resource"); - } - - public function testValidationOfPayloads() - { - $restHandler = new RestResourceHandler(ValidatedPayloadTestRestResource::class, [], ["post"]); - - $request = new WebRequest(); - $request->headerData["accept"] = "application/json"; - $request->serverData["REQUEST_METHOD"] = "post"; - $request->urlPath = "/anything/test"; - - $restHandler->setUrl("/anything/test"); - - $response = $restHandler->generateResponse($request); - $content = $response->getContent(); - - $this->assertFalse($content->result->status); - $this->assertEquals("The request payload isn't valid", $content->result->message); - } -} - -class ValidatedPayloadTestRestResource extends ItemRestResource -{ - public function validateRequestPayload($payload, $method) - { - throw new RestRequestPayloadValidationException("The request payload isn't valid"); - } - - public function post($restResource, RestHandler $handler = null) - { - // Simply return an empty resource for now. - return $this->get($handler); - } - - public function put($restResource, RestHandler $handler = null) - { - return $this->post($restResource, $handler); - } -} diff --git a/tests/unit/_bootstrap.php b/tests/unit/_bootstrap.php deleted file mode 100644 index b3d9bbc..0000000 --- a/tests/unit/_bootstrap.php +++ /dev/null @@ -1 +0,0 @@ -