PHPプログラミング診断室

第6回永遠のPHP 4(治療編)

修正コード

PHP 4は、もうお疲れ様なので、PHP 5に移行してみましょう。

前回は、主にPHP 4流の書き方を指摘したので、これらをPHP 5らしい今の書き方に修正しています。またコードが理解しやすいように制御構造の見直しを行い、深いインデントにならないような工夫をしています。さらに今回は昨今のPHP界隈でよく使われているオートローダを使って、クラスファイルを自動で読み込むテクニックを組み込んでみました。

では、治療後の修正コードをどうぞ。

controller/LoginController.php
<?php
namespace Acme\Controller;

require_once dirname(__DIR__).'/vendor/autoload.php';

use Acme\Lib\BasicController;
use Acme\Lib\Http;
use Acme\Lib\CheckExecuter;
use Acme\Lib\Login;
use Acme\Model\UserModel;

/**
 * <b>LoginControler</b>
 * ログインコントローラ
 *
 * @author hoge
 */
class LoginController extends BasicController
{
    const LOGIN_ID = 'login_id';
    const PASSWORD = 'password';
    const PASSWORD_MAX_LENGTH = 8;

    /**
     * @var string
     */
    protected $NEXT_PAGE = 'top.php';

    /**
     * コンストラクタ
     */
    public function __construct()
    {
        $this->init();

        // セッションクリア
        unset($_SESSION[UserModel::SESSION_USER]);

        if (Http::isPost()) {
            $this->values = $_POST;
        } else {
            $this->values = null;
        }
    }

    /**
     * メイン処理
     */
    public function execute()
    {
        if (!Http::isPost()) {
            return;
        }

        // 値チェック
        $checker = new CheckExecuter($this);
        $checker->check();
        $this->errorMessages = $checker->getErrorMessages();
        if ($this->isError()) {
            return;
        }

        // セッションクリア
        $_SESSION = array();

        // ログイン処理
        $userModel = Login::auth($this->values[static::LOGIN_ID], $this->values[static::PASSWORD]);
        if (empty($userModel)) {
            $this->addErrorMessage('error_not_login');
            return;
        }

        // セッションIDの振り直し
        session_regenerate_id(true);

        unset($_SESSION[UserModel::SESSION_USER]);
        $_SESSION[UserModel::SESSION_USER] = $userModel;

        // 次ページへ
        $this->transferPage($this->NEXT_PAGE);
    }

    /**
     * ログインIDチェック
     *
     * @return mixed 正常ならnull / エラーならコード?
     */
    public function checkLoginId()
    {
        if (isset($this->values[static::LOGIN_ID])
            && $this->values[static::LOGIN_ID]) {
            return null;
        } else {
            return 'check_login_id';
        }
    }

    /**
     * パスワードチェック
     * 半角英数8桁以内
     *
     * @return mixed 正常ならnull / エラーならコード?
     */
    public function checkPassword()
    {
        if (isset($this->values[static::PASSWORD])
            && $this->values[static::PASSWORD]
            && preg_match('/^[a-zA-Z0-9]+$/', $this->values[static::PASSWORD])
            && strlen($this->values[static::PASSWORD]) <= static::PASSWORD_MAX_LENGTH) {
            return null;
        } else {
            return 'check_password';
        }
    }
}
lib/Login.php
<?php
namespace Acme\Lib;

use Acme\Lib\Database;
use Acme\Model\UserModel;

/**
 * <b>Login</b>
 * ログインクラス
 * ログイン管理を行う
 *
 * @author hoge
 */
class Login
{
    /**
     * 認証
     *
     * @param string $loginId
     * @param string $password
     * @return UserModel
     */
    public static function auth($loginId, $password)
    {
        // PostgreSQL には事前に接続済とする
        $loginId = pg_escape_literal($loginId);
        $password = pg_escape_literal($password);
        $where = sprintf('login_id=%s and password=%s', $loginId, $password);

        $db = Database::getInstance();
        $record = $db->getOneRecord('*', 'v_user', $where);
        if (empty($record)) {
            return null;
        }

        $userModel = new UserModel();
        $userModel->setAttributes($record);

        return $userModel;
    }
}

