PHP初心者に向けてPHPの仕様や便利な関数の使い方など

# はじめに
これはPHP歴2年目の私がPHPを触る上で知っておくとよいだろうと思う仕様や便利な関数の使い方を紹介する記事です。（間違い等あれば指摘してください！）

## 前提
PHPのバージョンについて 8.2 を前提としています。

## isset、empty、is_nullの違い
「値が存在するかどうか」を判定するには基本的に以下の3つを使用します。

- isset
- empty
- is_null

取りうる値の違いについては以下の表がわかりやすいです。

|値|if($var)|isset|empty|is_null|
|:--|:--:|:--:|:--:|:--:|
|$var=1|true|true|false|false|
|$var="";|false|true|true|false|
|$var="0";|false|true|true|false|
|$var=0;|false|true|true|false|
|$var=NULL;|false|false|true|true|
|$var|false|false|true|true|
|$var=array()|false|true|true|false|
|$var=array(1)|true|true|false|false|
> 引用：[PHP isset, empty, is_null の違い早見表
](https://qiita.com/shinichi-takii/items/00aed26f96cf6bb3fe62)

ここで重要なのが、これらについて **値がどのような状態のときに真（true）となるのかちゃんと理解し使い分けられるか？** ということです。

PHPでガード節や例外処理等を実装する上でこの辺の挙動の違いはシビアです。
少しの認識違いや適切ではない関数の使用がバグや不具合を引き起こします。

以下2つの点を抑えておくと使い分けがスムーズになります。

### booleanへ変換（キャスト）されたときにfalseとみなされる値を知っておく
booleanへの明示的なキャストがされたときにfalseを返す値は以下の通りです。

- boolean の false
- integer の 0 (ゼロ)
- float の 0.0 および -0.0 (ゼロ)
- 空の文字列 ""、 および文字列の "0"
- 要素の数がゼロである 配列
- NULL

```php
var_dump((bool) false); // false
var_dump((bool) 0); // false
var_dump((bool) 0.0); // false
var_dump((bool) ""); // false
var_dump((bool) "0"); // false
var_dump((bool) []); // false
var_dump((bool) null); // false
```

https://www.php.net/manual/ja/language.types.boolean.php

この **booleanへキャストした時にfalseとみなされる値（以降この値を「falsyな値」と表現します。）を判定する時に使用するのが`empty`** です。

上の表を見返すと、`empty`では上記のパターンについてすべて真（true）となっていることが分かります。

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884629/a4cf9a55-abbd-2a93-611d-8cb8695d4d34.png">

また、この`empty`と全く逆の動きをするのが`if($var)`です。

つまり`if($var)`としたときに条件に合致するのは、booleanにキャストしたときにtrueとみなされる値（`empty`でfalseとなる値）です。

### issetは「値が未定義またはNULL」以外のときに真（true）となる
表を見ていただけたら分かりますが、**issetは値が「未定義もしくはNULL」ではないときにtrueを返します。**
つまり、この二つの場合以外はtrueとなる、ということです。
またis_nullはissetと全く逆の結果を返します（issetがtrueとなるときis_nullはfalseになります）。

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884629/ef591e70-a0ed-3796-220c-8e43fca9ea15.png">

https://www.php.net/manual/ja/types.comparisons.php

## 論理積（`&&`）、論理和（`||`）の評価の挙動
論理積は`&&`演算子の左右両者の値がtrueである場合に限りtrueを返しますが、左の値がfalseの場合は右の値を評価しません。つまり、以下のhoge関数が呼ばれることはありません。JSやTSをよく使う人は慣れ親しんだ挙動かもしれません。

```php
function hoge()
{
    echo 'hoge' . PHP_EOL;

    return true;
}

if (false && $hoge = hoge()) { // 左の値がfalseのため右の値（hoge関数）は評価されない
    echo 'fuga';
};

// 出力なし
```

一方、論理和については`||`演算子の左の値がtrueである場合、右の値は評価されません。（左の値がfalseである場合のみ右の値を評価します）

```php
function hoge()
{
    echo 'hoge' . PHP_EOL;

    return true;
}

if (true || $hoge = hoge()) { // 左の値がtrueのため右の値（hoge関数）は評価されない
    echo 'fuga';
};

// fuga
```

## 三項演算子の短縮形（エルビス演算子）とNull合体演算子を使い分ける
Null合体演算子は、左の値について、「issetでtrueと評価される場合」に左の値を、そうでない場合は右の値を返します。

```php:Null合体演算子
$arr = [];

$arr[0] = null;

$hoge = $arr[0] ?? 'fuga'; // 左の値がnull（issetでfalseと評価される）のため右の値を返す

var_dump($hoge); // 'fuga'
```

左の値が空文字や空配列の場合はissetでtrueとなるため左の値が返されます。

```php:Null合体演算子
$arr = [];

$arr[0] = '';
$arr[1] = [];

$hoge1 = $arr[0] ?? 'fuga';
$hoge2 = $arr[1] ?? 'fuga';

var_dump($hoge1); // ''
var_dump($hoge2); // []
```

左の値がfalsyな値の場合は右の値を返したい、というときにエルビス演算子（三項演算子の短縮形）が使えます。

```php:エルビス演算子
$arr = [];

$arr[0] = '';
$arr[1] = [];
$arr[2] = null;

$hoge1 = $arr[0] ?: 'fuga';
$hoge2 = $arr[1] ?: 'fuga';
$hoge3 = $arr[2] ?: 'fuga';

var_dump($hoge1); // 'fuga'
var_dump($hoge2); // 'fuga'
var_dump($hoge3); // 'fuga'
```

https://www.php.net/manual/ja/language.operators.comparison.php

## 動的プロパティが設定可能（だが使うべきではない）
PHPでは動的プロパティを難なく設定することができます。
```php
class Hoge{}

$hoge = new Hoge();

$hoge->test = 'test';

var_dump($hoge);
```
```php:出力
object(Hoge)#1 (1) {
  ["test"]=>
  string(4) "test"
}
```

ですが、**これは使わないようにしましょう。**（[PHP8.2では動的プロパティが非推奨](https://www.php.net/releases/8.2/ja.php#deprecate_dynamic_properties)となっています）
どこでどんなプロパティが設定されているのか分からないというのは意図しないバグや不具合の温床になります。

## array_filterでfalsyな値を除去する
以下のような配列からnullや空文字の値を取り除きたい、となったときにどうすればいいでしょう。

```php
$arr = [
    0 => 'hoge',
    1 => null,
    2 => 'test',
    3 => 3,
    4 => '',
];
```

配列の要素をフィルタリングしたいのでarray_filterを使えばよさそうです。
こんな風に...

```php
$newArr = array_filter($arr, fn ($value) => ! empty($value));

var_dump($newArr);
```

```php:出力
array(3) {
  [0]=>
  string(4) "hoge"
  [2]=>
  string(4) "test"
  [3]=>
  int(3)
}
```

ここで、**array_filterは第二引数に何も指定しなかった場合に、falsyな値（falsyな値については上記 [isset、empty、is_nullの違い](#issetemptyis_nullの違い) 参照）を除去する**、という性質があります。
したがって、上の記述は以下のようにすっきりとした書き方ができます。

```php
$newArr = array_filter($arr);

var_dump($newArr);
```

```php:出力
array(3) {
  [0]=>
  string(4) "hoge"
  [2]=>
  string(4) "test"
  [3]=>
  int(3)
}
```

https://www.php.net/manual/ja/function.array-filter.php

## array_valuesで添字を振り直す
上記 [array_filterでfalsyな値を除去する](#array_filterでfalsyな値を除去する) の最後の出力を見ていただいたら分かりますが、配列の添字が元の配列のものを保持しており、正しい連番ではありません。

```php:出力
array(3) {
  [0]=>
  string(4) "hoge"
  [2]=>
  string(4) "test"
  [3]=>
  int(3)
}
```

こういった配列の添字を正しい順序にしたいときに便利なのがarray_valuesです。（array_valuesは本来配列の全ての値を返す関数です）

```php:array_valuesで添字を振り直し
$arr = [
    0 => 'hoge',
    1 => null,
    2 => 'test',
    3 => 3,
    4 => '',
];

$newArr = array_filter($arr);

$newArr2 = array_values($newArr);

var_dump($newArr2);
```

```php:出力
array(3) {
  [0]=>
  string(4) "hoge"
  [1]=>
  string(4) "test"
  [2]=>
  int(3)
}
```

https://www.php.net/manual/ja/function.array-values.php

## array_mapのcallbackにnullを渡して配列のzip操作を行う
array_mapは第二引数以降に複数の配列を指定することが可能です。
そこで、第一引数にnullを渡して配列のzip操作を行うことができます。

```php
$arr = [
    1,
    2,
    3,
];

$arr2 = [
    'one',
    'two',
    'three',
];

$newArr = array_map(null, $arr, $arr2);

var_dump($newArr);
```

```php:出力
array(3) {
  [0]=>
  array(2) {
    [0]=>
    int(1)
    [1]=>
    string(3) "one"
  }
  [1]=>
  array(2) {
    [0]=>
    int(2)
    [1]=>
    string(3) "two"
  }
  [2]=>
  array(2) {
    [0]=>
    int(3)
    [1]=>
    string(5) "three"
  }
}
```

二つ以上の配列の同じ添字の値をグループ化したいときに使えます。

https://www.php.net/manual/ja/function.array-map.php

## switchとmatchの違い
**match式が使える状況であればなるべくmatch式を使った方がいいです。**
switchと違いmatch式は値の厳密な比較を行うのでバグが混入しにくいですし、値が返せるので使い勝手がいいです。

- switchを使う場合
```php
$variable = '1';

$result = '';

switch ($variable) {
    case 1:
        $result = 1;
        break;
    case 2:
        $result = 2;
    default:
        break;
}

var_dump($result); // int(1) 緩やかな比較のため型が異なっていてもエラーではない
```

- matchを使う場合（matchは式のため値を返せる）
```php
function hoge(string|int $variable): int
{
    return match ($variable) {
        '1' => 1,
        '2' => 2,
        default => 3
    };
}

var_dump(hoge('1')); // int(1)
var_dump(hoge(1)); // int(3) 厳格な比較のためintの場合はdefaultに
```

## json_decodeの第二引数にtrueを指定すると値が連想配列に
`json_decode`で返される値はデフォルトではオブジェクト（stdClass)です。

```php
$arr = [
    'hoge' => 'fuga',
];

$json = json_encode($arr);

$decodedValue = json_decode($json);

var_dump($decodedValue);
```

```php:出力
object(stdClass)#1 (1) {
  ["hoge"]=>
  string(4) "fuga"
}
```

第二引数にtrueを設定することで値を連想配列の形で受け取ることができます。
地味ですが知っておくと便利です。

```php
$arr = [
    'hoge' => 'fuga',
];

$json = json_encode($arr);

$decodedValue = json_decode($json, true);

var_dump($decodedValue);
```

```php
array(1) {
  ["hoge"]=>
  string(4) "fuga"
}
```

https://www.php.net/manual/ja/function.json-decode.php

## in_arrayの第３引数にはなるべくtrueを指定
**in_arrayの第三引数にtrueを指定することで第一引数の値と第二引数の配列の値について厳密な比較をしてくれます。**

switchとmatchの話でもありましたが、PHPではこの厳密な比較（型も見る）と緩やかな比較の話題が各所で登場します。

なるべく厳密な比較をつかって型安全なコードを書いていきましょう。（ただし渡ってくる値の型が正確に把握できない状況もあります。そのような時は緩やかな比較を使うこともあります。）

```php
$arr = [1, 2, 3];

$value = '1';

var_dump(in_array($value, $arr)); // true
```

```php
$arr = [1, 2, 3];

$value = '1';

var_dump(in_array($value, $arr, true)); // false
```

https://www.php.net/manual/ja/function.in-array.php

## 返り値の型を指定する
関数の返り値に型を指定しましょう。
返り値の型が指定した型ではない場合例外がスローされます。

>　関数のパラメータや戻り値、 クラスのプロパティ (PHP 7.4.0 以降) に対して型を宣言することができます。 これによって、その値が特定の型であることを保証できます。 その型でない場合は、TypeError がスローされます。
引用：[型宣言](https://www.php.net/manual/ja/language.types.declarations.php)

```php
function test(): int
{
    return 'test';
}

$test = test();

var_dump($test); // PHP Fatal error:  Uncaught TypeError: test(): Return value must be of type int, string returned
```

## PHP8系以上の機能を使う
PHP8系以上の機能を積極的に使っていきましょう。
8系以上でない場合は速やかにアップグレードしましょう。

中でも
- [名前付き引数](https://www.php.net/releases/8.0/en.php#named-arguments)
- [コンストラクタの省略記法](https://www.php.net/releases/8.0/en.php#constructor-property-promotion)
- [ユニオン型](https://www.php.net/releases/8.0/en.php#union-types)
- [Match式](https://www.php.net/releases/8.0/en.php#match-expression)
- [Nullセーフ演算子](https://www.php.net/releases/8.0/en.php#nullsafe-operator)
- [Enum](https://www.php.net/releases/8.1/ja.php#enumerations)
- [readonlyプロパティ](https://www.php.net/releases/8.1/ja.php#readonly_properties)
- [readonlyクラス](https://www.php.net/releases/8.2/ja.php#readonly_classes)

等はよく使いますし何より便利です。

## PHPの終了タグは省略する
PHPのみのファイルのPHP終了タグは省略しましょう。

> ファイルが PHP コードのみを含む場合は、ファイルの最後の終了タグは省略するのがおすすめです。 終了タグの後に余分な空白や改行があると、予期せぬ挙動を引き起こす場合があるからです。 余分な空白や改行のせいで PHP が出力バッファリングを開始し、その時点の内容を意図せず出力してしまうことになります。
引用：[PHPタグ](https://www.php.net/manual/ja/language.basic-syntax.phptags.php)

## まとめ
これからPHPを触るという人は一度公式に目を通しておくとよいと思います。

https://www.php.net/manual/ja/index.php

それからPHPのベストプラクティスが記載されているこちらも参考になるのでよければ見てみてください。

https://phptherightway.com/

## 最後に
#### GoQSystemでは一緒に働いてくれる仲間を募集中です！
ご興味がある方は以下リンクよりご確認ください。

https://goqsystem.recruit.site/?qiita=9e115cd14adc1b3bd593