PSRを実装してみる

公開日:

宮城県仙台市青葉区花京院TKPガーデンシティPREMIUM仙台西口8Fで開催された『PHPカンファレンス仙台2019』でレギュラーセッション(30分)として発表しました。

Download PDF

スライドテキスト

Page 1

PSRを実装してみる

Let's implement PSR

(PHP Standards Recommendations)

2019-01-26 PHP Conference Sendai

Sendai, Miyagi, Japan

#phpconsen

Page 2

お前誰よ
うさみけんた (@tadsan) / Zonu.EXE

  • GitHub/Packagistでは id: zonuexe
    • ピクシブ株式会社 pixiv運営本部
  • Emacs Lisper, PHPer
  • Emacs PHP Modeのメンテナ引き継ぎました
    • 好きなリスプはEmacs Lispです
    • Qiitaに記事を書いたり変なコメントしてるよ

Page 3

Page 4

Page 5

今回の目的

Page 6

PSRって何の ためにあるの

Page 7

強い人たちと話すと

PSRの不満についての 愚痴を聞くけれども、
なるほどわからん

Page 8

わかりたい

Page 9

それを明らかにしたい と思い、われわれ取材 班は仙台へと旅立った

Page 10

でも、英語も

仕様書を読むのも

苦手だお…

Page 11

       |
   \  __  / ピコーン

   _ (m) _  

そうだ、実装しながら理解しよう

      |ミ|
    /  `´  \
     ('A`)
     ノヽノヽ        くく

Page 12

実装

Page 13

仕様を雑に読む→

よくわからないので

ぐぐる→
既存実装を読んでみる→
雑に実装してみる→

(以下ループ)

Page 14

背景

Page 15

PHPのコンポーネントを 作って提供してる組織は
意外とたくさんある

Page 16

Page 17

Page 18

Page 19

Page 20

Page 21

Page 22

たくさんあるのはいいが
大同小異、同じような

ものが乱立してると困る

Page 23

使い回せるものが

あるなら折角だから

相互運用したい

Page 24

そのような現状でPSRは 何を提供しているのか、

実装しながら見てみよう

Page 25

おしながき

Page 26

PHP-FIGについて

PSR-0 PSR-3 PSR-1 PSR-6 PSR-2 PSR-13

そのほかのPSR

Page 27

ここから

駆け足になるので

最初にまとめ

Page 28

PSRにナンバリング

されてるからと言って

高品質で優先すべき

対象とは限らない

Page 29

PHP-FIG

フレームワーク相互運用グループ

Page 30

Page 31

PSRを策定

する団体

Page 32

フレームワーク/CMS/
そのほかPHP関連

開発者の寄合所帯

Page 33

さっき挙げた団体 の全てが加盟して
るわけではない

Page 34

抜けちゃったと ころもいろいろ
ある

Page 35

Zend,CakePHP,

Yii, Drupal, Slim

など多数

Page 36

Guzzle, Doctrine, Symfony, Laravel, The PHP Leagueは

脱退済み

Page 37

脱退したからと言っ てPSRを尊重して ないわけではない

Page 38

PSR

PHP標準勧告

Page 39

「勧告」と名前はついて るがPHP本体とは関係 なく、有志による規格

Page 40

グループの背景にある考えは、代表者たち がプロジェクト間の共通点について話し合
うことで協働する方法を探ることです。

グループの議論の主な観客は参加プロジェ クト同士ですが、そのほかの コミュニ

PHP

ティも注目していることはよく承知してい ます。ほかの人々が の成果物を採用す

PSR
ることは歓迎しますが、それは目的ではあ
りません。

Page 41

投票メンバーは標準を遵守しなければいけ
ませんか?

いいえ。 の投票メンバーになるこ

PHP-FIG

とで、すべての、あるいは任意の を実

PSR

装することを強制されることはありません。 プロジェクトはしかるべき時にアップグレー ドする際に後方互換性を考慮する必要があ ります。ほとんどのプロジェクトで最終的 に採用されると想定されますが、必須では
ありません。

Page 42

