Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Implementing an API 
in CakePHP

Implementing an API 
in CakePHP

You know the theory of RESTful APIs. But how does it translate to code? Join me as I walk you through the fundamentals of designing and implementing an API in CakePHP. I'll cover input validation, data filtering, internationalization, output and automated testing.

Anna Filina

May 28, 2016
Tweet

More Decks by Anna Filina

Other Decks in Programming

Transcript

  1. Plan • Basic API in a controller. • Refactor &

    generalize. • Validation & tests. 4
  2. Setup class GamesController extends AppController { public function beforeRender(Event $event)

    { $this->RequestHandler->renderAs($this, 'json'); $this->response->type('application/json'); } } 8
  3. Controller public function getList() { $gamesTable = TableRegistry::get('Games'); $games =

    $gamesTable ->find() ->hydrate(false) ->toList(); $response = [ 'data' => $games, ]; $this->set('response', $response); $this->set('_serialize', ['response']); } 9
  4. Search By Title $gamesTable = TableRegistry::get('Games'); $query = $gamesTable ->find()

    ->hydrate(false); $title = $this->request->query('title'); if ($title != null) { $query->where(['title LIKE' => '%'.$title.'%']); } $games = $query->toList(); 10
  5. Simplify Controller public function getList() { $apiCriteria = new ApiCriteria($this->request);

    $gamesTable = TableRegistry::get('Games'); $response = $gamesTable->getApiList($apiCriteria); $this->set('response', $response); $this->set('_serialize', ['response']); } 15
  6. Criteria class ApiCriteria { use AccessorTrait; private $userFilters = [];

    private $systemFilters = []; private $sort = []; private $pageSize = 10; private $pageNumber = 1; public function getFilters() { return $this->userFilters + $this->systemFilters; } } 16
  7. ApiCriteria public function __construct(Request $request) { foreach ($request->query as $param

    => $value) { switch ($param) { case 'pageSize': $this->pageSize = $value; break; case 'pageNumber': $this->pageNumber = $value; break; case 'sort': $this->sort = $value; break; default: $this->userFilters[$param] = $value; } } } 17
  8. Table class GamesTable extends Table { public function getApiList(ApiCriteria 


    $apiCriteria, $hydrate = false) { $this->alias('root'); $query = $this ->find('all') ->hydrate($hydrate) ->select(['root.id', 'root.title') ; $apiQuery = new ApiQuery($this, $query, $apiCriteria); $results = $apiQuery->getList(); return $results; } } 19
  9. ApiQuery public function addFilterCriteria(Query $query) { foreach ($this->apiCriteria->filters as $name

    => $value) { if ($value === '') { continue; } $this->table->{'add'.ucfirst($name).'Filter'}
 ($query, $value); } } public function addPageCriteria(Query $query) { $query->limit($this->apiCriteria->pageSize); $query->page($this->apiCriteria->pageNumber); } 21
  10. Table class GamesTable extends Table { public function addTitleFilter(Query $query,

    $value) { $query->andWhere(['root.title LIKE' => '%'.$value.'%']); } public function addTitleSort(Query $query, $order) { $query->order('root.title', $order=='-' ? 'DESC':'ASC'); } } 22
  11. Sorting // sort=title,-publisher case 'sort': $parts = explode(',', $value); foreach

    ($parts as $part) { preg_match_all('/^(\-)?([\w]+)$/', $part, $matches); if (empty($matches[0])) { throw new \Exception('Invalid sort format.', 1); } $name = $matches[2][0]; $order = $matches[1][0]; $this->sort[$name] = $order; } break; 23
  12. ApiQuery • Total results: only filters. • Limit + joins

    issue: select distinct ids first. • Items query: only filters by ids & sort. 26
  13. Count Query public function getList() { $this->table->alias('root'); $countQuery = $this->table

    ->query() ->hydrate(false) ->distinct(true) ->select(['count' => $this->query ->func()->count('root.id')]); $this->addFilterCriteria($countQuery); $count = $countQuery->toList()[0]['count']; if ($count == 0) { return [ 'data' => [], 'meta' => ['count' => 0, 'pages' => 0], ]; } } 28
  14. Ids Query $idsQuery = $this->table ->query() ->hydrate(false) ->distinct(true) ->select(['root.id']) ;

    $this->addFilterCriteria($idsQuery); $this->addSortCriteria($idsQuery); $this->addPageCriteria($idsQuery); $ids = array_map(function($item) { return $item['id']; }, $idsQuery->toList()); 29
  15. Item Query $itemQuery = $this->query ->where(['root.id' => $ids], ['root.id' =>

    'integer[]']) ; $this->addSortCriteria($itemQuery); $results = $itemQuery->toList(); return [ 'data' => $results, 'meta' => [ 'count' => $count, 'pages' => ceil($count/$this->apiCriteria->pageSize), ], ]; 30
  16. Output public function getList() { $apiCriteria = new ApiCriteria($this->request); $gamesTable

    = TableRegistry::get('Games'); $response = $gamesTable->getApiList($apiCriteria); $this->set('response', $response); $this->set('_serialize', ['response']); } 31
  17. How Much Code? • ApiCriteria: 22 lines, 2 methods •

    ApiQuery: 30 lines, 5 methods • GamesTable: 11 lines, 3 methods • GamesController: 26 lines, 2 methods • Total: 77 lines, 1.93 cyclomatic complexity 34
  18. Controller public function getItem($id) { $apiCriteria = new ApiCriteria($this->request); $apiCriteria->addUserFilter('id',

    $id); $gamesTable = TableRegistry::get('Games'); $response = $gamesTable->getApiItem($apiCriteria); $this->set('response', $response); $this->set('_serialize', ['response']); } 38
  19. Table public function getApiItem(ApiCriteria $apiCriteria, $hydrate = false) { $this->alias('root');

    $query = $this ->find('all') ->hydrate($hydrate) ->select(['root.id', 'root.title', 'root.image_path']) ; $apiQuery = new ApiQuery($this, $query, $apiCriteria); $results = $apiQuery->getItem(); return $results; } public function addIdFilter(Query $query, $value) { $query->andWhere(['root.id' => $value]); } 39
  20. ApiQuery public function getItem() { $itemQuery = $this->query; $this->addFilterCriteria($itemQuery); $results

    = $itemQuery->toList(); $data = null; if (count($results) == 1) { $data = $results[0]; } return [ 'data' => $data, 'meta' => [], ]; } 40
  21. Controller public function editItem($id) { $apiCriteria = new ApiCriteria($this->request); $apiCriteria->addUserFilter('id',

    $id); $apiInput = new ApiInput($this->request); $gamesTable = TableRegistry::get('Games'); $game = $gamesTable->getApiItem($apiCriteria, true)['data']; $response = $gamesTable->saveApiItem($apiInput, $game, false); if (isset($response['errors'])) { $this->response->statusCode(400); } $this->set('response', $response); $this->set('_serialize', ['response']); } 44
  22. Table public function saveApiItem(ApiInput $apiInput, Entity $item = null, $newRecord

    = true) { if ($newRecord) { $item = $this->newEntity($apiInput->data); } else { $item = $this->patchEntity($item, $apiInput->data); } $errors = $item->errors(); if (count($errors) > 0) { return [ 'errors' => $errors, ]; } $this->save($item); return [ 'data' => $item->toArray(), ]; } 45
  23. Table public function validationDefault(Validator $validator) { $validator ->requirePresence('title') ->notEmpty('title', 'Cannot

    be empty') ->add('title', [ 'length' => [ 'rule' => ['minLength', 2], 'message' => 'Min 2 characters', ] ]) ; return $validator; } 46
  24. Test class GamesControllerTest extends \PHPUnit_Framework_TestCase { private $client; public function

    setUp() { $this->client = new Client([ 'base_uri' => 'http://cakeapi.dev', 'timeout' => 2.0, ]); } } 48
  25. Test public function testEdit_WithCover_ReturnsSuccess() { $file = file_get_contents(__DIR__.'/bf4.png'); $response =

    $this->client->request('PATCH', '/games/2', [ 'http_errors' => false, 'json' => ['cover' => base64_encode($file)] ]); $this->assertEquals(200, $response->getStatusCode()); } 49
  26. Takeaways • No need for views, just serialize to JSON.

    • Input JSON data in the request body. • Keep all the filter and sort logic in the table. • Keep controllers lean, offload work to reusable classes. • Simple acceptance tests for API endpoints. • Unit tests for domain logic. 52
  27. Anna Filina • Development. • Fix bugs & performance issues.

    • Workshops on testing, frameworks & APIs. • Advisor on testing strategy, legacy code. 53