CodeIgniter4でREST APIを作成する

この記事は CodeIgniter Advent Calendar 2020 - Qiita の20日目です。まだ、空きがありますので、興味のある方は気軽に参加してください。

今日は、CodeIgniter4 で REST API を作成してみます。

動作確認環境

  • CodeIgniter 4.0.5 開発版
  • PHP 7.4.13
  • MySQL 5.7.32
  • macOS 10.15.7

MySQL データベースの準備

データベースとユーザを作成します。公式チュートリアル用に作成したもの と同じです。

$ mysql -uroot -p

開発用データベース

開発用データベース ci4tutorial を作成します。

CREATE DATABASE ci4tutorial DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

ユーザ dbuser を作成します。

GRANT ALL PRIVILEGES ON ci4tutorial.* TO dbuser@localhost IDENTIFIED BY 'dbpassword';

テスト用データベース

テスト用データベース ci4tutorial_test も用意しておきましょう。

CREATE DATABASE ci4tutorial_test DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

データベースユーザ dbuser に権限を付与します。

GRANT ALL PRIVILEGES ON ci4tutorial_test.* TO dbuser@localhost;

作成したユーザで MySQL にログインできることを確認します。

$ mysql -u dbuser -p ci4tutorial
Enter password: 

テーブル作成と初期データの挿入

テーブル news を作成します。

以下の記事に従い、マイグレーションファイルを作成し、実行します。

$ php spark migrate

作成したテーブルに初期データを挿入します。

以下の記事に従い、シーダーファイルを作成し、実行します。

$ php spark db:seed NewsSeeder

これで、テーブル news に以下のデータが挿入されました。

URI 設計

URI とコントローラの対応は以下のようにします。 これは、CodeIgniter4 の Resource Routes を使うと自動的に設定されるルート(の一部)です。

+--------+---------------+--------------------------------------+
| Method | Route         | Handler                              |
+--------+---------------+--------------------------------------+
| GET    | api/news      | \App\Controllers\Api\News::index     |
| GET    | api/news/(.*) | \App\Controllers\Api\News::show/$1   |
| POST   | api/news      | \App\Controllers\Api\News::create    |
| PATCH  | api/news/(.*) | \App\Controllers\Api\News::update/$1 |
| PUT    | api/news/(.*) | \App\Controllers\Api\News::update/$1 |
| DELETE | api/news/(.*) | \App\Controllers\Api\News::delete/$1 |
+--------+---------------+--------------------------------------+

なお、今回は PATCH メソッドは実装しません。

ルーティング設定

ルーティング設定ファイルに以下を追加します。これで上記のようにルーティングされます。

--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -32,6 +32,11 @@ $routes->setAutoRoute(true);
 // route since we don't have to scan directories.
 $routes->get('/', 'Home::index');

+$routes->resource(
+    'api/news',
+    ['only' => ['index', 'show', 'create', 'update', 'delete']]
+);
+
 $routes->match(['get', 'post'], 'news/create', 'News::create');
 $routes->get('news/(:segment)', 'News::view/$1');
 $routes->get('news', 'News::index');

PATCH メソッドは実装しないですが、$routes->resource() でルートを設定しないようにする方法はありませんでした。

どうしてもルート設定したくない場合は、$routes->resource() は使わずに、個別にルートを 1つずつ設定すれば可能です。

フィルタの設定

CSRF フィルタが有効になっている場合は、API の URI 以下だけ除外しておきます。