勧告される対象は我々
下々のPHPer各位

ではなくPHP-FIGに

任意で参加した団体

Page 43

勧告された参加団体が

PSRを準拠することも
また任意

Page 44

脱退したはずの団体が

PSRを準拠することも
また任意

Page 45

PSR自体が、ベスト

プラクティスを標榜し てたりもするけど…?

Page 46

それはそれ

これはこれ

Page 47

Page 48

PSR-0

Autoloading Standard

Page 49

失効済み!!!

PSR-0

Autoloading Standard

Page 50

オートロードに適合 したクラスの命名規 則とファイルの配置

Page 51

前提

PHP 5.0からクラスのオート

ローディング(遅延ロード)に対応

=必要あるまで読み込まない

それ以前は から

include_path

が主流

include/require(_once)

Page 52

ざっくりPSR-0

クラス名の構造の規則

\<Vendor Name>\(<Namespace>\)*<Class Name>

区切りの擬似名前空間も許容

_

\<Vendor Name>_(<Namespace>_)*<Class Name>

Page 53

ディレクトリ配置

.

Hoge

!""
Fuga
!""

Piyo.php
!""

class Hoge_Fuga_Piyo {}

または

//
namespace Hoge\Fuga;
class Piyo{}

Page 54

実装依存になる部分

ファイルシステム依存

大文字小文字の差異

Unicode NFD問題

Page 55

曖昧な部分

「ベンダー名」って何…?

自由に決められるので、

ただの紳士協定に過ぎない

トップレベルのクラス定義を
戒める効果はある(?)

Page 56

実装方式

A: クラスマップ方式

実行前にディレクトリ構造を

スキャンして対応表を作成

B: 逐次確認

実行時にファイルシステムを検査

Page 57

PSR-0準拠クラスローダー関数

function add_directory(string $base_dir): void {
$directory = rtrim($base_dir, '/\\');

spl_autoload_register(function ($fqcn) use ($base_dir) {
$namespaces = explode('\\', $fqcn);
$class = array_pop($namespaces);
$class_file = strtr($class, ['_' => '/']) .

'.php';

$ns_dir = implode('/', $namespaces);
$file_path = implode(‘/',

array_filter([$base_dir, $ns_dir, $class_file], 'strlen'));

if (is_file($file_path)) {
require_once $file_path;
}
});
}

Page 58

現状

以前はライブラリごとに

クラスローダーを提供してた

など

Requests::register_autoloader()
最近はComposerがオートロー ド機能を提供してくれるので
composer.jsonに書くだけ

Page 59

重要

PSR-0はPSR-4で代替された
ので廃止済み

Page 60

Composer以前

PEARでインストールする zipをダウンロードして、

そのディレクトリを
include_pathに追加する
同じホスト上で動作する別のPHP プロジェクトと共用されることも

Page 61

Composer以後

依存パッケージはプロジェクト内 のサブディレクトリにインストール
同じホスト上で動作する別の

プロジェクトと混在すること
は原則ない

Page 62

PSR-1

Basic Coding Standard

Page 63

基本的な(=相互運用 のため重要度の高い)
コーディング規約

Page 64

要件

名前空間・クラス名は

PSR-4/0に適合するように
文字コードはUTF-8(BOM無)
クラス定義で副作用を

起こしてはいけない

Page 65

命名規則

クラス名は

UpperCamelCase

定数名は

UPPER_SNAKE_CASE

メソッド名は

lowerCamelCase

変数・プロパティは規定しない

Page 66

理由

「あるクラスがロードされた副 作用として別のファイルが読 み込まれる」のような挙動は

わかりにくい
最低限のもの以外はinclude/ requireせずに遅延ロードに

任せるのが無難

Page 67

こういうコードはだめ

<?php /** Foo/Bar.php */

// 別のファイルを読み込もうとする

include_once __DIR__ . "/foo.php";

// エラーレベルを変更

error_reporting(0);

class Foo
{
// ...
}

Page 68

「実装します」

といった感じの

ものではないので

Page 69

PSR-2

Coding Style Guide

Page 70

