Skip to content

ふつうのpixivのつくりかた

公開日:

オンラインZoom会議で開催された『【PHP Tech Talk】PHPの今を語る!4社合同勉強会』で15分枠として発表しました。

Download PDF

スライドテキスト

Page 1

ふつうのpixivのつくりかた

An introduction to building pixiv for ordinary PHPers

pixiv Inc.

USAMI Kenta

2023-07-26 PHP の今を語る!4社合同勉強会

Page 2

お前誰よ

  • うさみけんた (@tadsan) / Zonu.EXE / にゃんだーすわん
  • ピクシブ株式会社 pixiv事業本部 Webエンジニアリングチーム PHPer
  • 2012年末から現職、APIとかCIとかいろいろなところを見つめてきました
  • 最近はピクシブ百科事典(dic.pixiv.net)も開発しています
  • Emacs PHP Modeを開発しています (2017年-)
  • プログラミング言語にちょっとこだわりのある素人

Page 3

PHPカンファレンス沖縄2023

Page 4

LLイベント

Page 5

の紹介

Page 6

pixiv.netとは

  • “好き”に出会えるイラスト・マンガ・小説作品の投稿プラットフォーム
  • 2007年9月10日に開始
  • 累計登録ユーザー数: 約9300万ID
  • 累計作品投稿作品数: 約1.2億作品 (うち小説1900万作品)
  • イラストブックマーク: 約200億件
  • Web・スマートフォンアプリで展開

Page 7

ThePHPF

Page 8

OSPO

Page 9

ふつう #とは

Page 10

フレームワークが使われていない

Page 11

ただのPHP

Page 12

※諸説あります

Page 13

諸説ありますが難しい構造にはなってない

Page 14

メタプログラミングは基本的にあまりやらない

Page 15

(こっそり使ってる部分もありますが大筋は追えるはず)

Page 16

PHP詳しくない人でも雰囲気に従って書けばセキュリティの問題は起こさないように

Page 17

pixivのPHP

  • PHPはApache+mod_php (一時fpmを使っていたこともある)
  • nginxからリバースプロキシしてApacheにリクエスト
  • PHPそのものは独自でビルドしてはいるがプレーン
  • DocumentRootにある.phpファイルから処理を辿っていけば全部わかる
  • LinuxとかMySQLとかよくあるLAMPスタックに載っている

Page 18

よくある質問

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

Page 19

10年前のコードのイメージ

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 20

2015年のコードのイメージ

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

include_once一個だけ

AppRunner::execute(new Www_HogeController);

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

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

2023年こうなりたい

... ですっきり

<?php // WwwRoutes.php public static function getRoutes() {return ['/' => Www_View_IndexController::main(...),'/hoge' => Www_View_HogeController::main(...),'/.well-known/change-password' => fn() =>Util_Http::redirect_and_exit(ReverseRoute::fullAccountsPasswordChange([])),],

Page 24

コントローラをdispatchする処理

<?php

whoops

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

パソコン版

