CodeIgniter4のCodeIgniter\Model (3)モデル内でのバリデーション

CodeIgniter4の CodeIgniter\Model でのバリデーションを使ってみましょう。

CodeIgniter4のチュートリアルのコードをモデル内でのバリデーションを使うように変更します。

NewsModelの変更

CodeIgniter\Model$validationRules プロパティにバリデーションルールを設定します。

--- a/app/Models/NewsModel.php
+++ b/app/Models/NewsModel.php
@@ -10,6 +10,11 @@ class NewsModel extends Model

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

+    protected $validationRules = [
+        'title' => 'required|min_length[3]|max_length[255]',
+        'body'  => 'required',
+    ];
+
     public function getNews($slug = false)
     {
         if ($slug === false) {

これで、保存時(insert(), update(), save())にバリデーションが自動的に実行されます。

Newsコントローラの変更

動作を確認するために、コントローラでのバリデーションは削除してみましょう。

$this->validate() を削除します。

--- a/app/Controllers/News.php
+++ b/app/Controllers/News.php
@@ -41,17 +41,19 @@ class News extends BaseController
     {
         $model = model(NewsModel::class);

-        if ($this->request->getMethod() === 'post' && $this->validate([
-            'title' => 'required|min_length[3]|max_length[255]',
-            'body'  => 'required',
-        ])) {
-            $model->save([
+        if ($this->request->getMethod() === 'post') {
+            $data = [
                 'title' => $this->request->getPost('title'),
                 'slug'  => url_title($this->request->getPost('title'), '-', true),
                 'body'  => $this->request->getPost('body'),
-            ]);
-
-            echo view('news/success');
+            ];
+            if ($model->save($data) === false) {
+                echo view('templates/header', ['title' => 'Create a news item']);
+                echo view('news/create', ['errors' => $model->errors()]);
+                echo view('templates/footer');
+            } else {
+                echo view('news/success');
+            }
         } else {
             echo view('templates/header', ['title' => 'Create a news item']);
             echo view('news/create');

create() メソッドは以下のようになります。

    public function create()
    {
        $model = model(NewsModel::class);

        if ($this->request->getMethod() === 'post') {
            $data = [
                'title' => $this->request->getPost('title'),
                'slug'  => url_title($this->request->getPost('title'), '-', true),
                'body'  => $this->request->getPost('body'),
            ];
            if ($model->save($data) === false) {
                echo view('templates/header', ['title' => 'Create a news item']);
                echo view('news/create', ['errors' => $model->errors()]);
                echo view('templates/footer');
            } else {
                echo view('news/success');
            }
        } else {
            echo view('templates/header', ['title' => 'Create a news item']);
            echo view('news/create');
            echo view('templates/footer');
        }
    }

検証に失敗すると、$model->save($data) が false を返します。

$model->errors() でエラーメッセージを取得できます。

ビューの変更

コントローラから渡された $errors を表示するように変更します。

--- a/app/Views/news/create.php
+++ b/app/Views/news/create.php
@@ -1,7 +1,13 @@
 <h2><?= esc($title) ?></h2>

 <?= session()->getFlashdata('error') ?>
-<?= service('validation')->listErrors() ?>
+<?php if (! empty($errors)): ?>
+    <div class="alert alert-danger">
+        <?php foreach ($errors as $field => $error): ?>
+            <p><?= $error ?></p>
+        <?php endforeach ?>
+    </div>
+<?php endif ?>

 <form action="/news/create" method="post">
     <?= csrf_field() ?>

これで、バリデーションエラーが表示されます。

$error をエスケープしていないことに注目してください。

これは、HTMLタグが使えるということと、エラーメッセージを動的に組み立てるとXSSが可能になるかも知れないということです。

ユーザーが変更可能な文字列を使い、エラーメッセージを動的に組み立てることはしないようにしてください。

バリデーションエラーの表示確認

記事作成フォームに何も入力せずに「Create news item」ボタンを押します。

バリデーションエラーが表示されました。

エラーメッセージの変更

エラーメッセージを変更してみましょう。

CodeIgniter\Model$validationMessages プロパティにエラーメッセージを設定します。

--- a/app/Models/NewsModel.php
+++ b/app/Models/NewsModel.php
@@ -15,6 +15,12 @@ class NewsModel extends Model
         'body'  => 'required',
     ];

+    protected $validationMessages = [
+        'title'        => [
+            'required' => 'タイトルは<strong style="color:#ff0000;">必須</strong>です。',
+        ],
+    ];
+
     public function getNews($slug = false)
     {
         if ($slug === false) {

エラーメッセージが変更されました。

どちらでバリデーションすべきか?

コントローラでのバリデーションとモデルでのバリデーション、どちらを使うべきでしょうか?

これは、どちらかを選ぶというものではありません。これらは用途が違います。

基本的にコントローラでは入力データについて必ずバリデーションをしてください。 外部からのデータをバリデーションなしで使うことは危険です。

モデルでのバリデーションはデータベースへの保存時におかしな値が保存されないようにするためのものです。 必要があれば、設定してください。

(2022-12-05 追記) モデルでのバリデーションは基本的にはお薦めしません。 「CodeIgniter4のバリデーション」 を参照してください。

参考

Tags: codeigniter, codeigniter4, database, validation

CodeIgniter4のCodeIgniter\Model (2)Entityクラスの利用

CodeIgniter4の CodeIgniter\Model でEntityクラスを使ってみましょう。

Entityクラスとは?

Entityクラスは、1つのテーブルのレコードを表すクラスです。POPOではありません。

テーブルのカラムを表す(マジック)プロパティを持ち、様々なデータをキャストするハンドラーを内蔵しています。

Entityクラスには、そのオブジェクトのビジネスロジックを実装するためのメソッドを追加します。

Entityクラスは自分自身を永続化する方法については何も知りません。永続化はモデルクラスが行います。

Entityの作成

CodeIgniter\Entity\Entity を継承したEntityクラスを作成します。

app/Entitles/News.php

<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;

class News extends Entity
{
}

これだけで動作します。

Entityの利用

データの検索

モデルクラスの $returnType にEntityクラス名の指定します。

--- a/app/Models/NewsModel.php
+++ b/app/Models/NewsModel.php
@@ -2,6 +2,7 @@

 namespace App\Models;

+use App\Entities\News;
 use CodeIgniter\Model;

 class NewsModel extends Model
@@ -10,6 +11,8 @@ class NewsModel extends Model

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

+    protected $returnType = News::class;
+
     public function getNews($slug = false)
     {
         if ($slug === false) {

これで、検索結果が配列から、Entityクラスのインスタンスに変わります。

$newsItem = $newsModel->find(1);
object(App\Entities\News)#78 (8) {
  ["datamap":protected]=>
  array(0) {
  }
  ["dates":protected]=>
  array(3) {
    [0]=>
    string(10) "created_at"
    [1]=>
    string(10) "updated_at"
    [2]=>
    string(10) "deleted_at"
  }
  ["casts":protected]=>
  array(0) {
  }
  ["castHandlers":protected]=>
  array(0) {
  }
  ["defaultCastHandlers":"CodeIgniter\Entity\Entity":private]=>
  array(14) {
    ["array"]=>
    string(33) "CodeIgniter\Entity\Cast\ArrayCast"
    ["bool"]=>
    string(35) "CodeIgniter\Entity\Cast\BooleanCast"
    ["boolean"]=>
    string(35) "CodeIgniter\Entity\Cast\BooleanCast"
    ["csv"]=>
    string(31) "CodeIgniter\Entity\Cast\CSVCast"
    ["datetime"]=>
    string(36) "CodeIgniter\Entity\Cast\DatetimeCast"
    ["double"]=>
    string(33) "CodeIgniter\Entity\Cast\FloatCast"
    ["float"]=>
    string(33) "CodeIgniter\Entity\Cast\FloatCast"
    ["int"]=>
    string(35) "CodeIgniter\Entity\Cast\IntegerCast"
    ["integer"]=>
    string(35) "CodeIgniter\Entity\Cast\IntegerCast"
    ["json"]=>
    string(32) "CodeIgniter\Entity\Cast\JsonCast"
    ["object"]=>
    string(34) "CodeIgniter\Entity\Cast\ObjectCast"
    ["string"]=>
    string(34) "CodeIgniter\Entity\Cast\StringCast"
    ["timestamp"]=>
    string(37) "CodeIgniter\Entity\Cast\TimestampCast"
    ["uri"]=>
    string(31) "CodeIgniter\Entity\Cast\URICast"
  }
  ["attributes":protected]=>
  array(4) {
    ["id"]=>
    string(1) "1"
    ["title"]=>
    string(13) "Elvis sighted"
    ["slug"]=>
    string(13) "elvis-sighted"
    ["body"]=>
    string(95) "Elvis was sighted at the Podunk internet cafe. It looked like he was writing a CodeIgniter app."
  }
  ["original":protected]=>
  array(4) {
    ["id"]=>
    string(1) "1"
    ["title"]=>
    string(13) "Elvis sighted"
    ["slug"]=>
    string(13) "elvis-sighted"
    ["body"]=>
    string(95) "Elvis was sighted at the Podunk internet cafe. It looked like he was writing a CodeIgniter app."
  }
  ["_cast":"CodeIgniter\Entity\Entity":private]=>
  bool(true)
}

$newsItem->titletitle カラムの値を取得できます。

複数レコードの場合は、オブジェクトの配列が返ります。

$newsItems = $newsModel->find([1, 2]);
array(2) {
  [0]=>
  object(App\Entities\News)#79 (8) {
    ["datamap":protected]=>
    array(0) {
    }
    ["dates":protected]=>
    array(3) {
      [0]=>
      string(10) "created_at"
      [1]=>
      string(10) "updated_at"
      [2]=>
      string(10) "deleted_at"
    }
    ["casts":protected]=>
    array(0) {
    }
    ["castHandlers":protected]=>
    array(0) {
    }
    ["defaultCastHandlers":"CodeIgniter\Entity\Entity":private]=>
    array(14) {
      ["array"]=>
      string(33) "CodeIgniter\Entity\Cast\ArrayCast"
      ["bool"]=>
      string(35) "CodeIgniter\Entity\Cast\BooleanCast"
      ["boolean"]=>
      string(35) "CodeIgniter\Entity\Cast\BooleanCast"
      ["csv"]=>
      string(31) "CodeIgniter\Entity\Cast\CSVCast"
      ["datetime"]=>
      string(36) "CodeIgniter\Entity\Cast\DatetimeCast"
      ["double"]=>
      string(33) "CodeIgniter\Entity\Cast\FloatCast"
      ["float"]=>
      string(33) "CodeIgniter\Entity\Cast\FloatCast"
      ["int"]=>
      string(35) "CodeIgniter\Entity\Cast\IntegerCast"
      ["integer"]=>
      string(35) "CodeIgniter\Entity\Cast\IntegerCast"
      ["json"]=>
      string(32) "CodeIgniter\Entity\Cast\JsonCast"
      ["object"]=>
      string(34) "CodeIgniter\Entity\Cast\ObjectCast"
      ["string"]=>
      string(34) "CodeIgniter\Entity\Cast\StringCast"
      ["timestamp"]=>
      string(37) "CodeIgniter\Entity\Cast\TimestampCast"
      ["uri"]=>
      string(31) "CodeIgniter\Entity\Cast\URICast"
    }
    ["attributes":protected]=>
    array(4) {
      ["id"]=>
      string(1) "1"
      ["title"]=>
      string(13) "Elvis sighted"
      ["slug"]=>
      string(13) "elvis-sighted"
      ["body"]=>
      string(95) "Elvis was sighted at the Podunk internet cafe. It looked like he was writing a CodeIgniter app."
    }
    ["original":protected]=>
    array(4) {
      ["id"]=>
      string(1) "1"
      ["title"]=>
      string(13) "Elvis sighted"
      ["slug"]=>
      string(13) "elvis-sighted"
      ["body"]=>
      string(95) "Elvis was sighted at the Podunk internet cafe. It looked like he was writing a CodeIgniter app."
    }
    ["_cast":"CodeIgniter\Entity\Entity":private]=>
    bool(true)
  }
  [1]=>
  object(App\Entities\News)#80 (8) {
    ["datamap":protected]=>
    array(0) {
    }
    ["dates":protected]=>
    array(3) {
      [0]=>
      string(10) "created_at"
      [1]=>
      string(10) "updated_at"
      [2]=>
      string(10) "deleted_at"
    }
    ["casts":protected]=>
    array(0) {
    }
    ["castHandlers":protected]=>
    array(0) {
    }
    ["defaultCastHandlers":"CodeIgniter\Entity\Entity":private]=>
    array(14) {
      ["array"]=>
      string(33) "CodeIgniter\Entity\Cast\ArrayCast"
      ["bool"]=>
      string(35) "CodeIgniter\Entity\Cast\BooleanCast"
      ["boolean"]=>
      string(35) "CodeIgniter\Entity\Cast\BooleanCast"
      ["csv"]=>
      string(31) "CodeIgniter\Entity\Cast\CSVCast"
      ["datetime"]=>
      string(36) "CodeIgniter\Entity\Cast\DatetimeCast"
      ["double"]=>
      string(33) "CodeIgniter\Entity\Cast\FloatCast"
      ["float"]=>
      string(33) "CodeIgniter\Entity\Cast\FloatCast"
      ["int"]=>
      string(35) "CodeIgniter\Entity\Cast\IntegerCast"
      ["integer"]=>
      string(35) "CodeIgniter\Entity\Cast\IntegerCast"
      ["json"]=>
      string(32) "CodeIgniter\Entity\Cast\JsonCast"
      ["object"]=>
      string(34) "CodeIgniter\Entity\Cast\ObjectCast"
      ["string"]=>
      string(34) "CodeIgniter\Entity\Cast\StringCast"
      ["timestamp"]=>
      string(37) "CodeIgniter\Entity\Cast\TimestampCast"
      ["uri"]=>
      string(31) "CodeIgniter\Entity\Cast\URICast"
    }
    ["attributes":protected]=>
    array(4) {
      ["id"]=>
      string(1) "2"
      ["title"]=>
      string(16) "Say it isn't so!"
      ["slug"]=>
      string(14) "say-it-isnt-so"
      ["body"]=>
      string(64) "Scientists conclude that some programmers have a sense of humor."
    }
    ["original":protected]=>
    array(4) {
      ["id"]=>
      string(1) "2"
      ["title"]=>
      string(16) "Say it isn't so!"
      ["slug"]=>
      string(14) "say-it-isnt-so"
      ["body"]=>
      string(64) "Scientists conclude that some programmers have a sense of humor."
    }
    ["_cast":"CodeIgniter\Entity\Entity":private]=>
    bool(true)
  }
}

データの保存

新規にレコードを保存したい場合は、以下のようなコードになります。

$model = model(NewsModel::class);

$news = new \App\Entities\News([
    'title' => $this->request->getPost('title'),
    'slug'  => url_title($this->request->getPost('title'), '-', true),
    'body'  => $this->request->getPost('body'),
]);
$model->save($news);

CodeIgniter4のCodeIgniter\Model (3)モデル内でのバリデーション へ続く。

参考

Tags: codeigniter, codeigniter4, database

CodeIgniter4のCodeIgniter\Model (1)CRUDメソッド

CodeIgniter4の CodeIgniter\Model の使い方をみてみましょう。

CodeIgniter\Model とは?

CodeIgniter4に標準で含まれているモデルクラスです。以下のような機能があります。

  • 自動データベース接続
  • 基本的なCRUDメソッド
  • モデル内でのバリデーション
  • 自動ページネーション
  • 論理削除

モデルの作成

CodeIgniter\Model を継承したモデルクラスを作成します。

app/Models/NewsModel.php

<?php

namespace App\Models;

use CodeIgniter\Model;

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

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

プロパティにはテーブル名($table)と更新できるカラムのリスト($allowedFields)を設定します。 テーブルの定義は、チュートリアルを参照してください。

プライマリーキーはカラム名が id の自動インクリメントがデフォルトです。 異なる場合は、$primaryKey にカラム名を、$useAutoIncrementfalse を設定します。 なお、複合プライマリーキーには対応していません。

基本的なCRUDメソッド

データの検索

find()

プライマリーキーでレコードを検索します。

$newsItem = $newsModel->find(1);
array(4) {
  ["id"]=>
  string(1) "1"
  ["title"]=>
  string(13) "Elvis sighted"
  ["slug"]=>
  string(13) "elvis-sighted"
  ["body"]=>
  string(95) "Elvis was sighted at the Podunk internet cafe. It looked like he was writing a CodeIgniter app."
}

配列を渡すことで複数のレコードを検索できます。

$newsItems = $newsModel->find([1, 2, 3]);
array(3) {
  [0]=>
  array(4) {
    ["id"]=>
    string(1) "1"
    ["title"]=>
    string(13) "Elvis sighted"
    ["slug"]=>
    string(13) "elvis-sighted"
    ["body"]=>
    string(95) "Elvis was sighted at the Podunk internet cafe. It looked like he was writing a CodeIgniter app."
  }
  [1]=>
  array(4) {
    ["id"]=>
    string(1) "2"
    ["title"]=>
    string(16) "Say it isn't so!"
    ["slug"]=>
    string(14) "say-it-isnt-so"
    ["body"]=>
    string(64) "Scientists conclude that some programmers have a sense of humor."
  }
  [2]=>
  array(4) {
    ["id"]=>
    string(1) "3"
    ["title"]=>
    string(18) "Caffeination, Yes!"
    ["slug"]=>
    string(16) "caffeination-yes"
    ["body"]=>
    string(74) "World's largest coffee shop open onsite nested coffee shop for staff only."
  }
}
findAll()

全レコードを検索します。

$newsItems = $newsModel->findAll();

WHERE句を指定して検索します。

$newsItems = $newsModel->where('id', 1)->findAll();

LIMITとOFFSETを指定して検索します。

$newsItems = $newsModel->findAll($limit, $offset);

データの保存

insert()

レコードを挿入します。

$data = [
    'title' => '記事タイトル',
    'slug'  => 'news-title',
    'body'  => '記事の本文です。',
];
$newsModel->insert($data);
update()

プライマリーキーを指定してレコードを更新します。

$data = [
    'title' => '更新した記事タイトル',
    'slug'  => 'updated-news-title',
    'body'  => '更新した記事の本文です。',
];
$newsModel->update(1, $data);
$data = [
    'body' => 'この記事は削除されました。',
];
$newsModel->update([1, 2, 3], $data);

WHERE句を指定してレコードを更新します。

$data = [
    'body' => 'この記事は削除されました。',
];
$newsModel->whereIn('id', [1, 2, 3])->set($data)->update();

(2022-12-24 追記) 「本当は危ないCodeIgniter4のModel::update()」 も参照してください。

save()

プライマリーキーがない場合は挿入します。

$data = [
    'title' => '新しい記事タイトル',
    'slug'  => 'new-news-title',
    'body'  => '新しい記事の本文です。',
];
$newsModel->save($data);

プライマリーキーがある場合は更新します。

$data = [
    'id'    => 3,
    'title' => '更新した記事タイトル',
    'slug'  => 'updated-news-title',
    'body'  => '更新した記事の本文です。',
];
$newsModel->save($data);

データの削除

delete()

プライマリーキーを指定してレコードを削除します。

$newsModel->delete(1);
$newsModel->delete([1, 2, 3]);

WHERE句を指定してレコードを削除します。

$newsModel->where('id', 1)->delete();

CodeIgniter4のCodeIgniter\Model (2)Entityクラスの利用 へ続く。

関連

参考

Tags: codeigniter, codeigniter4, database