Skip to content

「ふつうのPHP」がpixivになるまで

公開日:

大阪府大阪市北区グランフロント大阪 北館 タワーC 8階で開催された『PHPカンファレンス関西2018』でレギュラートーク(30分)として発表しました。

Download PDF

スライドテキスト

Page 1

「ふつうのPHP」が

になるまで

pixiv.inc

USAMI Kenta @tadsan

2018-07-14 PHPカンファレンス関西 #php

Page 2

お前誰よ

  • うさみけんた (@tadsan) / Zonu.EXE
  • ピクシブ株式会社 pixiv運営本部 技術基盤チーム
  • 2012年末から現職、WEB APIを実装したりしたよ
  • 今回発表するようなところを見つめてきたよ
  • Emacs PHP Mode 現行メンテナ/ https://github.com/emacs-php
  • Qiita: https://qiita.com/tadsan 適当な記事を書いてきたよ

Page 3

の紹介

Page 4

Page 5

イラストコミュニケーションサービス pixiv

  • 2007年9月10日に開始されたイラストSNS・投稿サイト
  • イラスト: 約7000万作品 (IDベース、画像枚数は1億4000万枚以上)
  • ユーザー: 約3000万アカウント (IDベース)
  • イラストブックマーク: 約35億回
  • アクセス数: 月間76億 (HTTPdログベース、一般的な意味のPVとは異なる)
  • サーバー台数: 53台 (pixivのデプロイ対象になっているApache HTTPdのみ)

Page 6

用語

  • pixiv本体 www.pixiv.net
  • デスクトップ版 (いわゆるPC向け)
  • モバイル版 (いわゆるスマートフォン向け、旧touch.pixiv.net)

Page 7

pixiv.git

  • pixivとデータベースを共有するコードは一個のリポジトリに含まれる
  • それ以前はサービス横断のgit submoduleで共有されてたが効率悪かった

! accounts.pixiv.net! admin! app-api.pixiv.net! batch! bin! bungei-api.pixiv.net! comic-api.pixiv.net! embed.pixiv.net! fanbox.pixiv.net! m.pixiv.net! me.pixiv.net" oauth.pixiv.net! pixiv-lib! public-api.pixiv.net! rpc.pixiv.private! sensei.pixiv.net! source.pixiv.net! spotlight.pics! ssl.pixiv.net! tests! touch.pixiv.net! util! www.pixiv.net" www.pixivision.net

Page 8

https://niconare.nicovideo.jp/watch/kn1259

Page 9

よくある質問

  • フレームワークは使ってないの?
  • 使ってません。マイクロフレームワークのようなのはいくつかある
  • べんりユーティリティの集合で実際フレームワークになってる
  • フレームワークを指向して開発されたものもいくつかある
  • フレームワーク/言語の移行やフルスクラッチしないの…?
  • 直近での予定はありません。過去に痛い目にも遭ってるし…

Page 10

の歴史

Page 11

お断り

数々の変更は@tadsanが直接行ったものではなく、歴代の開発者が改善を積み重ねてきたものです(むしろ恩恵を受けてきた立場)

Page 12

2007年

2011年

2012年

2008年

2016年

2017年

2010年

2013年

2014年

2018年

2015年

pixiv

pixiv

pixiv

pixiv

pixiv

pixiv

pixiv

pixiv

pixiv

国際版

国際版

公開

Public API

pixiv(WEB)

Private API

pixiv PHP5.5

pixiv FANBOX

pixiv FANBOX

pixiv(β)

pixiv(β)

開発

開発

公開

公開

終了

モバイル公開

モバイル終了

うごイラ機能リリ┃ス

ピクシブ百科事典公開

まだWeb制作会社

小説モバイル終了

小説モバイル公開

@tadsan 入社ここ

アプリ

PI公開

リニュ┃アル

(2012年11月)

スマ┃トフォン版公開

社名変更

スマ┃トフォン版ドメイン統合

リニュ┃アル

サービスの公開(終了)で見る年表

