Help us understand the problem. What is going on with this article?

初心者のためのプログラミングことはじめ〜ファイルを分ける〜

More than 1 year has passed since last update.

目次

本稿の目標

  • なぜファイルを分けるのかを知る
  • フレームワークが何をしてくれるのかわかる

ファイルを分けよう

前回作成したcontinuous_attack.phpが、表示したいものに対してちょっと複雑じゃないかと思えていないだろうか。
出力したいのは単純なhtmlなのに、phpでいろいろやっている部分がどうしても目についてしまう。
画面をもっとグラフィカルにしたいとき、htmlの変更が面倒くさくなりそう。
そんな思いはないだろうか。
そこで、思い切ってphp部分とhtml部分を分けてみよう。

サンプルコード

以下2つのファイルにする。

attack.php
<?php
/**
 * 敵が死んでいるか判断
 *
 * @return boolean 敵が死んでいればtrue
 */
function is_dead($enemy_life)
{
  return $enemy_life <= 0;
}

/**
 * 敵を倒した場合のみメッセージを表示する
 *
 * @return string 倒したときのメッセージ
 */
function get_message_if_defeat_enemy($enemy_life)
{
  $message = '';
  if (is_dead($enemy_life)){
    $message = '<strong>敵を倒した</strong>';
  }
  return $message;
}

/**
 * 攻撃を実行し、メッセージを表示する。
 *
 * @return int 攻撃後の敵ライフ
 */
function attack_with_message($enemy_life)
{
  $damage = 0;
  if (is_dead($enemy_life)){
    echo '敵はもういない';
  } else {
    $damage = rand(5,10);
    echo $damage . 'のダメージを与えた!';
    echo get_message_if_defeat_enemy($enemy_life);
  }
  return $enemy_life - $damage;
}
continuous_attack.php
<?php require_once('attack.php') ?>
<?php $attack_times = $_REQUEST['times']; ?>

<html>
  <head>
    <title>あなたの連続攻撃</title>
  </head>
  <body>
    <h1>あなたの連続攻撃</h1>
    <?php $enemy_life = 20 ?>
    <?php for($current = 0; $current < $attack_times; $current++) { ?>
      <p>あなたの攻撃!(<?php echo $current + 1?>回目)</p>
      <p style="padding-left: 2em;">
        <?php $enemy_life = attack_with_message($enemy_life) ?>
      </p>
    <?php } ?>
  </body>
</html>

注意点

attack.phpの最後は<?phpを閉じるための?>つけないこと。
ややこしい事情があるので詳述しないが、「そういうもの」だと思ってほしい。
具体的にはhtml中でphpの処理を呼びたいときのみ、終了を示すために閉じるものと思っていただければ当面は差し支えない。

解説

2つのファイルに分かれただけで、コード上の変化はほとんどない。

require_once('attack.php')

で、他のファイルを参照することを宣言しているわけだ。
本当にそれだけの内容なので、今回は覚えることがほとんどない。

ついでに

魔法攻撃も作ってしまおう。

magic_attack.php
<?php require_once('attack.php') ?>

<html>
  <head>
    <title>あなたの魔法攻撃</title>
  </head>
  <body>
    <h1>あなたの魔法攻撃</h1>
    <?php $enemy_life = 20 ?>
    <p>魔法を唱えた!</p>
    <p style="padding-left: 2em;">
      <?php $enemy_life = attack_with_message($enemy_life) ?>
    </p>
  </body>
</html>

http://localhost:8000/magic_attack.phpで魔法攻撃だ。
image.png

さすがに手抜き感が否めないが、まあよしとしよう。
ここで感じ取ってほしいことは、同じ処理を使い回す1ことで、お手軽に攻撃パターンを増やしたことだ。
このように、再利用性の観点からも、ロジック部分は分離しておくことが望ましい。

まとめ:なぜファイルを分けるのか

導入文で書いたように、処理が複雑になると表示したいものとプログラム的な処理が混在することによるデメリットが生まれてくる。

  • 画面をちょっといじりたいのにコードに惑わされる
    • フロントだけ触りたい人(フロントエンドエンジニア、デザイナ)に余計な情報を見せて混乱させてしまう
  • ロジックの再利用性を高めるため
    • 似たような処理がほしくなることはよくある
    • その都度コピペしていたら、ロジックを変更したいときに全部書き換えるのはつらい

