すぐできる!PHPで良いコードを書く方法

はじめに

今まで様々な業務に携わってきましたが、時にはレガシーコードに直面したり、ここ数年はコードレビューをする
機会が増え、良いコードを書くにはどうすればいいだろうと考える事が多くなりました。

私が考える良いコードというのは「シンプルで分かりやすい」コードです。
コードは1度作ったら終わりではなく、その後何年もかけ、経験やスキルもバラバラな様々な人の手によって、
改修を行いながら運用されていきます。

分かりやすいという事は、改修がしやすく、バグを生む可能性も少なくなります。
今回は、直ぐにでも出来る、良いコードを書く方法についていくつかご紹介します。

コーディング規約を守る

コーディングにもルールがあるので守りましょう。というお話です。
コーディング規約を守れば、良いコードに近付く事が出来ます。

PHPにはPSR(PHP Standards Recommendations)というコーディング規約があり、

人気のLaravelやCakePHP等のフレームワークでもこちらを採用しております。

その中でも、基本的なコーディング規約であるPSR-1と、拡張コーディング規約である
PSR-12で、個人的に大事だと思う部分を抜き出してみました。
全部知りたい!という方は公式ページを覗いてみて下さい。

PSR-1 基本的なコーディング規約

  • PHPコードは「<?php」及び「<?=」タグを使用
  • 文字コードはUTF-8(BOM無し)を使用
  • クラス名はアッパーキャメルケースで定義(例:SampleClass)
  • クラス定数は全て大文字とし、区切り文字にはアンダースコアを用いて定義(例:SAMPLE_CONST)
  • メソッド名はローワーキャメルケースで定義(例:sampleMethod)

PSR-12 拡張コーディング規約

ファイル
  • 改行コードはLFのみを使用する
  • ファイルの最後に空行を入れる
  • PHPのみを含むファイルからは終了タグ(?>)を省略する
  • 行末にスペースを含まない
  • 1行の長さは120文字以下が望ましい(80文字以下なら尚良し)
  • 空行は読みやすさを重視し適切に入れる
インデント
  • インデントは半角スペース4つ。タブはNG
予約語と型
  • PHPの予約語は小文字で使用(例:true, false, null)
クラス
  • クラスをインスタンス化する際は、引数が無い場合でも括弧を付ける
    new Foo();
  • クラスの開始と終了の波括弧は独立した行にする
メソッドと関数
  • メソッドにはアクセス修飾子を付ける
  • メソッド名と関数名は、メソッド名の後にスペースを入れない
  • privateメソッドのメソッド名の先頭にアンダースコアを付けない(例:_sampleMethod これはNG)
  • メソッド名の後にスペースを入れない
メソッドと関数の引数
  • 引数はコンマの前にスペースを入れず、コンマの後に1つのスペースを入れる
  • メソッドと関数のデフォルト値を持つ引数は、最後に定義する
  • メソッドと関数の開始と終了の波括弧は独立した行にする
※クラス、メソッドと関数、メソッドと関数の引数の規約をまとめて反映した例
  • 括弧、スペース、波括弧の位置に注目
    class ClassName
    {
        public function fooBarBaz($arg1, &$arg2, $arg3 = [])
        {
            // method body
        }
    }
if、elseif、else
  • else if は使わない
  • 以下の形式で記述する(括弧、スペース、波括弧の位置に注目)
    if ($expr1) {
        // if body
    } elseif ($expr2) {
        // elseif body
    } else {
        // else body;
    }
switch
  • switchは以下の形式で記述する(括弧、スペース、波括弧の位置に注目)
    switch ($expr) {
        case 0:
            echo 'First case, with a break';
            break;
        case 1:
            echo 'Second case, which falls through';
            // no break
        case 2:
        case 3:
            echo 'Third case, return instead of break';
            return;
        default:
            echo 'Default case';
            break;
    }
for
  • 以下の形式で記述する(括弧、スペース、波括弧の位置に注目)
    for ($i = 0; $i < 10; $i++) {
        // for body
    }
