Skip to content

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

タブでインデント

幅8で表示

# PHP - http://php.net/[*.php]indent_style = tab 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!"" Piyo.php

.!"" src!"" Fuga!"" 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 (日本語訳)