ちなみにこの連載の目次も、最初のうちは各記事に作っていて1記事投稿するたびに全ての目次を更新していた。
記事が増えるたびに更新する記事が増えてしまうので、目次として1記事きりだすことで更新の手間を省いたのだ。
読みやすさを犠牲としたが、エンジニアとしてそのような手間は許容しがたかったためである。

💩フレームワークを作ろう

今回はファイルを別に分けるだけの話なのだが、もう一歩踏み込んで実践的な事を学んでいただこう。
おそらくWeb開発に携わる多くの人は、何らかのフレームワークを利用しているかと思われる。
フレームワークの概要として「ここに書けばこういう風に使える」くらいの知識を与えられて開発して、フレームワークとはなにやら便利なものくらいの認識の人はいないだろうか。
もちろん、それでかまわないことも多い。
内燃機関の仕組みを知らなくてもアクセルを踏めば自動車は動き出すし、TCP/IPを知らなくてもインターネットは便利だ2
しかし、これを読んでいただいている方々は、少なくともWebに関しては、作り、提供することを望んでいるのだ。
エンジンの仕組みがわからないというディーラーに愛車を任せられないように、あなたにも是非フレームワークの仕組みを理解していただきたい。

概要:Webフレームワークがすること

MVCとかコンポーネントモデルとか小難しいことは置いておいて、現在主流なフレームワークは

  • ユーザのリクエストにしたがって、必要なものを特定したい
  • よく使うものは簡単に使いたい
    • データベースは大体のWebで使うよね
    • 表示する内容の一部も使い回したいよね
  • 管理しやすい方がいい
    • どのURLがどのファイルになるのかわかりやすくないと

みたいな要件に対応している。
必ずしもこれがベストというわけではないと信じているが、多くのWebアプリケーションが望んでいるものを汎用的に実現した結果がフレームワークなのだ。

実現方法

本稿は「ファイルをわける」セクションであることを思い出してもらいたい。
ざっくり言えば、ファイルを分けて、ユーザのリクエストにしたがって必要なファイルをかき集めてくることで、上記のような要求が実現できるのだ。
発想の転換をしよう。
今までは見せたいページがあって、そこに必要な部品を積み上げてきたのだが、先に構造を作っておいて、部品を後から入れることで最終的なものができるようにする。

image.png

複雑になったのではないかと思うかもしれないが、多くのものを管理するに従い、複雑さを解消することができる。
理屈をこねるよりも実際に経験した方が納得いくものなので、なかなか伝えづらいものかもしれない。

仕様を考える

フレームワークの仕様を考えるというのは、アプリケーション(データとコード)をどうやって管理するかを考えるに等しい。
王道がMVCであり、MVVMであったりするわけだ。
そしてそれらのフレームワークに則ると様々な用語が生まれてくる。
モデル、ビュー、コントローラ、ビューモデル、アクション、永続化データ……。
今回の主眼は管理しやすいフレームワークを学ぶことではなく、フレームワークがどのような方法で作られるのかの一端を見に行くことなので、簡素なもので学んでみよう。

要件

  • リクエストによって処理が変わる
  • データは決められた形で決められた場所に置けばすぐに利用できる
  • 全体で共有したいデータやロジックと、一部で共有したいデータ・ロジックが棲み分けできる
  • htmlはシンプルに作れて、htmlから利用したい機能を別で定義できる
  • htmlの中でいちいち必要な機能をrequire_onceとか書かないで良い

仕様

  • 目的の似た画面をfeatureとしてまとめて管理する
  • featureの中に複数のpageを作れる
  • featureの中のpageは、featureに定義した機能を全て利用できる
  • 全体で共通のデータと、featureのみで有効なデータを分けられる
  • featureをまたいでの機能/データ参照はしない

管理方法は以下の様なフォルダ構成とする

image.png

featureで必要な機能はprepareで実装するという寸法である。
そして実際に表示されるhtmlはpagesに用意しておく。

サンプルコード

そしてできたフレームワークとアプリケーションがこうなっている。
image.png