コーディング

スタイルの

ガイドライン

Page 71

要件

プロジェクト内で表記を一貫 して意識のずれを低減する
PSR-1に準拠することが前提

さまざまなプロジェクトのコーディ ングスタイルを調査した上で策定

Page 72

ホワイトスペース・インデント

インデントは4スペースが基本 単項演算子以外の演算子や() の前後にはスペースを入れる
改行コードはLF
一行あたりの文字数は80文字 を推奨、120文字以上は警告

Page 73

ざっくり

class Foo

← 改行する

{
public function bar($xs)

← 改行する

{
foreach ($xs as $x) {

← 改行しない

$f = function ($a) use ($x) {

← 改行しない

// ...
};
}
}
}

Page 74

{} と改行位置の癖

宣言文

( , , など)

class function namespace
の開始行と の間は改行する

{
それ以外の文 は

( , など)

if foreach

改行せず、開始行に を書く

{

無名関数は文ではなく式なので
改行せず、開始行に を書く

{

Page 75

重要なこと

Page 76

スタイルルールは、様々なプロジェクトの 共通内容から生み出されています。様々な

作者が複数プロジェクトを横断して協力し あうことで、全てのプロジェクトで有用な ガイドライン策定の助けとなります。従っ

て、このガイド本来の利点は、ルール自体 にはなくルールを共有することにあります。

http://www.infiniteloop.co.jp/docs/psr/psr-2-coding-style-guide.html

Page 77

その他本スタイルガイドでは、意図的に

7.
省略しているスタイルやプラクティスが多 くあります。例えば下記のような幾つかに

ついては、ここでは明記していません。

(中略)

なお、本スタイルガイドは将来的に様々な スタイルやプラクティスの登場に応じて改
定・拡張をできるものとします。

http://www.infiniteloop.co.jp/docs/psr/psr-2-coding-style-guide.html

Page 78

PSR-2は

コーディングスタイル策定の

ガイドライン

Page 79

PSR-2をカスタマイズする

定義文であっても { を同じ行
で書くとか
インデントを8タブにするとか 改行コードをCR+LFにするとか

Page 80

策定したガイドラインを守らせる

PHP-CS-Fixerを導入する EditorConfigを導入する
PhpStormの設定ファイルを共有

StyleCI

https://styleci.io

Page 81

PHP-CS-Fixerの設定例

<?php

$finder = PhpCsFixer\Finder::create()
->in(__DIR__ . '/src')
->in(__DIR__ . '/tests');

return PhpCsFixer\Config::create()
->setRules([

定義開始行と { を同じ行に

'@PSR1' => true, '@PSR2' => true,
'braces' => [
'position_after_functions_and_oop_constructs' => 'same',
],
])
->setFinder($finder)

タブでインデント

->setIndent("\t")
->setLineEnding("\n");

Page 82

.editorconfig

# EditorConfig is awesome: http://EditorConfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8

# PHP - http://php.net/
[*.php]

タブでインデント

indent_style = tab

幅8で表示

indent_size = 8

Page 83

PhpStormの設定ファイルを共有

PhpStormが生成する は

.idea

一部ファイルを除き、

共有した方がいいらしい

github/gitignore: JetBrains.gitignore
特に

.idea/codeStyles/Project.xml

がコーディングスタイル

Page 84

大事なこと

コーディングスタイルは人間が

レビューであれこれと指図したり

するよりも、機械的に変換した ほうがギクシャクせずに済む
「人間が気をつける」は破綻する 「自然にそうなる」 状態が理想

Page 85

PSR-2の現状

初版が2012年6月なので、

PHP7の時代には不足気味
可変長引数、無名クラスとか
現在、後継となるPSR-12

Extended Coding Style Guide
策定作業中

Page 86

PSR-3

Logger Interface

Page 87

Syslog風の
Loggerインター
フェイス

Page 88

要件

Syslogで定義された8種類の

レベルごとにログを記録
緊急度が高い順から

Emergency, Alert, Critical,
Error, Warning, Notice,
Informational, Debug

Page 89

実装

psr/logパッケージに含まれる

/
LoggerTrait AbstractLogger を利用して最小限で実装可能 今回実装したPSRのなかでは

機能がわかりやすく実装が

簡単なので初心者にオススメ

Page 90

雑感

普通はMonologを使えばいい ので代替実装に変更するメリッ
トが全然感じられない
Monologのハンドラーを実装 した方が拡張しやすいので、
そちらの方がオススメ

Page 91

さらに雑感

WebアプリからSyslogの

8種類のレベルはミスマッチ感
がある
正直、Error, Debug, Infoの

3レベルくらいにまとめた方が
扱いやすいのでは

Page 92

ヘルパー関数

<?php declare(strict_types=1);

namespace ZoPsr\Log3;

use Psr\Log\LogLevel;

function to_num(string $level): int {
return [
LogLevel::EMERGENCY => 0, LogLevel::ALERT => 1, LogLevel::CRITICAL => 2, LogLevel::ERROR => 3, LogLevel::WARNING => 4, LogLevel::NOTICE => 5, LogLevel::INFO => 6, LogLevel::DEBUG => 7,
][$level];
}

Page 93

行区切りJSON出力するロガー

final class FileLogger implements LoggerInterface {
use LoggerTrait;

/** @var resource */
private $_fp;
/** @var string */
private $_min_level;

/**
* @param resource $fp
* @param ?string $min_level
*/
public function __construct($fp, $min_level = null) {
$this->_fp = $fp;
$this->_min_level = $min_level ?? LogLevel::WARNING;
}

public function log($level, $message, array $context = []) {
if (to_num($level) <= to_num($this->_min_level)) {
$log = json_encode(['level' => $level, 'message' => $message, 'context' => $context],

JSON_UNESCAPED_SLASHES) ?: '{"json_encode_error":true}}';
fputs($this->_fp, $log); fputs($this->_fp, "\n");

Page 94

PSR-4

Autoloader

Page 95

オートロードに適合 したクラスの命名規 則とファイルの配置

Page 96

PSR-0の後釜

Page 97

オートロードに適合 したクラスの命名規 則とファイルの配置

Page 98

PSR-0との差異

_を使った擬似名前空間は

許容されなくなった
PSR-0は名前空間に対応する ディレクトリを用意しなけれ ばならなかったが、PSR-4で はディレクトリを浅くできる

Page 99

PSR-0との差異

prefixとbase directory

という概念が導入された

というクラス

\A\B\C\D
PSR-0:

src/A/B/C/D.php

PSR-4:

src/D.php
Composerを使ってたらお馴染

Page 100

ディレクトリ配置

. .

src src

!"" !""
Fuga Piyo.php

!"" !""

Piyo.php
!""

<?php

namespace Hoge\Fuga;


class Piyo{}

Page 101

クラスローダの実装(1)

final class ClassLoader {
/** @var array<string,string> */
private $directories = [];

public function add(string $prefix, string $directory): void {
$this->directories[$prefix] = $directory;
}

public function register(bool $prepend = false) {
spl_autoload_register([$this, 'autoload'], true, $prepend);
}

public function autoload(string $class): void {
$path = $this->_resolve($class);

if ($path !== null) { require_once $path;
}
}

Page 102

クラスローダの実装(2)

private function _resolve(string $class): ?string { foreach ($this->directories as $prefix => $dir) {
if (strpos($class, $prefix) !== 0) {
continue;
}

$path = $this->_makePath($class, $prefix, $dir);

if (is_file($path)) {
return $path;
}
}

return null;
}

private function _makePath($class, $prefix, $directory): string {
$relative = substr($class, strlen($prefix)); $path = strtr($relative, '\\', '/') . '.php';

return $directory . DIRECTORY_SEPARATOR . $path;
}
}

Page 103

ディレクトリの追加

<?php

use ZoPsr\Autoload4\ClassLoader;
use function ZoPsr\TestHelper\capture;

require_once __DIR__ . '/../../vendor/autoload.php';

$loader = new ClassLoader; $loader->register(false);

$loader->add('Foo\\', __DIR__ . '/1'); $loader->add('Hoge\\', __DIR__ . '/2');

Page 104

PSR-4を実装する意味はあるか

クラス名の規則として重要 クラスローダは自作せずに
Composerを使った方がパフォー
マンスの最適化できて便利
PHPのクラスロードの仕組みを
学ぶ上では重要

Page 105

PSR-13

Link definition interfaces

Page 106

要件

ハイパーメディアの「リンク」
を抽象化したクラス
HTMLのとか とか

<link>

HTTPのLinkヘッダとか

(RFC5988/8288 WebLinking)
……なんだけど

Page 107

仕様

基本はPSR-7と同じ

イミュータブルなオブジェクト
関連するRFCがやたらある
特にRFC6570(URI Template)、
それ混ぜる必要あったか…?
実装として混ざってるわけではない が、 という状態がある

isTemplated()

Page 108

URI Template

RFC6570
URIに変数展開するための

テンプレート言語
これ自体は需要ある規格だが、
Linkでテンプレートありなしの hrefを混在させて嬉しいことは

……ある?

Page 109

既存実装

crell/htmlmodel
PSR-13提案者の概念実証

php-fig/link-util
PHP-FIGによる実装
Linkクラスも実装されてる
あのSymfonyも独自実装してない

Page 110

仕様への文句

isTemplated()
必死にユースケースを考えてみ たけど、積極的にこの分岐をし たいケースが一切見当たらない

でも事実上

Symfony\WebLink
黙殺されてるように見える

Page 111

実装への文句

isTemplated()
hrefに か のどちらかが含

{ }

まれてたらURI Template文字
列って判定してる
すごく怪しい

Page 112

URIの仕様

https://example.com/{ のよ
うなURIは完全に合法
{と}はreservedな文字でも unreservedな文字でもない ので、パーセントエンコード
されてるとは限らない

Page 113

とてもバグりそうな雰囲気が

文字列を場当たり的な判定し て実行時に分岐するのは、

どう考えてもアンチパターン
せめて

TemplatedLinkInterface
のような別の型で表現するなら
気持ちはわかる

(けど嬉しくはない)

Page 114

そのほかのPSR

Page 115

ここから実装が間に 合わなかったPSRに ついて雑に話します

Page 116

PSR-7 message interface

HTTPのリクエスト/レスポン
スを抽象化するクラス
GuzzleのようなHTTPクライア
ントライブラリには好適
しかしHTTPミドルウェアの

媒介としては辛くないですか

Page 117

PSR-7とわたし

Guzzle使ったらPHPでめちゃんこ 簡単にHTTPプロキシ鯖を実装で
きたので好き
PHPでプロキシサーバーを作る
PHPのプロキシサーバーを説明する

Page 118

PSR-6 Caching Interface

実は今回は実装してみたけど、
特に感情がない
業務ではRedisなどのKVSをラッ プした独自の抽象クラスを使っ てるけど、移行する気は特に
湧かなかった

Page 119

PSR-5 PHPDoc (Draft)

PSRの番号が若いのに長年マー
ジされてない代表格
放棄されかけたがAPIドキュメ ント自動生成よりもIDEや静的 解析のデファクト標準として
重要なので議論復活した

Page 120

PSR-5とわたし

WEB+DB PRESS Vol.87に書いた

(総集編に収録されてるから買って) 2018年のPHPDoc事情とPSR-5 PhpStorm 2018.3でPSR-5ジェネ リクス記法はサポートされない

Page 121

PSR-5と静的解析

コード上の記述とPHPDocのコメン トを手がかりにして、怪しい部分
を高精度で検出してくれる

Phan, PHPStan, Psalm,
PhpStormなどのツールがある 今回の実装でも助けられました

Page 122

PSR-8 Mutually Assured Hug

2014年4月1日に提案された
「この標準は、オブジェクトが互いの感 謝と支持を表明するための、一般的な
方法を確立します」
こんなのにPSRの番号の帯域を
降ってよかったの…?
PSR-8 Hug (日本語訳)