foreach
  • 以下の形式で記述する(括弧、スペース、波括弧の位置に注目)
    foreach ($iterable as $key => $value) {
        // foreach body
    }
インクリメント/デクリメント演算子
  • 演算子とオペランドの間にスペースを入れない
    $i++;
    ++$j;
型キャスト演算子
  • 弧内にスペースを入れない
    $intValue = (int) $input;
三項演算子
  • ? と : の前後に1つのスペースを入れる
    $variable = $foo ? 'foo' : 'bar';

型の比較表をきちんと理解する

PHP 型の比較表

個人的にPHPマニュアルで一番大事なページだと思ってます。
利用する事の多い、empty()、is_null()、isset()、if ($x)が値により、
どの判定結果になるのかを把握すれば、バグを生みにくくなります。

また、PHPは比較の際に、通常の比較である「==」と、型まで厳密に比較する「===」の2つがあります。
この、通常の比較「==」の場合、自動的に型変換し、処理を行なっておりまして、
以下を実行した場合、「true」になります。

$a = 1;
$b = '1 string';

if ($a == $b) {
    echo 'true';
} else {
    echo 'false';
}

これは、$bは文字列ではありますが、先頭に数字が入っている為、内部で自動的にint型へ型変換され、
結果、「1 == 1」といった比較が行われる為です。

この様に、「==」は意図しないバグを生む原因となるケースが多く、
基本は「===」の厳密な比較を使うのをお勧めします。

三項演算子、Null合体演算子をうまく使う

以下の様なif文の場合、

if (isset($user['name'])) {
    $userName = $user['name'];
} else {
    $userName = '名無し';
}

三項演算子を使えば、以下の用にスッキリします。

$userName = isset($user['name']) ? $user['name'] : '名無し';

Null合体演算子を使えば、更にスッキリします。

$userName = $user['name'] ?? '名無し';

ただし、Null合体演算子が内部的に行なっている事はisset()なので、
もし、$user[‘name’]に空文字が入っている場合、判定がtrueになる為、
falseの場合のデフォルト値である「名無し」が入らない事に注意して下さい。

配列はまとめて定義する

この様に配列を定義をした場合、ゴチャゴチャしていて見辛いですよね。

$user = [];
$user['id'] = 'user1';
$user['name'] = 'カサレアル太郎';
$user['prefecture'] = '東京都';
$user['city'] = '港区';
$user['birthday'] = '19860101';
$user['gender'] = '男';
$user['license'] = '自動車免許';

それを以下の様にしてみます。

$user = [
    'id' => 'user1',
    'name' => 'カサレアル太郎',
    'prefecture' => '東京都',
    'city' => '港区',
    'birthday' => '19860101',
    'gender' => '男',
    'license' => '自動車免許',
];

スッキリ見やすくなりました。
この方法は、既に存在している配列に要素を追加する場合は使えませんが、
配列を新規で定義する場合に利用できます。

名前をきちんと付ける

例えば、ユーザー情報を取得し、ユーザー名を纏めて格納する処理の場合、
以下様なコードでは変数名や関数名からどの様な情報を扱っているのか分かり辛いです。

$result = getData();

$data = [];
foreach ($result as $val) {
    $data[] = $val['name'];
}

function getData()
{
    // サンプルの為、直接値を返しております
    return [
        [
            'id' => 'user1',
            'name' => 'テスト太郎',
        ],
        [
            'id' => 'user2',
            'name' => 'テスト次郎',
        ],
    ];
}

それを以下の様にしてみます。

$users = getUser();

$userNames = [];
foreach ($users as $user) {
    $userNames[] = $user['name'];
}

function getUser()
{
    // サンプルの為、直接値を返しております
    return [
        [
            'id' => '1',
            'name' => 'テスト太郎',
        ],
        [
            'id' => '2',
            'name' => 'テスト次郎',
        ],
    ];
}

変数と関数の名前を具体的にした事で、どの様な情報を扱っているのか分かり易くなりました。

これは、サンプルなので短いですが、実際の業務で扱っているコードとなると
数百〜数千行にも及ぶ事もあり、名前付けの重要さが身に染みて分かります。