www直下の各ファイルはindex.phpを除いて、今までのサンプルのなごりなので必須というわけではない。
それでは各ファイルのサンプルコードを見てみよう。
今回は初の関数や文法も多いので、全てを理解するのは難しいかもしれない。
そんなときには、まず写経のつもりで続けよう。
しかも、配列を多用しているのでデータの状態が見えないのは仕方ない。
配列に関してはとりあえず触って感触をつかんでからという意味で、次稿での解説予定としている。
もう少し耐えてみてほしい。

フレームワーク部分

index.php
<?php

/**
 * 保存されているデータ
 */
$GLOBALS['StoredData'] = [];

/**
 * libフォルダにあるphpファイルを全て読み込む
 */
function load_utilities()
{
  $utilities = glob('./lib/*.php');
  foreach ($utilities as $util_file){
    require_once($util_file);
  }
}

/**
 * 保存されたデータファイルを読み込む
 * 読み込むファイルの形式は、1行1データで、各行が
 * key=value
 * という形式のもの
 * 
 * $file [string] 読み込むファイル名
 */
function load_data($file)
{
  $content = file_get_contents($file);
  $lines = explode(PHP_EOL, $content);
  foreach($lines as $line){
    $key_value = explode('=', $line);
    $key = $key_value[0];
    if (!isset($key)) continue;
    $value = $key_value[1];
    $GLOBALS['StoredData'][$key] = $value;
  }
}

/**
 * どのページでも利用可能なデータを読み込む
 */
function load_global_data()
{
  $data_files = glob('./data/*.dat');
  foreach ($data_files as $file){
    load_data($file);
  }
}

load_global_data();
load_utilities();
render_page();
lib/route.php
<?php
/**
 * リクエストされたフィーチャー固有のデータを読み込む
 */
function load_feature_data($feature)
{
  $data_files = glob('./features/'.$feature.'/data/*.dat');
  foreach ($data_files as $file){
    load_data($file);
  }
}

/**
 * リクエストされたフィーチャーに必要なphpファイルを読み込む
 */
function load_feature_preparation($feature)
{
  $scripts = glob('./features/'.$feature.'/prepare/*.php');
  foreach ($scripts as $script){
    require_once($script);
  }
}

/**
 * リクエストされたURLから要求されているフィーチャーとページを特定する。
 * 
 * return array('feature' => feature, 'page' => page)
 */
function parse_request()
{
  $request_path = parse_url($_SERVER['REQUEST_URI'])['path'];
  $divided_path = array_filter(explode('/', $request_path), 'strlen');
  $ret = [];
  $ret['feature'] = array_shift($divided_path);
  $ret['page'] = array_shift($divided_path);
  return $ret;
}

/**
 * リクエストされたURLで必要なページをレンダリングする
 */
function render_page()
{
  $request = parse_request();
  $feature = $request['feature'];
  $page = $request['page'];
  load_feature_data($feature);
  load_feature_preparation($feature);
  require_once('./features/'.$feature.'/pages/'.$page.'.html.php');
}

feature実装部分

features/fight/prepare/attack.php
<?php
/**
 * 敵が死んでいるか判断
 *
 * @return boolean 敵が死んでいればtrue
 */
function is_dead($enemy_life)
{
  return $enemy_life <= 0;
}

/**
 * 敵を倒した場合のみメッセージを表示する
 *
 * @return string 倒したときのメッセージ
 */
function get_message_if_defeat_enemy($enemy_life)
{
  $message = '';
  if (is_dead($enemy_life)){
    $message = '<strong>敵を倒した</strong>';
  }
  return $message;
}

/**
 * 攻撃を実行し、メッセージを表示する。
 *
 * @return int 攻撃後の敵ライフ
 */
function attack_with_message($enemy_life, $base_status_name)
{
  $damage = 0;
  $status = $GLOBALS['StoredData'][$base_status_name];
  if (is_dead($enemy_life)){
    echo '敵はもういない';
  } else {
    $damage = rand($status, $status * 2);
    echo $damage . 'のダメージを与えた!';
    echo get_message_if_defeat_enemy($enemy_life);
  }
  return $enemy_life - $damage;
}
fight/pages/continuous_attack.html.php
<?php $attack_times = $_REQUEST['times']; ?>