アプリ刷新

用API開発

リニュ┃アル

PA化進行

pixiv以外のサービスも増えてく

(touch.pixiv.net)

Page 13

React

REST API

Anti REST

from Redmine

2012年

2016年

2017年

2013年

2014年

2018年

2015年

pixiv

pixiv

Thrift

国際版

GitLab

Jenkins

PHPUnit

Composer

Public API

導入

pixiv(WEB)

導入で

Private API

デプロイ刷新

導入

pixiv PHP5.5

pixiv PHP7.1

pixiv FANBOX

Symfony

pixiv(β)

リポジトリ統合

開発

Lime

開発

全面導入

公開

のCI導入

アプリ専用API開発

共通クラス自動ロ┃ド

最高便利

独立リポジトリを合体

小説モバイル公開

社内フレームワーク(PHPCon2013)

リバ┃スル┃ティング導入

リニュ┃アル

ActiveResource

アトミック

PEAR依存廃止

リニュ┃アル

(Rails)

規則的URL生成

正規表現ベ┃スのLint導入

スマ┃トフォン版ドメイン統合

リニュ┃アル

PA化進行

@tadsanが血走ってフレームワーク作る

使用技術の変遷で見る年表

ただしtadsan入社前後に限る

include

(touch.pixiv.net)

地獄

Page 14

お断り

今回の発表で言及されないものは概ね時間の制約で取捨選択されたか忘れてるだけなので、懇親会やTwitterで@tadsanにきいて

Page 15

ふつうのPHP #とは

Page 16

pixivのPHP

  • PHPはApache+mod_php (一時fpmを使ってた箇所あり)
  • フロントのnginxから、リバースプロキシしてApacheにリクエスト
  • むかしながらのDocumentRootにある .php ファイル = URL
  • 例: https://www.pixiv.net/member.php?id=105589
  • LinuxとかMySQLとか、よくあるLAMPのスタックに載ってる

Page 17

2012年のコードのイメージ

include_onceいっぱい

エラーハンドリング(してないページもあった)

<?php // www.pixiv.net/htdocs/hoge.php require_once __DIR__ . '/../inc/bootstrap.php';include_once INC_PATH . '/Hoge/Fuga.php';include_once INC_PATH . '/Hoge/Piyo.class.php';try {display()} catch (Exception $e) {error::exception_error($e);}

この下にもいっぱい

ファイルローカルな

