diff --git a/.travis.yml b/.travis.yml index 100c353..3644f11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: php php: - - '7.2' - '7.3' cache: @@ -18,11 +17,11 @@ before_script: after_script: - echo $TRAVIS_BRANCH - - if [[ ${TRAVIS_PHP_VERSION:0:3} == "7.2" ]]; then echo "Sending coverage report"; vendor/bin/test-reporter --coverage-report build/logs/clover.xml; fi - - if [[ ${TRAVIS_PHP_VERSION:0:3} == "7.2" ]]; then echo "Sending codecov report"; TRAVIS_CMD="" bash <(curl -s https://codecov.io/bash) -f build/logs/clover.xml; fi + - if [[ ${TRAVIS_PHP_VERSION:0:3} == "7.3" ]]; then echo "Sending coverage report"; vendor/bin/test-reporter --coverage-report build/logs/clover.xml; fi + - if [[ ${TRAVIS_PHP_VERSION:0:3} == "7.3" ]]; then echo "Sending codecov report"; TRAVIS_CMD="" bash <(curl -s https://codecov.io/bash) -f build/logs/clover.xml; fi script: - - if [[ ${TRAVIS_PHP_VERSION:0:3} != "7.2" ]]; then NC="--no-coverage"; fi + - if [[ ${TRAVIS_PHP_VERSION:0:3} != "7.3" ]]; then NC="--no-coverage"; fi - ./vendor/phpunit/phpunit/phpunit $NC notifications: diff --git a/src/Api.php b/src/Api.php index 7b639fe..aaaaacc 100644 --- a/src/Api.php +++ b/src/Api.php @@ -2,6 +2,7 @@ namespace atk4\api; +use atk4\data\Field; use atk4\data\Model; use Laminas\Diactoros\Request; use Laminas\Diactoros\Response\JsonResponse; @@ -41,6 +42,13 @@ class Api /** @var int Response options */ protected $response_options = JSON_PRETTY_PRINT | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT; + /** + * @var bool If set to true, the first array element of Model->export + * will be returned (GET single record) + * If not, the array will be returned as-is + */ + public $single_record = true; + /** * Reads everything off globals. * @@ -148,7 +156,18 @@ public function exec($callable, $vars = []) // if callable function returns agile data model, then export it // this is important for REST API implementation if ($ret instanceof Model) { - $ret = $this->exportModel($ret); + $data = []; + + $allowed_fields = $this->getAllowedFields($ret, 'read'); + if ($this->single_record) { + $data = $this->exportModel($ret, $allowed_fields); + } else { + foreach ($ret as $m) { + $data[] = $this->exportModel($m, $allowed_fields); + } + } + + $ret = $data; } // no response, just step out @@ -156,6 +175,10 @@ public function exec($callable, $vars = []) return; } + if ($ret === true) { // manage delete + $ret = []; + } + // emit successful response $this->successResponse($ret); } @@ -185,15 +208,23 @@ protected function call($callable, $vars = []) * * Extend this method to implement your own field restrictions. * - * @param Model $m + * @param Model $m Model + * @param array $allowed_fields Allowed fields * * @throws \atk4\data\Exception + * @throws \atk4\core\Exception * * @return array */ - protected function exportModel(Model $m) + protected function exportModel(Model $m, array $allowed_fields = []) { - return $m->export($this->getAllowedFields($m, 'read')); + $data = []; + foreach ($allowed_fields as $fieldName) { + $field = $m->getField($fieldName); + $data[$field->actual ?? $fieldName] = $field->toString(); + } + + return $data; } /** @@ -241,7 +272,7 @@ protected function loadModelByValue(Model $m, $value) protected function getAllowedFields(Model $m, $action = 'read') { // take model only_fields into account - $fields = is_array($m->only_fields) ? $m->only_fields : []; + $fields = is_array($m->only_fields) && !empty($m->only_fields) ? $m->only_fields : array_keys($m->getFields()); // limit by apiFields if (isset($m->apiFields, $m->apiFields[$action])) { @@ -296,7 +327,7 @@ protected function successResponse($response) // for testing purposes there can be situations when emitter is disabled. then do nothing. if ($this->emitter) { $this->emitter->emit($this->response); - exit; + exit; // @todo find a solution to remove this exit. } // @todo Should we also stop script execution if no emitter is defined or just ignore that? @@ -394,6 +425,7 @@ public function rest($pattern, $model = null, $methods = ['read', 'modify', 'del // GET all records if (in_array('read', $methods)) { $f = function (...$params) use ($model) { + $this->single_record = false; if (is_callable($model)) { $model = $this->call($model, $params); } @@ -416,8 +448,9 @@ public function rest($pattern, $model = null, $methods = ['read', 'modify', 'del $model->onlyFields($this->getAllowedFields($model, 'read')); // load model and get field values - return $this->loadModelByValue($model, $id)->get(); + return $this->loadModelByValue($model, $id); }; + $this->get($pattern.'/:id', $f); } @@ -437,7 +470,7 @@ public function rest($pattern, $model = null, $methods = ['read', 'modify', 'del $this->loadModelByValue($model, $id)->save($this->request_data); $model->onlyFields($this->getAllowedFields($model, 'read')); - return $model->get(); + return $model; }; $this->patch($pattern.'/:id', $f); $this->post($pattern.'/:id', $f); @@ -457,7 +490,8 @@ public function rest($pattern, $model = null, $methods = ['read', 'modify', 'del $model->onlyFields($this->getAllowedFields($model, 'read')); $this->response_code = 201; // http code for created - return $model->get(); + + return $model; }; $this->post($pattern, $f); } diff --git a/tests/ApiTesterRestTest.php b/tests/ApiTesterRestTest.php index b86bd55..2cfa290 100644 --- a/tests/ApiTesterRestTest.php +++ b/tests/ApiTesterRestTest.php @@ -12,32 +12,36 @@ class ApiTesterRestTest extends \atk4\core\PHPUnit_AgileTestCase /** @var Persistence\SQL */ protected $db; - /** @var Country */ - protected $model; - /** @var Api */ - private $api; + protected $api; + + protected static $init = false; public static function tearDownAfterClass() { - unlink('./sqlite.db'); + unlink(__DIR__.'/sqlite.db'); } public function setUp() { parent::setUp(); - touch('./sqlite.db'); - - $this->db = new Persistence\SQL('sqlite:./sqlite.db'); - $this->model = new Country($this->db); - Migration::of($this->model)->run(); + $filename = __DIR__.'/sqlite.db'; + touch($filename); + + $this->db = new Persistence\SQL('sqlite:'.$filename); + if (!self::$init) { + self::$init = true; + Migration::of(new Country($this->db))->run(); + } } - public function processRequest(Request $request) + public function processRequest(Request $request, $model = null) { + $model_default = new Country($this->db); + $this->api = new Api($request); $this->api->emitter = false; - $this->api->rest('/client', $this->model); + $this->api->rest('/client', $model ?? $model_default); return json_decode($this->api->response->getBody()->getContents(), true); } @@ -51,6 +55,9 @@ public function testCreate() 'iso3' => 'ITA', 'numcode' => '666', 'phonecode' => '39', + 'date' => null, + 'datetime' => null, + 'time' => null, ]; $request = new Request( @@ -68,41 +75,61 @@ public function testCreate() $this->assertEquals([ 'id' => '1', 'name' => 'test', - 'sys_name' => 'test', + 'nicename' => 'test', 'iso' => 'IT', 'iso3' => 'ITA', 'numcode' => '666', 'phonecode' => '39', + 'date' => null, + 'datetime' => null, + 'time' => null, ], $response); } - public function testGETOne() + public function testCreate2() { + $data = [ + 'name' => 'test2', + 'sys_name' => 'test2', + 'iso' => 'DE', + 'iso3' => 'DEU', + 'numcode' => '999', + 'phonecode' => '43', + 'date' => null, + 'datetime' => null, + 'time' => null, + ]; + $request = new Request( - 'http://localhost/client/1', - 'GET', + 'http://localhost/client', + 'POST', 'php://memory', [ 'Content-Type' => 'application/json', ] ); + $request->getBody()->write(json_encode($data)); $response = $this->processRequest($request); + $this->assertEquals(201, $this->api->response_code); $this->assertEquals([ - 'id' => '1', - 'name' => 'test', - 'sys_name' => 'test', - 'iso' => 'IT', - 'iso3' => 'ITA', - 'numcode' => '666', - 'phonecode' => '39', + 'id' => '2', + 'name' => 'test2', + 'nicename' => 'test2', + 'iso' => 'DE', + 'iso3' => 'DEU', + 'numcode' => '999', + 'phonecode' => '43', + 'date' => null, + 'datetime' => null, + 'time' => null, ], $response); } - public function testGETOneByField() + public function testGETOne() { $request = new Request( - 'http://localhost/client/name:test', + 'http://localhost/client/1', 'GET', 'php://memory', [ @@ -114,11 +141,14 @@ public function testGETOneByField() $this->assertEquals([ 'id' => '1', 'name' => 'test', - 'sys_name' => 'test', + 'nicename' => 'test', 'iso' => 'IT', 'iso3' => 'ITA', 'numcode' => '666', 'phonecode' => '39', + 'date' => null, + 'datetime' => null, + 'time' => null, ], $response); } @@ -135,7 +165,7 @@ public function testGETAll() $response = $this->processRequest($request); $this->assertEquals([ - 0 => [ + [ 'id' => '1', 'name' => 'test', 'nicename' => 'test', @@ -143,6 +173,21 @@ public function testGETAll() 'iso3' => 'ITA', 'numcode' => '666', 'phonecode' => '39', + 'date' => null, + 'datetime' => null, + 'time' => null, + ], + [ + 'id' => '2', + 'name' => 'test2', + 'nicename' => 'test2', + 'iso' => 'DE', + 'iso3' => 'DEU', + 'numcode' => '999', + 'phonecode' => '43', + 'date' => null, + 'datetime' => null, + 'time' => null, ], ], $response); } @@ -159,7 +204,9 @@ public function testModify() ); $data = $this->processRequest($request); - $data['name'] = 'test modified'; + $data['sys_name'] = 'test modified'; + $data['name'] = $data['nicename']; + unset($data['nicename']); $request = $request->withMethod('POST'); $request->getBody()->write(json_encode($data)); @@ -168,11 +215,14 @@ public function testModify() $this->assertEquals([ 'id' => '1', 'name' => 'test modified', - 'sys_name' => 'test', + 'nicename' => 'test', 'iso' => 'IT', 'iso3' => 'ITA', 'numcode' => '666', 'phonecode' => '39', + 'date' => null, + 'datetime' => null, + 'time' => null, ], $response); } @@ -189,39 +239,96 @@ public function testDelete() ] ); - $this->processRequest($request); + $response = $this->processRequest($request); + $this->assertEquals([], $response); + } + + public function testSerialization() + { + $date = $datetime = new \Datetime(); + $date->setTime(6, 6, 6); // this is for you imanst ;) + + $data = [ + 'sys_name' => 'test_time', + 'name' => 'test', + 'iso' => 'IT', + 'iso3' => 'ITA', + 'numcode' => '666', + 'phonecode' => '39', + 'date' => $date->format('Y-m-d'), + 'datetime' => $date->format('Y-m-d H:i:s'), + 'time' => $date->format('H:i:s'), + ]; - // check via getAll + // create new record $request = new Request( 'http://localhost/client', - 'GET', + 'POST', 'php://memory', [ 'Content-Type' => 'application/json', ] ); + $request->getBody()->write(json_encode($data)); + $response = $this->processRequest($request); + // check on post + $this->assertEquals([ + 'id' => '3', + 'name' => 'test_time', + 'nicename' => 'test', + 'iso' => 'IT', + 'iso3' => 'ITA', + 'numcode' => '666', + 'phonecode' => '39', + 'date' => $date->format('Y-m-d'), + 'datetime' => $date->format('Y-m-d\TH:i:sP'), + 'time' => $date->format('H:i:s'), + ], $response); + + // retrive created record + $request = new Request( + 'http://localhost/client/sys_name:test_time', + 'GET', + 'php://memory', + [ + 'Content-Type' => 'application/json', + ] + ); $response = $this->processRequest($request); - $this->assertEquals([], $response); + + // check on get + $this->assertEquals([ + 'id' => '3', + 'name' => 'test_time', + 'nicename' => 'test', + 'iso' => 'IT', + 'iso3' => 'ITA', + 'numcode' => '666', + 'phonecode' => '39', + 'date' => $date->format('Y-m-d'), + 'datetime' => $date->format('Y-m-d\TH:i:sP'), + 'time' => $date->format('H:i:s'), + ], $response); } public function testOnlyApiFields() { - $this->model->apiFields = [ + $model = new Country($this->db); + $model->apiFields = [ 'read' => [ - 'name', + 'sys_name', 'iso', + 'iso3', 'numcode', ], ]; $data = [ - 'name' => 'test', - 'sys_name' => 'test', - 'iso' => 'IT', - 'iso3' => 'ITA', - 'numcode' => '666', - 'phonecode' => '39', + 'name' => 'USA', + 'iso' => 'US', + 'iso3' => 'USA', + 'numcode' => '999', ]; $request = new Request( @@ -234,12 +341,13 @@ public function testOnlyApiFields() ); $request->getBody()->write(json_encode($data)); - $response = $this->processRequest($request); + $response = $this->processRequest($request, $model); $this->assertEquals(201, $this->api->response_code); $this->assertEquals([ - 'name' => 'test', - 'iso' => 'IT', - 'numcode' => '666', + 'name' => 'USA', + 'iso' => 'US', + 'iso3' => 'USA', + 'numcode' => '999', ], $response); } } @@ -275,6 +383,10 @@ public function init() $this->addField('numcode', ['caption'=>'ISO Numeric Code', 'type'=>'number', 'required'=>true]); $this->addField('phonecode', ['caption'=>'Phone Prefix', 'type'=>'number']); + $this->addField('date', ['caption'=>'Test Datetime', 'type'=>'date']); + $this->addField('datetime', ['caption'=>'Test Datetime', 'type'=>'datetime']); + $this->addField('time', ['caption'=>'Test Datetime', 'type'=>'time']); + $this->onHook('beforeSave', function ($m) { if (!$m['sys_name']) { $m['sys_name'] = strtoupper($m['name']);