From 6e696c3d98a174f25434cf21ce59c621c39f6060 Mon Sep 17 00:00:00 2001 From: DarkSide Date: Tue, 12 Jun 2018 17:27:54 +0300 Subject: [PATCH 1/2] implement field limiting and make class better extendable --- examples/test.php | 2 +- src/Api.php | 174 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 143 insertions(+), 33 deletions(-) diff --git a/examples/test.php b/examples/test.php index 9daefc8..06093dd 100644 --- a/examples/test.php +++ b/examples/test.php @@ -50,7 +50,7 @@ public function validate($intent = null) } } session_start(); -$db = new \atk4\data\Persistence_SQL('mysql:dbname=atk4;host=localhost', 'root', 'root'); +$db = new \atk4\data\Persistence_SQL('mysql:dbname=atk4;host=localhost', 'root', ''); $api->get('/ping/', function () { return 'Hello, World'; diff --git a/src/Api.php b/src/Api.php index 8de04f7..3cc136b 100644 --- a/src/Api.php +++ b/src/Api.php @@ -117,23 +117,15 @@ public function match($pattern) * @param callable $callable * @param array $vars */ - public function call($callable, $vars = []) + public function exec($callable, $vars = []) { // try to call callable function - try { - $ret = call_user_func_array($callable, $vars); - } catch (\Exception $e) { - $this->caughtException($e); - } + $ret = $this->call($callable, $vars); // if callable function returns agile data model, then export it // this is important for REST API implementation if ($ret instanceof \atk4\data\Model) { - if ($ret->only_fields) { - $ret = $ret->export($ret->only_fields); // use only_fields to not add system fields by default - } else { - $ret = $ret->export(); // all fields including allsystem fields - } + $ret = $this->exportModel($ret); } // no response, just step out @@ -141,11 +133,108 @@ public function call($callable, $vars = []) return; } + // emit successful response + $this->successResponse($ret); + } + + /** + * Call callable and return response. + * + * @param callable $callable + * @param array $vars + * + * @return mixed + */ + protected function call($callable, $vars = []) + { + // try to call callable function + try { + $ret = call_user_func_array($callable, $vars); + } catch (\Exception $e) { + $this->caughtException($e); + } + + return $ret; + } + + /** + * Exports data model. + * + * Extend this method to implement your own field restrictions. + * + * @param \atk4\data\Model $m + * + * @return array + */ + protected function exportModel(\atk4\data\Model $m) + { + return $m->export($this->getAllowedFields($m, 'read')); + } + + /** + * Returns list of model field names which allow particular action - read or modify. + * Also takes model->only_fields into account if that's defined. + * + * It uses custom model property apiFields[$action] which should contain array of allowed field names. + * + * @param \atk4\data\Model $m + * @param string $action read|modify + * + * @return null|array of field names + */ + protected function getAllowedFields(\atk4\data\Model $m, $action = 'read') + { + $fields = null; + + // take model only_fields into account + if ($m->only_fields) { + $fields = $m->only_fields; + } + + // limit by apiFields + if (isset($m->apiFields[$action])) { + $allowed = $m->apiFields[$action]; + $fields = $fields ? array_intersect($fields, $allowed) : $allowed; + } + + return $fields; + } + + /** + * Filters data array by only allowed fields. + * + * Extend this method to implement your own field restrictions. + * + * @param \atk4\data\Model $m + * @param array $data + * + * @return array + */ + /* not used and maybe will not be needed too + protected function filterData(\atk4\data\Model $m, array $data) + { + $allowed = $this->getAllowedFields($m, 'modify'); + + if ($allowed) { + $data = array_intersect_key($data, array_flip($allowed)); + } + + return $data; + } + */ + + /** + * Emit successful response. + * + * @param mixed $response + */ + protected function successResponse($response) + { // create response object if (!$this->response) { $this->response = new \Zend\Diactoros\Response\JsonResponse( - $ret, + $response, 200, [], JSON_PRETTY_PRINT | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT @@ -159,7 +248,7 @@ public function call($callable, $vars = []) exit; } - // @todo Should we also stop script execution if no response is received or just ignore that? + // @todo Should we also stop script execution if no emitter is defined or just ignore that? //exit; } @@ -174,7 +263,7 @@ public function call($callable, $vars = []) public function get($pattern, $callable = null) { if ($this->request->getMethod() === 'GET' && $this->match($pattern)) { - return $this->call($callable, $this->_vars); + return $this->exec($callable, $this->_vars); } } @@ -189,7 +278,7 @@ public function get($pattern, $callable = null) public function post($pattern, $callable = null) { if ($this->request->getMethod() === 'POST' && $this->match($pattern)) { - return $this->call($callable, $this->_vars); + return $this->exec($callable, $this->_vars); } } @@ -204,7 +293,7 @@ public function post($pattern, $callable = null) public function patch($pattern, $callable = null) { if ($this->request->getMethod() === 'PATCH' && $this->match($pattern)) { - return $this->call($callable, $this->_vars); + return $this->exec($callable, $this->_vars); } } @@ -219,7 +308,7 @@ public function patch($pattern, $callable = null) public function delete($pattern, $callable = null) { if ($this->request->getMethod() === 'DELETE' && $this->match($pattern)) { - return $this->call($callable, $this->_vars); + return $this->exec($callable, $this->_vars); } } @@ -238,7 +327,7 @@ public function rest($pattern, $model = null) $args = func_get_args(); if (is_callable($model)) { - $model = call_user_func_array($model, $args); + $model = $this->call($model, $args); } return $model; @@ -250,9 +339,12 @@ public function rest($pattern, $model = null) $id = array_pop($args); // pop last element of args array, it's :id if (is_callable($model)) { - $model = call_user_func_array($model, $args); + $model = $this->call($model, $args); } + // limit fields + $model->onlyFields($this->getAllowedFields($model, 'read')); + return $model->load($id)->get(); }); @@ -262,10 +354,15 @@ public function rest($pattern, $model = null) $id = array_pop($args); // pop last element of args array, it's :id if (is_callable($model)) { - $model = call_user_func_array($model, $args); + $model = $this->call($model, $args); } - return $model->load($id)->save($this->requestData)->get(); + // limit fields + $model->onlyFields($this->getAllowedFields($model, 'modify')); + $model->load($id)->save($this->requestData); + $model->onlyFields($this->getAllowedFields($model, 'read')); + + return $model->get(); }); // POST :id - update one record @@ -274,33 +371,46 @@ public function rest($pattern, $model = null) $id = array_pop($args); // pop last element of args array, it's :id if (is_callable($model)) { - $model = call_user_func_array($model, $args); + $model = $this->call($model, $args); } - return $model->load($id)->save($this->requestData)->get(); + // limit fields + $model->onlyFields($this->getAllowedFields($model, 'modify')); + $model->load($id)->save($this->requestData); + $model->onlyFields($this->getAllowedFields($model, 'read')); + + return $model->get(); }); - // DELETE :id - delete one record - $this->delete($pattern.'/:id', function () use ($model) { + // POST - insert new record + $this->post($pattern, function () use ($model) { $args = func_get_args(); - $id = array_pop($args); // pop last element of args array, it's :id if (is_callable($model)) { - $model = call_user_func_array($model, $args); + $model = $this->call($model, $args); } - return !$model->load($id)->delete()->loaded(); + // limit fields + $model->onlyFields($this->getAllowedFields($model, 'modify')); + $model->unload()->save($this->requestData); + $model->onlyFields($this->getAllowedFields($model, 'read')); + + return $model->get(); }); - // POST - insert new record - $this->post($pattern, function () use ($model) { + // DELETE :id - delete one record + $this->delete($pattern.'/:id', function () use ($model) { $args = func_get_args(); + $id = array_pop($args); // pop last element of args array, it's :id if (is_callable($model)) { - $model = call_user_func_array($model, $args); + $model = $this->call($model, $args); } - return $model->unload()->save($this->requestData)->get(); + // limit fields (not necessary, but will limit field list for performance) + $model->onlyFields($this->getAllowedFields($model, 'read')); + + return !$model->delete($id)->loaded(); }); } From 53b8ed55e2b5efa4ee3f79ec0c1c8e95e14b2bd3 Mon Sep 17 00:00:00 2001 From: DarkSide Date: Tue, 12 Jun 2018 17:32:13 +0300 Subject: [PATCH 2/2] avoid code duplication --- src/Api.php | 44 +++++++++++++++++--------------------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/src/Api.php b/src/Api.php index 3cc136b..5318064 100644 --- a/src/Api.php +++ b/src/Api.php @@ -323,7 +323,7 @@ public function delete($pattern, $callable = null) public function rest($pattern, $model = null) { // GET all records - $this->get($pattern, function () use ($model) { + $f = function () use ($model) { $args = func_get_args(); if (is_callable($model)) { @@ -331,10 +331,11 @@ public function rest($pattern, $model = null) } return $model; - }); + }; + $this->get($pattern, $f); // GET :id - one record - $this->get($pattern.'/:id', function () use ($model) { + $f = function () use ($model) { $args = func_get_args(); $id = array_pop($args); // pop last element of args array, it's :id @@ -346,27 +347,12 @@ public function rest($pattern, $model = null) $model->onlyFields($this->getAllowedFields($model, 'read')); return $model->load($id)->get(); - }); - - // PATCH :id - update one record (same as POST :id) - $this->patch($pattern.'/:id', function () use ($model) { - $args = func_get_args(); - $id = array_pop($args); // pop last element of args array, it's :id - - if (is_callable($model)) { - $model = $this->call($model, $args); - } - - // limit fields - $model->onlyFields($this->getAllowedFields($model, 'modify')); - $model->load($id)->save($this->requestData); - $model->onlyFields($this->getAllowedFields($model, 'read')); - - return $model->get(); - }); + }; + $this->get($pattern.'/:id', $f); // POST :id - update one record - $this->post($pattern.'/:id', function () use ($model) { + // PATCH :id - update one record (same as POST :id) + $f = function () use ($model) { $args = func_get_args(); $id = array_pop($args); // pop last element of args array, it's :id @@ -380,10 +366,12 @@ public function rest($pattern, $model = null) $model->onlyFields($this->getAllowedFields($model, 'read')); return $model->get(); - }); + }; + $this->patch($pattern.'/:id', $f); + $this->post($pattern.'/:id', $f); // POST - insert new record - $this->post($pattern, function () use ($model) { + $f = function () use ($model) { $args = func_get_args(); if (is_callable($model)) { @@ -396,10 +384,11 @@ public function rest($pattern, $model = null) $model->onlyFields($this->getAllowedFields($model, 'read')); return $model->get(); - }); + }; + $this->post($pattern, $f); // DELETE :id - delete one record - $this->delete($pattern.'/:id', function () use ($model) { + $f = function () use ($model) { $args = func_get_args(); $id = array_pop($args); // pop last element of args array, it's :id @@ -411,7 +400,8 @@ public function rest($pattern, $model = null) $model->onlyFields($this->getAllowedFields($model, 'read')); return !$model->delete($id)->loaded(); - }); + }; + $this->delete($pattern.'/:id', $f); } /**