<html>
  <head>
    <title>あなたの連続攻撃</title>
  </head>
  <body>
    <h1>あなたの連続攻撃</h1>
    <?php $enemy_life = $GLOBALS['StoredData']['slime_hp'] ?>
    <?php for($current = 0; $current < $attack_times; $current++) { ?>
      <p>あなたの攻撃!(<?php echo $current + 1?>回目)</p>
      <p style="padding-left: 2em;">
        <?php $enemy_life = attack_with_message($enemy_life, 'strength') ?>
      </p>
    <?php } ?>
  </body>
</html>
fight/pages/magic_attack.html.php
<html>
  <head>
    <title>あなたの魔法攻撃</title>
  </head>
  <body>
    <h1>あなたの魔法攻撃</h1>
    <?php $enemy_life = $GLOBALS['StoredData']['slime_hp'] ?>
    <p>魔法を唱えた!</p>
    <p style="padding-left: 2em;">
      <?php $enemy_life = attack_with_message($enemy_life, 'magic') ?>
    </p>
  </body>
</html>
data/enemies.dat
slime_hp=10
bat=15
features/fight/data/status.dat
strength=5
magic=8

これでhttp://localhost:8000/fight/magic_attackhttp://localhost:8000/fight/continuous_attack?times=3にアクセスしてみよう。

解説・・・の前にクイズ

上記のURLでアクセスしたときに、どのphpコードがブラウザにレスポンスを返しているのかわかるだろうか。
言い換えれば、このURLがどのphpファイルを呼び出したのか、でもある。
今までは、URLの中でxxxx.phpを記載していたが、今回はそれがない。
ただ、magic_attack.html.phpというファイルがあって、それっぽいURLだから各ページのhtml.phpファイルが呼ばれているのだろうか?
導入編で言及したように、コンピュータは書いてあること、命令したとおりにしか動作しない。
人の目で見て「それっぽい」はある意味で正しい3が、そのような仕様はどこにあるだろうか。

正解

index.phpが呼ばれて、レスポンスを返している。
phpが動作するサーバ(apache4、nginx4や今回のビルトインサーバ)は、「対象のディレクトリがなければ、ルートのindex.phpが処理する」事になっている5
そのため、今回はfightというディレクトリがwww直下にないことでindex.phpに処理をまわして、そこから💩フレームワークが動き出す算段である。
ここまで書いてきてこの構成の💩仕様に気付いた。
libdatafeaturesという名前のfeatureを作れないことだ。
仮にそのようなfeatureを用意した場合、index.phpを呼ばずに該当ディレクトリ内に該当ファイルがないことによる404エラーが発生する。

解説

今回は分量が多めなのでそれぞれの解説は薄くなるが、何かあれば身近なエンジニアに聞いてみてほしい。
またはコメントもいただければ誠意を持ってお答えするので、ご遠慮なくコメントをお寄せいただきたい。

index.php

上述したようにここが全ての起点となる。
ここの流れはこうなっている。

$GLOBALS['StoredData'] = [];

読み込んだデータを各featureやpageで利用するためにグローバル変数を定義している。
読み込まれたデータは全てこのグローバル変数に入るので、どこからでも参照可能となる目論見だ6
その後複数の関数定義があるが、定義しているだけなので実行自体は先へ進み、

load_global_data();
load_utilities();
render_page();

このようになっている。

  1. フレームワーク全体で利用可能なデータを読込
  2. フレームワーク全体で利用可能なユーティリティ(関数群)を読込
  3. (ユーティリティで定義されている)ページのレンダリングを実行

このページレンダリングがあることで、index.phpによって各featureのpageを表示することが可能になっている。

各関数の中身については、globで該当するファイルを拾って、foreachで読み込んだりrequireしたりしている。
foreachは配列を伴う動作の為、次稿をお待ちいただきたい。

route.php

index.phpでユーティリティ読込の際に読まれる対象となっている。
ここでは、リクエストのURLを解してfeatureとpageを特定し、レンダリングする処理が定義されている。
featureが決まったら該当フォルダのデータとphpファイルを読み込むことで、そのfeature内で利用可能な関数やデータを使えるようになる。
そして最後にpageに該当する.html.phpファイルを読み込む7ことで、レンダリングが完了するわけだ。