--- a/app/Config/Filters.php
+++ b/app/Config/Filters.php
@@ -16,7 +16,7 @@ class Filters extends BaseConfig
    public $globals = [
        'before' => [
            //'honeypot'
-           'csrf',
+           'csrf' => ['except' => 'api/*'],
        ],
        'after'  => [
            'toolbar',

なお、本番環境では CSRF 対策が必要になる場合もあります。

CodeIgniter4 の CSRF フィルタは POST メソッドしかフィルタしませんので、必要な場合は独自に CSRF フィルタを実装してください。

NewsModel の作成

モデルは公式チュートリアルの NewsModel そのままで OK です。 getNews() メソッドが公式チュートリアルにはありますが、なくても構いません。

app/Models/NewsModel.php

<?php
namespace App\Models;

use CodeIgniter\Model;

class NewsModel extends Model
{
    protected $table = 'news';

    protected $allowedFields = ['title', 'slug', 'body'];

    public function getNews($slug = false)
    {
        if ($slug === false)
        {
            return $this->findAll();
        }

        return $this->asArray()
            ->where(['slug' => $slug])
            ->first();
    }
}

$table にこのモデルが操作するテーブル名を、$allowedFields にこのモデルから変更可能なカラム名を設定します。

News コントローラの作成

News コントローラは CodeIgniter4 の ResourceController を継承し作成します。 以下のメソッドが必要になります。

app/Controllers/Api/News.php

<?php

namespace App\Controllers\Api;

use CodeIgniter\RESTful\ResourceController;
use App\Models\NewsModel;

class News extends ResourceController
{
    protected $modelName = NewsModel::class;
    protected $format = 'json';

    /**
     * @var NewsModel
     */
    protected $model;

    /**
     * GET api/news
     */
    public function index()
    {
    }

    /**
     * GET api/news/{id}
     */
    public function show($id = null)
    {
    }

    /**
     * POST api/news
     */
    public function create()
    {
    }

    /**
     * PUT api/news/{id}
     */
    public function update($id = null)
    {
    }

    /**
     * DELETE api/news/{id}
     */
    public function delete($id = null)
    {
    }
}

$modelName に使用するモデルのクラス名を、$format に出力フォーマットを設定します。 今回は JSON で出力します。

GET api/news

全ての記事を取得する API です。

News コントローラに以下の index() メソッドを追加します。

    /**
     * GET api/news
     *
     * @return mixed
     */
    public function index()
    {
        $response = [
            'status' => 200,
            'error' => null,
            'news' => $this->model->findAll(),
        ];

        return $this->respond($response);
    }

http://localhost:8080/api/news に GET リクエストを送信してみましょう。

ここでは Postman を使います。

全ての記事が JSON で返りました。

GET api/news/{id}

指定の記事を取得する API です。

News コントローラに以下の show() メソッドを追加します。

    /**
     * GET api/news/{id}
     *
     * @param string|null $id
     * @return mixed
     */
    public function show($id = null)
    {
        if ($id === null) {
            return $this->failNotFound('No news found');
        }

        $news = $this->model->find($id);
        if ($news === null) {
            return $this->failNotFound('No news found');
        }

        $response = [
            'status' => 200,
            'error' => null,
            'news' => $news,
        ];

        return $this->respond($response);
    }

(2022-12-30 追記) $id が null の場合のエラー処理を追加しました。

http://localhost:8080/api/news/1 に GET リクエストを送信してみましょう。

指定した記事が JSON で返りました。

存在しない ID を指定すると、404 が返ります。

POST api/news

記事を新規作成する API です。

News コントローラに以下の create() メソッドを追加します。 データは Form で送信される前提のコードです。

    /**
     * POST api/news
     *
     * @return mixed
     * @throws \ReflectionException
     */
    public function create()
    {
        if (!$this->validate(
            [
                'title' => 'required|min_length[3]|max_length[255]',
                'body' => 'required',
            ]
        )) {
            $errors = implode(' ', $this->validator->getErrors());

            return $this->failValidationError($errors);
        }

        $this->model->insert(
            [
                'title' => $this->request->getPost('title'),
                'slug' => url_title(
                    $this->request->getPost('title'),
                    '-',
                    true
                ),
                'body' => $this->request->getPost('body'),
            ]
        );

        $response = [
            'status' => $this->codes['created'],
            'error' => null,
            'messages' => [
                'success' => 'News successfully created',
            ]
        ];

        return $this->respondCreated($response);
    }

http://localhost:8080/api/news に POST リクエストを送信してみましょう。

記事が作成されました。

PUT api/news/{id}

記事を更新する API です。

News コントローラに以下の update() メソッドを追加します。 POST と同じくデータは Form で送信される前提のコードです。

    /**
     * PUT api/news/{id}
     *
     * @param string|null $id
     * @return mixed
     * @throws \ReflectionException
     */
    public function update($id = null)
    {
        if ($this->request->getMethod() === 'patch') {
            return $this->fail('PATCH is not implemented');
        }

        if ($id === null) {
            return $this->failNotFound('No news found');
        }

        if (!$this->validate(
            [
                'title' => 'required|min_length[3]|max_length[255]',
                'body' => 'required',
            ]
        )) {
            $errors = implode(' ', $this->validator->getErrors());

            return $this->failValidationError($errors);
        }

        $news = $this->model->find($id);
        if ($news === null) {
            return $this->failNotFound('No news found');
        }

        $input = $this->request->getRawInput();
        $data = [
            'title' => $input['title'],
            'slug' => url_title(
                $input['title'],
                '-',
                true
            ),
            'body' => $input['body']
        ];
        $this->model->update($id, $data);

        $response = [
            'status' => $this->codes['updated'],
            'error' => null,
            'messages' => [
                'success' => 'News successfully updated',
            ]
        ];

        return $this->respond($response);
    }

(2022-12-30 追記) $id が null の場合のエラー処理を追加しました。

http://localhost:8080/api/news/4 に PUT リクエストを送信してみましょう。

記事が更新されました。

DELETE api/news/{id}

記事を削除する API です。

News コントローラに以下の delete() メソッドを追加します。

    /**
     * DELETE api/news/{id}
     *
     * @param string|null $id
     * @return mixed
     */
    public function delete($id = null)
    {
        if ($id === null) {
            return $this->failNotFound('No news found');
        }

        $news = $this->model->find($id);
        if ($news === null) {
            return $this->failNotFound('No news found');
        }

        $this->model->where('id', $id)->delete($id);

        $response = [
            'status' => $this->codes['deleted'],
            'error' => null,
            'messages' => [
                'success' => 'News successfully deleted',
            ]
        ];

        return $this->respondDeleted($response);
    }

(2022-12-30 追記) $id が null の場合のエラー処理を追加しました。

http://localhost:8080/api/news/4 に DELETE リクエストを送信してみましょう。

記事が削除されました。

これでニュース記事の CRUD が一通り作成できました。

CodeIgniter4のREST APIのテストを書く へ続きます。

この記事は CodeIgniter Advent Calendar 2020 - Qiita の20日目です。まだ、空きがありますので、興味のある方は気軽に参加してください。

参考

Date: 2020/12/20

Tags: codeigniter, codeigniter4, database, rest