function display() {// ...

グローバル関数

Page 18

2015年のコードのイメージ

<?php // www.pixiv.net/htdocs/hoge.php require_once __DIR__ . '/../inc/bootstrap.php';

include_once一個だけ

AppRunner::execute(new Www_HogeController);

このクラスは自動ロードされる

Page 19

AppRunner (名前の由来は知らない)

<?php

whoops

final class PCAppRunner {public static function execute(Controller $controller) {try {Controller_Util::turnOnWhoops();Controller_Util::redirectToHttps();

パソコン版

$controller->main();} catch (\Throwable $exception) {PCAppRunner::setHttpStatus(500);Controller_Util::displayWhoops($exception);

Page 20

AppRunnerとエラーハンドリング

  • サービスごとの例外ハンドリングはAppRunnerが受け持つ
  • whoops! はエラー処理用のフレームワークなので、これだけを使って

例外/エラー処理を完結させることも可能ではあるが、割と複雑になるので、開発環境でエラー画面を表示するだけの目的に専念してる

  • エラーログはPHPの標準機能は利用せずfile_put_contents()でログファイル

に書き込んでる

  • whoops! とエラーログの話はWEB+DB PRESS Vol.96 に書きました

Page 21

2018年のコードのイメージ

htdocs/hoge.phpは消滅

URLとファイルは切り離され

マップの一要素に

全URLが1ファイルに

final class Controller_WwwRoutes {public static function getRoutes() {$route_map = ['/' => ['action' => function () {Www_IndexController::main();},],'/hoge.php' => ['action' => function () {Www_HogeController::main();},

Page 22

PHPカンファレンス2017で発表

Page 23

命名規則

  • クラス名はPSR-0的な _ を使った擬似名前空間
  • 組織内で一貫してればPSRに拘る必要がない
  • 「PSRの誤解」 https://qiita.com/tadsan/items/942a381e952e12a8fa5a
  • 基本はstaticで、クラスはインスタンス化せずに利用
  • クラスを擬似的な関数・定数置き場にしている
  • どうしてこんなことを続けてるのかは去年のLTで話した

Page 24

去年の当日募集LTで話した

Page 25

開発しやすさのための取り組み

Page 26

PHPで開発しにくいところ

  • クエリパラメータのハンドリング
  • PDOの機能の貧弱さ
  • テンプレートエンジンとURLの問題
  • Railsにあるようなカッコイイ機能がない
  • かっこいいエラー表示
  • 対話環境 (rails console)

Page 27

クエリパラメータの問題

$id = $_GET['user_id'];if (is_numeric($id)) User_Common::getById($id);else error("不正な入力です");

  • こういうコードを書いてはいけない
  • クエリパラメータは数字ではない値が入ってくる可能性
  • 入力が空、入力が任意の文字列、入力が不正な数字列、入力が配列
  • filter_input() をかけると安全にはなるが、それでも面倒

Page 28

  • ParamHelper

$id = ParamHelper::getPositiveInt('id');User_Common::getById($id);

  • クエリパラメータ (またはform) から値を取り出すヘルパー
  • 未入力や不正な入力があると例外を投げる
  • 前述のAppRunnerでキャッチしてエラー画面を描画する

Page 29

  • ParamHelper

$mode = ParamHelper::getEnum('mode', ['hoge', 'fuga'], ['post' => 'only']);

  • ?mode=hoge または ?mode=fuga のみを期待するようなパターン
  • 不正な形式や ?mode=piyo のような期待しない入力で例外を投げる

Page 30

テンプレートとURL生成の問題

<a href="{$smarty.const.SYSTEM_URL_WWW|escape}member_illust.php?id={$user.id|escape}">{$user.name|escape}</a>

  • こういうコードを書くと事故が入り込みやすい
  • 純粋にtypoの危険性
  • idなどのパラメータに不正な値を入れるリスク

Page 31

  • ReverseRoute

<a href="{reverse_route page='fullWwwMemberProfile' id=$user.id}">{$user.name|escape}</a>

  • あるページに名前をつけて、reverse_route関数に page 引数で渡す
  • 生成結果は変更前と同じ
  • ルーティングの逆関数にあたるので、一部のフレームワークは

ReverseRoutingやURLヘルパーなどの名前でサポートしてる

Page 32

  • ReverseRoute

/*** @route\example https://www.pixiv.net/member.php?id=12345 {id: 12345}* @route\example https://www.pixiv.net/member.php?id=12345&utm_source=xxxxx{id: 12345, utm_source: "xxxxx"}*/public static function fullWwwMemberProfile(array $params){Util_Assert::num($params['id']);

return ReverseRoute::buildUrl(SYSTEM_URL_WWW, '/member.php', ['id'],$params);}

Page 33

blogに書きました

https://devpixiv.hatenablog.com/entry/2016/10/25/093000

Page 34

PDOの問題

  • SQLは自動生成じゃなくて手で書きたい…

でもPDOで複雑なクエリを書こうとすると文字列結合が避けられない…

  • PDOで書きにくいクエリの代表例: IN
  • もう ? を並べてインデックスをインクリメントしながらbindValueはやだお
  • PDOのクエリのplaceholderに ? を書くか :hoge で書くか問題
  • 型チェックできるように書くのもちょっとめんどう

Page 35

PxvSql

Page 36

PxvSql

  • 文字列結合を一切やらなくてもクエリが書けるようになった
  • 実行時に型検査をするのでnullなどの不穏な値は入り込まない
  • エラーチェックのボイラープレートがグッと減る
  • SQLi(脆弱性)のリスクなし (ただしmysqlndの実装の不備はないと信じる)
  • :hoge@int で整数の埋め込み、 :fuga_ids@int[] で複数の整数を展開する
  • %if や %for の記法で条件分岐や複数SETなども可能
  • 問題意識はQiitaに書いた https://qiita.com/tadsan/items/e615a779baa6eabdab47

Page 37

PDOのラッパー

  • これは、少なくとも2011年以前からあった仕組み
  • 書き込み用(master)と読み込み用(slave)の系統を明示的に分けて利用可能
  • slaveにINSERTやUPDATEなどのクエリを発行しようとすると例外
  • 全クエリにSQLのクエリ単一行にして、トレースをコメントにして埋め込む
  • スロークエリのログにどのコントローラに起因するか集計しやすい
  • スロークエリを一覧できるビューアーもある

Page 38

新機能をリリース時にどうする?

  • A: 新機能が実装されたブランチをリリースするときマージする
  • B: リリース前にあらかじめマージして、実行されないようにしておく
  • よくある問題
  • Aは機能の規模が大きいとビッグバンマージを引き起こし、

予期せぬ問題を引き起こすことがある

  • 理想としてはBで、一般ユーザー向けに実行されないとしても

鮮度のよいうちにマージしておきたい (リリース時の変更を最小限に)

Page 39

A/Bテスト/機能の有効化/無効化

if ($_SERVER['REMOTE_ADDR'] === OFFICE_IP) {hogehoge();}

  • 新しい機能をサービスに実装して、社内でだけ有効化したいとする。
  • 無造作にこんなコードを書かれると後から意味不明になる
  • そもそも社外と社内での挙動が別物になるので嫌な予感しかしない

Page 40

DevToggle (A/Bテストフレームワーク)

if (ABTest_DevToggle::isEnabledDevToggle('hogeFunc')) {hogehoge();}

  • 専用のコンソール画面でボタンを押すことで有効化/無効化される
  • 設定を一行足すだけで一般ユーザーに対してリリースできる
  • リリース後に障害などが発生しないことが確認できたらifを消す
  • 元はA/Bテストのための機構だった (一般ユーザーに対して確率で適用)

Page 41

まとめ

Page 42

何もない野原に秩序をつくる

  • べんり機能を導入すると、部分的に生産性が体感で数倍になったりする
  • 一からサービスを作るとしたら同じことを繰り返せるかは、悩む
  • いまだったらLaravelを入れるかもしれない
  • ただ、それは2018年のチート未来人だからできる発言
  • サービスの価値はコードではなく、その結果のユーザー体験
  • フレームワークがなかったとしても、

pixivが2007年からユーザーに価値を提供してきた結果は変らない

Page 43

続きはWebで (あとWEB+DB PRESS)

  • ニコナレ https://niconare.nicovideo.jp/users/5962
  • Real World PHP in pixiv改
  • pixiv inside
  • DocCommentでPHPのユニットテストの書きやすさを劇的に改善する手法
  • WEB+DB PRESS連載『PHP大規模開発入門』を振り返る
  • pixivの基盤ノウハウ大公開!PHPカンファレンス2017登壇レポート

Page 44

続きはWebで (あとWEB+DB PRESS)

  • Qiita
  • 憂鬱なSQLのためのアレ、またはPDOと仲良くして枕を高くしてねむる
  • PSRの誤解
  • インスパイヤされて掲示板を作りたくなった シリーズ
  • includeって書きたくない僕たちのためのオートローディングとComposer
  • GitHub
  • whoopsやエラー処理の例 https://github.com/zonuexe/wdb-php-96-sample

Page 45

続きはWebで (あとWEB+DB PRESS)

  • これ一冊を購入すれば歴代のPHP連載が全部

読めるのでおすすめ

  • まとめ https://inside.pixiv.blog/tadsan/3991
  • 今回の内容に近いものだと
  • vol.81, vol.87, vol.91, vol.94, vol. 96
  • 買って読んでね! Webにサンプルコードもあるよ