$action();} catch (\Throwable $exception) {PCAppRunner::setHttpStatus(500);Controller_Util::displayWhoops($exception);

Page 25

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

Page 26

CI (Jenkins, GitLab CI)

  • GitLabにブランチをpush、MergeRequestを作るとCIが走る
  • PHPUnit、PHPStanと独自の正規表現ベースのlinterでチェック
  • PHPStanは2018年から運用開始
  • 時間帯にもよるけどpushして10分かからずに結果がコメントされる

Page 27

PHPで開発しにくいところ

  • 外部入力(クエリパラメータ・フォーム・JSON)の取得・検証
  • PDOの機能の貧弱さ
  • テンプレートエンジンとURLの問題
  • RailsやLaravelにあるようなカッコイイ機能がない
  • かっこいいエラー表示
  • 対話環境/REPL (rails console)

Page 28

クエリパラメータの問題

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

  • こういうコードを書いてはいけない
  • クエリパラメータに数字ではない値が入ってくる可能性
  • 入力が空、入力が任意の文字列、入力が不正な数字列、入力が配列

filter_input() をかけると安全にはなるが、それでも面倒

Page 29

ParamHelper

$id = ParamHelper::get('id')->asPositiveInt();$user = User_Common::getById($id);

  • 外部入力から値を取り出すヘルパー
  • クエリパラメータ・フォーム・URL・JSONなどに対応・拡張可能
  • 未入力や不正な入力があると例外を投げる
  • キャッチしてエラー画面を描画する

Page 30

ParamHelper

$mode = ParamHelper::get('mode')->asStringInArray['hoge', 'fuga']);

  • ?mode=hoge または ?mode=fuga のみを期待するようなパターン
  • 不正な形式や ?mode=piyo のような期待しない入力で例外を投げる
  • PHPStan拡張で 'hoge'|'fuga' という型をつけている
  • 型安全と実行時安全の両立! (PHPカンファレンス沖縄で話します)

Page 31

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

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

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

Page 32

  • ReverseRoute

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

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

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

Page 33

  • 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 34

  • Attribute最高!!

#[RouteExample('https://www.pixiv.net/member.php?id=12345', ['id'=> 12345])#[RouteExample('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 35

blogに書きました

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

Page 36

PDOの問題

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

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

  • PDOで書きにくいクエリの代表例: id IN (1, 2, 3)
  • もう ? を並べてインデックスをインクリメントしながらbindValueはやだ…
  • PDOのクエリのplaceholderに ? を書くか :hoge で書くか問題
  • 型チェックできるように書くのもちょっとめんどう
  • だからテンプレートエンジンを作ろう → PxvSql

Page 37

PxvSql

Page 38

PxvSql

  • 文字列結合を一切やらなくてもクエリが書けるようになった
  • 実行時に型検査をするのでnullなどの不穏な値は入り込まない
  • エラーチェックのボイラープレートがグッと減る SQLi(脆弱性)のリスクなし
  • :hoge@int で整数の埋め込み、 :fuga_ids@int[] で複数の整数を展開

する %if や %for の記法で条件分岐や複数SETなども可能

Page 39

PxvSql

  • 問題意識はQiitaに書いた

Page 40

TetoSQL

  • tadsanが個人の責任で再実装したバージョン (PHP≧5.4)
  • https://github.com/BaguettePHP/TetoSQL
  • 記法を自由に拡張できるように作られている
  • 各社で必要な記法や型を柔軟に実装することも可能!

Page 41

PDOのラッパー

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

リを一覧できるビューアーもある

  • 最近ではDatadogのAPMを使えば不要になるかもしれない

Page 42

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

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

予期せぬ問題を引き起こすことがある (コンフリクト解決が大変)

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

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

Page 43

機能の有効化/無効化

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

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

内での挙動が別物になるので嫌な予感しかしない

Page 44

フィーチャートグル

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

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

Page 45

まとめ

Page 46

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

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

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

Page 47

まとめ

  • 今回の内容はPHPカンファレンス関西2018で発表したスライド
  • 2018年以来、良くも悪くもAPIが非常に安定している
  • そろそろPHP 8.2に全面移行できるので、次のスタンダードを考えたい
  • AttributesはPHP 8に全面移行できていなくても使えるので先行導入
  • pixivで開発者体験と安全性のフォーカスしたアプリケーションを

いっしょに開発していきましょう!!!!

Page 48

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

  • pixiv FANBOX @tadsan
  • pixiv inside
  • DocCommentでPHPのユニットテストの書きやすさを劇的に改善する手法
  • WEB+DB PRESS連載『PHP大規模開発入門』を振り返る
  • pixivの基盤ノウハウ大公開!PHPカンファレンス2017登壇レポート

Page 49

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

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

Page 50

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

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

読めるのでおすすめ

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