治療ポイント1. PHP 5の書き方へ

PHP 4の流儀で書かれた個所をPHP 5の書き方に変更しています。

診断編で指摘した個所以外では、メンバ変数やメソッドにアクセス修飾子を付けました。PHP 4では、メンバ変数はvarキーワード、メソッドはアクセス修飾子なしで定義するしかなかったのですが、PHP 5では「public」⁠protected」⁠private」の3種類のアクセス権を設定することができます[1]⁠。

PHP 4の形式で記述した場合はpublicが設定されているのと同じ意味となります。修正コードでは、メンバ変数$nextPageLoginControllerのみで利用されているのでprotectedを、メソッドについてはpublicを設定しています。

どのアクセス権を用いるのが適切なのかは、構築するアプリケーションや利用するフレームワークなどによって変わります。特に制限がなければ、まずはprotectedを基本にして、公開する必要があればpublicを、継承クラスからも隠す必要があればprivateを用いるというのがよいでしょう。

治療ポイント2. 名前空間の設定

各PHPファイルに名前空間を設定しました。

名前空間にはAcmeをトップレベルに指定しています。Acmeはサンプルでよく使われる名前空間です。さらに、配置ディレクトリlib/controller/model/ごとにサブネームスペースAcme\LibAcme\ControllerAcme\Modelを指定しています。

これにより、LoginControllerクラスはAcme\Controller\LoginControllerLoginクラスはAcme\Lib\Loginが完全なクラス名となります。もしほかのライブラリを利用した際にLoginというクラスが存在しても、名前空間が異なれば競合することはありません。

また、ファイル先頭付近にあるuse文にて、名前空間のインポートを行っています。完全なクラス名を毎回記述するのは冗長ですが、この文により名前空間を省略したクラス名を利用することができます。

たとえば下記のようなコードであれば、Acme\Lib\DatabaseというクラスをDatabaseという名前で参照できるようになります。

use Acme\Lib\Database;

治療ポイント3. 定数をクラス定数に変更

define文にて定義している定数をクラス定数として定義しています。

define文で定義した定数はグローバルな名前空間に属することになるので、アプリケーション全体で一意となる名前を付ける必要があります。これをクラス定数にすることで、クラス名が名前空間となり、同じ名前の定数でも異なるクラスに定義されれば、競合することはありません。

LoginControllerクラスではLOGIN_IDPASSWORDという定数をクラス定数として定義しています。もしも仮に、FooControllerクラスにも同じLOGIN_IDPASSWORDというクラス定数を定義されていても、クラス名が異なるので問題ありません。

クラス定数にする利点は、クラス固有の情報として役割がわかりやすくなるということが大きいですが、こうした名前の競合を避けるメリットもあるということを知っておくとよいでしょう。

治療ポイント4. オートローダの対応

各ファイルの先頭で記述していたrequire_once文によるクラスファイルの読み込みをオートローダで行うようにしました。

オートローダは、参照しようとしたクラスが定義されていない場合に自動でクラスファイルを読み込む機構です。この処理は自分で書くこともできるのですが、ここではComposerに付属しているオートローダを利用します。

Composerのオートローダを利用するには、まずComposerをダウンロードします。下記コマンドを実行するとcomposer.pharというファイルがダウンロードされます。

$ curl -sS https://getcomposer.org/installer | php
$ ls
composer.phar

次にオートローダの設定をcomposer.jsonというJSONファイルに記述します。

{
  "autoload": {
    "psr-4": {
      "Acme\\Lib\\": "lib/",
      "Acme\\Controller\\": "controller/",
      "Acme\\Model\\": "model/"
    }
  }
}

