Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Composerオートロードと重複する冗長なrequire_onceを静的解析で自動検出する
Search
しゅんぺい
October 03, 2026
Programming
78
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Composerオートロードと重複する冗長なrequire_onceを静的解析で自動検出する
https://github.com/lll-lll-lll-lll/depone
しゅんぺい
October 03, 2026
Other Decks in Programming
See All in Programming
Railsバージョンを4倍にした話
optim
0
460
Kotlinの15年とServer-Side Kotlin
n_takehata
1
150
UPDATE をやめる — EF Core でマスタをバージョン管理する
panda728
PRO
0
1.2k
Agents on Rails - Rails at Scale 2026
irinanazarova
0
570
Herb in Rails 8.2: Your ERB views, now HTML-aware @ Rails World 2026, Austin, Texas
marcoroth
0
220
GemmaをJevのように使ってみる / Use Gemma like Jev
kishida
5
800
月間100万人のサービス刷新で、 技術選定をHonoに振り切った話
yutobara
8
6.7k
PumaからFalconに移行したRailsアプリケーションでの実践
falcon8823
0
180
Event is what happens
moro
2
860
AI時代のコードレビューは人に向けるな、仕組みに向けろ
texmeijin
5
3.7k
気持ちぃ〜角丸 Squircle
ryu1013
0
320
S3 TablesのIcebergデータに、AIエージェントからアクセスしてみた / 20261016developersio2026joetsu-accessing-iceberg-in-s3tables-via-agent
kasacchiful
PRO
0
170
Featured
See All Featured
Digital Ethics as a Driver of Design Innovation
axbom
PRO
1
460
Bioeconomy Workshop: Dr. Julius Ecuru, Opportunities for a Bioeconomy in West Africa
akademiya2063
PRO
1
410
Thoughts on Productivity
jonyablonski
76
5.4k
Agile that works and the tools we love
rasmusluckow
331
22k
The World Runs on Bad Software
bkeepers
PRO
72
12k
Mind Mapping
helmedeiros
1
400
Leveraging LLMs for student feedback in introductory data science courses - posit::conf(2025)
minecr
1
440
How to make the Groovebox
asonas
2
2.5k
Applied NLP in the Age of Generative AI
inesmontani
PRO
4
2.5k
KATA
mclloyd
PRO
35
16k
AI in Enterprises - Java and Open Source to the Rescue
ivargrimstad
0
1.5k
Templates, Plugins, & Blocks: Oh My! Creating the theme that thinks of everything
marktimemedia
31
2.9k
Transcript
Composerオートロードと重複する冗⻑な require_onceを静的解析で⾃動検出する PR TIMES 中⼭舜平 PHPカンファレンス愛媛2026
自己紹介 name: shunpei nakayama job: ソフトウェアエンジニア company: PR TIMES
今⽇話すこと • ツールを作った背景 • 削除候補を絞る仕組み • 実装して分かった難しさ
⼤量のrequire_onceが残っていた require_once __DIR__ . "/../../setting.php"; require_once __DIR__ . "/../environment.php"; require_once
__DIR__ . "/../config/constants.php"; require_once __DIR__ . "/../../vendor/autoload.php"; if (!defined('CLASS_DIR_PATH')) { define("CLASS_DIR_PATH", BASE_DIR_PATH . "classes/"); } require_once CLASS_DIR_PATH . "Example.php";
Composerに任せられる読み込みは消したい 最⼩限のrequire_onceだけにしたい require_once __DIR__ . "/../../environment.php"; require_once __DIR__ . "/../../vendor/autoload.php";
$example = new Example();
こんな⼿作業コードジャンプはやってられない require_once CLASS_DIR_PATH . 'Example.php'; CLASS_DIR_PATH にコードジャンプする ↓ define("CLASS_DIR_PATH", BASE_DIR_PATH
. "classes/"); CLASS_DIR_PATH を定義するために値の定数にさらにコードジャンプ する ↓ define("BASE_DIR_PATH", "/main/");
こんな読み込み先を特定する作業もやってられない front/init.php require_once '../web.php'; $example = new Example(); ↘ 読み込み先を開く
web.php require_once __DIR__ . '/common.php'; ↘ さらに読み込み先を開く common.php require_once __DIR__ . '/../vendor/autoload.php'; require_once CLASS_DIR_PATH . 'Example.php';
部分的にでも静的に重複を⾒つける⽅法はないのか?
require_onceの読み込み先を⾒つけられそう? ディレクトリの絶対パスを返す ファイル名 /app/src/main.php require_once __DIR__ . '/Example.php'; token_get_all関数でコードを解析して T_REQUIRE_ONCE
トークンを抽出できる パス結合の結果 /app/src/Example.php
Composerの読み込み先も⾒つけられそう? Composerのローダー クラス名 $loader->findFile('App\\Example'); findFileメソッドでクラス名から Composerの読み込み先のパスを 取得できる /app/src/Example.php
静的に重複を⾒つけられそう?
require_onceとComposerの読み込み先のパスが ⼀致するか確認できれば静的に重複判定できるのでは?
重複を検出するdeponeを作りました vendor/bin/depone redundant_require_once=2 削除候補は2件 読み込み先ファイル public/index.php:4 => src/Greeting.php public/index.php:5 =>
src/Legacy/MailerGreeting.php require_onceがあるファイルと行番号
deponeは3段階の静的解析で削除候補を絞る PhpTokenでrequire_onceを解析する パス式を静的評価して読み込み先を特定する php-parserで読み込み先のPHPをAST解析する 宣⾔された型名とクラス外の構⽂を取得する ComposerのfindFile()で照合する Composerでも同じファイルが解決されるか確認する
require_onceとComposerの読み込み先を確認する クラス名からComposerの読み込み先と元のファイルを⽐較する $resolved = $resolver->resolve($requireOnceClassName); // Composerの読み込み先を問い合わせる if ( $resolved
!== null //パス表記を揃え⽐較する && PathHelper::normalize($resolved) !== $normalizedTarget // 同じファイルか確認 && !$this->sameRealFile($resolved, $normalizedTarget) ) { // 別ファイルなら削除候補外とする }
1クラスの⼀致だけでは削除候補にできない 1クラスが⼀致しても同じファイル内の別クラスまで同じファイルに解決されるわけで はないので全てのクラスを確認する必要がある $classNames = $classExtractor->extractTopLevel($file_content); $allRoundTrip = true;// 全てのクラスで、Composerの読み込み先が元のファイルに⼀致する
か foreach ($classNames as $className) { $resolved = $resolver->resolve($className); // ..... } // 読み込み先の⼀致確認 if ($resolved === null) { $allRoundTrip = false; }
全てのクラスが⼀致してもrequire_onceはまだ消せない Composerのオートロード対象は関数呼び出しでは発⽕しない // ReportGenerator.php class ReportGenerator {} function format_report_title() {}
// クラスがまだ読み込まれていない状態で実⾏。 // 他からの読み込みはない前提 format_report_title(); // 未定義エラー
require_onceは⽂に到達したときにファイル全体を読む オートロードは未定義のクラスが必要なときにファイル全 体を読む
クラス定義以外のトップレベル構⽂を確認する クラス外の関数宣⾔や実⾏処理を含むファイルを候補から外す foreach ($this->topLevelStmts($stmts) as $stmt) { $allowed = $stmt
instanceof ClassLike // オートロードで読み込みたい対象 || $stmt instanceof Use_ // 単体ではファイル読み込み、関数実⾏は起きない || $stmt instanceof GroupUse // use App\{Foo, Bar}; のようなまとめた use宣言 } || $stmt instanceof Declare_ // コード設定のため || $stmt instanceof Nop; // 何も実⾏しないから if (!$allowed) { return false; }
require_onceの読み込み先のファイルを調べ Composerでも同じファイルが読めることと クラス外の関数宣⾔や実⾏処理がないことを確認できた
削除候補を絞り込むことができましたが どの順序で読み込まれているかを⾒て 安全か確認したい
読み込み元をたどりエントリーポイント候補を探す foreach ($edges as $edge) { $this->callers[$edge['to']][] = $edge['from']; }
private function trace(string $file): void { $callers = $this->callers[$file] ?? []; if ($callers === []) { $this->entrypoints[] = $file; return; } foreach ($callers as $caller) { $this->trace($caller); } } $edge にはrequire_once の 静的解析で得た「どのファイル が、どのファイルを読み込むか」 という情報が入っている 他のファイルからの読み込みがな い場合はエントリーポイントとする 読み込み元をさらにたどる
別のエントリーポイントで静的に解決できない ケースもある
静的に解決できなかった箇所の理由を出⼒する // require_once の後ろにあるパスの式を取り出す $exprTokens = $this->includeExprParser ->readIncludeExprTokens($tokens, $i); //
require_once の式を静的に評価し、パス⽂字列を求める $raw = $this->includeExprParser ->evalStaticExpr($exprTokens, $consts, $absolutePath); // パス⽂字列を求められなければ、該当箇所‧ // パスを静的に解析できない理由(実⾏時に決まる値など)‧元の式を記録する if ($raw === null) { $unresolved[] = [ 'file' => $relativePath, 'line' => $line, 'reason' => $this->includeExprParser ->classifyUnresolvableReason($exprTokens), 'expr' => TokenHelper::tokensToString($exprTokens), ]; continue; }
どんなケースを静的に判断できなかったか
別ファイルで定義した定数なら? パス式をASTに変換して既知の定数を使って評価する 別ファイルの定数は引き継いでいないため値を特定できない // config.php define('APP_DIR', __DIR__ . '/src/'); //
report.php require_once 'config.php'; require_once APP_DIR . 'ReportGenerator.php';
同じファイルで代⼊していれば分かる? ASTから配列への参照は分かるが⼊っている値は分からない 前の代⼊から値を引き継ぐには値の変化を追う必要がある $config["kind"] = "Report"; // ..... require_once(APP_DIR .
"{$config["kind"]}.php");
735件中50件を削除候補として静的に絞り込めた
⼿作業でたどる調査を減らせた • require_onceの式を読み解いて読み込み先のパスを特定する • 宣⾔された全型がComposerでも同じファイルに解決するか調 べる • 読み込み元をたどってエントリーポイント候補を探す
まとめ • Composerと重複するrequire_onceを静的に絞り込むことはで きそうなことがわかった • 単純に読み込み先が同じなら削除するようなシンプルな実装で は実⾏順序に依存して壊れてしまう可能性がある • 読み込み経路と静的に解決できなかった箇所を出⼒し削除前に 確認すべき箇所を絞り込んで安全か確認できるようにした
None