ネストを深くしない

ネストが深くなるパターンは色々ありますが、例えば特定の条件に一致するユーザーの判定を行う場合、

  • 住所が東京都港区
  • 生年月日が1980年以降
  • 性別が男性
  • 自動車免許を所持

以下の様にネストが深くなりがちです。

function isTargetUser($user)
{
    $isTargetUser = false;

    // 住所
    if ($user['prefecture'] === '東京都' && $user['city'] === '港区') {
        $birthday = new DateTime($user['birthday']);
        // 生年月日
        if ($birthday->format('Y') >= 1980) {
            // 性別
            if ($user['gender'] === '男') {
                // 免許
                if ($user['license'] === '自動車免許') {
                    $isTargetUser = true;
                }
            }
        }
    }

    return $isTargetUser;
}

以下も条件を繋げているだけで、分かり辛い事には変わりありません。

function isTargetUser($user)
{
    $isTargetUser = false;

    $birthday = new DateTime($user['birthday']);

    // 住所、生年月日、性別、免許
    if ($user['prefecture'] === '東京都' && $user['city'] === '港区' && $birthday->format('Y') >= 1980 && $user['gender'] === '男' && $user['license'] === '自動車免許') {
        $isTargetUser = true;
    }

    return $isTargetUser;
}

その為、見方を変え、条件に一致しない人を除いていく様にし、
アーリーリターンという方法で書くとシンプルに分かりやすくなります。
また、後に条件が変更になった場合でも容易に修正が出来ます。

コードの行数が増える事を気にするより、今後運用保守しやすいコードがいいですよね。

function isTargetUser($user)
{
    // 都道府県
    if ($user['prefecture'] !== '東京都') {
        return false;
    }

    // 市区町村
    if ($user['city'] !== '港区') {
        return false;
    }

    // 生年月日
    $birthday = new DateTime($user['birthday']);
    if ($birthday->format('Y') < 1980) {
        return false;
    }

    // 性別
    if ($user['gender'] === '女') {
        return false;
    }

    // 免許
    if ($user['license'] !== '自動車免許') {
        return false;
    }

    return true;
}

シングルクォートとダブルクォートを使い分ける

PHPの場合、ダブルクォートは変数展開が出来ます。
何となくダブルクォートを使っている方もいると思いますが、
人によっては、ダブルクォートが記述されていた場合、
「変数展開するのかな?」という疑問が最初に浮かびます。

その、ちょっとした迷いを消してあげる為にも、変数展開をしない場合は
シングルクォートを使いましょう。

尚、PHP4以前の頃はシングルクォートとダブルクォートで明確に処理速度の差があり、
ダブルクォートを使わず、シングルクォートと結合演算子で文字結合を行うのが良いとされていました。

$name = 'カサレアル太郎'
$text = '氏名:' . $name;

しかし、PHP5以降その差は誤差レベルになった為、何億、何兆の大量データを扱い、
速度が重視される様な処理でない限り、気にする必要はありません。

PHP5以降であれば、見やすさを重視してダブルクォートを使用した変数展開をオススメします。

また、「コード内で統一されていた方が良いので全部ダブルクォート」という意見もあると思います。
もし、どちらも使うのであれば、それぞれに意味を持たせた方がよいと思いますので、
その際は、変数展開の有無で使い分ける事を推奨します。

終わりに

冒頭にも記述した通り、実際の業務システムは何年もかけ、経験やスキルもバラバラな様々な人の手によって、
改修が行われていく為、どうしても例に上げた様なコードに直面する機会が出てきます。
経験やスキルが違う為、それ自体は仕方の無い事で、いかにそれを無くしていくかが課題と考えており、
現在はチームで以下を実施し、私自身も含め、チーム全体のスキルアップを日々目指しております。

  • コーディング規約の統一
  • コードレビュー
  • 情報共有(勉強会)

最後になりますが、おすすめの書籍を紹介しておきます。


リーダブルコード -より良いコードを書くためのシンプルで実践的なテクニック


--------------------------
システム開発のご要望・ご相談はこちらから