attack.php

features/fight/prepareに配置することで、`features/pages/'以下のpageで利用可能な関数を定義している。
せっかく定義データを使えるようにしたので、ダメージ計算でステータスを参照するようにした。

  $status = $GLOBALS['StoredData'][$base_status_name];
    $damage = rand($status, $status * 2);

の部分だ。これで、呼び出し元でなんのステータスを参照してダメージ計算するかを指定できるようになる。
やっと魔法攻撃と連続攻撃の差を作れた。

その他特筆すべき変更点はないはずだ。

*.html.php

この拡張子に大きな意味はない。
htmlっぽいファイルであることを主張しているだけだ8

require_once('attack.php')

が消えてすっきりしたことがおわかりだろうか。
今回は1つのファイルだったが、これが複数ファイルを読み込むような場合にはよくわかるだろう。
require忘れや、ファイルの分割などが発生した場合に全てを変更するなどの手間もなくなる。
そしてhtmlらしく整った形になったことで、デザイナとの協業が少し楽になるだろう。
この💩フレームワークがもっとも効果を発揮する部分である!
そして、今まで主役だったhtmlは、index.phpがレスポンスを返すための「材料」となっていることを意識できたなら、本稿で伝えたかったことの8割は完了している。

フレームワークとは

フレームワーク作りを通して次のことを学んでもらった。

  • ファイルを分割することで役割を明確にし、一貫した管理方法を作ること
  • requireなどの"お約束"をフレームワークが吸収し、スマートなソースコードを保つこと
  • 何か変更が発生するときにも、関連している場所全てを変更しないで良いように構成する意味
  • フレームワークを利用した場合に、自分の書いたソースコードが直接レスポンスしているのではなく、様々なルールの中で途中で読み込まれることでレスポンスを実現していること

今あなたが利用しているフレームワークはどのようなルールを持っているだろうか。
あるいは今後あなたが利用するフレームワークは?
そのルールは何を目的として、あなたの開発の何を支援しているだろうか。
管理方法やフレームワークが標準的に提供している機能をひもとくと、その思想の一端を知ることができるので、時間のあるときにでも調べると良いだろう。

まとめ

本稿では処理するファイルを分割することについて学んだ。

  • 主な目的
    • 管理のしやすさ
    • 再利用性の向上
  • フレームワークのしていること
    • 一定のルールに則ってリクエストに合わせたファイルを読み込む
    • ルールの作り方、管理方法や便利な機能を提案することで、フレームワークの思想が生まれる
    • 構成からそのフレームワークが何を成し遂げようとして助けてくれるのかを理解することができる

脚注


  1. むりやり再利用したのでその意味があったのかは疑問だが。 

  2. その昔、インターネットを256倍使う本という本があり、ウェブサーフィン(死語)で満足してるんじゃないよ、マウス猿になっちゃうよ、ネットワークを理解してサービス提供側に回ろうよ(ここまで意訳)、と書いてあったが、事実GAFAと呼ばれる巨大なインターネット企業は、インターネットのあり方そのものを根本的に変えることで大きくなったのだ。生み出した価値は256倍なんて目ではなかった。 

  3. こういった当たりをつける認識の良さは、開発を迅速に進める上で重要である。理屈はわからないがここを変更すればここが変わる、という直感や経験によるものだ。しかし、一介のエンジニアとしては、仕組みの理解を諦めるようなことはしたくないものだ。 

  4. 厳密にはそれらwebサーバに組み込むプラグインモジュールなどが制御している部分 

  5. 実はこのあたりは「経験的に知っている」ことで、どこが制御を握っててどんなルールを持っているのか確証が持てていない。この記事を書くに当たってドキュメントなども探したり、php-src/を1時間程度さらっと読んでみたが、うまく見つけられずにいる。知見のある方からご指摘いただければ幸いである。 

  6. 本来であればグローバル変数は使わない方が良い。しかし、各featureで使えるようにするには他の方法が必要となりシンプルさを失うため、この💩フレームワークは許容している。💩の一欠片である。 

  7. こういったViewっぽいファイルの読込はエラーがあっても処理の止まらないincludeが良いと思われるが、本稿ではrequireに統一した。 

  8. symfonyに影響された部分は否定しない。 

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away