今回はPSR-4[2]によるオートロードを利用します。このcomposer.jsonでは、Acme\Lib名前空間はlib/ディレクトリ、Acme\Controller名前空間はcontroller/ディレクトリ、Acme\Model名前空間はmodel/ディレクトリからクラスファイルを読み込むことを指定しています。

このようにPSR-4では、名前空間と対応するディレクトリを記述することで柔軟にオートロードを設定することができます。

次にこのcomposer.jsonに記述した内容をオートローダに反映するために次のコマンドを実行します。

$ php composer.phar dump-autoload
Generating autoload files

最後に、このオートローダをLoginControllerで利用するように、require_once文でオートローダを読み込みます。

require_once dirname(__DIR__).'/vendor/autoload.php';

これでComposerのオートローダが利用できるようになりました。クラスファイルはrequire_once文で読み込まなくても、そのクラスを利用する際に自動で読み込まれるようになっています。

オートローダを用いることで、実際にそのクラスが利用される場面までクラスファイルの読み込みを遅らせることができるので、無駄なクラスファイルの読み込みを避ける効果もあります。

治療ポイント5. 制御構造の見直し

LoginControllerクラスのexecute()メソッドの実装について、制御構造の見直しを行いました。

execute()メソッド内の制御構造のみを抜き出したのが下記です。ログイン処理を行うために3つのif文による判定を経る必要があるため、実際のログイン処理は深いインデントの中で記述されています。

if (Http::isPost()) {
    if (!$this->isError()) {
        if (Login::auth($userModel)) {
            // ログイン処理
        }
    }
}

この制御構造を修正コードで組み直したのが以下です。それぞれのif文にてログイン処理が成立しない場合はreturnで処理を終了するようにしています。このようにすればインデントを深くする必要がなく、以後の処理では前のif文の判定を気にしなくてよいので、理解しやすいコードとなります。

if (!Http::isPost()) {
    return;
}
if ($this->isError()) {
    return;
}
if (empty($userModel)) {
    $this->addErrorMessage('error_not_login');
    return;
}
// ログイン処理

治療ポイント6. Login::auth()メソッドのシグネチャ見直し

Loginクラスのauth()メソッドのシグネチャを変更しました。

元のコードでは、下記のようにUserModelクラスオブジェクトを引数で受け取り、そこからログインIDとパスワードを取得していました。戻り値には、処理結果の正否だけをbooleanで返し、引数で渡された$userModelに必要な値をセットしています。呼び出し元では、引数で渡した$userModelを以降の処理で利用する形になっています。

  function auth(&$userModel) {

auth()メソッドでは、ログインIDとパスワードを外部から受け取り、データベースから取得したユーザ情報を返すという処理を行います。修正コードでは、この内容がわかりやすいようにシグネチャを変更して、ログインIDをパスワードを引数として受け取るようにしました。メソッド内部で、UserModelクラスのオブジェクトを生成し、それを戻り値として返しています。

  public static function auth($loginId, $password)

このようにすれば、メソッドのIN/OUTがシンプルになり、理解しやすくなります。元のコードで意図していた参照渡しを行う必要もありません。

治療を終えて

今回は、PHP 4のコードをPHP 5に変えるというテーマで診断を行いました。PHP 4のころは良いとされていた書き方も、PHP 5ではそう書く必要がなくなったり、むしろ良くない書き方となっている場合があります。オブジェクトの参照渡しなどは、他言語でプログラミングを行ってきた人のほうが書いてしまいがちなコードです。

PHPが進化してきたように、PHPコードも時代に合わせて変化していくことが必要となります。合わせて、PHPを取り巻くツールも変遷しています。今回紹介したComposerは、いまPHPで開発するなら必須とも言えるツールです。こうしたツールをうまく使うのも、今のPHPの書き方と言えるでしょう。

今回の診察はここまでです。また次回お会いしましょう。まだまだ寒い日が続くので風邪など引かれませんように。

おすすめ記事

記事・ニュース一覧