From 52950edd36e0b0dc43b665e799051284aa91b77e Mon Sep 17 00:00:00 2001 From: Andrew Cuthbert Date: Thu, 29 Nov 2018 21:51:21 +0000 Subject: [PATCH 01/48] v3 WIP --- CHANGELOG.md | 4 + composer.json | 3 +- src/Adapters/ModelResourceAdapter.php | 162 +++++ src/Adapters/ResourceAdapter.php | 69 ++ src/Authentication/AuthenticationProvider.php | 30 - ...alsLoginProviderAuthenticationProvider.php | 77 -- .../IpRestrictedAuthenticationProvider.php | 51 -- ...delLoginProviderAuthenticationProvider.php | 42 -- .../TokenAuthenticationProviderBase.php | 69 -- src/Clients/BasicAuthenticatedRestClient.php | 45 -- src/Clients/RestClient.php | 119 --- src/Clients/RestHttpRequest.php | 92 --- src/Clients/TokenAuthenticatedRestClient.php | 125 ---- src/Exceptions/InsertException.php | 26 - src/Exceptions/ResourceNotFoundException.php | 10 + .../RestAuthenticationException.php | 26 - .../RestImplementationException.php | 29 - .../RestRequestPayloadValidationException.php | 26 - .../RestResourceNotFoundException.php | 27 - src/Exceptions/RestResponseException.php | 42 -- src/Exceptions/UpdateException.php | 26 - src/Middleware/Middleware.php | 11 + src/Modelling/ApiModel.php | 81 --- src/Presenters/TestHarnessPresenter.php | 65 -- src/Presenters/TestHarnessView.php | 91 --- src/Resources/ApiDescriptionResource.php | 25 - src/Resources/BinaryRestResource.php | 16 - src/Resources/CollectionRestResource.php | 200 ----- src/Resources/ItemRestResource.php | 71 -- src/Resources/ListResource.php | 16 + src/Resources/ModelRestResource.php | 681 ------------------ .../MultiPartFormDataPayloadTrait.php | 36 - src/Resources/Range.php | 9 + src/Resources/RestResource.php | 229 ------ .../TokenAuthorisationRequiredResponse.php | 31 - src/UrlHandlers/BinaryRestResourceHandler.php | 74 -- .../PostOnlyRestCollectionHandler.php | 29 - .../ReadOnlyRestCollectionHandler.php | 29 - src/UrlHandlers/RestApiHandler.php | 137 ++++ src/UrlHandlers/RestApiRootHandler.php | 54 -- src/UrlHandlers/RestCollectionHandler.php | 131 ---- src/UrlHandlers/RestHandler.php | 248 ------- src/UrlHandlers/RestResourceHandler.php | 308 -------- .../UnauthenticatedRestCollectionHandler.php | 34 - .../UnauthenticatedRestResourceHandler.php | 35 - tests/_bootstrap.php | 5 - tests/_data/.gitkeep | 0 tests/_output/.gitignore | 2 - tests/_support/AcceptanceTester.php | 26 - tests/_support/FunctionalTester.php | 26 - tests/_support/Helper/Acceptance.php | 10 - tests/_support/Helper/Functional.php | 10 - tests/_support/Helper/Unit.php | 10 - tests/_support/UnitTester.php | 26 - tests/_support/_generated/.gitignore | 2 - tests/acceptance.suite.yml | 12 - tests/functional.suite.yml | 12 - tests/unit.suite.yml | 9 - ...ProviderRestAuthenticationProviderTest.php | 185 ----- .../TokenAuthenticationProviderBaseTest.php | 89 --- .../UnitTestingConstructedRestResource.php | 24 - .../unit/Fixtures/UnitTestingRestHandler.php | 88 --- .../unit/Fixtures/UnitTestingRestResource.php | 51 -- .../unit/Resources/ModelRestResourceTest.php | 487 ------------- tests/unit/Resources/RestResourceTest.php | 62 -- .../UrlHandlers/RestCollectionHandlerTest.php | 59 -- tests/unit/UrlHandlers/RestHandlerTest.php | 142 ---- .../UrlHandlers/RestResourceHandlerTest.php | 84 --- tests/unit/_bootstrap.php | 1 - 69 files changed, 419 insertions(+), 4744 deletions(-) create mode 100644 src/Adapters/ModelResourceAdapter.php create mode 100644 src/Adapters/ResourceAdapter.php delete mode 100644 src/Authentication/AuthenticationProvider.php delete mode 100644 src/Authentication/CredentialsLoginProviderAuthenticationProvider.php delete mode 100644 src/Authentication/IpRestrictedAuthenticationProvider.php delete mode 100644 src/Authentication/ModelLoginProviderAuthenticationProvider.php delete mode 100644 src/Authentication/TokenAuthenticationProviderBase.php delete mode 100644 src/Clients/BasicAuthenticatedRestClient.php delete mode 100644 src/Clients/RestClient.php delete mode 100644 src/Clients/RestHttpRequest.php delete mode 100644 src/Clients/TokenAuthenticatedRestClient.php delete mode 100644 src/Exceptions/InsertException.php create mode 100644 src/Exceptions/ResourceNotFoundException.php delete mode 100644 src/Exceptions/RestAuthenticationException.php delete mode 100644 src/Exceptions/RestImplementationException.php delete mode 100644 src/Exceptions/RestRequestPayloadValidationException.php delete mode 100644 src/Exceptions/RestResourceNotFoundException.php delete mode 100644 src/Exceptions/RestResponseException.php delete mode 100644 src/Exceptions/UpdateException.php create mode 100644 src/Middleware/Middleware.php delete mode 100644 src/Modelling/ApiModel.php delete mode 100644 src/Presenters/TestHarnessPresenter.php delete mode 100644 src/Presenters/TestHarnessView.php delete mode 100644 src/Resources/ApiDescriptionResource.php delete mode 100644 src/Resources/BinaryRestResource.php delete mode 100644 src/Resources/CollectionRestResource.php delete mode 100644 src/Resources/ItemRestResource.php create mode 100644 src/Resources/ListResource.php delete mode 100644 src/Resources/ModelRestResource.php delete mode 100644 src/Resources/MultiPartFormDataPayloadTrait.php create mode 100644 src/Resources/Range.php delete mode 100644 src/Resources/RestResource.php delete mode 100644 src/Response/TokenAuthorisationRequiredResponse.php delete mode 100644 src/UrlHandlers/BinaryRestResourceHandler.php delete mode 100644 src/UrlHandlers/PostOnlyRestCollectionHandler.php delete mode 100644 src/UrlHandlers/ReadOnlyRestCollectionHandler.php create mode 100644 src/UrlHandlers/RestApiHandler.php delete mode 100644 src/UrlHandlers/RestApiRootHandler.php delete mode 100644 src/UrlHandlers/RestCollectionHandler.php delete mode 100644 src/UrlHandlers/RestHandler.php delete mode 100644 src/UrlHandlers/RestResourceHandler.php delete mode 100644 src/UrlHandlers/UnauthenticatedRestCollectionHandler.php delete mode 100644 src/UrlHandlers/UnauthenticatedRestResourceHandler.php delete mode 100644 tests/_bootstrap.php delete mode 100644 tests/_data/.gitkeep delete mode 100644 tests/_output/.gitignore delete mode 100644 tests/_support/AcceptanceTester.php delete mode 100644 tests/_support/FunctionalTester.php delete mode 100644 tests/_support/Helper/Acceptance.php delete mode 100644 tests/_support/Helper/Functional.php delete mode 100644 tests/_support/Helper/Unit.php delete mode 100644 tests/_support/UnitTester.php delete mode 100644 tests/_support/_generated/.gitignore delete mode 100644 tests/acceptance.suite.yml delete mode 100644 tests/functional.suite.yml delete mode 100644 tests/unit.suite.yml delete mode 100644 tests/unit/Authentication/LoginProviderRestAuthenticationProviderTest.php delete mode 100644 tests/unit/Authentication/TokenAuthenticationProviderBaseTest.php delete mode 100644 tests/unit/Fixtures/UnitTestingConstructedRestResource.php delete mode 100644 tests/unit/Fixtures/UnitTestingRestHandler.php delete mode 100644 tests/unit/Fixtures/UnitTestingRestResource.php delete mode 100644 tests/unit/Resources/ModelRestResourceTest.php delete mode 100644 tests/unit/Resources/RestResourceTest.php delete mode 100644 tests/unit/UrlHandlers/RestCollectionHandlerTest.php delete mode 100644 tests/unit/UrlHandlers/RestHandlerTest.php delete mode 100644 tests/unit/UrlHandlers/RestResourceHandlerTest.php delete mode 100644 tests/unit/_bootstrap.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 6436a34..cec6a69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +### 3.0.0 + +Changed: Total slim down + ### 2.0.8 Changed: Support for including null values on rest resources. diff --git a/composer.json b/composer.json index fa52030..ffeeb0f 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,7 @@ "license": "Apache 2.0", "autoload": { "psr-4": { - "Rhubarb\\RestApi\\": "src/", - "Rhubarb\\RestApi\\Tests\\": "tests/unit/" + "Rhubarb\\RestApi\\": "src/" } }, "require": { diff --git a/src/Adapters/ModelResourceAdapter.php b/src/Adapters/ModelResourceAdapter.php new file mode 100644 index 0000000..2817782 --- /dev/null +++ b/src/Adapters/ModelResourceAdapter.php @@ -0,0 +1,162 @@ +modelClassName = $modelClassName; + $this->resourceClass = $resourceClass; + } + + /** + * @param $id + * @return object + * @throws ResourceNotFoundException + */ + public function makeResourceByIdentifier($id) + { + try { + $model = new $this->modelClassName($id); + } catch(RecordNotFoundException $er){ + throw new ResourceNotFoundException(); + } + + return $this->makeResourceFromData($model); + } + + public function makeModelFromResource($resource) + { + $modelClass = $this->modelClassName; + /** + * @var $model Model + */ + $model = new $modelClass(); + + $this->applyResourceToModel($resource, $model); + + return $model; + } + + private function getResourcePropertyMap() + { + $reflection = new \ReflectionClass($this->resourceClass); + $props = $reflection->getProperties(\ReflectionProperty::IS_PUBLIC); + + $lcaseProps = []; + + foreach($props as $prop) { + $lcaseProps[strtolower($prop->name)] = $prop->name; + } + + return $lcaseProps; + } + + public function makeResourceFromData($data) + { + $lcaseProps = $this->getResourcePropertyMap(); + + $reflection = new \ReflectionClass($this->resourceClass); + $resource = $reflection->newInstance(); + $resource->id = $data->getUniqueIdentifier(); + + foreach($data->exportData() as $prop => $value) { + if (isset($lcaseProps[strtolower($prop)])){ + $propName = $lcaseProps[strtolower($prop)]; + $resource->$propName = $value; + } + } + + return $resource; + } + + private function applyResourceToModel($resource, Model $model) + { + $lcaseProps = []; + + foreach($resource as $key => $value){ + $lcaseProps[strtolower($key)] = $key; + } + + $columns = $model->getSchema()->getColumns(); + + foreach($columns as $column){ + $lowerCaseColumnName = strtolower($column->columnName); + + if (isset($lcaseProps[$lowerCaseColumnName])){ + $model[$column->columnName] = $resource[$lcaseProps[$lowerCaseColumnName]]; + } + } + + return $model; + } + + + public function putResource($resource) + { + $modelClass = $this->modelClassName; + /** + * @var $model Model + */ + $model = new $modelClass($resource["id"]); + + $this->applyResourceToModel($resource, $model); + + $model->save(); + + return $model; + } + + protected function filterCollection(Collection $collection, $params, ?WebRequest $request = null) + { + + } + + private function getCollection($rangeStart, $rangeEnd, $params, ?WebRequest $request = null) + { + $modelClassName = $this->modelClassName; + + $list = $modelClassName::all(); + $list->setRange($rangeStart, $rangeEnd); + + $this->filterCollection($list, $params, $request); + + return $list; + } + + protected function getItems($rangeStart, $rangeEnd, $params, ?WebRequest $request = null) + { + $collection = $this->getCollection($rangeStart, $rangeEnd, $params, $request); + + $resources = []; + + foreach($collection as $item){ + $resources[] = $this->makeResourceFromData($item); + } + + return $resources; + } + + protected function countItems($rangeStart, $rangeEnd, $params, ?WebRequest $request = null) + { + return count($this->getCollection($rangeStart, $rangeEnd, $params, $request)); + } + + public function post($payload, $params, WebRequest $request) + { + $model = $this->makeModelFromResource($payload); + $model->save(); + + return $this->makeResourceFromData($model); + } +} \ No newline at end of file diff --git a/src/Adapters/ResourceAdapter.php b/src/Adapters/ResourceAdapter.php new file mode 100644 index 0000000..e0d9205 --- /dev/null +++ b/src/Adapters/ResourceAdapter.php @@ -0,0 +1,69 @@ +get($params, $request); + + if ($resource->id != $payload["id"]){ + throw new ResourceNotFoundException(); + } + + $this->putResource($payload); + + return $this->get($params, $request); + } + + public function get($params, ?WebRequest $request) + { + $id = $params["id"]; + + $payload = $this->makeResourceByIdentifier($id); + + return $payload; + } + + public abstract function putResource($resource); + + public abstract function makeResourceByIdentifier($id); + + public abstract function makeResourceFromData($data); + + public function list($params, ?WebRequest $request = null) + { + $start = $request->get("start", 0); + $end = $request->get("end", 99); + + $response = new ListResource(); + $response->range->start = $start; + $response->range->end = $end; + + $response->count = $this->countItems($start, $end, $params, $request); + $response->items = $this->getItems($start, $end, $params, $request); + + return $response; + } + + public abstract function post($payload, $params, WebRequest $request); + + protected abstract function countItems($rangeStart, $rangeEnd, $params, ?WebRequest $request); + + protected abstract function getItems($rangeStart, $rangeEnd, $params, ?WebRequest $request); +} \ No newline at end of file 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/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/Exceptions/InsertException.php b/src/Exceptions/InsertException.php deleted file mode 100644 index 732d5fb..0000000 --- a/src/Exceptions/InsertException.php +++ /dev/null @@ -1,26 +0,0 @@ -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/ListResource.php b/src/Resources/ListResource.php new file mode 100644 index 0000000..c081b2e --- /dev/null +++ b/src/Resources/ListResource.php @@ -0,0 +1,16 @@ +range = new Range(); + } + +} \ 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/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 @@ - [], + "post" => [], + "put" => [], + "delete" => [] + ]; + + /** + * @var Middleware[] + */ + private $middlewares = []; + + public function addMiddleware(Middleware $middleware) + { + $this->middlewares[] = $middleware; + } + + public function get($endPoint, $adapter) + { + $this->routes["get"][$endPoint] = $adapter; + + krsort($this->routes["get"]); + } + + public function post($endPoint, $adapter) + { + $this->routes["post"][$endPoint] = $adapter; + + krsort($this->routes["post"]); + } + + public function put($endPoint, $adapter) + { + $this->routes["put"][$endPoint] = $adapter; + + krsort($this->routes["put"]); + } + + public function delete($endPoint, $adapter) + { + $this->routes["delete"][$endPoint] = $adapter; + + krsort($this->routes["delete"]); + } + + /** + * Return the response if appropriate or false if no response could be generated. + * + * @param mixed $request + * @return bool|Response + */ + protected function generateResponseForRequest($request = null) + { + if (count($this->middlewares)){ + $x = -1; + + $middlewareOutput = null; + + $runMiddleware = function($runMiddleware) use(&$x, $request, &$middlewareOutput){ + $x++; + $middleware = $this->middlewares[$x]; + $callable = ($x + 1 == count($this->middlewares)) ? function(){} : function() use ($runMiddleware, &$middlewareOutput){ + $runMiddleware($runMiddleware); + }; + + $output = $middleware->handleRequest($request, $callable); + + if ($output){ + $middlewareOutput = $output; + } + }; + + $output = $runMiddleware($runMiddleware); + + if ($output){ + $middlewareOutput = $output; + } + + if ($middlewareOutput){ + return $middlewareOutput; + } + } + + /** + * @var WebRequest $request; + */ + + $method = strtolower($request->server("REQUEST_METHOD")); + + $endPoints = $this->routes[$method]; + + $remainingUrl = str_replace($this->matchingUrl, "", $request->urlPath); + $jsonResponse = new JsonResponse(); + $responseBody = new \stdClass(); + + foreach($endPoints as $endPoint => $callable){ + $endPoint = preg_replace("|/:([^/]+)|", "/(?<\\1>[^/]+)",$endPoint); + + if (preg_match('|'.$endPoint.'|', $remainingUrl, $matches)){ + + try { + $responseBody = $callable($matches, $request); + } catch (ResourceNotFoundException $er){ + $response = new NotFoundResponse(); + $response->setContent("The resource could not be located."); + return $response; + } catch (\Throwable $er){ + $response = new Response(); + $response->setResponseCode(500); + $response->setResponseMessage("An internal error occurred."); + + return $response; + } + + break; + } + } + + $jsonResponse->setContent($responseBody); + + return $jsonResponse; + } +} \ No newline at end of file diff --git a/src/UrlHandlers/RestApiRootHandler.php b/src/UrlHandlers/RestApiRootHandler.php deleted file mode 100644 index 507439c..0000000 --- a/src/UrlHandlers/RestApiRootHandler.php +++ /dev/null @@ -1,54 +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 @@ - Date: Thu, 29 Nov 2018 21:56:34 +0000 Subject: [PATCH 02/48] v3 WIP --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ffeeb0f..2d8a2a4 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "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/" From 2e825440b72ef09a3b91afd27d29adb07ed2915e Mon Sep 17 00:00:00 2001 From: Andrew Cuthbert Date: Fri, 30 Nov 2018 00:07:44 +0000 Subject: [PATCH 03/48] WIP --- src/Endpoints/CallableEndpoint.php | 25 ++++ src/Endpoints/Endpoint.php | 35 ++++++ src/Middleware/MiddlewareProcessingTrait.php | 50 ++++++++ src/UrlHandlers/RestApiHandler.php | 125 +++++++++++-------- 4 files changed, 185 insertions(+), 50 deletions(-) create mode 100644 src/Endpoints/CallableEndpoint.php create mode 100644 src/Endpoints/Endpoint.php create mode 100644 src/Middleware/MiddlewareProcessingTrait.php diff --git a/src/Endpoints/CallableEndpoint.php b/src/Endpoints/CallableEndpoint.php new file mode 100644 index 0000000..d4c5a29 --- /dev/null +++ b/src/Endpoints/CallableEndpoint.php @@ -0,0 +1,25 @@ +callable = $callable; + } + + protected function handleRequest($params, WebRequest $request) + { + $callable = $this->callable; + return $callable($params, $request); + } +} \ No newline at end of file diff --git a/src/Endpoints/Endpoint.php b/src/Endpoints/Endpoint.php new file mode 100644 index 0000000..57cb1d7 --- /dev/null +++ b/src/Endpoints/Endpoint.php @@ -0,0 +1,35 @@ +processMiddlewares($this->middlewares, $request); + + if ($middlewareResponse){ + return $middlewareResponse; + } + + return $this->handleRequest($params, $request); + } + + public function with(Middleware $middleware): Endpoint + { + $this->middlewares[] = $middleware; + + return $this; + } +} \ No newline at end of file diff --git a/src/Middleware/MiddlewareProcessingTrait.php b/src/Middleware/MiddlewareProcessingTrait.php new file mode 100644 index 0000000..5b1c94c --- /dev/null +++ b/src/Middleware/MiddlewareProcessingTrait.php @@ -0,0 +1,50 @@ +middlewares)) ? function(){} : function() use ($runMiddleware, &$middlewareOutput){ + $runMiddleware($runMiddleware); + }; + + $output = $middleware->handleRequest($request, $callable); + + if ($output){ + $middlewareOutput = $output; + } + }; + + $output = $runMiddleware($runMiddleware); + + if ($output){ + $middlewareOutput = $output; + } + + if ($middlewareOutput){ + return $middlewareOutput; + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/UrlHandlers/RestApiHandler.php b/src/UrlHandlers/RestApiHandler.php index 4f6d0bc..f2987dd 100644 --- a/src/UrlHandlers/RestApiHandler.php +++ b/src/UrlHandlers/RestApiHandler.php @@ -2,6 +2,8 @@ namespace Rhubarb\RestApi\UrlHandlers; +use Rhubarb\RestApi\Endpoints\CallableEndpoint; +use Rhubarb\RestApi\Endpoints\Endpoint; use Rhubarb\RestApi\Exceptions\ResourceNotFoundException; use Rhubarb\Crown\Request\WebRequest; use Rhubarb\Crown\Response\JsonResponse; @@ -9,9 +11,12 @@ use Rhubarb\Crown\Response\Response; use Rhubarb\Crown\UrlHandlers\UrlHandler; use Rhubarb\RestApi\Middleware\Middleware; +use Rhubarb\RestApi\Middleware\MiddlewareProcessingTrait; class RestApiHandler extends UrlHandler { + use MiddlewareProcessingTrait; + private $routes = [ "get" => [], "post" => [], @@ -29,32 +34,72 @@ public function addMiddleware(Middleware $middleware) $this->middlewares[] = $middleware; } - public function get($endPoint, $adapter) + /** + * Registers a route + * + * @param $type string get, post, put or delete + * @param $route string + * @param $endpoint callable|Endpoint + * @return Endpoint + */ + protected function registerRoute($type, $route, $endpoint): Endpoint { - $this->routes["get"][$endPoint] = $adapter; + if (is_callable($endpoint)){ + $endpoint = new CallableEndpoint($endpoint); + } + + $this->routes[$type][$route] = $endpoint; + krsort($this->routes[$type]); - krsort($this->routes["get"]); + return $endpoint; } - public function post($endPoint, $adapter) + /** + * Registers a get route + * + * @param $route string + * @param $endpoint callable|Endpoint + * @return Endpoint + */ + public function get($route, $endpoint):Endpoint { - $this->routes["post"][$endPoint] = $adapter; - - krsort($this->routes["post"]); + return $this->registerRoute("get", $route, $endpoint); } - public function put($endPoint, $adapter) + /** + * Registers a post route + * + * @param $route string + * @param $endpoint callable|Endpoint + * @return Endpoint + */ + public function post($route, $endpoint):Endpoint { - $this->routes["put"][$endPoint] = $adapter; - - krsort($this->routes["put"]); + return $this->registerRoute("post", $route, $endpoint); } - public function delete($endPoint, $adapter) + /** + * Registers a put route + * + * @param $route string + * @param $endpoint callable|Endpoint + * @return Endpoint + */ + public function put($route, $endpoint):Endpoint { - $this->routes["delete"][$endPoint] = $adapter; + return $this->registerRoute("put", $route, $endpoint); + } - krsort($this->routes["delete"]); + /** + * Registers a delete route + * + * @param $route string + * @param $endpoint callable|Endpoint + * @return Endpoint + */ + public function delete($route, $endpoint):Endpoint + { + return $this->registerRoute("delete", $route, $endpoint); } /** @@ -65,34 +110,10 @@ public function delete($endPoint, $adapter) */ protected function generateResponseForRequest($request = null) { - if (count($this->middlewares)){ - $x = -1; - - $middlewareOutput = null; - - $runMiddleware = function($runMiddleware) use(&$x, $request, &$middlewareOutput){ - $x++; - $middleware = $this->middlewares[$x]; - $callable = ($x + 1 == count($this->middlewares)) ? function(){} : function() use ($runMiddleware, &$middlewareOutput){ - $runMiddleware($runMiddleware); - }; - - $output = $middleware->handleRequest($request, $callable); - - if ($output){ - $middlewareOutput = $output; - } - }; + $middlewareResponse = $this->processMiddlewares($this->middlewares, $request); - $output = $runMiddleware($runMiddleware); - - if ($output){ - $middlewareOutput = $output; - } - - if ($middlewareOutput){ - return $middlewareOutput; - } + if ($middlewareResponse){ + return $middlewareResponse; } /** @@ -101,36 +122,40 @@ protected function generateResponseForRequest($request = null) $method = strtolower($request->server("REQUEST_METHOD")); - $endPoints = $this->routes[$method]; + $routes = $this->routes[$method]; $remainingUrl = str_replace($this->matchingUrl, "", $request->urlPath); $jsonResponse = new JsonResponse(); $responseBody = new \stdClass(); - foreach($endPoints as $endPoint => $callable){ - $endPoint = preg_replace("|/:([^/]+)|", "/(?<\\1>[^/]+)",$endPoint); + foreach($routes as $route => $endpoint){ + /** + * @var Endpoint $endpoint + */ + $route = preg_replace("|/:([^/]+)|", "/(?<\\1>[^/]+)",$route); - if (preg_match('|'.$endPoint.'|', $remainingUrl, $matches)){ + if (preg_match('|'.$route.'|', $remainingUrl, $matches)){ try { - $responseBody = $callable($matches, $request); + $response = $endpoint->processRequest($matches, $request); } catch (ResourceNotFoundException $er){ $response = new NotFoundResponse(); $response->setContent("The resource could not be located."); - return $response; } catch (\Throwable $er){ $response = new Response(); $response->setResponseCode(500); $response->setResponseMessage("An internal error occurred."); - - return $response; } break; } } - $jsonResponse->setContent($responseBody); + if ($response instanceof Response){ + return $response; + } + + $jsonResponse->setContent($response); return $jsonResponse; } From caf82c1ac9eaf2e79a470cfc3863788807e2d174 Mon Sep 17 00:00:00 2001 From: Andrew Cuthbert Date: Fri, 30 Nov 2018 21:15:26 +0000 Subject: [PATCH 04/48] Small bug --- src/UrlHandlers/RestApiHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UrlHandlers/RestApiHandler.php b/src/UrlHandlers/RestApiHandler.php index f2987dd..b7922de 100644 --- a/src/UrlHandlers/RestApiHandler.php +++ b/src/UrlHandlers/RestApiHandler.php @@ -126,7 +126,7 @@ protected function generateResponseForRequest($request = null) $remainingUrl = str_replace($this->matchingUrl, "", $request->urlPath); $jsonResponse = new JsonResponse(); - $responseBody = new \stdClass(); + $response = new \stdClass(); foreach($routes as $route => $endpoint){ /** From e8495862bb964856a3a97818ef904cae141fe1bd Mon Sep 17 00:00:00 2001 From: Andrew Cuthbert Date: Thu, 6 Dec 2018 23:45:15 +0000 Subject: [PATCH 05/48] Fix for full match of url --- src/UrlHandlers/RestApiHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UrlHandlers/RestApiHandler.php b/src/UrlHandlers/RestApiHandler.php index b7922de..e1a80e2 100644 --- a/src/UrlHandlers/RestApiHandler.php +++ b/src/UrlHandlers/RestApiHandler.php @@ -134,7 +134,7 @@ protected function generateResponseForRequest($request = null) */ $route = preg_replace("|/:([^/]+)|", "/(?<\\1>[^/]+)",$route); - if (preg_match('|'.$route.'|', $remainingUrl, $matches)){ + if (preg_match('|^'.$route.'|', $remainingUrl, $matches)){ try { $response = $endpoint->processRequest($matches, $request); From 1011f9131324ee01edc16424ee2d7727fae49e7f Mon Sep 17 00:00:00 2001 From: Andrew Cuthbert Date: Fri, 7 Dec 2018 15:07:12 +0000 Subject: [PATCH 06/48] Moved middle ware so it can accept params from the route. --- src/Endpoints/Endpoint.php | 2 +- src/Middleware/Middleware.php | 2 +- src/Middleware/MiddlewareProcessingTrait.php | 6 +++--- src/UrlHandlers/RestApiHandler.php | 15 +++++++++------ 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Endpoints/Endpoint.php b/src/Endpoints/Endpoint.php index 57cb1d7..6585da0 100644 --- a/src/Endpoints/Endpoint.php +++ b/src/Endpoints/Endpoint.php @@ -17,7 +17,7 @@ protected abstract function handleRequest($params, WebRequest $request); public final function processRequest($params, WebRequest $request) { - $middlewareResponse = $this->processMiddlewares($this->middlewares, $request); + $middlewareResponse = $this->processMiddlewares($this->middlewares, $params, $request); if ($middlewareResponse){ return $middlewareResponse; diff --git a/src/Middleware/Middleware.php b/src/Middleware/Middleware.php index 4660d26..a3be2b1 100644 --- a/src/Middleware/Middleware.php +++ b/src/Middleware/Middleware.php @@ -7,5 +7,5 @@ abstract class Middleware { - public abstract function handleRequest(WebRequest $request, callable $next): ?Response; + public abstract function handleRequest($params, WebRequest $request, callable $next): ?Response; } \ No newline at end of file diff --git a/src/Middleware/MiddlewareProcessingTrait.php b/src/Middleware/MiddlewareProcessingTrait.php index 5b1c94c..eca79e5 100644 --- a/src/Middleware/MiddlewareProcessingTrait.php +++ b/src/Middleware/MiddlewareProcessingTrait.php @@ -13,21 +13,21 @@ trait MiddlewareProcessingTrait * @param $middlewares Middleware[] The middlewares to process. * @return null|Response */ - protected function processMiddlewares($middlewares, WebRequest $request): ?Response + protected function processMiddlewares($middlewares, $params, WebRequest $request): ?Response { if (count($middlewares)){ $x = -1; $middlewareOutput = null; - $runMiddleware = function($runMiddleware) use(&$x, $middlewares, $request, &$middlewareOutput){ + $runMiddleware = function($runMiddleware) use(&$x, $middlewares, $request, $params, &$middlewareOutput){ $x++; $middleware = $middlewares[$x]; $callable = ($x + 1 == count($this->middlewares)) ? function(){} : function() use ($runMiddleware, &$middlewareOutput){ $runMiddleware($runMiddleware); }; - $output = $middleware->handleRequest($request, $callable); + $output = $middleware->handleRequest($params, $request, $callable); if ($output){ $middlewareOutput = $output; diff --git a/src/UrlHandlers/RestApiHandler.php b/src/UrlHandlers/RestApiHandler.php index e1a80e2..20f305f 100644 --- a/src/UrlHandlers/RestApiHandler.php +++ b/src/UrlHandlers/RestApiHandler.php @@ -110,12 +110,6 @@ public function delete($route, $endpoint):Endpoint */ protected function generateResponseForRequest($request = null) { - $middlewareResponse = $this->processMiddlewares($this->middlewares, $request); - - if ($middlewareResponse){ - return $middlewareResponse; - } - /** * @var WebRequest $request; */ @@ -128,6 +122,8 @@ protected function generateResponseForRequest($request = null) $jsonResponse = new JsonResponse(); $response = new \stdClass(); + $response = new JsonResponse(); + foreach($routes as $route => $endpoint){ /** * @var Endpoint $endpoint @@ -137,6 +133,13 @@ protected function generateResponseForRequest($request = null) if (preg_match('|^'.$route.'|', $remainingUrl, $matches)){ try { + + $middlewareResponse = $this->processMiddlewares($this->middlewares, $matches, $request); + + if ($middlewareResponse){ + return $middlewareResponse; + } + $response = $endpoint->processRequest($matches, $request); } catch (ResourceNotFoundException $er){ $response = new NotFoundResponse(); From d2571e21bf951f23e52307851c59b5624d1cb452 Mon Sep 17 00:00:00 2001 From: Andrew Cuthbert Date: Fri, 7 Dec 2018 15:07:43 +0000 Subject: [PATCH 07/48] Moved middle ware so it can accept params from the route. --- src/UrlHandlers/RestApiHandler.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/UrlHandlers/RestApiHandler.php b/src/UrlHandlers/RestApiHandler.php index 20f305f..e838f6c 100644 --- a/src/UrlHandlers/RestApiHandler.php +++ b/src/UrlHandlers/RestApiHandler.php @@ -120,7 +120,6 @@ protected function generateResponseForRequest($request = null) $remainingUrl = str_replace($this->matchingUrl, "", $request->urlPath); $jsonResponse = new JsonResponse(); - $response = new \stdClass(); $response = new JsonResponse(); From 175c5e0110d037bc66e12df0c5a6e59bad33ed88 Mon Sep 17 00:00:00 2001 From: Andrew Cuthbert Date: Mon, 10 Dec 2018 16:59:36 +0000 Subject: [PATCH 08/48] Fix for resource adapater --- src/Adapters/ModelResourceAdapter.php | 13 +++++++++---- src/UrlHandlers/RestApiHandler.php | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Adapters/ModelResourceAdapter.php b/src/Adapters/ModelResourceAdapter.php index 2817782..ca03490 100644 --- a/src/Adapters/ModelResourceAdapter.php +++ b/src/Adapters/ModelResourceAdapter.php @@ -62,15 +62,15 @@ private function getResourcePropertyMap() return $lcaseProps; } - public function makeResourceFromData($data) + public function makeResourceFromModel(Model $model) { $lcaseProps = $this->getResourcePropertyMap(); $reflection = new \ReflectionClass($this->resourceClass); $resource = $reflection->newInstance(); - $resource->id = $data->getUniqueIdentifier(); + $resource->id = $model->getUniqueIdentifier(); - foreach($data->exportData() as $prop => $value) { + foreach($model->exportData() as $prop => $value) { if (isset($lcaseProps[strtolower($prop)])){ $propName = $lcaseProps[strtolower($prop)]; $resource->$propName = $value; @@ -80,7 +80,12 @@ public function makeResourceFromData($data) return $resource; } - private function applyResourceToModel($resource, Model $model) + public function makeResourceFromData($data) + { + return $this->makeResourceFromModel($data); + } + + protected function applyResourceToModel($resource, Model $model) { $lcaseProps = []; diff --git a/src/UrlHandlers/RestApiHandler.php b/src/UrlHandlers/RestApiHandler.php index e838f6c..3daa3fc 100644 --- a/src/UrlHandlers/RestApiHandler.php +++ b/src/UrlHandlers/RestApiHandler.php @@ -2,6 +2,7 @@ namespace Rhubarb\RestApi\UrlHandlers; +use Rhubarb\Crown\Response\HtmlResponse; use Rhubarb\RestApi\Endpoints\CallableEndpoint; use Rhubarb\RestApi\Endpoints\Endpoint; use Rhubarb\RestApi\Exceptions\ResourceNotFoundException; @@ -116,6 +117,16 @@ protected function generateResponseForRequest($request = null) $method = strtolower($request->server("REQUEST_METHOD")); + if ($method == "options"){ + $response = new HtmlResponse(); + $response->setHeader("Access-Control-Allow-Origin", "*"); + $response->setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, DELETE, OPTIONS"); + $response->setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + $response->setHeader("Access-Control-Max-Age", "86400"); + + return $response; + } + $routes = $this->routes[$method]; $remainingUrl = str_replace($this->matchingUrl, "", $request->urlPath); @@ -157,6 +168,10 @@ protected function generateResponseForRequest($request = null) return $response; } + $jsonResponse->setHeader("Access-Control-Allow-Origin", "*"); + $jsonResponse->setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, DELETE, OPTIONS"); + $jsonResponse->setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + $jsonResponse->setHeader("Access-Control-Max-Age", "86400"); $jsonResponse->setContent($response); return $jsonResponse; From 316423d1f23110e8d53859f299455e04e4f8574d Mon Sep 17 00:00:00 2001 From: Michael Miscampbell Date: Wed, 30 Jan 2019 08:07:41 +0000 Subject: [PATCH 09/48] Fixing issue where the Rest API would return status code 200 even when the endpoint did not exist --- src/UrlHandlers/RestApiHandler.php | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/UrlHandlers/RestApiHandler.php b/src/UrlHandlers/RestApiHandler.php index 3daa3fc..e355092 100644 --- a/src/UrlHandlers/RestApiHandler.php +++ b/src/UrlHandlers/RestApiHandler.php @@ -130,9 +130,8 @@ protected function generateResponseForRequest($request = null) $routes = $this->routes[$method]; $remainingUrl = str_replace($this->matchingUrl, "", $request->urlPath); - $jsonResponse = new JsonResponse(); - $response = new JsonResponse(); + $response = null; foreach($routes as $route => $endpoint){ /** @@ -150,7 +149,9 @@ protected function generateResponseForRequest($request = null) return $middlewareResponse; } - $response = $endpoint->processRequest($matches, $request); + $payload = $endpoint->processRequest($matches, $request); + $response = new JsonResponse(); + $response->setContent($payload); } catch (ResourceNotFoundException $er){ $response = new NotFoundResponse(); $response->setContent("The resource could not be located."); @@ -165,15 +166,17 @@ protected function generateResponseForRequest($request = null) } if ($response instanceof Response){ + $response->setHeader("Access-Control-Allow-Origin", "*"); + $response->setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, DELETE, OPTIONS"); + $response->setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + $response->setHeader("Access-Control-Max-Age", "86400"); + return $response; } - $jsonResponse->setHeader("Access-Control-Allow-Origin", "*"); - $jsonResponse->setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, DELETE, OPTIONS"); - $jsonResponse->setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); - $jsonResponse->setHeader("Access-Control-Max-Age", "86400"); - $jsonResponse->setContent($response); + $response = new NotFoundResponse(); + $response->setContent("The resource could not be located."); - return $jsonResponse; + return $response; } } \ No newline at end of file From ca906ec74c9c6b31db2663d6e2d95bd5db842183 Mon Sep 17 00:00:00 2001 From: Michael Miscampbell Date: Wed, 30 Jan 2019 10:16:58 +0000 Subject: [PATCH 10/48] Fix for issue with RestApiHandler not returning the response object created from the Middleware layer --- src/UrlHandlers/RestApiHandler.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/UrlHandlers/RestApiHandler.php b/src/UrlHandlers/RestApiHandler.php index e355092..ce658af 100644 --- a/src/UrlHandlers/RestApiHandler.php +++ b/src/UrlHandlers/RestApiHandler.php @@ -150,8 +150,13 @@ protected function generateResponseForRequest($request = null) } $payload = $endpoint->processRequest($matches, $request); - $response = new JsonResponse(); - $response->setContent($payload); + + if ($payload instanceof Response) { + $response = $payload; + } else { + $response = new JsonResponse(); + $response->setContent($payload); + } } catch (ResourceNotFoundException $er){ $response = new NotFoundResponse(); $response->setContent("The resource could not be located."); From a70f68e7c0ef43dc5d03f048627907f5718ea90f Mon Sep 17 00:00:00 2001 From: Michael Miscampbell Date: Wed, 30 Jan 2019 14:53:35 +0000 Subject: [PATCH 11/48] Agreed with Rob this resource and payload 'id' check is not required because any put request should specify the id to update in the url --- src/Adapters/ResourceAdapter.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Adapters/ResourceAdapter.php b/src/Adapters/ResourceAdapter.php index e0d9205..c657e5a 100644 --- a/src/Adapters/ResourceAdapter.php +++ b/src/Adapters/ResourceAdapter.php @@ -22,10 +22,6 @@ public function put($payload, $params, ?WebRequest $request) { $resource = $this->get($params, $request); - if ($resource->id != $payload["id"]){ - throw new ResourceNotFoundException(); - } - $this->putResource($payload); return $this->get($params, $request); From a3aea390001ffea4dac60dfa4e2264fcb1fdea95 Mon Sep 17 00:00:00 2001 From: Michael Miscampbell Date: Wed, 30 Jan 2019 16:47:22 +0000 Subject: [PATCH 12/48] Adding delete method to support the delete method from RestApiHandler --- src/Adapters/ModelResourceAdapter.php | 4 ++++ src/Adapters/ResourceAdapter.php | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/Adapters/ModelResourceAdapter.php b/src/Adapters/ModelResourceAdapter.php index ca03490..0240932 100644 --- a/src/Adapters/ModelResourceAdapter.php +++ b/src/Adapters/ModelResourceAdapter.php @@ -164,4 +164,8 @@ public function post($payload, $params, WebRequest $request) return $this->makeResourceFromData($model); } + + public function delete($payload, $params, ?WebRequest $request) + { + } } \ No newline at end of file diff --git a/src/Adapters/ResourceAdapter.php b/src/Adapters/ResourceAdapter.php index c657e5a..5bf9c0b 100644 --- a/src/Adapters/ResourceAdapter.php +++ b/src/Adapters/ResourceAdapter.php @@ -59,6 +59,8 @@ public function list($params, ?WebRequest $request = null) public abstract function post($payload, $params, WebRequest $request); + public abstract function delete($payload, $params, ?WebRequest $request); + protected abstract function countItems($rangeStart, $rangeEnd, $params, ?WebRequest $request); protected abstract function getItems($rangeStart, $rangeEnd, $params, ?WebRequest $request); From f945fba8cbea737249697df3489b2bfc5f36666e Mon Sep 17 00:00:00 2001 From: Michael Miscampbell Date: Wed, 30 Jan 2019 19:56:45 +0000 Subject: [PATCH 13/48] Addressing the issue where the id for a model is not sent as part of the body. Added Validation for an empty payload being sent. --- src/Adapters/ModelResourceAdapter.php | 14 +++++++++ src/Adapters/ResourceAdapter.php | 29 +++++++++++++++++++ .../RequestPayloadValidationException.php | 10 +++++++ src/UrlHandlers/RestApiHandler.php | 5 ++++ 4 files changed, 58 insertions(+) create mode 100644 src/Exceptions/RequestPayloadValidationException.php diff --git a/src/Adapters/ModelResourceAdapter.php b/src/Adapters/ModelResourceAdapter.php index 0240932..732a564 100644 --- a/src/Adapters/ModelResourceAdapter.php +++ b/src/Adapters/ModelResourceAdapter.php @@ -110,6 +110,11 @@ protected function applyResourceToModel($resource, Model $model) public function putResource($resource) { $modelClass = $this->modelClassName; + + if (!isset($resource["id"])) { + throw new ResourceNotFoundException(); + } + /** * @var $model Model */ @@ -122,6 +127,15 @@ public function putResource($resource) return $model; } + protected function applyParamsToPayload($payload, $params) + { + if (isset($params["id"])) { + $payload["id"] = $params["id"]; + } + + return parent::applyParamsToPayload($payload, $params); + } + protected function filterCollection(Collection $collection, $params, ?WebRequest $request = null) { diff --git a/src/Adapters/ResourceAdapter.php b/src/Adapters/ResourceAdapter.php index 5bf9c0b..627ec05 100644 --- a/src/Adapters/ResourceAdapter.php +++ b/src/Adapters/ResourceAdapter.php @@ -4,6 +4,7 @@ use Rhubarb\Crown\Request\WebRequest; use Rhubarb\RestApi\Exceptions\ResourceNotFoundException; +use Rhubarb\RestApi\Exceptions\RequestPayloadValidationException; use Rhubarb\RestApi\Resources\ListResource; /** @@ -20,6 +21,10 @@ abstract class ResourceAdapter */ public function put($payload, $params, ?WebRequest $request) { + $payload = $this->validatePutRequestPayload($payload); + + $payload = $this->applyParamsToPayload($payload, $params); + $resource = $this->get($params, $request); $this->putResource($payload); @@ -27,6 +32,13 @@ public function put($payload, $params, ?WebRequest $request) return $this->get($params, $request); } + protected function validatePutRequestPayload($payload) + { + $this->validateRequestPayload($payload); + + return $payload; + } + public function get($params, ?WebRequest $request) { $id = $params["id"]; @@ -59,9 +71,26 @@ public function list($params, ?WebRequest $request = null) public abstract function post($payload, $params, WebRequest $request); + protected function validatePostRequestPayload($payload) + { + $this->validateRequestPayload($payload); + } + public abstract function delete($payload, $params, ?WebRequest $request); protected abstract function countItems($rangeStart, $rangeEnd, $params, ?WebRequest $request); protected abstract function getItems($rangeStart, $rangeEnd, $params, ?WebRequest $request); + + protected function applyParamsToPayload($payload, $params) + { + return $payload; + } + + private final function validateRequestPayload($payload) + { + if (!is_array($payload)) { + throw new RequestPayloadValidationException("POST and PUT options require a JSON encoded resource object in the body of the request."); + } + } } \ No newline at end of file diff --git a/src/Exceptions/RequestPayloadValidationException.php b/src/Exceptions/RequestPayloadValidationException.php new file mode 100644 index 0000000..6597d1d --- /dev/null +++ b/src/Exceptions/RequestPayloadValidationException.php @@ -0,0 +1,10 @@ +setContent("The resource could not be located."); + } catch (RequestPayloadValidationException $er){ + $response = new NotAuthorisedResponse(); + $response->setContent($er->getMessage()); } catch (\Throwable $er){ $response = new Response(); $response->setResponseCode(500); From 1daf38ddbce2bfbab74e5bbf1baa7566dce14ffe Mon Sep 17 00:00:00 2001 From: Michael Miscampbell Date: Wed, 30 Jan 2019 21:15:54 +0000 Subject: [PATCH 14/48] Adding validation to the post method on ModelResourceAdapter. This will now validate the body has data --- src/Adapters/ModelResourceAdapter.php | 2 ++ src/Adapters/ResourceAdapter.php | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Adapters/ModelResourceAdapter.php b/src/Adapters/ModelResourceAdapter.php index 732a564..debfc24 100644 --- a/src/Adapters/ModelResourceAdapter.php +++ b/src/Adapters/ModelResourceAdapter.php @@ -173,6 +173,8 @@ protected function countItems($rangeStart, $rangeEnd, $params, ?WebRequest $requ public function post($payload, $params, WebRequest $request) { + $payload = $this->validatePostRequestPayload($payload); + $model = $this->makeModelFromResource($payload); $model->save(); diff --git a/src/Adapters/ResourceAdapter.php b/src/Adapters/ResourceAdapter.php index 627ec05..b7d5b9c 100644 --- a/src/Adapters/ResourceAdapter.php +++ b/src/Adapters/ResourceAdapter.php @@ -73,7 +73,7 @@ public abstract function post($payload, $params, WebRequest $request); protected function validatePostRequestPayload($payload) { - $this->validateRequestPayload($payload); + return $this->validateRequestPayload($payload); } public abstract function delete($payload, $params, ?WebRequest $request); @@ -92,5 +92,7 @@ private final function validateRequestPayload($payload) if (!is_array($payload)) { throw new RequestPayloadValidationException("POST and PUT options require a JSON encoded resource object in the body of the request."); } + + return $payload; } } \ No newline at end of file From ad58e5a5e39c9927a075e6610a1514ee90ec5c7b Mon Sep 17 00:00:00 2001 From: Michael Miscampbell Date: Wed, 30 Jan 2019 22:38:04 +0000 Subject: [PATCH 15/48] Removing abstract methods. This will enable us to override the functionality of post/put methods and add validation. Added RestImplementationException --- src/Adapters/ModelResourceAdapter.php | 6 +- src/Adapters/ResourceAdapter.php | 84 +++++++++++++------ .../RestImplementationException.php | 13 +++ src/UrlHandlers/RestApiHandler.php | 7 +- 4 files changed, 78 insertions(+), 32 deletions(-) create mode 100644 src/Exceptions/RestImplementationException.php diff --git a/src/Adapters/ModelResourceAdapter.php b/src/Adapters/ModelResourceAdapter.php index debfc24..44f24be 100644 --- a/src/Adapters/ModelResourceAdapter.php +++ b/src/Adapters/ModelResourceAdapter.php @@ -171,11 +171,9 @@ protected function countItems($rangeStart, $rangeEnd, $params, ?WebRequest $requ return count($this->getCollection($rangeStart, $rangeEnd, $params, $request)); } - public function post($payload, $params, WebRequest $request) + public function postResource($resource) { - $payload = $this->validatePostRequestPayload($payload); - - $model = $this->makeModelFromResource($payload); + $model = $this->makeModelFromResource($resource); $model->save(); return $this->makeResourceFromData($model); diff --git a/src/Adapters/ResourceAdapter.php b/src/Adapters/ResourceAdapter.php index b7d5b9c..6d03903 100644 --- a/src/Adapters/ResourceAdapter.php +++ b/src/Adapters/ResourceAdapter.php @@ -12,6 +12,15 @@ */ abstract class ResourceAdapter { + public function get($params, ?WebRequest $request) + { + $id = $params["id"]; + + $payload = $this->makeResourceByIdentifier($id); + + return $payload; + } + /** * @param $payload * @param $params @@ -39,20 +48,58 @@ protected function validatePutRequestPayload($payload) return $payload; } - public function get($params, ?WebRequest $request) + public function putResource($resource) { - $id = $params["id"]; + throw new RestImplementationException("Missing implementation of " . __FUNCTION__); + } - $payload = $this->makeResourceByIdentifier($id); + public function post($payload, $params, WebRequest $request) + { + $payload = $this->validatePostRequestPayload($payload); + + return $this->postResource($payload); + } + + protected function validatePostRequestPayload($payload) + { + return $this->validateRequestPayload($payload); + } + + public function postResource($resource) + { + throw new RestImplementationException("Missing implementation of " . __FUNCTION__); + } + + public function delete($payload, $params, ?WebRequest $request) + { + throw new RestImplementationException("Missing implementation of " . __FUNCTION__); + } + + + protected function applyParamsToPayload($payload, $params) + { + return $payload; + } + + private final function validateRequestPayload($payload) + { + if (!is_array($payload)) { + throw new RequestPayloadValidationException("POST and PUT options require a JSON encoded resource object in the body of the request."); + } return $payload; } - public abstract function putResource($resource); - public abstract function makeResourceByIdentifier($id); + public function makeResourceByIdentifier($id) + { + throw new RestImplementationException("Missing implementation of " . __FUNCTION__); + } - public abstract function makeResourceFromData($data); + public function makeResourceFromData($data) + { + throw new RestImplementationException("Missing implementation of " . __FUNCTION__); + } public function list($params, ?WebRequest $request = null) { @@ -69,30 +116,13 @@ public function list($params, ?WebRequest $request = null) return $response; } - public abstract function post($payload, $params, WebRequest $request); - - protected function validatePostRequestPayload($payload) - { - return $this->validateRequestPayload($payload); - } - - public abstract function delete($payload, $params, ?WebRequest $request); - - protected abstract function countItems($rangeStart, $rangeEnd, $params, ?WebRequest $request); - - protected abstract function getItems($rangeStart, $rangeEnd, $params, ?WebRequest $request); - - protected function applyParamsToPayload($payload, $params) + protected function countItems($rangeStart, $rangeEnd, $params, ?WebRequest $request) { - return $payload; + throw new RestImplementationException("Missing implementation of " . __FUNCTION__); } - private final function validateRequestPayload($payload) + protected function getItems($rangeStart, $rangeEnd, $params, ?WebRequest $request) { - if (!is_array($payload)) { - throw new RequestPayloadValidationException("POST and PUT options require a JSON encoded resource object in the body of the request."); - } - - return $payload; + throw new RestImplementationException("Missing implementation of " . __FUNCTION__); } } \ No newline at end of file diff --git a/src/Exceptions/RestImplementationException.php b/src/Exceptions/RestImplementationException.php new file mode 100644 index 0000000..c256dbd --- /dev/null +++ b/src/Exceptions/RestImplementationException.php @@ -0,0 +1,13 @@ +setContent($er->getMessage()); - } catch (\Throwable $er){ + } catch (RestImplementationException $exception) { + $response = new Response(); + $response->setResponseCode(Response::HTTP_STATUS_CLIENT_ERROR_BAD_REQUEST); + $response->setResponseMessage("Bad request received"); + } catch(\Throwable $er){ $response = new Response(); $response->setResponseCode(500); $response->setResponseMessage("An internal error occurred."); From 2dfcc6a74f8422203f1ecbe4a1b8a4d55bae33ae Mon Sep 17 00:00:00 2001 From: Michael Miscampbell Date: Thu, 31 Jan 2019 16:22:30 +0000 Subject: [PATCH 16/48] Adding Helper method to remove unwanted properties from the payload received --- src/Helpers/ResourceHelper.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/Helpers/ResourceHelper.php diff --git a/src/Helpers/ResourceHelper.php b/src/Helpers/ResourceHelper.php new file mode 100644 index 0000000..1211f2e --- /dev/null +++ b/src/Helpers/ResourceHelper.php @@ -0,0 +1,26 @@ + $value) { + if (self::isNotAllowedProperty($key, $allowedProperties)) { + unset($payload[$key]); + } + } + + return $payload; + } + + private static function isNotAllowedProperty(string $property, array $allowedProperties) + { + return !in_array($property, $allowedProperties); + } +} \ No newline at end of file From 71b5241fe5f8dc65b6fd3d4f613090ac8cfdcbbe Mon Sep 17 00:00:00 2001 From: Michael Miscampbell Date: Thu, 31 Jan 2019 18:23:40 +0000 Subject: [PATCH 17/48] Changing code syntax as per recommendation from Bert --- src/Helpers/ResourceHelper.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Helpers/ResourceHelper.php b/src/Helpers/ResourceHelper.php index 1211f2e..8b0ca50 100644 --- a/src/Helpers/ResourceHelper.php +++ b/src/Helpers/ResourceHelper.php @@ -4,23 +4,25 @@ class ResourceHelper { - public static function removeInvalidProperties($allowedProperties, $payload) + /** + * Used to remove properties of the payload that are not allowed + * + * @param $allowedProperties + * @param $payload + * @return mixed + */ + public static function removeProperties($allowedProperties, $payload) { if (!isset($allowedProperties)) { return $payload; } foreach ($payload as $key => $value) { - if (self::isNotAllowedProperty($key, $allowedProperties)) { + if (!in_array($key, $allowedProperties)) { unset($payload[$key]); } } return $payload; } - - private static function isNotAllowedProperty(string $property, array $allowedProperties) - { - return !in_array($property, $allowedProperties); - } } \ No newline at end of file From 924c42c2d4f6f5c06371769b757e0f656003d7ba Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Mon, 4 Feb 2019 14:16:12 +0000 Subject: [PATCH 18/48] out with the old --- docs/advanced.md | 92 ----- docs/basics.md | 189 --------- docs/index.md | 103 ----- docs/model-bound.md | 360 ------------------ src/Adapters/ModelResourceAdapter.php | 185 --------- src/Endpoints/CallableEndpoint.php | 25 -- src/Endpoints/Endpoint.php | 35 -- .../RequestPayloadValidationException.php | 10 - src/Exceptions/ResourceNotFoundException.php | 10 - .../RestImplementationException.php | 13 - src/Helpers/ResourceHelper.php | 28 -- src/Middleware/Middleware.php | 11 - src/Middleware/MiddlewareProcessingTrait.php | 50 --- src/Resources/ListResource.php | 16 - src/Resources/Range.php | 9 - src/RhubarbApiModule.php | 14 + src/RhubarbRestAPIApplication.php | 52 +++ src/UrlHandlers/RestApiHandler.php | 197 ---------- 18 files changed, 66 insertions(+), 1333 deletions(-) delete mode 100644 docs/advanced.md delete mode 100644 docs/basics.md delete mode 100644 docs/index.md delete mode 100644 docs/model-bound.md delete mode 100644 src/Adapters/ModelResourceAdapter.php delete mode 100644 src/Endpoints/CallableEndpoint.php delete mode 100644 src/Endpoints/Endpoint.php delete mode 100644 src/Exceptions/RequestPayloadValidationException.php delete mode 100644 src/Exceptions/ResourceNotFoundException.php delete mode 100644 src/Exceptions/RestImplementationException.php delete mode 100644 src/Helpers/ResourceHelper.php delete mode 100644 src/Middleware/Middleware.php delete mode 100644 src/Middleware/MiddlewareProcessingTrait.php delete mode 100644 src/Resources/ListResource.php delete mode 100644 src/Resources/Range.php create mode 100644 src/RhubarbApiModule.php create mode 100644 src/RhubarbRestAPIApplication.php delete mode 100644 src/UrlHandlers/RestApiHandler.php 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/src/Adapters/ModelResourceAdapter.php b/src/Adapters/ModelResourceAdapter.php deleted file mode 100644 index 44f24be..0000000 --- a/src/Adapters/ModelResourceAdapter.php +++ /dev/null @@ -1,185 +0,0 @@ -modelClassName = $modelClassName; - $this->resourceClass = $resourceClass; - } - - /** - * @param $id - * @return object - * @throws ResourceNotFoundException - */ - public function makeResourceByIdentifier($id) - { - try { - $model = new $this->modelClassName($id); - } catch(RecordNotFoundException $er){ - throw new ResourceNotFoundException(); - } - - return $this->makeResourceFromData($model); - } - - public function makeModelFromResource($resource) - { - $modelClass = $this->modelClassName; - /** - * @var $model Model - */ - $model = new $modelClass(); - - $this->applyResourceToModel($resource, $model); - - return $model; - } - - private function getResourcePropertyMap() - { - $reflection = new \ReflectionClass($this->resourceClass); - $props = $reflection->getProperties(\ReflectionProperty::IS_PUBLIC); - - $lcaseProps = []; - - foreach($props as $prop) { - $lcaseProps[strtolower($prop->name)] = $prop->name; - } - - return $lcaseProps; - } - - public function makeResourceFromModel(Model $model) - { - $lcaseProps = $this->getResourcePropertyMap(); - - $reflection = new \ReflectionClass($this->resourceClass); - $resource = $reflection->newInstance(); - $resource->id = $model->getUniqueIdentifier(); - - foreach($model->exportData() as $prop => $value) { - if (isset($lcaseProps[strtolower($prop)])){ - $propName = $lcaseProps[strtolower($prop)]; - $resource->$propName = $value; - } - } - - return $resource; - } - - public function makeResourceFromData($data) - { - return $this->makeResourceFromModel($data); - } - - protected function applyResourceToModel($resource, Model $model) - { - $lcaseProps = []; - - foreach($resource as $key => $value){ - $lcaseProps[strtolower($key)] = $key; - } - - $columns = $model->getSchema()->getColumns(); - - foreach($columns as $column){ - $lowerCaseColumnName = strtolower($column->columnName); - - if (isset($lcaseProps[$lowerCaseColumnName])){ - $model[$column->columnName] = $resource[$lcaseProps[$lowerCaseColumnName]]; - } - } - - return $model; - } - - - public function putResource($resource) - { - $modelClass = $this->modelClassName; - - if (!isset($resource["id"])) { - throw new ResourceNotFoundException(); - } - - /** - * @var $model Model - */ - $model = new $modelClass($resource["id"]); - - $this->applyResourceToModel($resource, $model); - - $model->save(); - - return $model; - } - - protected function applyParamsToPayload($payload, $params) - { - if (isset($params["id"])) { - $payload["id"] = $params["id"]; - } - - return parent::applyParamsToPayload($payload, $params); - } - - protected function filterCollection(Collection $collection, $params, ?WebRequest $request = null) - { - - } - - private function getCollection($rangeStart, $rangeEnd, $params, ?WebRequest $request = null) - { - $modelClassName = $this->modelClassName; - - $list = $modelClassName::all(); - $list->setRange($rangeStart, $rangeEnd); - - $this->filterCollection($list, $params, $request); - - return $list; - } - - protected function getItems($rangeStart, $rangeEnd, $params, ?WebRequest $request = null) - { - $collection = $this->getCollection($rangeStart, $rangeEnd, $params, $request); - - $resources = []; - - foreach($collection as $item){ - $resources[] = $this->makeResourceFromData($item); - } - - return $resources; - } - - protected function countItems($rangeStart, $rangeEnd, $params, ?WebRequest $request = null) - { - return count($this->getCollection($rangeStart, $rangeEnd, $params, $request)); - } - - public function postResource($resource) - { - $model = $this->makeModelFromResource($resource); - $model->save(); - - return $this->makeResourceFromData($model); - } - - public function delete($payload, $params, ?WebRequest $request) - { - } -} \ No newline at end of file diff --git a/src/Endpoints/CallableEndpoint.php b/src/Endpoints/CallableEndpoint.php deleted file mode 100644 index d4c5a29..0000000 --- a/src/Endpoints/CallableEndpoint.php +++ /dev/null @@ -1,25 +0,0 @@ -callable = $callable; - } - - protected function handleRequest($params, WebRequest $request) - { - $callable = $this->callable; - return $callable($params, $request); - } -} \ No newline at end of file diff --git a/src/Endpoints/Endpoint.php b/src/Endpoints/Endpoint.php deleted file mode 100644 index 6585da0..0000000 --- a/src/Endpoints/Endpoint.php +++ /dev/null @@ -1,35 +0,0 @@ -processMiddlewares($this->middlewares, $params, $request); - - if ($middlewareResponse){ - return $middlewareResponse; - } - - return $this->handleRequest($params, $request); - } - - public function with(Middleware $middleware): Endpoint - { - $this->middlewares[] = $middleware; - - return $this; - } -} \ No newline at end of file diff --git a/src/Exceptions/RequestPayloadValidationException.php b/src/Exceptions/RequestPayloadValidationException.php deleted file mode 100644 index 6597d1d..0000000 --- a/src/Exceptions/RequestPayloadValidationException.php +++ /dev/null @@ -1,10 +0,0 @@ - $value) { - if (!in_array($key, $allowedProperties)) { - unset($payload[$key]); - } - } - - return $payload; - } -} \ No newline at end of file diff --git a/src/Middleware/Middleware.php b/src/Middleware/Middleware.php deleted file mode 100644 index a3be2b1..0000000 --- a/src/Middleware/Middleware.php +++ /dev/null @@ -1,11 +0,0 @@ -middlewares)) ? function(){} : function() use ($runMiddleware, &$middlewareOutput){ - $runMiddleware($runMiddleware); - }; - - $output = $middleware->handleRequest($params, $request, $callable); - - if ($output){ - $middlewareOutput = $output; - } - }; - - $output = $runMiddleware($runMiddleware); - - if ($output){ - $middlewareOutput = $output; - } - - if ($middlewareOutput){ - return $middlewareOutput; - } - } - - return null; - } -} \ No newline at end of file diff --git a/src/Resources/ListResource.php b/src/Resources/ListResource.php deleted file mode 100644 index c081b2e..0000000 --- a/src/Resources/ListResource.php +++ /dev/null @@ -1,16 +0,0 @@ -range = new Range(); - } - -} \ No newline at end of file diff --git a/src/Resources/Range.php b/src/Resources/Range.php deleted file mode 100644 index 5a334f9..0000000 --- a/src/Resources/Range.php +++ /dev/null @@ -1,9 +0,0 @@ -app->getContainer(); + $container['errorHandler'] = $container['phpErrorHandler'] = function () { + return new DefaultErrorHandler(); + }; + } + + protected function registerMiddleware() + { + + } + + 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/RestApiHandler.php b/src/UrlHandlers/RestApiHandler.php deleted file mode 100644 index b8b4f26..0000000 --- a/src/UrlHandlers/RestApiHandler.php +++ /dev/null @@ -1,197 +0,0 @@ - [], - "post" => [], - "put" => [], - "delete" => [] - ]; - - /** - * @var Middleware[] - */ - private $middlewares = []; - - public function addMiddleware(Middleware $middleware) - { - $this->middlewares[] = $middleware; - } - - /** - * Registers a route - * - * @param $type string get, post, put or delete - * @param $route string - * @param $endpoint callable|Endpoint - * @return Endpoint - */ - protected function registerRoute($type, $route, $endpoint): Endpoint - { - if (is_callable($endpoint)){ - $endpoint = new CallableEndpoint($endpoint); - } - - $this->routes[$type][$route] = $endpoint; - krsort($this->routes[$type]); - - return $endpoint; - } - - /** - * Registers a get route - * - * @param $route string - * @param $endpoint callable|Endpoint - * @return Endpoint - */ - public function get($route, $endpoint):Endpoint - { - return $this->registerRoute("get", $route, $endpoint); - } - - /** - * Registers a post route - * - * @param $route string - * @param $endpoint callable|Endpoint - * @return Endpoint - */ - public function post($route, $endpoint):Endpoint - { - return $this->registerRoute("post", $route, $endpoint); - } - - /** - * Registers a put route - * - * @param $route string - * @param $endpoint callable|Endpoint - * @return Endpoint - */ - public function put($route, $endpoint):Endpoint - { - return $this->registerRoute("put", $route, $endpoint); - } - - /** - * Registers a delete route - * - * @param $route string - * @param $endpoint callable|Endpoint - * @return Endpoint - */ - public function delete($route, $endpoint):Endpoint - { - return $this->registerRoute("delete", $route, $endpoint); - } - - /** - * Return the response if appropriate or false if no response could be generated. - * - * @param mixed $request - * @return bool|Response - */ - protected function generateResponseForRequest($request = null) - { - /** - * @var WebRequest $request; - */ - - $method = strtolower($request->server("REQUEST_METHOD")); - - if ($method == "options"){ - $response = new HtmlResponse(); - $response->setHeader("Access-Control-Allow-Origin", "*"); - $response->setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, DELETE, OPTIONS"); - $response->setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); - $response->setHeader("Access-Control-Max-Age", "86400"); - - return $response; - } - - $routes = $this->routes[$method]; - - $remainingUrl = str_replace($this->matchingUrl, "", $request->urlPath); - - $response = null; - - foreach($routes as $route => $endpoint){ - /** - * @var Endpoint $endpoint - */ - $route = preg_replace("|/:([^/]+)|", "/(?<\\1>[^/]+)",$route); - - if (preg_match('|^'.$route.'|', $remainingUrl, $matches)){ - - try { - - $middlewareResponse = $this->processMiddlewares($this->middlewares, $matches, $request); - - if ($middlewareResponse){ - return $middlewareResponse; - } - - $payload = $endpoint->processRequest($matches, $request); - - if ($payload instanceof Response) { - $response = $payload; - } else { - $response = new JsonResponse(); - $response->setContent($payload); - } - } catch (ResourceNotFoundException $er){ - $response = new NotFoundResponse(); - $response->setContent("The resource could not be located."); - } catch (RequestPayloadValidationException $er){ - $response = new NotAuthorisedResponse(); - $response->setContent($er->getMessage()); - } catch (RestImplementationException $exception) { - $response = new Response(); - $response->setResponseCode(Response::HTTP_STATUS_CLIENT_ERROR_BAD_REQUEST); - $response->setResponseMessage("Bad request received"); - } catch(\Throwable $er){ - $response = new Response(); - $response->setResponseCode(500); - $response->setResponseMessage("An internal error occurred."); - } - - break; - } - } - - if ($response instanceof Response){ - $response->setHeader("Access-Control-Allow-Origin", "*"); - $response->setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, DELETE, OPTIONS"); - $response->setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); - $response->setHeader("Access-Control-Max-Age", "86400"); - - return $response; - } - - $response = new NotFoundResponse(); - $response->setContent("The resource could not be located."); - - return $response; - } -} \ No newline at end of file From 1bcaecaa642cfbea5a323cc0745c11abd0c93eaa Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Mon, 4 Feb 2019 14:16:26 +0000 Subject: [PATCH 19/48] in with the new --- CHANGELOG.md | 3 +- composer.json | 3 +- composer.lock | 964 ++++++++++++------- resources/boot.php | 26 + src/Calls/FakeCall.php | 42 + src/ErrorHandlers/DefaultErrorHandler.php | 23 + src/Exceptions/ApiException.php | 7 + src/Exceptions/MethodNotAllowedException.php | 11 + 8 files changed, 739 insertions(+), 340 deletions(-) create mode 100644 resources/boot.php create mode 100644 src/Calls/FakeCall.php create mode 100644 src/ErrorHandlers/DefaultErrorHandler.php create mode 100644 src/Exceptions/ApiException.php create mode 100644 src/Exceptions/MethodNotAllowedException.php diff --git a/CHANGELOG.md b/CHANGELOG.md index cec6a69..5e992b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ### 3.0.0 -Changed: Total slim down +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 diff --git a/composer.json b/composer.json index 2d8a2a4..b437aaa 100644 --- a/composer.json +++ b/composer.json @@ -9,8 +9,7 @@ }, "require": { "rhubarbphp/rhubarb": "^1.4.2", - "rhubarbphp/module-leaf": "^1.0.0", - "rhubarbphp/module-stem": "^1.5.1" + "slim/slim": "^3.12" }, "require-dev": { "codeception/codeception": "^2.3" diff --git a/composer.lock b/composer.lock index 742c6ec..ed5d32a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,39 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "8d8f83b39152443c75ba9fd93ef031e9", + "content-hash": "e7c4ff3cca503e7825062684ca459d61", "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,191 +164,116 @@ ], "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", - "autoload": { - "psr-4": { - "Rhubarb\\Custard\\": "src/", - "Rhubarb\\Custard\\Tests\\": "tests/" + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" } }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "description": "Provides command line tools for Rhubarb projects.", - "homepage": "http://www.rhubarbphp.com/", - "keywords": [ - "cli", - "command-line", - "custard", - "framework", - "php", - "rhubarb", - "tools" - ], - "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": "Provides javascript validation patterns for HTML forms", - "time": "2017-08-17T14:34:15+00:00" - }, - { - "name": "rhubarbphp/module-leaf", - "version": "1.3.13", - "source": { - "type": "git", - "url": "https://github.com/RhubarbPHP/Module.Leaf.git", - "reference": "0a14a51b4afa11b8d1babd4a06e2808195b6cca1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/RhubarbPHP/Module.Leaf/zipball/0a14a51b4afa11b8d1babd4a06e2808195b6cca1", - "reference": "0a14a51b4afa11b8d1babd4a06e2808195b6cca1", - "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": "*" - }, - "type": "library", "autoload": { "psr-4": { - "Rhubarb\\Leaf\\": "src/", - "Rhubarb\\Leaf\\Tests\\": "tests/unit/" + "Psr\\Container\\": "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 Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", "keywords": [ - "framework", - "leaf", - "mvp", - "php", - "presenter", - "rhubarb" + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" ], - "time": "2017-10-24T13:50:44+00:00" + "time": "2017-02-14T16:28:37+00:00" }, { - "name": "rhubarbphp/module-stem", - "version": "1.5.1", + "name": "psr/http-message", + "version": "1.0.1", "source": { "type": "git", - "url": "https://github.com/RhubarbPHP/Module.Stem.git", - "reference": "14de4fef64322d9a87020db1b34d818abb2dece4" + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/RhubarbPHP/Module.Stem/zipball/14de4fef64322d9a87020db1b34d818abb2dece4", - "reference": "14de4fef64322d9a87020db1b34d818abb2dece4", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", "shasum": "" }, "require": { - "rhubarbphp/rhubarb": "^1.1.0" - }, - "require-dev": { - "codeception/codeception": "^2.1.8", - "pdepend/pdepend": "^2.0.0", - "phploc/phploc": "^3.0.0", - "phpmd/phpmd": "^2.0.0", - "rhubarbphp/custard": "^1.0.0", - "rhubarbphp/module-build-status-updater": "^1.0.4", - "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\\Stem\\": "src/", - "Rhubarb\\Stem\\Tests\\": "tests/" + "Psr\\Http\\Message\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache-2.0" + "MIT" ], - "description": "A data modelling and ORM module for the Rhubarb PHP framework", - "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": [ - "Modelling", - "data", - "framework", - "model", - "orm", - "php" + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" ], - "time": "2017-11-15T15:14:27+00:00" + "time": "2016-08-06T14:39:51+00:00" }, { "name": "rhubarbphp/rhubarb", @@ -367,110 +324,39 @@ "time": "2017-11-16T12:38:01+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 +364,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 +986,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 +1595,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 +1619,7 @@ }, "autoload": { "psr-4": { - "Psr\\Http\\Message\\": "src/" + "Psr\\Log\\": "Psr/Log/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1635,17 +1632,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 +2302,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 +2427,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 +2651,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 +2911,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/resources/boot.php b/resources/boot.php new file mode 100644 index 0000000..2bcf521 --- /dev/null +++ b/resources/boot.php @@ -0,0 +1,26 @@ +initialise() + ->run(); +} catch (\Error $error) { + error_log($error->getMessage() . ' ' . $error->getFile() . ':' . $error->getLine()); + http_response_code(500); +} + 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/ErrorHandlers/DefaultErrorHandler.php b/src/ErrorHandlers/DefaultErrorHandler.php new file mode 100644 index 0000000..bda4198 --- /dev/null +++ b/src/ErrorHandlers/DefaultErrorHandler.php @@ -0,0 +1,23 @@ +getCode(); + + if (($code > 399 && $code < 600) || !$code) { + error_log($exception->getMessage() . ' ' . $exception->getFile() . ':' . $exception->getLine()); + $error = 'Something went wrong'; + $code = 500; + } else { + $error = $exception->getMessage(); + } + return $response->write($error)->withStatus($code); + } +} 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 @@ + Date: Mon, 4 Feb 2019 17:42:25 +0000 Subject: [PATCH 20/48] fixes --- resources/boot.php | 2 ++ src/ErrorHandlers/DefaultErrorHandler.php | 6 +++--- src/RhubarbRestAPIApplication.php | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/resources/boot.php b/resources/boot.php index 2bcf521..9dd5341 100644 --- a/resources/boot.php +++ b/resources/boot.php @@ -2,6 +2,8 @@ http_response_code(500); +include_once __DIR__ . '/../../../autoload.php'; + $apiClass = getenv('API_CLASS'); if (!$apiClass) { diff --git a/src/ErrorHandlers/DefaultErrorHandler.php b/src/ErrorHandlers/DefaultErrorHandler.php index bda4198..66d0b9b 100644 --- a/src/ErrorHandlers/DefaultErrorHandler.php +++ b/src/ErrorHandlers/DefaultErrorHandler.php @@ -7,17 +7,17 @@ class DefaultErrorHandler { - function __invoke(Request $request, Response $response, \Throwable $exception = null) + public function __invoke(Request $request, Response $response, \Throwable $exception = null) { $code = $exception->getCode(); - if (($code > 399 && $code < 600) || !$code) { + 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->write($error)->withStatus($code); + return $response->withStatus($code, $error); } } diff --git a/src/RhubarbRestAPIApplication.php b/src/RhubarbRestAPIApplication.php index f66a571..a6a63fc 100644 --- a/src/RhubarbRestAPIApplication.php +++ b/src/RhubarbRestAPIApplication.php @@ -2,6 +2,7 @@ namespace Rhubarb\RestApi; +use Rhubarb\RestApi\ErrorHandlers\DefaultErrorHandler; use Slim\App; abstract class RhubarbRestAPIApplication From f1e533612a7e43e5aa188b394bfcd00675e53487 Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Tue, 5 Feb 2019 08:41:56 +0000 Subject: [PATCH 21/48] Added: EntityAdapterRouterFactory and EntityAdapterInterace --- src/Adapters/EntityAdapterInterface.php | 19 ++++++ src/Adapters/EntityAdapterRouterFactory.php | 71 +++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 src/Adapters/EntityAdapterInterface.php create mode 100644 src/Adapters/EntityAdapterRouterFactory.php diff --git a/src/Adapters/EntityAdapterInterface.php b/src/Adapters/EntityAdapterInterface.php new file mode 100644 index 0000000..fa69fce --- /dev/null +++ b/src/Adapters/EntityAdapterInterface.php @@ -0,0 +1,19 @@ +get('/', self::entityAdapterList($entityAdapter, $di)); + $app->get('/{id}', self::entityAdapterGet($entityAdapter, $di)); + $app->post('/', self::entityAdapterPost($entityAdapter, $di)); + $app->put('/{id}', self::entityAdapterPut($entityAdapter, $di)); + $app->delete('/{id}', self::entityAdapterDelete($entityAdapter, $di)); + }; + } + + public static function entityAdapterList(string $entityAdapter, bool $di = false) + { + return function ($request, $response) use ($entityAdapter, $di) { + if ($di) { + $entityAdapter = get_class(Container::current()->getInstance($entityAdapter)); + } + return $entityAdapter::list($request, $response); + }; + } + + public static function entityAdapterGet(string $entityAdapter, bool $di = false) + { + return function ($request, $response, $routeVariables) use ($entityAdapter, $di) { + if ($di) { + $entityAdapter = get_class(Container::current()->getInstance($entityAdapter)); + } + return $entityAdapter::get($routeVariables['id'], $request, $response); + }; + } + + public static function entityAdapterPost(string $entityAdapter, bool $di = false) + { + return function ($request, $response) use ($entityAdapter, $di) { + if ($di) { + $entityAdapter = get_class(Container::current()->getInstance($entityAdapter)); + } + return $entityAdapter::post($request, $response); + }; + } + + public static function entityAdapterPut(string $entityAdapter, bool $di = false) + { + return function ($request, $response, $routeVariables) use ($entityAdapter, $di) { + if ($di) { + $entityAdapter = get_class(Container::current()->getInstance($entityAdapter)); + } + return $entityAdapter::put($routeVariables['id'], $request, $response); + }; + } + + public static function entityAdapterDelete(string $entityAdapter, bool $di = false) + { + return function ($request, $response, $routeVariables) use ($entityAdapter, $di) { + if ($di) { + $entityAdapter = get_class(Container::current()->getInstance($entityAdapter)); + } + return $entityAdapter::delete($routeVariables['id'], $request, $response); + }; + } +} + From e50d74befd5f32dbaf8b9fcaec6c030369910870 Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Tue, 5 Feb 2019 09:09:05 +0000 Subject: [PATCH 22/48] Added: DIEntityAdapter --- src/Adapters/DIEntityAdapter.php | 56 ++++++++++++++++++ src/Adapters/EntityAdapterRouterFactory.php | 65 +++++++++------------ 2 files changed, 83 insertions(+), 38 deletions(-) create mode 100644 src/Adapters/DIEntityAdapter.php diff --git a/src/Adapters/DIEntityAdapter.php b/src/Adapters/DIEntityAdapter.php new file mode 100644 index 0000000..43d8a7e --- /dev/null +++ b/src/Adapters/DIEntityAdapter.php @@ -0,0 +1,56 @@ +get('/', self::entityAdapterList($entityAdapter, $di)); - $app->get('/{id}', self::entityAdapterGet($entityAdapter, $di)); - $app->post('/', self::entityAdapterPost($entityAdapter, $di)); - $app->put('/{id}', self::entityAdapterPut($entityAdapter, $di)); - $app->delete('/{id}', self::entityAdapterDelete($entityAdapter, $di)); + return function () use ($entityAdapter, $app) { + $app->get('/', self::entityAdapterList($entityAdapter)); + $app->get('/{id}', self::entityAdapterGet($entityAdapter)); + $app->post('/', self::entityAdapterPost($entityAdapter)); + $app->put('/{id}', self::entityAdapterPut($entityAdapter)); + $app->delete('/{id}', self::entityAdapterDelete($entityAdapter)); }; } - public static function entityAdapterList(string $entityAdapter, bool $di = false) + public static function entityAdapterList(string $entityAdapter) { - return function ($request, $response) use ($entityAdapter, $di) { - if ($di) { - $entityAdapter = get_class(Container::current()->getInstance($entityAdapter)); - } - return $entityAdapter::list($request, $response); + return function ($request, $response) use ($entityAdapter) { + $method = 'list'; + return $entityAdapter::$method($request, $response); }; } - public static function entityAdapterGet(string $entityAdapter, bool $di = false) + public static function entityAdapterGet(string $entityAdapter) { - return function ($request, $response, $routeVariables) use ($entityAdapter, $di) { - if ($di) { - $entityAdapter = get_class(Container::current()->getInstance($entityAdapter)); - } - return $entityAdapter::get($routeVariables['id'], $request, $response); + return function ($request, $response, $routeVariables) use ($entityAdapter) { + $method = 'get'; + return $entityAdapter::$method($routeVariables['id'], $request, $response); }; } - public static function entityAdapterPost(string $entityAdapter, bool $di = false) + public static function entityAdapterPost(string $entityAdapter) { - return function ($request, $response) use ($entityAdapter, $di) { - if ($di) { - $entityAdapter = get_class(Container::current()->getInstance($entityAdapter)); - } - return $entityAdapter::post($request, $response); + return function ($request, $response) use ($entityAdapter) { + $method = 'post'; + return $entityAdapter::$method($request, $response); }; } - public static function entityAdapterPut(string $entityAdapter, bool $di = false) + public static function entityAdapterPut(string $entityAdapter) { - return function ($request, $response, $routeVariables) use ($entityAdapter, $di) { - if ($di) { - $entityAdapter = get_class(Container::current()->getInstance($entityAdapter)); - } - return $entityAdapter::put($routeVariables['id'], $request, $response); + return function ($request, $response, $routeVariables) use ($entityAdapter) { + $method = 'put'; + return $entityAdapter::$method($routeVariables['id'], $request, $response); }; } - public static function entityAdapterDelete(string $entityAdapter, bool $di = false) + public static function entityAdapterDelete(string $entityAdapter) { - return function ($request, $response, $routeVariables) use ($entityAdapter, $di) { - if ($di) { - $entityAdapter = get_class(Container::current()->getInstance($entityAdapter)); - } - return $entityAdapter::delete($routeVariables['id'], $request, $response); + return function ($request, $response, $routeVariables) use ($entityAdapter) { + $method = 'delete'; + return $entityAdapter::$method($routeVariables['id'], $request, $response); }; } } From f4d039b34c2cd6d11b7093a1337a0f0075170f29 Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Tue, 5 Feb 2019 09:12:42 +0000 Subject: [PATCH 23/48] use LSB instead of abstract method --- src/Adapters/DIEntityAdapter.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Adapters/DIEntityAdapter.php b/src/Adapters/DIEntityAdapter.php index 43d8a7e..ca31a81 100644 --- a/src/Adapters/DIEntityAdapter.php +++ b/src/Adapters/DIEntityAdapter.php @@ -8,12 +8,10 @@ abstract class DIEntityAdapter implements EntityAdapterInterface { - abstract protected static function getEntityAdapterClass(): string; - private static function getEntityAdapter(): string { $getInstance = function (): EntityAdapterInterface { - return Container::instance(static::getEntityAdapter()); + return Container::instance(static::class); }; return get_class($getInstance()); From 540dddd1c118223cdb2e2339a6799da9e9a2b5d1 Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Tue, 5 Feb 2019 14:54:20 +0000 Subject: [PATCH 24/48] abstract Stem adapter classes --- composer.json | 1 + composer.lock | 68 ++++++++++++++-- src/Adapters/DIEntityAdapter.php | 10 +-- src/Adapters/LegacyStemEntityAdapter.php | 43 +++++++++++ src/Adapters/StemEntityAdapter.php | 81 ++++++++++++++++++++ src/Exceptions/ResourceNotFoundException.php | 13 ++++ 6 files changed, 203 insertions(+), 13 deletions(-) create mode 100644 src/Adapters/LegacyStemEntityAdapter.php create mode 100644 src/Adapters/StemEntityAdapter.php create mode 100644 src/Exceptions/ResourceNotFoundException.php diff --git a/composer.json b/composer.json index b437aaa..4a12a75 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ }, "require": { "rhubarbphp/rhubarb": "^1.4.2", + "rhubarbphp/module-stem": "^1.8", "slim/slim": "^3.12" }, "require-dev": { diff --git a/composer.lock b/composer.lock index ed5d32a..b2e8ec7 100644 --- a/composer.lock +++ b/composer.lock @@ -1,10 +1,10 @@ { "_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": "e7c4ff3cca503e7825062684ca459d61", + "content-hash": "8d6afe7e8d59b437c45335e32c01d94e", "packages": [ { "name": "container-interop/container-interop", @@ -275,22 +275,74 @@ ], "time": "2016-08-06T14:39:51+00:00" }, + { + "name": "rhubarbphp/module-stem", + "version": "1.8.1", + "source": { + "type": "git", + "url": "https://github.com/RhubarbPHP/Module.Stem.git", + "reference": "a17dab06a636f73e2b77798247c12c14b7c53c36" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/RhubarbPHP/Module.Stem/zipball/a17dab06a636f73e2b77798247c12c14b7c53c36", + "reference": "a17dab06a636f73e2b77798247c12c14b7c53c36", + "shasum": "" + }, + "require": { + "rhubarbphp/rhubarb": "^1.5.2" + }, + "require-dev": { + "codeception/codeception": "^2.1.8", + "pdepend/pdepend": "^2.0.0", + "phploc/phploc": "^3.0.0", + "phpmd/phpmd": "^2.0.0", + "rhubarbphp/custard": "^1.0.0", + "rhubarbphp/module-build-status-updater": "^1.0.4", + "sebastian/phpcpd": "^2.0.0", + "squizlabs/php_codesniffer": "^2.0.0", + "tan-tan-kanarek/github-php-client": "^1.0.0", + "theseer/phpdox": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Rhubarb\\Stem\\": "src/", + "Rhubarb\\Stem\\Tests\\": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "A data modelling and ORM module for the Rhubarb PHP framework", + "homepage": "http://www.rhubarbphp.com/", + "keywords": [ + "Modelling", + "data", + "framework", + "model", + "orm", + "php" + ], + "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": { @@ -321,7 +373,7 @@ "framework", "php" ], - "time": "2017-11-16T12:38:01+00:00" + "time": "2018-10-22T16:18:02+00:00" }, { "name": "slim/slim", diff --git a/src/Adapters/DIEntityAdapter.php b/src/Adapters/DIEntityAdapter.php index ca31a81..157c88f 100644 --- a/src/Adapters/DIEntityAdapter.php +++ b/src/Adapters/DIEntityAdapter.php @@ -20,35 +20,35 @@ private static function getEntityAdapter(): string final public static function list(Request $request, Response $response): Response { $entityAdapter = self::getEntityAdapter(); - $method = __METHOD__; + $method = 'list'; return $entityAdapter::$method(...func_get_args()); } final public static function get($id, Request $request, Response $response): Response { $entityAdapter = self::getEntityAdapter(); - $method = __METHOD__; + $method = 'get'; return $entityAdapter::$method(...func_get_args()); } final public static function post(Request $request, Response $response): Response { $entityAdapter = self::getEntityAdapter(); - $method = __METHOD__; + $method = 'post'; return $entityAdapter::$method(...func_get_args()); } final public static function put($id, Request $request, Response $response): Response { $entityAdapter = self::getEntityAdapter(); - $method = __METHOD__; + $method = 'put'; return $entityAdapter::$method(...func_get_args()); } final public static function delete($id, Request $request, Response $response): Response { $entityAdapter = self::getEntityAdapter(); - $method = __METHOD__; + $method = 'delete'; return $entityAdapter::$method(...func_get_args()); } } diff --git a/src/Adapters/LegacyStemEntityAdapter.php b/src/Adapters/LegacyStemEntityAdapter.php new file mode 100644 index 0000000..d509dc6 --- /dev/null +++ b/src/Adapters/LegacyStemEntityAdapter.php @@ -0,0 +1,43 @@ +getMessage(), $exception); + } + } + + protected static function getPayloadForEntity(Model $model): array + { + return $model->exportPublicData(); + } + + protected static function createEntity($payload, $id = null): Model + { + $modelClass = self::getModelClass(); + /** @var Model $model */ + $model = new $modelClass($id); + $model->importData($payload); + $model->save(); + return $model; + } + + protected static function deleteEntity(Model $entity) + { + $entity->delete(); + } +} diff --git a/src/Adapters/StemEntityAdapter.php b/src/Adapters/StemEntityAdapter.php new file mode 100644 index 0000000..1218cf0 --- /dev/null +++ b/src/Adapters/StemEntityAdapter.php @@ -0,0 +1,81 @@ +setRange($offset, $pageSize); + } + + protected static function getPayloadForEntityList(Collection $collection, $request): array + { + $payloads = []; + foreach ($collection as $entity) { + $payloads[] = self::getPayloadForEntity($entity); + } + return $payloads; + } + + final public static function list(Request $request, Response $response): Response + { + $offset = (int)$request->getQueryParam('offset', $request->getQueryParam('from', 1) - 1); + $pageSize = (int)$request->getQueryParam('pageSize', $request->getQueryParam('to', 10 - $offset)); + + $list = self::getEntityList( + $offset, + $pageSize, + $request + ); + $total = $list->count(); + return $response + ->withJson(self::getPayloadForEntityList($list, $request)) + ->withAddedHeader('X-Total', $total) + ->withAddedHeader('X-Offset', $offset) + ->withAddedHeader('X-PageSize', $pageSize) + ->withAddedHeader('X-From', $offset + 1) + ->withAddedHeader('X-To', $offset + $pageSize); + } + + final public static function get($id, Request $request, Response $response): Response + { + return $response->withJson(self::getPayloadForEntity(self::getEntityForId($id))); + } + + final public static function post(Request $request, Response $response): Response + { + return $response->withJson(self::getPayloadForEntity( + self::createEntity($request->getParsedBody()) + )); + } + + final public static function put($id, Request $request, Response $response): Response + { + return $response->withJson(self::getPayloadForEntity( + self::createEntity($request->getParsedBody(), $id) + )); + } + + final public static function delete($id, Request $request, Response $response): Response + { + self::deleteEntity(self::getEntityForId($id)); + + return $response; + } +} diff --git a/src/Exceptions/ResourceNotFoundException.php b/src/Exceptions/ResourceNotFoundException.php new file mode 100644 index 0000000..5780652 --- /dev/null +++ b/src/Exceptions/ResourceNotFoundException.php @@ -0,0 +1,13 @@ + Date: Tue, 5 Feb 2019 14:57:57 +0000 Subject: [PATCH 25/48] namespaced stem adapters --- src/Adapters/{ => Stem}/LegacyStemEntityAdapter.php | 4 ++-- src/Adapters/{ => Stem}/StemEntityAdapter.php | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) rename src/Adapters/{ => Stem}/LegacyStemEntityAdapter.php (91%) rename src/Adapters/{ => Stem}/StemEntityAdapter.php (96%) diff --git a/src/Adapters/LegacyStemEntityAdapter.php b/src/Adapters/Stem/LegacyStemEntityAdapter.php similarity index 91% rename from src/Adapters/LegacyStemEntityAdapter.php rename to src/Adapters/Stem/LegacyStemEntityAdapter.php index d509dc6..1d33f09 100644 --- a/src/Adapters/LegacyStemEntityAdapter.php +++ b/src/Adapters/Stem/LegacyStemEntityAdapter.php @@ -1,12 +1,12 @@ Date: Tue, 5 Feb 2019 15:57:14 +0000 Subject: [PATCH 26/48] treat all urls as ending in slash makes router rules simpler --- src/RhubarbRestAPIApplication.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/RhubarbRestAPIApplication.php b/src/RhubarbRestAPIApplication.php index a6a63fc..5f69b08 100644 --- a/src/RhubarbRestAPIApplication.php +++ b/src/RhubarbRestAPIApplication.php @@ -4,6 +4,8 @@ use Rhubarb\RestApi\ErrorHandlers\DefaultErrorHandler; use Slim\App; +use Slim\Http\Request; +use Slim\Http\Response; abstract class RhubarbRestAPIApplication { @@ -20,6 +22,17 @@ protected function registerErrorHandlers() 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); + }); } From 5d148fb9463776156979963f0f708a71019a1aa4 Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Tue, 5 Feb 2019 15:57:31 +0000 Subject: [PATCH 27/48] selective CRUD --- src/Adapters/EntityAdapterRouterFactory.php | 26 +++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Adapters/EntityAdapterRouterFactory.php b/src/Adapters/EntityAdapterRouterFactory.php index 5bc32d0..1aa2ba9 100644 --- a/src/Adapters/EntityAdapterRouterFactory.php +++ b/src/Adapters/EntityAdapterRouterFactory.php @@ -6,17 +6,29 @@ class EntityAdapterRouterFactory { - public static function crud(App $app, string $entityAdapter): callable + const LIST = 1; + const ITEM_GET = 2; + const ITEM_POST = 4; + const ITEM_PUT = 8; + const ITEM_DELETE = 16; + const ALL = 31; + + public static function crud(App $app, string $entityAdapter, $allowed = self::ALL): callable { - return function () use ($entityAdapter, $app) { - $app->get('/', self::entityAdapterList($entityAdapter)); - $app->get('/{id}', self::entityAdapterGet($entityAdapter)); - $app->post('/', self::entityAdapterPost($entityAdapter)); - $app->put('/{id}', self::entityAdapterPut($entityAdapter)); - $app->delete('/{id}', self::entityAdapterDelete($entityAdapter)); + return function () use ($entityAdapter, $app, $allowed) { + $allowed & self::LIST && $app->get('/', self::entityAdapterList($entityAdapter)); + $allowed & self::ITEM_GET && $app->get('/{id}/', self::entityAdapterGet($entityAdapter)); + $allowed & self::ITEM_POST && $app->post('/', self::entityAdapterPost($entityAdapter)); + $allowed & self::ITEM_PUT && $app->put('/{id}/', self::entityAdapterPut($entityAdapter)); + $allowed & self::ITEM_DELETE && $app->delete('/{id}/', self::entityAdapterDelete($entityAdapter)); }; } + public static function readOnly(App $app, string $entityAdapter): callable + { + return self::crud($app, $entityAdapter, self::LIST | self::ITEM_GET); + } + public static function entityAdapterList(string $entityAdapter) { return function ($request, $response) use ($entityAdapter) { From 657e5a096b3f2a5f3667286231ab6a5422f4b660 Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Wed, 6 Feb 2019 09:25:20 +0000 Subject: [PATCH 28/48] EntityAdapter --- src/Adapters/BaseEntityAdapter.php | 78 +++++++++++ src/Adapters/EntityAdapter.php | 37 +++++ src/Adapters/EntityAdapterRouterFactory.php | 14 +- src/Adapters/ResourceAdapter.php | 128 ------------------ src/Adapters/Stem/LegacyStemEntityAdapter.php | 81 ++++++++++- src/Adapters/Stem/StemEntityAdapter.php | 82 ----------- src/Entities/SearchCriteriaEntity.php | 26 ++++ src/Entities/SearchResponseEntity.php | 26 ++++ 8 files changed, 253 insertions(+), 219 deletions(-) create mode 100644 src/Adapters/BaseEntityAdapter.php create mode 100644 src/Adapters/EntityAdapter.php delete mode 100644 src/Adapters/ResourceAdapter.php delete mode 100644 src/Adapters/Stem/StemEntityAdapter.php create mode 100644 src/Entities/SearchCriteriaEntity.php create mode 100644 src/Entities/SearchResponseEntity.php diff --git a/src/Adapters/BaseEntityAdapter.php b/src/Adapters/BaseEntityAdapter.php new file mode 100644 index 0000000..30925d2 --- /dev/null +++ b/src/Adapters/BaseEntityAdapter.php @@ -0,0 +1,78 @@ +getQueryParam('offset', $request->getQueryParam('from', 1) - 1); + $pageSize = (int)$request->getQueryParam('pageSize', $request->getQueryParam('to', 10 - $offset)); + $sort = $request->getQueryParam('sort'); + + $list = self::getEntityList( + $offset, + $pageSize, + $sort, + $request + ); + return $response + ->withJson(array_map( + function ($entity) { + return self::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 static function get($id, Request $request, Response $response): Response + { + return $response->withJson(self::getPayloadForEntity(self::getEntityForId($id))); + } + + final public static function put($id, Request $request, Response $response): Response + { + $entity = self::getEntityForPayload($request->getParsedBody(), $id); + self::storeEntity($entity); + return $response->withJson(self::getPayloadForEntity( + self::getPayloadForEntity($entity) + )); + } + + final public static function post(Request $request, Response $response): Response + { + return self::put(null, $request, $response); + } + + final public static function delete($id, Request $request, Response $response): Response + { + self::deleteEntity(self::getEntityForId($id)); + return $response; + } +} diff --git a/src/Adapters/EntityAdapter.php b/src/Adapters/EntityAdapter.php new file mode 100644 index 0000000..a69fdaa --- /dev/null +++ b/src/Adapters/EntityAdapter.php @@ -0,0 +1,37 @@ +get('/', self::entityAdapterList($entityAdapter)); $allowed & self::ITEM_GET && $app->get('/{id}/', self::entityAdapterGet($entityAdapter)); $allowed & self::ITEM_POST && $app->post('/', self::entityAdapterPost($entityAdapter)); $allowed & self::ITEM_PUT && $app->put('/{id}/', self::entityAdapterPut($entityAdapter)); $allowed & self::ITEM_DELETE && $app->delete('/{id}/', self::entityAdapterDelete($entityAdapter)); + if($additional !== null) { + $additional($app, $entityAdapter); + } }; } diff --git a/src/Adapters/ResourceAdapter.php b/src/Adapters/ResourceAdapter.php deleted file mode 100644 index 6d03903..0000000 --- a/src/Adapters/ResourceAdapter.php +++ /dev/null @@ -1,128 +0,0 @@ -makeResourceByIdentifier($id); - - return $payload; - } - - /** - * @param $payload - * @param $params - * @param null|WebRequest $request - * @return mixed - * @throws ResourceNotFoundException - */ - public function put($payload, $params, ?WebRequest $request) - { - $payload = $this->validatePutRequestPayload($payload); - - $payload = $this->applyParamsToPayload($payload, $params); - - $resource = $this->get($params, $request); - - $this->putResource($payload); - - return $this->get($params, $request); - } - - protected function validatePutRequestPayload($payload) - { - $this->validateRequestPayload($payload); - - return $payload; - } - - public function putResource($resource) - { - throw new RestImplementationException("Missing implementation of " . __FUNCTION__); - } - - public function post($payload, $params, WebRequest $request) - { - $payload = $this->validatePostRequestPayload($payload); - - return $this->postResource($payload); - } - - protected function validatePostRequestPayload($payload) - { - return $this->validateRequestPayload($payload); - } - - public function postResource($resource) - { - throw new RestImplementationException("Missing implementation of " . __FUNCTION__); - } - - public function delete($payload, $params, ?WebRequest $request) - { - throw new RestImplementationException("Missing implementation of " . __FUNCTION__); - } - - - protected function applyParamsToPayload($payload, $params) - { - return $payload; - } - - private final function validateRequestPayload($payload) - { - if (!is_array($payload)) { - throw new RequestPayloadValidationException("POST and PUT options require a JSON encoded resource object in the body of the request."); - } - - return $payload; - } - - - public function makeResourceByIdentifier($id) - { - throw new RestImplementationException("Missing implementation of " . __FUNCTION__); - } - - public function makeResourceFromData($data) - { - throw new RestImplementationException("Missing implementation of " . __FUNCTION__); - } - - public function list($params, ?WebRequest $request = null) - { - $start = $request->get("start", 0); - $end = $request->get("end", 99); - - $response = new ListResource(); - $response->range->start = $start; - $response->range->end = $end; - - $response->count = $this->countItems($start, $end, $params, $request); - $response->items = $this->getItems($start, $end, $params, $request); - - return $response; - } - - protected function countItems($rangeStart, $rangeEnd, $params, ?WebRequest $request) - { - throw new RestImplementationException("Missing implementation of " . __FUNCTION__); - } - - protected function getItems($rangeStart, $rangeEnd, $params, ?WebRequest $request) - { - throw new RestImplementationException("Missing implementation of " . __FUNCTION__); - } -} \ No newline at end of file diff --git a/src/Adapters/Stem/LegacyStemEntityAdapter.php b/src/Adapters/Stem/LegacyStemEntityAdapter.php index 1d33f09..07b8dec 100644 --- a/src/Adapters/Stem/LegacyStemEntityAdapter.php +++ b/src/Adapters/Stem/LegacyStemEntityAdapter.php @@ -2,15 +2,31 @@ namespace Rhubarb\RestApi\Adapters\Stem; +use Rhubarb\RestApi\Adapters\BaseEntityAdapter; +use Rhubarb\RestApi\Entities\SearchCriteriaEntity; +use Rhubarb\RestApi\Entities\SearchResponseEntity; use Rhubarb\RestApi\Exceptions\ResourceNotFoundException; +use Rhubarb\Stem\Collections\Collection; use Rhubarb\Stem\Exceptions\RecordNotFoundException; +use Rhubarb\Stem\Filters\Filter; use Rhubarb\Stem\Models\Model; +use Slim\Http\Request; -abstract class LegacyStemEntityAdapter extends StemEntityAdapter +/** + * Class LegacyStemEntityAdapter + * @package Rhubarb\RestApi\Adapters\Stem + * @deprecated Use \Rhubarb\RestApi\Adapters\EntityAdapter with use cases and proper entities + */ +abstract class LegacyStemEntityAdapter extends BaseEntityAdapter { abstract protected static function getModelClass(): string; - protected static function getEntityForId($id): Model + /** + * @param $id + * @return Model + * @throws ResourceNotFoundException + */ + protected static function getEntityForId($id) { try { /** @var Model $modelClass */ @@ -21,23 +37,74 @@ protected static function getEntityForId($id): Model } } - protected static function getPayloadForEntity(Model $model): array + /** + * @param Model $entity + * @param bool $resultList + * @return array + */ + protected static function getPayloadForEntity($entity, $resultList = false): array { - return $model->exportPublicData(); + return $entity->exportPublicData(); } - protected static function createEntity($payload, $id = null): Model + protected static function getEntityForPayload($payload, $id = null) { $modelClass = self::getModelClass(); /** @var Model $model */ $model = new $modelClass($id); $model->importData($payload); - $model->save(); return $model; } - protected static function deleteEntity(Model $entity) + /** + * @param Model $entity + * @throws \Rhubarb\Stem\Exceptions\DeleteModelException + */ + final protected static function deleteEntity($entity) { $entity->delete(); } + + /** + * @param Request $request + * @return Filter[] + */ + protected function getListFilterForRequest(Request $request): array + { + return []; + } + + /** + * @param int $offset + * @param int $pageSize + * @param Request $request + * @return Collection + */ + final protected static function getEntityList( + int $offset, + int $pageSize, + string $sort = null, + Request $request + ): SearchResponseEntity { + $criteria = new SearchCriteriaEntity($offset, $pageSize, $sort); + $response = new SearchResponseEntity($criteria); + /** @var Model $modelClass */ + $modelClass = self::getModelClass(); + $collection = $modelClass::find(...self::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 static function storeEntity($entity) + { + $entity->save(); + } } diff --git a/src/Adapters/Stem/StemEntityAdapter.php b/src/Adapters/Stem/StemEntityAdapter.php deleted file mode 100644 index c6f7b1c..0000000 --- a/src/Adapters/Stem/StemEntityAdapter.php +++ /dev/null @@ -1,82 +0,0 @@ -setRange($offset, $pageSize); - } - - protected static function getPayloadForEntityList(Collection $collection, $request): array - { - $payloads = []; - foreach ($collection as $entity) { - $payloads[] = self::getPayloadForEntity($entity); - } - return $payloads; - } - - final public static function list(Request $request, Response $response): Response - { - $offset = (int)$request->getQueryParam('offset', $request->getQueryParam('from', 1) - 1); - $pageSize = (int)$request->getQueryParam('pageSize', $request->getQueryParam('to', 10 - $offset)); - - $list = self::getEntityList( - $offset, - $pageSize, - $request - ); - $total = $list->count(); - return $response - ->withJson(self::getPayloadForEntityList($list, $request)) - ->withAddedHeader('X-Total', $total) - ->withAddedHeader('X-Offset', $offset) - ->withAddedHeader('X-PageSize', $pageSize) - ->withAddedHeader('X-From', $offset + 1) - ->withAddedHeader('X-To', $offset + $pageSize); - } - - final public static function get($id, Request $request, Response $response): Response - { - return $response->withJson(self::getPayloadForEntity(self::getEntityForId($id))); - } - - final public static function post(Request $request, Response $response): Response - { - return $response->withJson(self::getPayloadForEntity( - self::createEntity($request->getParsedBody()) - )); - } - - final public static function put($id, Request $request, Response $response): Response - { - return $response->withJson(self::getPayloadForEntity( - self::createEntity($request->getParsedBody(), $id) - )); - } - - final public static function delete($id, Request $request, Response $response): Response - { - self::deleteEntity(self::getEntityForId($id)); - - return $response; - } -} 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..1c50422 --- /dev/null +++ b/src/Entities/SearchResponseEntity.php @@ -0,0 +1,26 @@ +criteria = $criteria; + } +} From d5e8ff78c5f2d689749ac491fac57fae2e3e49da Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Wed, 6 Feb 2019 09:38:56 +0000 Subject: [PATCH 29/48] =?UTF-8?q?don=E2=80=99t=20force=20return=20type=20o?= =?UTF-8?q?n=20payload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Adapters/BaseEntityAdapter.php | 2 +- src/Adapters/Stem/LegacyStemEntityAdapter.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Adapters/BaseEntityAdapter.php b/src/Adapters/BaseEntityAdapter.php index 30925d2..5e5ccde 100644 --- a/src/Adapters/BaseEntityAdapter.php +++ b/src/Adapters/BaseEntityAdapter.php @@ -12,7 +12,7 @@ abstract protected static function getEntityForId($id); abstract protected static function deleteEntity($entity); - abstract protected static function getPayloadForEntity($entity, $resultList = false): array; + abstract protected static function getPayloadForEntity($entity, $resultList = false); abstract protected static function getEntityForPayload($payload, $id = null); diff --git a/src/Adapters/Stem/LegacyStemEntityAdapter.php b/src/Adapters/Stem/LegacyStemEntityAdapter.php index 07b8dec..5409b4a 100644 --- a/src/Adapters/Stem/LegacyStemEntityAdapter.php +++ b/src/Adapters/Stem/LegacyStemEntityAdapter.php @@ -42,7 +42,7 @@ protected static function getEntityForId($id) * @param bool $resultList * @return array */ - protected static function getPayloadForEntity($entity, $resultList = false): array + protected static function getPayloadForEntity($entity, $resultList = false) { return $entity->exportPublicData(); } From bda60a90910a926631e2bef048c39a417f3dfffb Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Wed, 6 Feb 2019 09:45:16 +0000 Subject: [PATCH 30/48] late static binding --- src/Adapters/BaseEntityAdapter.php | 18 +++++++++--------- src/Adapters/EntityAdapter.php | 4 ++-- src/Adapters/Stem/LegacyStemEntityAdapter.php | 8 ++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Adapters/BaseEntityAdapter.php b/src/Adapters/BaseEntityAdapter.php index 5e5ccde..f7c570a 100644 --- a/src/Adapters/BaseEntityAdapter.php +++ b/src/Adapters/BaseEntityAdapter.php @@ -12,7 +12,7 @@ abstract protected static function getEntityForId($id); abstract protected static function deleteEntity($entity); - abstract protected static function getPayloadForEntity($entity, $resultList = false); + abstract protected static function getPayloadForEntity($entity, $resultList = false): array; abstract protected static function getEntityForPayload($payload, $id = null); @@ -31,7 +31,7 @@ final public static function list(Request $request, Response $response): Respons $pageSize = (int)$request->getQueryParam('pageSize', $request->getQueryParam('to', 10 - $offset)); $sort = $request->getQueryParam('sort'); - $list = self::getEntityList( + $list = static::getEntityList( $offset, $pageSize, $sort, @@ -40,7 +40,7 @@ final public static function list(Request $request, Response $response): Respons return $response ->withJson(array_map( function ($entity) { - return self::getPayloadForEntity($entity, true); + return static::getPayloadForEntity($entity, true); }, $list->results )) @@ -53,15 +53,15 @@ function ($entity) { final public static function get($id, Request $request, Response $response): Response { - return $response->withJson(self::getPayloadForEntity(self::getEntityForId($id))); + return $response->withJson(static::getPayloadForEntity(static::getEntityForId($id))); } final public static function put($id, Request $request, Response $response): Response { - $entity = self::getEntityForPayload($request->getParsedBody(), $id); - self::storeEntity($entity); - return $response->withJson(self::getPayloadForEntity( - self::getPayloadForEntity($entity) + $entity = static::getEntityForPayload($request->getParsedBody(), $id); + static::storeEntity($entity); + return $response->withJson(static::getPayloadForEntity( + static::getPayloadForEntity($entity) )); } @@ -72,7 +72,7 @@ final public static function post(Request $request, Response $response): Respons final public static function delete($id, Request $request, Response $response): Response { - self::deleteEntity(self::getEntityForId($id)); + static::deleteEntity(static::getEntityForId($id)); return $response; } } diff --git a/src/Adapters/EntityAdapter.php b/src/Adapters/EntityAdapter.php index a69fdaa..6cea6f8 100644 --- a/src/Adapters/EntityAdapter.php +++ b/src/Adapters/EntityAdapter.php @@ -30,8 +30,8 @@ final protected static function getEntityList( string $sort = null, Request $request ): SearchResponseEntity { - $response = new SearchResponseEntity(self::getSearchCriteriaEntity($offset, $pageSize, $sort, $request)); - self::performSearch($response); + $response = new SearchResponseEntity(static::getSearchCriteriaEntity($offset, $pageSize, $sort, $request)); + static::performSearch($response); return $response; } } diff --git a/src/Adapters/Stem/LegacyStemEntityAdapter.php b/src/Adapters/Stem/LegacyStemEntityAdapter.php index 5409b4a..71e7f11 100644 --- a/src/Adapters/Stem/LegacyStemEntityAdapter.php +++ b/src/Adapters/Stem/LegacyStemEntityAdapter.php @@ -30,7 +30,7 @@ protected static function getEntityForId($id) { try { /** @var Model $modelClass */ - $modelClass = self::getModelClass(); + $modelClass = static::getModelClass(); return new $modelClass($id); } catch (RecordNotFoundException $exception) { throw new ResourceNotFoundException($exception->getMessage(), $exception); @@ -49,7 +49,7 @@ protected static function getPayloadForEntity($entity, $resultList = false) protected static function getEntityForPayload($payload, $id = null) { - $modelClass = self::getModelClass(); + $modelClass = static::getModelClass(); /** @var Model $model */ $model = new $modelClass($id); $model->importData($payload); @@ -89,8 +89,8 @@ final protected static function getEntityList( $criteria = new SearchCriteriaEntity($offset, $pageSize, $sort); $response = new SearchResponseEntity($criteria); /** @var Model $modelClass */ - $modelClass = self::getModelClass(); - $collection = $modelClass::find(...self::getListFilterForRequest($request))->setRange($offset, $pageSize); + $modelClass = static::getModelClass(); + $collection = $modelClass::find(...static::getListFilterForRequest($request))->setRange($offset, $pageSize); $response->total = $collection->count(); foreach ($collection as $model) { $response->results[] = $model; From 076eb67351be8848f5d3aa1d42bc7c8dbe44423b Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Wed, 6 Feb 2019 09:48:44 +0000 Subject: [PATCH 31/48] again --- src/Adapters/BaseEntityAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/BaseEntityAdapter.php b/src/Adapters/BaseEntityAdapter.php index f7c570a..eded32e 100644 --- a/src/Adapters/BaseEntityAdapter.php +++ b/src/Adapters/BaseEntityAdapter.php @@ -12,7 +12,7 @@ abstract protected static function getEntityForId($id); abstract protected static function deleteEntity($entity); - abstract protected static function getPayloadForEntity($entity, $resultList = false): array; + abstract protected static function getPayloadForEntity($entity, $resultList = false); abstract protected static function getEntityForPayload($payload, $id = null); From 7e344cdc0e7c767a29859ef522a13866d93a2830 Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Wed, 6 Feb 2019 09:50:19 +0000 Subject: [PATCH 32/48] method should be static --- src/Adapters/Stem/LegacyStemEntityAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Stem/LegacyStemEntityAdapter.php b/src/Adapters/Stem/LegacyStemEntityAdapter.php index 71e7f11..1be5660 100644 --- a/src/Adapters/Stem/LegacyStemEntityAdapter.php +++ b/src/Adapters/Stem/LegacyStemEntityAdapter.php @@ -69,7 +69,7 @@ final protected static function deleteEntity($entity) * @param Request $request * @return Filter[] */ - protected function getListFilterForRequest(Request $request): array + protected static function getListFilterForRequest(Request $request): array { return []; } From 8704d85404e38bd41e090a20179c5539038079fe Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Wed, 6 Feb 2019 11:16:28 +0000 Subject: [PATCH 33/48] instance adapters --- src/Adapters/BaseEntityAdapter.php | 40 +++++----- src/Adapters/DIEntityAdapter.php | 41 ++++------ src/Adapters/EntityAdapter.php | 12 +-- src/Adapters/EntityAdapterInterface.php | 10 +-- src/Adapters/EntityAdapterRouterFactory.php | 74 ++++++++----------- src/Adapters/Stem/LegacyStemEntityAdapter.php | 24 +++--- 6 files changed, 89 insertions(+), 112 deletions(-) diff --git a/src/Adapters/BaseEntityAdapter.php b/src/Adapters/BaseEntityAdapter.php index eded32e..7e5aaf6 100644 --- a/src/Adapters/BaseEntityAdapter.php +++ b/src/Adapters/BaseEntityAdapter.php @@ -8,30 +8,30 @@ abstract class BaseEntityAdapter implements EntityAdapterInterface { - abstract protected static function getEntityForId($id); + abstract protected function getEntityForId($id); - abstract protected static function deleteEntity($entity); + abstract protected function deleteEntity($entity); - abstract protected static function getPayloadForEntity($entity, $resultList = false); + abstract protected function getPayloadForEntity($entity, $resultList = false); - abstract protected static function getEntityForPayload($payload, $id = null); + abstract protected function getEntityForPayload($payload, $id = null); - abstract protected static function storeEntity($entity); + abstract protected function storeEntity($entity); - abstract protected static function getEntityList( + abstract protected function getEntityList( int $offset, int $pageSize, string $sort = null, Request $request ): SearchResponseEntity; - final public static function list(Request $request, Response $response): Response + final public function list(Request $request, Response $response): Response { $offset = (int)$request->getQueryParam('offset', $request->getQueryParam('from', 1) - 1); $pageSize = (int)$request->getQueryParam('pageSize', $request->getQueryParam('to', 10 - $offset)); $sort = $request->getQueryParam('sort'); - $list = static::getEntityList( + $list = $this->getEntityList( $offset, $pageSize, $sort, @@ -40,7 +40,7 @@ final public static function list(Request $request, Response $response): Respons return $response ->withJson(array_map( function ($entity) { - return static::getPayloadForEntity($entity, true); + return $this->getPayloadForEntity($entity, true); }, $list->results )) @@ -51,28 +51,28 @@ function ($entity) { ->withAddedHeader('X-To', $offset + $pageSize); } - final public static function get($id, Request $request, Response $response): Response + final public function get(Request $request, Response $response, $id): Response { - return $response->withJson(static::getPayloadForEntity(static::getEntityForId($id))); + return $response->withJson($this->getPayloadForEntity($this->getEntityForId($id))); } - final public static function put($id, Request $request, Response $response): Response + final public function put(Request $request, Response $response, $id): Response { - $entity = static::getEntityForPayload($request->getParsedBody(), $id); - static::storeEntity($entity); - return $response->withJson(static::getPayloadForEntity( - static::getPayloadForEntity($entity) + $entity = $this->getEntityForPayload($request->getParsedBody(), $id); + $this->storeEntity($entity); + return $response->withJson($this->getPayloadForEntity( + $this->getPayloadForEntity($entity) )); } - final public static function post(Request $request, Response $response): Response + final public function post(Request $request, Response $response): Response { - return self::put(null, $request, $response); + return $this->put(null, $request, $response); } - final public static function delete($id, Request $request, Response $response): Response + final public function delete(Request $request, Response $response, $id): Response { - static::deleteEntity(static::getEntityForId($id)); + $this->deleteEntity($this->getEntityForId($id)); return $response; } } diff --git a/src/Adapters/DIEntityAdapter.php b/src/Adapters/DIEntityAdapter.php index 157c88f..4ac1631 100644 --- a/src/Adapters/DIEntityAdapter.php +++ b/src/Adapters/DIEntityAdapter.php @@ -8,47 +8,36 @@ abstract class DIEntityAdapter implements EntityAdapterInterface { - private static function getEntityAdapter(): string - { - $getInstance = function (): EntityAdapterInterface { - return Container::instance(static::class); - }; + /** @var EntityAdapterInterface */ + private $entityAdapter; - return get_class($getInstance()); + public function __construct() + { + $this->entityAdapter = Container::instance(static::class); } - final public static function list(Request $request, Response $response): Response + final public function list(Request $request, Response $response): Response { - $entityAdapter = self::getEntityAdapter(); - $method = 'list'; - return $entityAdapter::$method(...func_get_args()); + return $this->entityAdapter->list(...func_get_args()); } - final public static function get($id, Request $request, Response $response): Response + final public function get(Request $request, Response $response, $id): Response { - $entityAdapter = self::getEntityAdapter(); - $method = 'get'; - return $entityAdapter::$method(...func_get_args()); + return $this->entityAdapter->get(...func_get_args()); } - final public static function post(Request $request, Response $response): Response + final public function post(Request $request, Response $response): Response { - $entityAdapter = self::getEntityAdapter(); - $method = 'post'; - return $entityAdapter::$method(...func_get_args()); + return $this->entityAdapter->post(...func_get_args()); } - final public static function put($id, Request $request, Response $response): Response + final public function put(Request $request, Response $response, $id): Response { - $entityAdapter = self::getEntityAdapter(); - $method = 'put'; - return $entityAdapter::$method(...func_get_args()); + return $this->entityAdapter->put(...func_get_args()); } - final public static function delete($id, Request $request, Response $response): Response + final public function delete(Request $request, Response $response, $id): Response { - $entityAdapter = self::getEntityAdapter(); - $method = 'delete'; - return $entityAdapter::$method(...func_get_args()); + return $this->entityAdapter->delete(...func_get_args()); } } diff --git a/src/Adapters/EntityAdapter.php b/src/Adapters/EntityAdapter.php index 6cea6f8..0262b0f 100644 --- a/src/Adapters/EntityAdapter.php +++ b/src/Adapters/EntityAdapter.php @@ -8,14 +8,14 @@ abstract class EntityAdapter extends BaseEntityAdapter { - abstract protected static function performSearch(SearchResponseEntity $response); + abstract protected function performSearch(SearchResponseEntity $response); - protected static function getSearchResponseEntity(SearchCriteriaEntity $criteria): SearchResponseEntity + protected function getSearchResponseEntity(SearchCriteriaEntity $criteria): SearchResponseEntity { return new SearchResponseEntity($criteria); } - protected static function getSearchCriteriaEntity( + protected function getSearchCriteriaEntity( int $offset, int $pageSize, string $sort = null, @@ -24,14 +24,14 @@ protected static function getSearchCriteriaEntity( return new SearchCriteriaEntity($offset, $pageSize, $sort); } - final protected static function getEntityList( + final protected function getEntityList( int $offset, int $pageSize, string $sort = null, Request $request ): SearchResponseEntity { - $response = new SearchResponseEntity(static::getSearchCriteriaEntity($offset, $pageSize, $sort, $request)); - static::performSearch($response); + $response = new SearchResponseEntity($this->getSearchCriteriaEntity($offset, $pageSize, $sort, $request)); + $this->performSearch($response); return $response; } } diff --git a/src/Adapters/EntityAdapterInterface.php b/src/Adapters/EntityAdapterInterface.php index fa69fce..67443bd 100644 --- a/src/Adapters/EntityAdapterInterface.php +++ b/src/Adapters/EntityAdapterInterface.php @@ -7,13 +7,13 @@ interface EntityAdapterInterface { - public static function list(Request $request, Response $response): Response; + public function list(Request $request, Response $response): Response; - public static function get($id, Request $request, Response $response): Response; + public function get(Request $request, Response $response, $id): Response; - public static function post(Request $request, Response $response): Response; + public function post(Request $request, Response $response): Response; - public static function put($id, Request $request, Response $response): Response; + public function put(Request $request, Response $response, $id): Response; - public static function delete($id, Request $request, Response $response): Response; + public function delete(Request $request, Response $response, $id): Response; } diff --git a/src/Adapters/EntityAdapterRouterFactory.php b/src/Adapters/EntityAdapterRouterFactory.php index 336a1e4..b83bd09 100644 --- a/src/Adapters/EntityAdapterRouterFactory.php +++ b/src/Adapters/EntityAdapterRouterFactory.php @@ -17,65 +17,53 @@ class EntityAdapterRouterFactory * @param App $app * @param string $entityAdapter * @param int $allowed - * @param callable|null $additional function(App $app, string $entityAdapter) If provided allows definition of additional routes for this base + * @param callable|null $additional function(App $app, EntityAdapterInterface $entityAdapter) If provided allows definition of additional routes for this base * @return callable */ - public static function crud(App $app, string $entityAdapter, $allowed = self::ALL, callable $additional = null): callable - { + public static function crud( + App $app, + string $entityAdapter, + $allowed = self::ALL, + callable $additional = null + ): callable { return function () use ($entityAdapter, $app, $allowed, $additional) { - $allowed & self::LIST && $app->get('/', self::entityAdapterList($entityAdapter)); - $allowed & self::ITEM_GET && $app->get('/{id}/', self::entityAdapterGet($entityAdapter)); - $allowed & self::ITEM_POST && $app->post('/', self::entityAdapterPost($entityAdapter)); - $allowed & self::ITEM_PUT && $app->put('/{id}/', self::entityAdapterPut($entityAdapter)); - $allowed & self::ITEM_DELETE && $app->delete('/{id}/', self::entityAdapterDelete($entityAdapter)); - if($additional !== null) { + $entityAdapter = new $entityAdapter(); + $allowed & self::LIST && $app->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_DELETE + && $app->delete('/{id}/', self::entityAdapterWithRouteIDHandler($entityAdapter, 'delete')); + if ($additional !== null) { $additional($app, $entityAdapter); } }; } - public static function readOnly(App $app, string $entityAdapter): callable - { - return self::crud($app, $entityAdapter, self::LIST | self::ITEM_GET); - } - - public static function entityAdapterList(string $entityAdapter) - { - return function ($request, $response) use ($entityAdapter) { - $method = 'list'; - return $entityAdapter::$method($request, $response); - }; - } - - public static function entityAdapterGet(string $entityAdapter) - { - return function ($request, $response, $routeVariables) use ($entityAdapter) { - $method = 'get'; - return $entityAdapter::$method($routeVariables['id'], $request, $response); - }; - } - - public static function entityAdapterPost(string $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 function ($request, $response) use ($entityAdapter) { - $method = 'post'; - return $entityAdapter::$method($request, $response); - }; + return self::crud($app, $entityAdapter, self::LIST | self::ITEM_GET, $additional); } - public static function entityAdapterPut(string $entityAdapter) + private static function entityAdapterWithRouteIDHandler(EntityAdapterInterface $entityAdapter, $adapterMethod) { - return function ($request, $response, $routeVariables) use ($entityAdapter) { - $method = 'put'; - return $entityAdapter::$method($routeVariables['id'], $request, $response); + return function ($request, $response, $routeVariables) use ($entityAdapter, $adapterMethod) { + return $entityAdapter->$adapterMethod($request, $response, $routeVariables['id']); }; } - public static function entityAdapterDelete(string $entityAdapter) + public static function entityAdapterHandler(EntityAdapterInterface $entityAdapter, $adapterMethod) { - return function ($request, $response, $routeVariables) use ($entityAdapter) { - $method = 'delete'; - return $entityAdapter::$method($routeVariables['id'], $request, $response); + return function (...$params) use ($entityAdapter, $adapterMethod) { + return $entityAdapter->$adapterMethod(...$params); }; } } diff --git a/src/Adapters/Stem/LegacyStemEntityAdapter.php b/src/Adapters/Stem/LegacyStemEntityAdapter.php index 1be5660..9cc38c5 100644 --- a/src/Adapters/Stem/LegacyStemEntityAdapter.php +++ b/src/Adapters/Stem/LegacyStemEntityAdapter.php @@ -19,18 +19,18 @@ */ abstract class LegacyStemEntityAdapter extends BaseEntityAdapter { - abstract protected static function getModelClass(): string; + abstract protected function getModelClass(): string; /** * @param $id * @return Model * @throws ResourceNotFoundException */ - protected static function getEntityForId($id) + protected function getEntityForId($id) { try { /** @var Model $modelClass */ - $modelClass = static::getModelClass(); + $modelClass = $this->getModelClass(); return new $modelClass($id); } catch (RecordNotFoundException $exception) { throw new ResourceNotFoundException($exception->getMessage(), $exception); @@ -42,14 +42,14 @@ protected static function getEntityForId($id) * @param bool $resultList * @return array */ - protected static function getPayloadForEntity($entity, $resultList = false) + protected function getPayloadForEntity($entity, $resultList = false) { return $entity->exportPublicData(); } - protected static function getEntityForPayload($payload, $id = null) + protected function getEntityForPayload($payload, $id = null) { - $modelClass = static::getModelClass(); + $modelClass = $this->getModelClass(); /** @var Model $model */ $model = new $modelClass($id); $model->importData($payload); @@ -60,7 +60,7 @@ protected static function getEntityForPayload($payload, $id = null) * @param Model $entity * @throws \Rhubarb\Stem\Exceptions\DeleteModelException */ - final protected static function deleteEntity($entity) + final protected function deleteEntity($entity) { $entity->delete(); } @@ -69,7 +69,7 @@ final protected static function deleteEntity($entity) * @param Request $request * @return Filter[] */ - protected static function getListFilterForRequest(Request $request): array + protected function getListFilterForRequest(Request $request): array { return []; } @@ -80,7 +80,7 @@ protected static function getListFilterForRequest(Request $request): array * @param Request $request * @return Collection */ - final protected static function getEntityList( + final protected function getEntityList( int $offset, int $pageSize, string $sort = null, @@ -89,8 +89,8 @@ final protected static function getEntityList( $criteria = new SearchCriteriaEntity($offset, $pageSize, $sort); $response = new SearchResponseEntity($criteria); /** @var Model $modelClass */ - $modelClass = static::getModelClass(); - $collection = $modelClass::find(...static::getListFilterForRequest($request))->setRange($offset, $pageSize); + $modelClass = $this->getModelClass(); + $collection = $modelClass::find(...$this->getListFilterForRequest($request))->setRange($offset, $pageSize); $response->total = $collection->count(); foreach ($collection as $model) { $response->results[] = $model; @@ -103,7 +103,7 @@ final protected static function getEntityList( * @throws \Rhubarb\Stem\Exceptions\ModelConsistencyValidationException * @throws \Rhubarb\Stem\Exceptions\ModelException */ - protected static function storeEntity($entity) + protected function storeEntity($entity) { $entity->save(); } From e326a624831722f2757997e25db80e2d10ceb376 Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Wed, 6 Feb 2019 15:20:32 +0000 Subject: [PATCH 34/48] fixed adapter --- src/Adapters/BaseEntityAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/BaseEntityAdapter.php b/src/Adapters/BaseEntityAdapter.php index 7e5aaf6..ab04672 100644 --- a/src/Adapters/BaseEntityAdapter.php +++ b/src/Adapters/BaseEntityAdapter.php @@ -67,7 +67,7 @@ final public function put(Request $request, Response $response, $id): Response final public function post(Request $request, Response $response): Response { - return $this->put(null, $request, $response); + return $this->put($request, $response, null); } final public function delete(Request $request, Response $response, $id): Response From 4e9e3e07a237f97318110a50141777118eb7896f Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Wed, 6 Feb 2019 17:58:12 +0000 Subject: [PATCH 35/48] not found handler --- src/ErrorHandlers/NotFoundHandler.php | 16 ++++++++++++++++ src/RhubarbRestAPIApplication.php | 7 +++---- 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 src/ErrorHandlers/NotFoundHandler.php diff --git a/src/ErrorHandlers/NotFoundHandler.php b/src/ErrorHandlers/NotFoundHandler.php new file mode 100644 index 0000000..8befda6 --- /dev/null +++ b/src/ErrorHandlers/NotFoundHandler.php @@ -0,0 +1,16 @@ +withStatus(404); + }; + } +} diff --git a/src/RhubarbRestAPIApplication.php b/src/RhubarbRestAPIApplication.php index 5f69b08..b15f1af 100644 --- a/src/RhubarbRestAPIApplication.php +++ b/src/RhubarbRestAPIApplication.php @@ -3,6 +3,7 @@ namespace Rhubarb\RestApi; use Rhubarb\RestApi\ErrorHandlers\DefaultErrorHandler; +use Rhubarb\RestApi\ErrorHandlers\NotFoundHandler; use Slim\App; use Slim\Http\Request; use Slim\Http\Response; @@ -15,9 +16,8 @@ abstract class RhubarbRestAPIApplication protected function registerErrorHandlers() { $container = $this->app->getContainer(); - $container['errorHandler'] = $container['phpErrorHandler'] = function () { - return new DefaultErrorHandler(); - }; + $container['errorHandler'] = $container['phpErrorHandler'] = DefaultErrorHandler::class; + $container['notFoundHandler'] = NotFoundHandler::class; } protected function registerMiddleware() @@ -33,7 +33,6 @@ protected function registerMiddleware() return $next($request, $response); }); - } final protected function registerModule(RhubarbApiModule $module) From fa3605a825c99c6cbc78ef2fe4d0f5d2dd394669 Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Wed, 6 Feb 2019 18:02:06 +0000 Subject: [PATCH 36/48] empty results --- composer.lock | 2 +- src/Entities/SearchResponseEntity.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.lock b/composer.lock index b2e8ec7..732d9d4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8d6afe7e8d59b437c45335e32c01d94e", + "content-hash": "974176868063ec9a75e70126c1acb8c3", "packages": [ { "name": "container-interop/container-interop", diff --git a/src/Entities/SearchResponseEntity.php b/src/Entities/SearchResponseEntity.php index 1c50422..9bfe91a 100644 --- a/src/Entities/SearchResponseEntity.php +++ b/src/Entities/SearchResponseEntity.php @@ -17,7 +17,7 @@ class SearchResponseEntity /** * @var array */ - public $results; + public $results = []; public function __construct(SearchCriteriaEntity $criteria) { From ac655ce3c311e0a6b7dff61d0820dcb3cd29aa30 Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Wed, 6 Feb 2019 18:05:27 +0000 Subject: [PATCH 37/48] error handlers are not involkable --- src/RhubarbRestAPIApplication.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/RhubarbRestAPIApplication.php b/src/RhubarbRestAPIApplication.php index b15f1af..aefbb56 100644 --- a/src/RhubarbRestAPIApplication.php +++ b/src/RhubarbRestAPIApplication.php @@ -16,8 +16,12 @@ abstract class RhubarbRestAPIApplication protected function registerErrorHandlers() { $container = $this->app->getContainer(); - $container['errorHandler'] = $container['phpErrorHandler'] = DefaultErrorHandler::class; - $container['notFoundHandler'] = NotFoundHandler::class; + $container['errorHandler'] = $container['phpErrorHandler'] = function () { + return new DefaultErrorHandler(); + }; + $container['notFoundHandler'] = function () { + return new NotFoundHandler(); + }; } protected function registerMiddleware() From 1a9e072b69342fb32f73fb79b66728aabfc76127 Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Wed, 6 Feb 2019 18:42:58 +0000 Subject: [PATCH 38/48] fixed middleware signatures --- src/ErrorHandlers/DefaultErrorHandler.php | 2 +- src/ErrorHandlers/NotFoundHandler.php | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ErrorHandlers/DefaultErrorHandler.php b/src/ErrorHandlers/DefaultErrorHandler.php index 66d0b9b..afbf2e4 100644 --- a/src/ErrorHandlers/DefaultErrorHandler.php +++ b/src/ErrorHandlers/DefaultErrorHandler.php @@ -7,7 +7,7 @@ class DefaultErrorHandler { - public function __invoke(Request $request, Response $response, \Throwable $exception = null) + public function __invoke(Request $request, Response $response, \Throwable $exception = null): Response { $code = $exception->getCode(); diff --git a/src/ErrorHandlers/NotFoundHandler.php b/src/ErrorHandlers/NotFoundHandler.php index 8befda6..77738ea 100644 --- a/src/ErrorHandlers/NotFoundHandler.php +++ b/src/ErrorHandlers/NotFoundHandler.php @@ -7,10 +7,8 @@ class NotFoundHandler { - public function __invoke(Request $request, Response $response) + public function __invoke(Request $request, Response $response): Response { - return function ($request, $response) { - return $response->withStatus(404); - }; + return $response->withStatus(404); } } From e6f54f51d5cfe01c0777dc9062196e06e04e9a40 Mon Sep 17 00:00:00 2001 From: Michael Miscampbell Date: Fri, 8 Feb 2019 11:27:25 +0000 Subject: [PATCH 39/48] Fixing issue with passing from/to. Fixed issue with from going to a negative offset. Ensured the from will be inclusive. So From 0 To 4 will return 5 records when possible --- src/Adapters/BaseEntityAdapter.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Adapters/BaseEntityAdapter.php b/src/Adapters/BaseEntityAdapter.php index ab04672..016af74 100644 --- a/src/Adapters/BaseEntityAdapter.php +++ b/src/Adapters/BaseEntityAdapter.php @@ -28,7 +28,10 @@ abstract protected function getEntityList( final public function list(Request $request, Response $response): Response { $offset = (int)$request->getQueryParam('offset', $request->getQueryParam('from', 1) - 1); - $pageSize = (int)$request->getQueryParam('pageSize', $request->getQueryParam('to', 10 - $offset)); + if ($offset < 0) { + $offset = 0; + } + $pageSize = (int)$request->getQueryParam('pageSize', $request->getQueryParam('to', 10 - $offset) + 1); $sort = $request->getQueryParam('sort'); $list = $this->getEntityList( From b29b7e77e026dec294b6a564a4ab1980a7fc7d85 Mon Sep 17 00:00:00 2001 From: Michael Miscampbell Date: Tue, 12 Feb 2019 21:38:43 +0000 Subject: [PATCH 40/48] Adding support to see the Request being processed by each HTTP method --- src/Adapters/BaseEntityAdapter.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Adapters/BaseEntityAdapter.php b/src/Adapters/BaseEntityAdapter.php index 016af74..036a769 100644 --- a/src/Adapters/BaseEntityAdapter.php +++ b/src/Adapters/BaseEntityAdapter.php @@ -25,8 +25,13 @@ abstract protected function getEntityList( Request $request ): SearchResponseEntity; + /** @var \Slim\Http\Request $request */ + protected $request; + final public function list(Request $request, Response $response): Response { + $this->request = $request; + $offset = (int)$request->getQueryParam('offset', $request->getQueryParam('from', 1) - 1); if ($offset < 0) { $offset = 0; @@ -56,11 +61,13 @@ function ($entity) { final public function get(Request $request, Response $response, $id): Response { + $this->request = $request; return $response->withJson($this->getPayloadForEntity($this->getEntityForId($id))); } final public function put(Request $request, Response $response, $id): Response { + $this->request = $request; $entity = $this->getEntityForPayload($request->getParsedBody(), $id); $this->storeEntity($entity); return $response->withJson($this->getPayloadForEntity( @@ -75,6 +82,7 @@ final public function post(Request $request, Response $response): Response final public function delete(Request $request, Response $response, $id): Response { + $this->request = $request; $this->deleteEntity($this->getEntityForId($id)); return $response; } From ec5b013d8292b667a89dcc47076c35b5668a4992 Mon Sep 17 00:00:00 2001 From: Michael Miscampbell Date: Wed, 13 Feb 2019 08:52:03 +0000 Subject: [PATCH 41/48] Removing logic added to expose the Slim Request --- src/Adapters/BaseEntityAdapter.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Adapters/BaseEntityAdapter.php b/src/Adapters/BaseEntityAdapter.php index 036a769..016af74 100644 --- a/src/Adapters/BaseEntityAdapter.php +++ b/src/Adapters/BaseEntityAdapter.php @@ -25,13 +25,8 @@ abstract protected function getEntityList( Request $request ): SearchResponseEntity; - /** @var \Slim\Http\Request $request */ - protected $request; - final public function list(Request $request, Response $response): Response { - $this->request = $request; - $offset = (int)$request->getQueryParam('offset', $request->getQueryParam('from', 1) - 1); if ($offset < 0) { $offset = 0; @@ -61,13 +56,11 @@ function ($entity) { final public function get(Request $request, Response $response, $id): Response { - $this->request = $request; return $response->withJson($this->getPayloadForEntity($this->getEntityForId($id))); } final public function put(Request $request, Response $response, $id): Response { - $this->request = $request; $entity = $this->getEntityForPayload($request->getParsedBody(), $id); $this->storeEntity($entity); return $response->withJson($this->getPayloadForEntity( @@ -82,7 +75,6 @@ final public function post(Request $request, Response $response): Response final public function delete(Request $request, Response $response, $id): Response { - $this->request = $request; $this->deleteEntity($this->getEntityForId($id)); return $response; } From 37a0b7abafd43ab93e1edd6ce5c335f071c550c7 Mon Sep 17 00:00:00 2001 From: Michael Miscampbell Date: Wed, 13 Feb 2019 15:56:07 +0000 Subject: [PATCH 42/48] Removed duplicated method call to getPayloadForEntity on the BaseEntityAdapter.php --- src/Adapters/BaseEntityAdapter.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Adapters/BaseEntityAdapter.php b/src/Adapters/BaseEntityAdapter.php index 016af74..92eaf91 100644 --- a/src/Adapters/BaseEntityAdapter.php +++ b/src/Adapters/BaseEntityAdapter.php @@ -63,9 +63,7 @@ final public function put(Request $request, Response $response, $id): Response { $entity = $this->getEntityForPayload($request->getParsedBody(), $id); $this->storeEntity($entity); - return $response->withJson($this->getPayloadForEntity( - $this->getPayloadForEntity($entity) - )); + return $response->withJson($this->getPayloadForEntity($entity)); } final public function post(Request $request, Response $response): Response From 35b2eaaec4269c8330e668a87a12a30e062fd350 Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Thu, 21 Feb 2019 10:28:33 +0000 Subject: [PATCH 43/48] patch support --- src/Adapters/BaseEntityAdapter.php | 10 +++++ src/Adapters/EntityAdapterRouterFactory.php | 37 ++++++++++++++----- src/Adapters/Stem/LegacyStemEntityAdapter.php | 9 +++++ 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/Adapters/BaseEntityAdapter.php b/src/Adapters/BaseEntityAdapter.php index 92eaf91..56b9f7c 100644 --- a/src/Adapters/BaseEntityAdapter.php +++ b/src/Adapters/BaseEntityAdapter.php @@ -16,6 +16,8 @@ abstract protected function getPayloadForEntity($entity, $resultList = false); abstract protected function getEntityForPayload($payload, $id = null); + abstract protected function updateEntityWithPayload($entity, $payload); + abstract protected function storeEntity($entity); abstract protected function getEntityList( @@ -59,6 +61,14 @@ 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); diff --git a/src/Adapters/EntityAdapterRouterFactory.php b/src/Adapters/EntityAdapterRouterFactory.php index b83bd09..2756070 100644 --- a/src/Adapters/EntityAdapterRouterFactory.php +++ b/src/Adapters/EntityAdapterRouterFactory.php @@ -10,8 +10,9 @@ class EntityAdapterRouterFactory const ITEM_GET = 2; const ITEM_POST = 4; const ITEM_PUT = 8; - const ITEM_DELETE = 16; - const ALL = 31; + const ITEM_PATCH = 16; + const ITEM_DELETE = 32; + const ALL = 63; /** * @param App $app @@ -28,14 +29,30 @@ public static function crud( ): callable { return function () use ($entityAdapter, $app, $allowed, $additional) { $entityAdapter = new $entityAdapter(); - $allowed & self::LIST && $app->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_DELETE - && $app->delete('/{id}/', self::entityAdapterWithRouteIDHandler($entityAdapter, 'delete')); + $allowed & self::LIST && $app->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); } diff --git a/src/Adapters/Stem/LegacyStemEntityAdapter.php b/src/Adapters/Stem/LegacyStemEntityAdapter.php index 9cc38c5..d500002 100644 --- a/src/Adapters/Stem/LegacyStemEntityAdapter.php +++ b/src/Adapters/Stem/LegacyStemEntityAdapter.php @@ -56,6 +56,15 @@ protected function getEntityForPayload($payload, $id = null) return $model; } + /** + * @param Model $entity + * @param array $payload + */ + protected function updateEntityWithPayload($entity, $payload) + { + $entity->importData($payload); + } + /** * @param Model $entity * @throws \Rhubarb\Stem\Exceptions\DeleteModelException From 1e80d476a23188a03dd2c44f385fc23c0e943905 Mon Sep 17 00:00:00 2001 From: Andrew Cuthbert Date: Tue, 26 Feb 2019 14:25:38 +0000 Subject: [PATCH 44/48] Tweaks after backport to CSL --- src/Adapters/BaseEntityAdapter.php | 10 ++++++---- src/Adapters/DIEntityAdapter.php | 2 +- src/Adapters/EntityAdapter.php | 12 +++++++----- src/Adapters/EntityAdapterInterface.php | 2 +- src/Adapters/Stem/LegacyStemEntityAdapter.php | 5 +++-- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/Adapters/BaseEntityAdapter.php b/src/Adapters/BaseEntityAdapter.php index 56b9f7c..85b440a 100644 --- a/src/Adapters/BaseEntityAdapter.php +++ b/src/Adapters/BaseEntityAdapter.php @@ -23,11 +23,12 @@ abstract protected function storeEntity($entity); abstract protected function getEntityList( int $offset, int $pageSize, - string $sort = null, - Request $request + ?string $sort = null, + Request $request = null, + $arguments = [] ): SearchResponseEntity; - final public function list(Request $request, Response $response): Response + final public function list(Request $request, Response $response, $arguments = []): Response { $offset = (int)$request->getQueryParam('offset', $request->getQueryParam('from', 1) - 1); if ($offset < 0) { @@ -40,7 +41,8 @@ final public function list(Request $request, Response $response): Response $offset, $pageSize, $sort, - $request + $request, + $arguments ); return $response ->withJson(array_map( diff --git a/src/Adapters/DIEntityAdapter.php b/src/Adapters/DIEntityAdapter.php index 4ac1631..7d93c05 100644 --- a/src/Adapters/DIEntityAdapter.php +++ b/src/Adapters/DIEntityAdapter.php @@ -16,7 +16,7 @@ public function __construct() $this->entityAdapter = Container::instance(static::class); } - final public function list(Request $request, Response $response): Response + final public function list(Request $request, Response $response, $arguments = []): Response { return $this->entityAdapter->list(...func_get_args()); } diff --git a/src/Adapters/EntityAdapter.php b/src/Adapters/EntityAdapter.php index 0262b0f..27d5eb2 100644 --- a/src/Adapters/EntityAdapter.php +++ b/src/Adapters/EntityAdapter.php @@ -18,8 +18,9 @@ protected function getSearchResponseEntity(SearchCriteriaEntity $criteria): Sear protected function getSearchCriteriaEntity( int $offset, int $pageSize, - string $sort = null, - Request $request + ?string $sort = null, + Request $request = null, + $arguments = [] ): SearchCriteriaEntity { return new SearchCriteriaEntity($offset, $pageSize, $sort); } @@ -27,10 +28,11 @@ protected function getSearchCriteriaEntity( final protected function getEntityList( int $offset, int $pageSize, - string $sort = null, - Request $request + ?string $sort = null, + Request $request = null, + $arguments = [] ): SearchResponseEntity { - $response = new SearchResponseEntity($this->getSearchCriteriaEntity($offset, $pageSize, $sort, $request)); + $response = new SearchResponseEntity($this->getSearchCriteriaEntity($offset, $pageSize, $sort, $request, $arguments)); $this->performSearch($response); return $response; } diff --git a/src/Adapters/EntityAdapterInterface.php b/src/Adapters/EntityAdapterInterface.php index 67443bd..a2d868c 100644 --- a/src/Adapters/EntityAdapterInterface.php +++ b/src/Adapters/EntityAdapterInterface.php @@ -7,7 +7,7 @@ interface EntityAdapterInterface { - public function list(Request $request, Response $response): Response; + public function list(Request $request, Response $response, $arguments = []): Response; public function get(Request $request, Response $response, $id): Response; diff --git a/src/Adapters/Stem/LegacyStemEntityAdapter.php b/src/Adapters/Stem/LegacyStemEntityAdapter.php index d500002..d0a8f26 100644 --- a/src/Adapters/Stem/LegacyStemEntityAdapter.php +++ b/src/Adapters/Stem/LegacyStemEntityAdapter.php @@ -92,8 +92,9 @@ protected function getListFilterForRequest(Request $request): array final protected function getEntityList( int $offset, int $pageSize, - string $sort = null, - Request $request + ?string $sort = null, + Request $request = null, + $arguments = [] ): SearchResponseEntity { $criteria = new SearchCriteriaEntity($offset, $pageSize, $sort); $response = new SearchResponseEntity($criteria); From 04a8608af0ad7fe72b12ae9046dd95ab1b37326f Mon Sep 17 00:00:00 2001 From: Andrew Cuthbert Date: Tue, 26 Feb 2019 14:28:55 +0000 Subject: [PATCH 45/48] Changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e992b0..c080f36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +### 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). From 8b72d3bb2a66d93a4be7e98194aa421d594023a0 Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Tue, 30 Apr 2019 12:13:00 +0100 Subject: [PATCH 46/48] Fixed: To/From pagination --- CHANGELOG.md | 3 +++ src/Adapters/BaseEntityAdapter.php | 5 +---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c080f36..a8fce10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +### 3.0.2 +Fixed: To/From pagination + ### 3.0.1 Fixed: Optional argument issue diff --git a/src/Adapters/BaseEntityAdapter.php b/src/Adapters/BaseEntityAdapter.php index 85b440a..03ea6e0 100644 --- a/src/Adapters/BaseEntityAdapter.php +++ b/src/Adapters/BaseEntityAdapter.php @@ -31,10 +31,7 @@ abstract protected function getEntityList( final public function list(Request $request, Response $response, $arguments = []): Response { $offset = (int)$request->getQueryParam('offset', $request->getQueryParam('from', 1) - 1); - if ($offset < 0) { - $offset = 0; - } - $pageSize = (int)$request->getQueryParam('pageSize', $request->getQueryParam('to', 10 - $offset) + 1); + $pageSize = (int)$request->getQueryParam('pageSize', $request->getQueryParam('to', 10 - $offset)); $sort = $request->getQueryParam('sort'); $list = $this->getEntityList( From f4113f555d5fec8bd9cba11de99589346381abe0 Mon Sep 17 00:00:00 2001 From: Sam Thompson Date: Tue, 30 Apr 2019 13:23:17 +0100 Subject: [PATCH 47/48] Added: Current Request middleware. --- CHANGELOG.md | 3 +++ src/Middleware/CurrentRequestMiddleware.php | 24 +++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 src/Middleware/CurrentRequestMiddleware.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a8fce10..d578edf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +### 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 diff --git a/src/Middleware/CurrentRequestMiddleware.php b/src/Middleware/CurrentRequestMiddleware.php new file mode 100644 index 0000000..ab2fdf5 --- /dev/null +++ b/src/Middleware/CurrentRequestMiddleware.php @@ -0,0 +1,24 @@ + Date: Tue, 30 Apr 2019 13:59:20 +0100 Subject: [PATCH 48/48] Revert "Tweaks after backport to CSL" This reverts commit 1e80d476a23188a03dd2c44f385fc23c0e943905. --- CHANGELOG.md | 3 +++ src/Adapters/BaseEntityAdapter.php | 12 +++++------- src/Adapters/DIEntityAdapter.php | 2 +- src/Adapters/EntityAdapter.php | 12 +++++------- src/Adapters/EntityAdapterInterface.php | 2 +- src/Adapters/Stem/LegacyStemEntityAdapter.php | 8 ++++---- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d578edf..ae759de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # 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()` diff --git a/src/Adapters/BaseEntityAdapter.php b/src/Adapters/BaseEntityAdapter.php index 03ea6e0..95190fe 100644 --- a/src/Adapters/BaseEntityAdapter.php +++ b/src/Adapters/BaseEntityAdapter.php @@ -21,25 +21,23 @@ abstract protected function updateEntityWithPayload($entity, $payload); abstract protected function storeEntity($entity); abstract protected function getEntityList( + Request $request, int $offset, int $pageSize, - ?string $sort = null, - Request $request = null, - $arguments = [] + ?string $sort = null ): SearchResponseEntity; - final public function list(Request $request, Response $response, $arguments = []): Response + final public function list(Request $request, Response $response): Response { $offset = (int)$request->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, - $request, - $arguments + $sort ); return $response ->withJson(array_map( diff --git a/src/Adapters/DIEntityAdapter.php b/src/Adapters/DIEntityAdapter.php index 7d93c05..4ac1631 100644 --- a/src/Adapters/DIEntityAdapter.php +++ b/src/Adapters/DIEntityAdapter.php @@ -16,7 +16,7 @@ public function __construct() $this->entityAdapter = Container::instance(static::class); } - final public function list(Request $request, Response $response, $arguments = []): Response + final public function list(Request $request, Response $response): Response { return $this->entityAdapter->list(...func_get_args()); } diff --git a/src/Adapters/EntityAdapter.php b/src/Adapters/EntityAdapter.php index 27d5eb2..9241996 100644 --- a/src/Adapters/EntityAdapter.php +++ b/src/Adapters/EntityAdapter.php @@ -16,23 +16,21 @@ protected function getSearchResponseEntity(SearchCriteriaEntity $criteria): Sear } protected function getSearchCriteriaEntity( + Request $request, int $offset, int $pageSize, - ?string $sort = null, - Request $request = null, - $arguments = [] + string $sort = null ): SearchCriteriaEntity { return new SearchCriteriaEntity($offset, $pageSize, $sort); } final protected function getEntityList( + Request $request, int $offset, int $pageSize, - ?string $sort = null, - Request $request = null, - $arguments = [] + ?string $sort = null ): SearchResponseEntity { - $response = new SearchResponseEntity($this->getSearchCriteriaEntity($offset, $pageSize, $sort, $request, $arguments)); + $response = new SearchResponseEntity($this->getSearchCriteriaEntity($request, $offset, $pageSize, $sort)); $this->performSearch($response); return $response; } diff --git a/src/Adapters/EntityAdapterInterface.php b/src/Adapters/EntityAdapterInterface.php index a2d868c..67443bd 100644 --- a/src/Adapters/EntityAdapterInterface.php +++ b/src/Adapters/EntityAdapterInterface.php @@ -7,7 +7,7 @@ interface EntityAdapterInterface { - public function list(Request $request, Response $response, $arguments = []): Response; + public function list(Request $request, Response $response): Response; public function get(Request $request, Response $response, $id): Response; diff --git a/src/Adapters/Stem/LegacyStemEntityAdapter.php b/src/Adapters/Stem/LegacyStemEntityAdapter.php index d0a8f26..1845852 100644 --- a/src/Adapters/Stem/LegacyStemEntityAdapter.php +++ b/src/Adapters/Stem/LegacyStemEntityAdapter.php @@ -84,17 +84,17 @@ protected function getListFilterForRequest(Request $request): array } /** + * @param Request $request * @param int $offset * @param int $pageSize - * @param Request $request + * @param string|null $sort * @return Collection */ final protected function getEntityList( + Request $request, int $offset, int $pageSize, - ?string $sort = null, - Request $request = null, - $arguments = [] + ?string $sort = null ): SearchResponseEntity { $criteria = new SearchCriteriaEntity($offset, $pageSize, $sort); $response = new SearchResponseEntity($criteria);