それが僕には楽しかったんです。

いろんなレイヤーに居ます

Laravelの動く環境をdocker-compose(PHP 7.2 + nginx + MySQL)でいい感じにする

はじめに

Laravelを

$ php artisan serve

で動かすのにすごくモヤモヤしていたのでdocker-compose をつかってLaravelのプロジェクトが動くインフラ(PHP 7.2 + nginx + MySQL)を構築してみたからまとめる。

ディレクトリ構成は以下の感じになった。

$ tree -L 1
.
├── LICENSE
├── README.md
├── app
├── artisan
├── bootstrap
├── composer.json
├── composer.lock
├── config
├── database
├── docker
├── docker-compose.yml
├── package.json
├── phpunit.xml
├── public
├── readme.md
├── resources
├── routes
├── server.php
├── storage
├── tests
└── webpack.mix.js

完全に好みである。

docker-compose

例のファイル(docker-compose.yml)を書く。

version: '3'
services:
    nginx:
        build:
            context: ./docker/nginx
        depends_on:
            - php
        ports:
            - 80:80
        volumes:
            - ./:/src

    php:
        build:
            context: ./docker/php
        environment:
            DB_HOST: mysql
        volumes:
            - ./:/src

    mysql:
        image: mysql:5.7
        volumes:
            - ./docker/mysql:/var/lib/mysql
        environment:
            - MYSQL_ROOT_PASSWORD=root
            - MYSQL_USER=sample
            - MYSQL_PASSWORD=sample
            - MYSQL_DATABASE=sample
        ports:
            - 3306:3306

見ればわかるようにプロジェクトルートを/srcとして設定している。
コンテナ側では基本的に/srcに入るようになっていてそれを公開する形になっている。

またこの設定ファイルにあるように、docker-composeで使用するコンテナの設定ファイル群は./dockerディレクトリ以下にある。
PHPのDockerfileは以下の通り

FROM php:7.2.8-fpm

WORKDIR /src

PHP 7.2を持ってきている。


そしてこれがnginxのDockerfile (./docker/nginx/Dockerfile)

FROM nginx:latest
COPY ./default.conf /etc/nginx/conf.d/default.conf

こっちが一番重要なnginxの設定ファイル (./docker/nginx/default.conf)。
今回は、https化まではしていないから結構シンプルになっている。

server {
    
    listen 80;
    server_name _;

    root /src/public;
    index index.php;

    access_log /src/docker/nginx/logs/access.log;
    error_log  /src/docker/nginx/logs/error.log;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;    
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
        
}

諸々のパスはコンテナ側に準拠している。
そして一番重要なのがrootを設定すること。

Laravelではpublicディレクトリ以下のindex.phpにアクセスが飛ぶためここをrootに設定する。

環境を立ち上げる

ログファイルを格納するディレクトリを作る
$ mkdir ./docker/nginx/logs

MySQLで使用するディレクトリを作る
$ mkdir ./docker/mysql

githubで管理した時にignoreされたファイルをなかったら作る
$ echo '' >> ./storage/logs/laravel.log

権限を与える
$ sudo chmod 777 -R ./storage/ ./bootstrap/

$ composer install

Laravelのおまじない
$ cp .env.example .env
$ php artisan key:generate

$ docker-compose up -d

これでLaravelが動く最低限の環境ができた

yarn + webpack を感覚で使っていたので勉強がてら手元に環境を構築してみる

はじめに

今までJavaScala、Cなど型が割としっかりしている言語を使って開発を続けてきた。
しかし、その経験がweb系の開発を行うときに思わぬ障害になった。
PHPとJSがわからない、あいつら抽象的過ぎてわからないという現象にぶち当たった。

そしてなんだ、こいつらwebpackとyarnってなんや!!ってなったからしっかりやっていく。

yarnとは

yarn はパッケージマネージャの npm の上位互換のようなツールと言われている。
だったら npm でいいじゃん!と思うかも知れないけど npm は色々な問題を抱えているため、 yarn が使われることが多いみたい。

yarn ではダウンロードしたパッケージをキャッシュしているので、一度使ったパッケージはすぐに再利用することができる。
このあたりは composer に似ている。

yarn は npm で管理されているけど、その方法でUbuntuにインストールするのは推奨されていない。

また package.json に対応しているから npm で package.json を使っている場合はすぐに移行できるらしい。

webpackとは

webpack はWebアプリケーションで使用しているリソースの依存関係を解決し、js や css などのアセットを生成するビルドツール。
つまり、1つ以上のモジュールをひとつにまとめたファイルを吐ける。

yarn の導入

Ubuntu 16.04に突っ込むことを前提にする。
しかし apt では入らないので、公式にかいてあるようにコマンドを叩いて突っ込む

$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
$ sudo apt-get update
$ sudo apt-get install yarn

version 1.7.0の安定版を入れるので

$ yarn --version
1.7.0

と、でればok

webpack の導入

をやろうとしたら、nodejsのバージョンが低いっぽいので先に nvm を入れる。

nvm の導入

次のコマンドを叩いて導入する

$ git clone https://github.com/creationix/nvm.git ~/.nvm
$ source ~/.nvm/nvm.sh

それが終わったら、雑に node のバージョンを指定して突っ込む。

$ nvm install 8.11.3
Downloading and installing node v8.11.3...
Downloading https://nodejs.org/dist/v8.11.3/node-v8.11.3-linux-x64.tar.xz...
######################################################################## 100.0%
Computing checksum with sha256sum
Checksums matched!
Now using node v8.11.3 (npm v5.6.0)
Creating default alias: default -> 8.11.3 (-> v8.11.3)

webpackの導入をやる

適当にディレクトリを作ってそれをプロジェクトとする。
その前に、yarn を使って以下のことをする必要がある。

$ yarn init

これで例の package.json が生成される。

{
  "name": "SampleWebpack",
  "version": "1.0.0",
  "main": "index.js",
  "author": "lrf141",
  "license": "MIT"
}


ここまできたら、いよいよ webpack を導入する。

$ yarn add webpack webpack-dev-server --dev
yarn add v1.7.0
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.4: The platform "linux" is incompatible with this module.
info "fsevents@1.2.4" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...

success Saved 2 new dependencies.
info Direct dependencies
├─ webpack-dev-server@3.1.4
└─ webpack@4.12.0
info All dependencies
├─ webpack-dev-server@3.1.4
└─ webpack@4.12.0
Done in 3.31s.

これで導入は完了。

{
  "name": "SampleWebpack",
  "version": "1.0.0",
  "main": "index.js",
  "author": "lrf141",
  "license": "MIT",
  "devDependencies": {
    "webpack": "^4.12.0",
    "webpack-dev-server": "^3.1.4"
  }
}

package.json も上記の様に変化している。

次に webpack.config.js という webpack の設定ファイルを書く。

module.exports = {
    entry: "./src/index.js",
    output: {
        filename: "index.bundle.js",
        path: __dirname + "/build"
    }
}

サンプルのjsファイルを作る。

window.onload = function()
{
    var elem = document.getElementById('sample');
    elem.innerHTML = 'I love Jason Statham.';
}
<script src="index.bundle.js"></script>
<body>
    <div id="sample"></div>
</body>

実際にビルドしてみる。

$ yarn run webpack-dev-server -- --inline

と、失敗するので yarn を使って webpack-cli を導入する

$ yarn add webpack-cli
{
  "name": "SampleWebpack",
  "version": "1.0.0",
  "main": "index.js",
  "author": "lrf141",
  "license": "MIT",
  "devDependencies": {
    "webpack": "^4.12.0",
    "webpack-dev-server": "^3.1.4"
  },
  "dependencies": {
    "webpack-cli": "^3.0.8"
  }
}

もう一回やってみる。

$ yarn run webpack-dev-server -- --inline
yarn run v1.7.0
warning From Yarn 1.0 onwards, scripts don't require "--" for options to be forwarded. In a future version, any explicit "--" will be forwarded as-is to the scripts.
$ /home/rabbitfoot/webProject/SampleWebpack/node_modules/.bin/webpack-dev-server --inline
ℹ 「wds」: Project is running at http://localhost:8080/
ℹ 「wds」: webpack output is served from /
⚠ 「wdm」: Hash: 485e1d2511df65a321f5
Version: webpack 4.12.0
Time: 427ms
Built at: 2018-06-24 01:12:22
          Asset     Size  Chunks             Chunk Names
index.bundle.js  139 KiB       0  [emitted]  main
Entrypoint main = index.bundle.js
 [2] ./src/index.js 127 bytes {0} [built]
 [4] (webpack)/hot/emitter.js 77 bytes {0} [built]
 [6] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {0} [built]
 [9] ./node_modules/html-entities/index.js 231 bytes {0} [built]
[10] ./node_modules/ansi-html/index.js 4.16 KiB {0} [built]
[11] (webpack)-dev-server/client/overlay.js 3.58 KiB {0} [built]
[12] ./node_modules/sockjs-client/dist/sockjs.js 176 KiB {0} [built]
[13] (webpack)-dev-server/client/socket.js 1.05 KiB {0} [built]
[14] ./node_modules/loglevel/lib/loglevel.js 7.68 KiB {0} [built]
[15] ./node_modules/ansi-regex/index.js 135 bytes {0} [built]
[16] ./node_modules/strip-ansi/index.js 161 bytes {0} [built]
[19] ./node_modules/querystring-es3/index.js 127 bytes {0} [built]
[23] ./node_modules/url/url.js 22.8 KiB {0} [built]
[24] (webpack)-dev-server/client?http://localhost:8080 7.75 KiB {0} [built]
[25] multi (webpack)-dev-server/client?http://localhost:8080 ./src/index.js 40 bytes {0} [built]
    + 11 hidden modules

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/
ℹ 「wdm」: Compiled with warnings.

これで画面に文字列が無事に表示された。

このあと、webpack の設定をいじってビルド周りのコマンドを登録したりして
効率化するべきなんだろうけど、そこら辺は必要になったときに追ってやっていく

Laravelを使いたかったので雑に入門した

はじめに

なぜか急にwebシステムを作る必要がでてきたため、Laravelを使ってみようとおもった。
だが使ったことないので備忘録も兼ねてまとめていこうと思う。

対象

  • webアプリケーションを構築したことがある
  • 他のフレームワークを使用した経験がある
  • PHPを書いたことがある
  • PHPでなくてもなんかコードは書いたことある

などなど、何が言いたいかというと初心者に向けた記事じゃなくて
自分用のメモ兼ある程度のサーバサイド経験者に共有したい内容となっている

Laravelとは

Laravel は、MVCのWebアプリケーション開発用の無料・オープンソースPHPで書かれたWebアプリケーションフレームワークのこと。
PHPのwebアプリケーションフレームワークといえば、CakePHPFuelPHPあたりを想像するだろうが、最近はLaravelが熱いらしい。

Laravelの環境を構築する


composerが入っていることを前提にする。
その上で最初にComposerを使用し、Laravelインストーラをダウンロードする。

composer global require "laravel/installer"

またはcomposerを使ってプロジェクトを作る

composer create-project --prefer-dist laravel/laravel project-name

プロジェクトを作成したらそのディレクトリに移動して、ローカル開発サーバを起動する。
その前にドキュメントによるとアプリケーションキーの設定と設定ファイルのあれこれを行う。

設定ファイルをリネームする

cp -p .env.example .env

そしてアプリケーションキーを作成する。

php artisan key:generate


ここまで完了した段階で、以下のコマンドを実行することで最初のHello,World的なページが表示される。

$ php artisan serve
Laravel development server started: <http://127.0.0.1:8000>

ブラウザからlocalhost:8000にアクセスすると…
f:id:RabbitFoot141:20180607104430p:plain
ここまででとりあえず最低限起動するところまでは完了した。


ちなみにLaravelではpublicディレクトリにあるindex.phpがフロントコントローラとなっている。

ルーティング

webアプリケーションを作る上でまずやらないといけないのがルーティング
普段はLeague\RouteをつかってやっているがLaravelではどうおこなうかわからなかったのでまとめてみる。

まずルーティングがどこにあるかというとroutesディレクトリにおいてある。
そうするといくつかのファイルがあると思うが、 web.php がwebインターフェースのルーティングを定義する。

Laravelではルーティング内で毎回App\Http\Controllerを指定しなくて済むようにデフォルトでRouteServiceProviderが設定している。

Laravelで利用できるルート定義メソッドは以下の通り

Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);

HTTPのリクエストメソッドに対応している。
引数のタイプは、URIクロージャーになっている。

複数のHTTPリクエストメソッドを利用したい場合はmatchメソッドを使用して

<?php
Route::match(['get', 'post'], '/', function () {
    //
});

Route::any('foo', function () {
    //
});

のように書くこともできる
anyはすべてのHTTPリクエストメソッドに対してルーティングを登録する場合にもちいられる。

また一般的なGETリクエストを使用するルーティングは次のように書かれる

<?php
Route::get('/hello', 'HogeController@index');

第一引数については言う必要はないと思うが、問題は第二引数。
呼び出すコントローラのメソッドを@で指定している。

また、単純にViewを表示するだけなら以下のようにも書ける。

<?php
Route::view('/welcome', 'welcome');
Route::view('/welcome', 'welcome', ['name' => 'Taylor']);


シンプルなルーティングはこれでかけるがもう少し詳しく書きたい場合がある。
そういう時にルートパラメータは次のように書ける。

<?php
Route::get('user/{id}', function ($id) {
    return 'User '.$id;
});

Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) {
    //
});


パラメータ名の後ろに?をつけると任意パラメータになる。

<?php
Route::get('user/{name?}', function ($name = null) {
    return $name;
});

Route::get('user/{name?}', function ($name = 'John') {
    return $name;
});

さらに詳しいことは公式ドキュメントに書いてある。
ルーティング 5.6 Laravel

リクエスト周り

LaravelでHTTPリクエストのインスタンスを利用するには
Illuminate\Http\Requestクラスをコントローラのメソッドに指定する必要がある

ひとまず、以下のクラスとルーティングを前提に説明していく

<?php

namespace App\Http\Controllers;

use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request;

class SampleController extends BaseController
{

    public function index()
    {
        return 'hello,world';
    }

    public function sampleRequest(Request $request)
    {
        $name = $request->name;
        return $name;
    }
    
    public function sampleRequestWithParam(Request $request, $val)
    {
        return $id;
    }

}
<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Route::get('hello', function()
{
    return 'Hello,World';
});

Route::get('/sample', 'SampleController@index');
Route::get('/sample2/{name}', 'SampleController@sampleRequest');
Route::get('/sample3/{val}', 'SampleController@sampleRequestWithParam');

このコードにおいて、 パラメータの値を取得する方法は2つある。
ひとつ目が、

<?php
$name = $request->name;

上記の部分。
こちらは動的プロパティを参照する方法となる。

ふたつ目が

<?php
public function sampleRequestWithParam(Request $request, $val)
    {
        return $id;
    }

の引数部分。
プロパティ名を変数として宣言するとそちらに直接代入される。


プロパティだけでなく、URL全体を取得するのは次のように行う。

<?php
// クエリストリングなし
$url = $request->url();

// クエリストリング付き
$url = $request->fullUrl();

クエリストリングから値を取り出したい場合は以下のようにする

<?php
$name = $request->query('name');


またこのRequestクラスは、PSR7を満たしている以下のパッケージで置き換えることができる

composer require symfony/psr-http-message-bridge
composer require zendframework/zend-diactoros

その他、リクエストまわりは先程と同様に公式に全てまとまっている
HTTPリクエスト 5.6 Laravel

ビュー

LaravelのViewはBladeを使用している。
Bladeは .blade.php という拡張子でテンプレートを用意し
サーバサイドから呼び出しとデータを渡して表示する。という手順で利用する

//sample_view.blade.php
<html>
    <body>
        <h1>Hello, {{ $name }}</h1>
    </body>
</html>

ルーティングでは以下のように直接viewを呼び出す方式を今回は取ってみる。

<?php
Route::get('/sample4', function (){
    return view('sample_view', ['name' => 'scala']);
    //view('sample_view')->with('name', 'scala');でも同様に行える
});

雑感だけど、League/Plateと似ている
諸々のテンプレートエンジンとほとんど使い方自体は同じ

コントローラ

コントローラはapp/Http/Controllers以下のディレクトリに配置する。
namespaceはPSR準拠のものを設定する。

ドキュメントに乗っているサンプルコントローラは以下の通り

<?php

namespace App\Http\Controllers;

use App\User;
use App\Http\Controllers\Controller;

class UserController extends Controller
{
    /**
     * 指定ユーザーのプロフィール表示
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        return view('user.profile', ['user' => User::findOrFail($id)]);
    }
}

このように、コントローラクラスを継承し拡張することでコントローラは作成できる
メソッドではURLパラメータを受け取ることも、PSR7準拠のリクエストを受け取ることも出来る。

ルーティングと関連するところはコントローラの名前空間で、ルーティングではApp\Http\Controllerまでは自動で設定されているので
それより深いnamespaceを持つ場合は適宜指定しないと行けない。


シングルアクションコントローラでは__invokeメソッドを使用することができる。
この場合のルーティングは クラス名@アクションメソッド ではなく、 クラス名 となる。

モデル

モデル関連は公式ドキュメントに情報が少ない。
それもそれで何故かわかった。

そもそも、MVCのモデルは開発者やベンダーで割と作り方がバラバラでそれに対応するためにLaravelでも自由に作れるようになっているとあった。
たしかに、コントローラやビューほどある一定の形式を保つ必要が無いからその理由に同意できる。

Laravelではモデルはartisanコマンドをつかって作る
それらは以下のサイトが参考になった。
udemy.benesse.co.jp

おわりに

題名にあるように相当雑な入門記事が出来上がった。
特にモデルなんかはひどい。

しかし、思った以上にLaravelのドキュメントが整っていて
入門記事とうたって機能の大半を紹介するには分量が最高に多くなってしまうため
MVCとルーティング、リクエストというWebアプリを構築する上でこれだけわかっておけばどうにかなりそうなあれこれを最高に雑にまとめてみた

このあと、気合で勉強してその都度、機能を深く掘り下げていきたい

PHPでheaders already sentを解消するためだけに雑にテンプレートエンジンを作った話

はじめに

どうもよく訓練されたJavaer、けんつです。
PHPの勉強がてらオレオレTwitterクライアントを作ろうとしていたら次のエラーにぶち当たった。

Warning: Cannot modify header information - headers already sent by
(output started at /some/file.php:12) in /some/file.php on line 23

調べると

このエラーは HTTP ヘッダーを変更する関数(後述)の呼び出しより前に、すでに何かがアウトプットされているために発生します

とのことで、ならMVCモデルのViewでレンダリングするときはどうすれば良いのだろうかと疑問に思った。
今回はそれを理解するためだけに、最小構成オレオレテンプレートエンジンを雑に作ってみたのでそれについて書いて見ようと思う。

github.com

2018/05/13の段階でとりあえずレンダリングはできる状態にあるが、細かい所や設計面を作りこめていないので今後改良していく。
composerをつかっている環境であれば以下のコマンドで導入できる。

$ composer require lrf141/rook

packagist.org

エラーの原因と解決策

まず原因はHTTPヘッダーを変更する関数をコールするまえに、何かしらを出力していることである。
なのでよくある解決策としてはレンダリングしたいものを

<?
ob_start();

などのように内部バッファに溜め込んで最終的にヘッダーに追加することが挙げられる。

もしくは、サーバ側の設定でアウトプットバッファリングを有効にすることでも回避できる。
今回は前者でやってみる。

テンプレートエンジンの実装

今回自作したテンプレートエンジンのソースは次のような構成になっている。

./src/
├── Directory.php
├── Engine.php
├── FileExtension.php
├── Name.php
└── Template.php

何をやったかというと、雑に説明するなら
テンプレート内で使用したいデータ群を配列として受け取ったらそれを変数として展開し
テンプレートとして利用するファイル(.php)をテンプレートエンジン内でincludeしてレンダリングするというもの。
その過程で、 バッファを利用している。

まず基礎となるEngineクラスは次のように実装した。

<?php
namespace Lrf141\Rook;
class Engine
{

    /**
     * Default template directory
     * @var Directory
     */
    private $directory;

    /**
     * Template file extension
     * @var FileExtension
     */
    private $fileExtension;

    /**
     * create new Engine Instance
     * @param string $base_dir
     * @param string $ext file extension
     */
    public function __construct(string $base_dir, string $ext = 'php')
    {
        $this->directory = new Directory($base_dir);
        $this->fileExtension = new FileExtension($ext);
    }

    /**
     * generate Template
     * 
     * @param string $name template name
     * @return Template
     *
     */
    public function make(string $name): Template
    {
        return new Template($this, $name);
    }

    /**
     * render based on template
     *
     * @param string $template template name
     * @param array $data wanna use in template
     * @return string rendering result
     */
    public function render(string $template, array $data = []): string
    {
        return $this->make($template)->render($data);
    }

    /**
     * get path to templates Directory
     * @return Directory
     */
    public function getDirectory(): Directory
    {
        return $this->directory;
    }

    /**
     * get templates file extension
     * @return FileExtension
     */
    public function getFileExtension(): FileExtension
    {
        return $this->fileExtension;
    }
}

このクラスのインスタンスを生成した時点で基準となるディレクトリと拡張子(ここではphp)を指定してそれらを管理するクラスのインスタンスを保持している。
Engineクラスのインスタンスからrenderメソッドをコールするとテンプレートが生成されて、レンダリング結果のhtmlがstringで返ってくる仕組みとなっている。

諸々の関連クラスのあれこれは省略するとして、一番重要なのがTemplateクラス。

<?php
namespace Lrf141\Rook;
use LogicException;
use Throwable;
use Exception;
class Template
{

    /**
     * @var Engine
     */
    private $engine;

    /**
     * @var Name
     */
    private $name;

    /**
     * @var array
     */
    private $sections = array();

    /**
     * @var array
     */
    private $data = array();

    /**
     * generate new Template Instance
     * @param Engine $engine
     * @param string $name
     */
    public function __construct(Engine $engine, string $name)
    {
        $this->engine = $engine;
        $this->name = new Name($name);
    }

    /**
     * rendering based on template 
     * @param array $data
     * @return string
     */
    public function render(array $data = []): string
    {
        //expands array as var
        $this->data($data);
        unset($data);
        extract($this->data);
        try {
            $level = ob_get_level();
            //dump buffer
            ob_start();
            include $this->path();
            
            $content = ob_get_clean();
            
            return $content;
        } catch (Throwable $e) {
            while (ob_get_level() > $level) {
                ob_end_clean();
            }
            throw $e;
        } catch (Exception $e) {
            while (ob_get_level() > $level) {
                ob_end_clean();
            }
            throw $e;
        }
    }

    /**
     * add data
     * @param array $data
     * @return array|none
     */
    public function data(array $data = null)
    {
        if (is_null($data)) {
            return $this->data;
        }
        $this->data = array_merge($this->data, $data);
    }

    /**
     * generate path
     * @return string
     */
    public function path(): string
    {
        $dir = $this->engine->getDirectory()->get();
        $ext = $this->engine->getFileExtension()->get();
        $name = $this->name->get();
        $path = $dir . '/' . $name . '.' . $ext;
        if (!$this->isExist($path)) {
            throw LogicException(
                `the template path "` . $path . '" does not exist.'
            );
        }
        return $path;
    }

    /**
     * check the path is true
     * @param string $path
     * @return bool
     */
    public function isExist(string $path): bool
    {
        return file_exists($path);
    }
}

この中で重要なのがrenderメソッドで、次の部分で配列を変数として展開している。

<?php

//expands array as var
$this->data($data);
unset($data);
extract($this->data);

特にextract関数がそれをやっている。

その後に

<?php

ob_start();

で、出力で内部バッファの利用を開始する。
その後に

<?php
 include $this->path();
 $content = ob_get_clean();

指定したパスにあるphpファイルをincludeして
ob_get_clean();でバッファの内容をstringで取得して、クリアしている。
これによって、直接出力しなくてもバッファに溜め込んで取得できるため上記のエラーが起きなくなる。
ただ最終的には取得した内容を吐き出す必要が出てくるのでその段階で工夫がまた必要にはなってくる。

それはPSR-7で制定されているHTTPメッセージ辺りの扱いを上手くやっていく必要があるのだと思う。
ただ出力を乱立させないで、テンプレートエンジンを利用することである程度まとめられるのでエラーは起こしにくくなるだろう。

おわりに

エラーを潰すためにこれだけやったがやったことをざっくりとまとめると内部バッファを使っただけなので
その辺り、PSR-7辺りを上手く使って行こうと思う。

PHPでLeague/Routeを使ってルーティングを構成する

はじめに

どうもよく訓練されたJVM教のけんつです。
JVM環境でしか使い物にならないと言われてきた自分がWebサーバサイドのコードをPHPで書く機会がここ半年ほどで多くなってきた。
それでも、Webやったことないしわからんぞ!という感じなので素のPHPでオレオレTwitterクライアントを書く事にした。
とりあえず、イカしたdocker-composeを適当に引っ張ってきてイマドキのなうでヤングな環境を構築するところまではやった。

結構順調だったので、このまま最小構成を作ってしまおうとした矢先に自分を苦しめる存在が出てきた。
そうそれこそが

「素のPHPでルーティングどうやんの?」

という疑問である。


とりあえず、「PHP ルーティング」でぐぐってみると

  • apache 環境では…。いや使ってるのnginxだし!!
  • Cakeでルーティングはこうする…。 いや素のPHPで書きたいんだ!!
  • Laravelではこうする…。 だからフレームワークは使わんと言った!!
  • なかったから自作したお。 わけわからん!!

といった調子になった。

それでもいくつか、FastRouteのような使えそうなライブラリを見つけた。
しかし、以前Play Frameworkで色々やっていた自分に取ってみれば「書く事が多すぎてだるい!!」に尽きる。
それでも探し続けて、やっと「League/Route」なるものを見つけた。

色々とサンプルをみて、「これええやん!」となったが問題はPHPに浸かっている人間でなかったのでわかりにくいというか何をやっているかわからないという個人的な点にあった。
なので、ここでは自分なりに頑張って色々調べてみて「League/Route」を使うための公式にあるサンプルが何をやっているのかまとめてみようと思う。

route.thephpleague.com

環境

まずPHPのバージョンだが、Routeは5.4以降に対応しているので大抵の環境で動作するだろう。

Routeを使うにはcomposerかautoloadで持ってくるのを前提にしている。
自分はPHPを書くときはcomposerを使うのでそちらを参考にする。

$ composer require league/route

またこのライブラリはPSR-7を前提にしているのでそちらも解決する。

composer require zendframework/zend-diactoros

あとの文章に出てくるLeague\Containerでは2.x系のものを使用している。
こちらはバージョンによって仕様がかなり違うので注意が必要。

と環境はここまでで構築できるはず。

ルーティングを構成する基本要素

環境を構築したら、次のようにコードを書けばルーティングが実装できるよ。と公式には書いているが

<?php

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

$container = new League\Container\Container;

$container->share('response', Zend\Diactoros\Response::class);
$container->share('request', function () {
    return Zend\Diactoros\ServerRequestFactory::fromGlobals(
        $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
    );
});

$container->share('emitter', Zend\Diactoros\Response\SapiEmitter::class);

$route = new League\Route\RouteCollection($container);

$route->map('GET', '/', function (ServerRequestInterface $request, ResponseInterface $response) {
    $response->getBody()->write('<h1>Hello, World!</h1>');

    return $response;
});

$response = $route->dispatch($container->get('request'), $container->get('response'));

$container->get('emitter')->emit($response);

ここらへんを全てフレームワークに投げていた人にはよく分からない。
なので、該当するPSRなどを読んで理解していく。

まず、先頭にあるこのコード

$container = new League\Container\Container;

League\Containerを呼び出しているのだが、ここで指すコンテナってなんぞ?とおもったので調べてみた。

Dependency Injection Container (PSR-11)

DIコンテナと言われるものでPSR-11内で決定されている。
DIコンテナと言われても分からない。なので調べた。
PSR-11については以下のページが
qiita.com

DIについては以下のページが個人的に分かりやすかった。
qiita.com

そもそもDIとは

つまりこれは何かと言われると、例えば以下の処理をしようとする。

<?php
class Sample
{
    private $engine;

    public function __construct()
    {
        $this->engine = new Engine();
    }
}

ここではnew Engineとしたがこれはテンプレートエンジンでもいいし、エンジンでなくてもいい。
依存性を直接コンストラクタやメソッドのスコープ内で生成してしまうコードを指す。

これらは上記のqiita記事でも書かれているが
上のコードだと何が嬉しくないというと

  • Engineクラスを別のクラスに変えたりするとSampleクラスを直接変更しなければならない。
  • Engineが上手く動作してくれないとそもそも、このクラスのテストが出来ない。

という点にある。

それを次のようにすると嬉しいことがある。

<?php
class Car
{
    /**
     * @var EngineInterface
     */
    private $engine;

    /**
     * @param EngineInterface $engine
     */
    public function __construct(EngineInterface $engine)
    {
        $this->engine = $engine;
    }
}

何が嬉しいかというと、依存性が外部で完結しこちらのクラスではそれを受け取っているだけなので変更も容易でテストもしやすい。

しかしこれも残念な点を抱えている。
何かというと色んな依存関係を解消しようとすると引数がめちゃくちゃ長くなりやすいという点。
それと結局依存を注入する手続きは手動で行っており,抽象ではなく具象に対して依存するコードを書くことになるという点。
それらの問題を解決するためにDIコンテナが存在する。

DIコンテナとは

DIコンテナについては以下のサイトがわかりやすくまとまっていた。
qiita.com

つまり、今まで抽象っぽくなってたけど結局具象な上に手動で行っていたものを別な場所(DIコンテナ)に突っ込んで管理しよう。というのを実現するためにある。(らしい)

今回の環境ではLeague/Containerを使っているので公式から使用例を引用してくる。
container.thephpleague.com

まずは前提として以下のようなコードがあるとする。

<?php

namespace Acme\Service;

class SomeService
{
    // ...
}

これに対してDIコンテナを次のように使うことができる

<?php

$container = new League\Container\Container;

// register the service as a prototype against an alias
$container->add('service', 'Acme\Service\SomeService');

// now to retrieve this service we can just retrieve the alias
// each time we `get` the service it will be a new instance
$service1 = $container->get('service');
$service2 = $container->get('service');

var_dump($service1 instanceof Acme\Service\SomeService); // true
var_dump($service1 === $service2); // false

使い方としてはコンテナにDIしたいサービスのクラスを渡すこと。
それをgetすることで実際にそれらを利用することができる。
しかし、add/getではgetするたびに新しいインスタンスになるため

$service1 === $service2

という、評価式がfalseを返す。この点には注意したい。

実際にルーティングを組むときにはgetではなくshareメソッドが呼ばれているが実装は以下のようになっている。

<?php

    /**
     * {@inheritdoc}
     */
    public function share($alias, $concrete = null)
    {
        return $this->add($alias, $concrete, true);
    }

addメソッドに$shareというフラグをtrueにして呼び出しているだけである。
公式ドキュメントでは$shareがtrueになっているもの関しては先程の評価式がtrueになる。
つまり同一のインスタンスが返ることになる。
同一インスタンスが返る場合としては、addメソッドをコールした場合でもクラスインスタンスを直接渡す場合もなると公式に書いていた。


ここまでの内容を理解すれば、ルーティング設定時のこの部分を理解できる。

<?php

$container = new League\Container\Container;

$container->share('response', Zend\Diactoros\Response::class);
$container->share('request', function () {
    return Zend\Diactoros\ServerRequestFactory::fromGlobals(
        $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
    );
});

$container->share('emitter', Zend\Diactoros\Response\SapiEmitter::class);

まずDIコンテナを作り、そこにサービスをshareメソッドを使って追加している。
具体的にはPSR-7に準拠したサービスである、ResponseとRequestにEmitterを追加している。

が、しかしPSR-7とは何者か知らなかったので次はそれをまとめる。

HTTP message interfaces (PSR-7)

PSR-7は何を決めているかというと、題名の通りPHPでHTTP通信を扱うときのリクエストとレスポンスの標準となる仕様を決定している。
これに関しては公式と以下のサイトが役に立った。

www.php-fig.org
qiita.com

全てのHTTPリクエストには特定の形式が存在しRequestは以下のような

POST /path HTTP/1.1
Host: example.com

foo=bar&baz=bat

Responseは次のような形式をとる。

HTTP/1.1 200 OK
Content-Type: text/plain

This is the response body

これをPSR-7では次の2つのインターフェースを使って宣言している

Psr\Http\Message\RequestInterface
Psr\Http\Message\ResponseInterface

これらを使うことで、自分でああだこうだしないでHTTPリクエストをPHPで扱うことができる。
ここをまとめようと思うと、DIコンテナの比にならないくらい文章量が多くなってしまうのでここでは割愛。後日別記事にする。

しかし、何故これが必要なのかというとルーティングを構成する次の部分で使っているからである。

<?php
$route->map('GET', '/', function (ServerRequestInterface $request, ResponseInterface $response) {
    $response->getBody()->write('<h1>Hello, World!</h1>');

    return $response;
});

そう、この部分をみてわかるようにルーティングを構成して呼び出すメソッドはPSR-7準拠のインターフェースを引数に持ち$responseを返すことになっているからだ。
今上記のコードだと、HTTPレスポンスのボディにHTMLを書き込みそれを返しているのでHello,World!が表示されるといった状況になっている。
これはこの後、クラスを指定する場合でも同様に呼び出すメソッドには関係してくる。

Dispatchから先

残りはこの部分。

<?php

$response = $route->dispatch($container->get('request'), $container->get('response'));

$container->get('emitter')->emit($response);

ここではdispatchに先ほど設定したDIコンテナの値を使っている。
それをdispatchメソッドに渡している。dispatchメソッドの実装は次の通り。

<?php
    /**
     * Dispatch the route based on the request.
     *
     * @param \Psr\Http\Message\ServerRequestInterface $request
     * @param \Psr\Http\Message\ResponseInterface      $response
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    public function dispatch(ServerRequestInterface $request, ResponseInterface $response)
    {
        $dispatcher = $this->getDispatcher($request);
        $execChain  = $dispatcher->handle($request);
        foreach ($this->getMiddlewareStack() as $middleware) {
            $execChain->middleware($middleware);
        }
        try {
            return $execChain->execute($request, $response);
        } catch (Exception $exception) {
            $middleware = $this->getStrategy()->getExceptionDecorator($exception);
            return (new ExecutionChain)->middleware($middleware)->execute($request, $response);
        }
    }

dispatcherを取得した後にDispatcherクラスのhandleメソッドをコールしてミドルウェアを適用しそれらを実行している。
最後に今までのあれこれをemit(うまく表現できる日本語がわからなかった)することでルーティングで設定したあれこれが実行される。

ルーティング周りをもう少し頑張ってみる

これまでまとめたところが実は公式で言うところのHello Worldでしかない。
PHPをやったこともない人間が雑に触ろうとした結果PSR-7,11を調べたりしないと行けないのですごく大変だった。
ただ、ここまでやった段階だけでは無名関数を毎回書いて呼び出す必要があったり、もしルーティング内でワイルドカードを使用したい場合などどうするんだとなるのでそのあたりを公式サイトをみてまとめていく。

ワイルドカードを使いたい場合

これが自分自身このライブラリを使いたかった理由のひとつでもあるが、ルーティングとしてpathを設定するときにワイルドカードは次のように使える。

<?php

$router = new League\Route\RouteCollection;

$router->map('GET', '/user/{id}/{name}', function (ServerRequestInterface $request, ResponseInterface $response, array $args) {
    // $args = [
    //     'id'   => {id},  // the actual value of {id}
    //     'name' => {name} // the actual value of {name}
    // ];

    return $response;
});

もっと値を限定して使うなら次のようにする。

<?php

$router = new League\Route\RouteCollection;

// this route will only match if {id} is numeric and {name} is a alpha
$router->map('GET', '/user/{id:number}/{name:word}', function (ServerRequestInterface $request, ResponseInterface $response, array $args) {
    // $args = [
    //     'id'   => {id},  // the actual value of {id}
    //     'name' => {name} // the actual value of {name}
    // ];

    return $response;
});

ここで指定できる種別には以下のものがある。

  • number
  • word
  • alphanum_dash
  • slug
  • uuid

ここまででも十分便利なのに正規表現を適用することもできる。

<?php

$router = new League\Route\RouteCollection;

$router->addPatternMatcher('wordStartsWithM', '(m|M)[a-zA-Z]+');

$router->map('GET', 'user/mTeam/{name:wordStartsWithM}', function (ServerRequestInterface $request, ResponseInterface $response, array $args) {
    // $args = [
    //     'id'   => {id},  // the actual value of {id}
    //     'name' => {name} // the actual value of {name}
    // ];

    return $response;
});

HTTPメソッドでルーティングを設定

今まではmapを使った例が多かったがもう少し便利なものがある。

<?php
$route = new League\Route\RouteCollection;

$route->get('/acme/route', 'Acme\Controller::getMethod');
$route->post('/acme/route', 'Acme\Controller::postMethod');
$route->put('/acme/route', 'Acme\Controller::putMethod');
$route->patch('/acme/route', 'Acme\Controller::patchMethod');
$route->delete('/acme/route', 'Acme\Controller::deleteMethod');
$route->head('/acme/route', 'Acme\Controller::headMethod');
$route->options('/acme/route', 'Acme\Controller::optionsMethod');

直接指定することもできるし、HTTPメソッドごとに処理を分けることもできる。

ホストやスキームを指定する

なんてこともできる。

<?php

$route = new League\Route\RouteCollection;

// this route will respond to http://example.com/acme/route
// or https://example.com/acme/route
$route->map('GET', '/acme/route', 'AcmeController::method')->setHost('example.com');

// this route will only respond to https://example.com/acme/route
$route->map('GET', '/acme/route', 'AcmeController::method')->setScheme('https')->setHost('example.com');

ルーティングをグループとしてまとめる

これが一番使いたかった機能だと思う。
ルーティングをグループ化して追加できるのはスゴク便利。

<?php

$route = new League\Route\RouteCollection;

$route->group('/admin', function ($route) {
    $route->map('GET', '/acme/route1', 'AcmeController::actionOne');
    $route->map('GET', '/acme/route2', 'AcmeController::actionTwo');
    $route->map('GET', '/acme/route3', 'AcmeController::actionThree');
});

もちろんこれらも個別にホストやスキームを設定できる。

<?php

$route = new League\Route\RouteCollection;

$route->group('/admin', function ($route) {
    $route->map('GET', '/acme/route1', 'AcmeController::actionOne');
    $route->map('GET', '/acme/route2', 'AcmeController::actionTwo')->setScheme('https');
    $route->map('GET', '/acme/route3', 'AcmeController::actionThree');
})
    ->setScheme('http')
    ->setHost('example.com')
;

そして実際のリクエストは次のようになる

GET http://example.com/admin/acme/route1
GET https://example.com/admin/acme/route2
GET http://example.com/admin/acme/route3

これスゴク便利。

さいごに

とりあえず、ここまでが自分の使いたかった機能を一通り網羅した。
ルーティングについて雑に書くはずがDIコンテナやPSRについてまで触れるとは思わなかったけどかなり理解が深まった。
関連するPSRやその他についてもこの後まとめていきたい。

あと自分はWebあたりの知識が割と不足していてわからないことだらけだったので、単純に語句とか調べてなるほどなとなるより
コードをみてこういうふうになっているのかと理解する方がよいということがわかったのは収穫だった

KLabの技術系インターンに参加してきた話

はじめに

人生初のインターンということでKLabさんの技術系インターンに参加してきました。
www.klab.com

今回、期間中お世話になったメンターは山本雅也さんです。
twitter.com


進捗払いに関してはどこのインターンに行った方も載せていますが、これに関しては割愛します。
詳しくは僕のツイッターを漁ってください。

インターンに行くまで

まず、何があったかというと昨年の9月頃にこんなことがありました。




はい、つまりはそういうことです。

この前に、chikuwait君と話していて
「ネットワーク、興味あるから勉強したいんだよねぇ」
と、雑に話した途端にこれです。

というわけで、この後に山本さんに直接連絡を取りリモートで面談を行い
すごくあっさりと決まってしまいました。

ただ、ネットワーク周りを本当にやったことがなかったのでインターンまでに
Linux カーネルモジュールで最速のEchoサーバを作る」

DHCPの仕様をざっくりと理解してくる」
といったことを事前に行いました。

進捗払いに関してはどこのインターンに行った方も載せていますが、これに関しては割愛します。
詳しくは僕のツイッターを漁ってください。

目標

今回のインターンでの目標はメンターである山本さんが作っているユーザ空間で動作するTCP/IPプロトコル・スタックである「microps」にDHCPクライアント機能を追加実装する。
ただ、それだけです。

micropsとは

micropsは前述の通り、ユーザ空間で動作するTCP/IPプロトコル・スタックで、Ethernetフレームの送受信を行います。
加えてカーネル内のプロトコル・スタックを使用していません。
僕が機能追加する前の段階でSocketライクなAPIを提供する機能もありました。

github.com

インターン(0日目)

飛行機代を抑えるために早朝の便をとり、なんと前日の昼前に東京着という意味不明なスケジュールで行動していました。
初の東京だったのでまずどこに行こうかと思い、やっぱりアキバにいってしまいました。
その後、紆余曲折を経てホテルに到着。初日からの戦闘に向けて美味しいものも食べてきました。

インターン(1日目)

この日は、10:30ごろにKLabさんが入っている六本木ヒルズ森タワーに行きました。
気合が入りすぎて5時ごろには起床していたので、何故か江東区から歩いていくという強行に出ました。

この日はまず
インターンのガイダンス説明から始まり
メンターさんの紹介やその他諸々の手続きを行い
その後すぐに会議室に移りmicropsの仕様とDHCPに関する説明を30分ほど受けました。

それからはひたすらに開発環境の構築やmicropsのビルドなどを行い、その日は終了。

インターン(2日目)

この日はDISCOVERパケットを構築すること、またそれを送信し返ってくるOFFERパケットを解析することが目標だった。
DHCPクライアントがどのようにIP等を手に入れているかというと、ざっくりと以下の4プロセスを踏む

  • DISCOVER パケットをクライアントがブロードキャストで送信
  • OFFER パケットが返ってくるので情報を解析
  • 必要な情報を取り出しREQUEST パケットに組み込み送信
  • ACK or NACKが返ってきて終了

2日目はこの前半2つを行った。

まずはRFCや事前資料からDISCOVERパケットを構築するところから始めた。
パケット自体は固定形式部分が234byte、可変形式のオプションが64byteという構成になっている。
またIPを取得していない状態でsocket等を使用するため、送受信は全てブロードキャストを使用する。
そのため、ブロードキャストフラグも使用し明示的にブロードキャストを指定。

オプションは4byteのマジックコードに始まり、パケットの種別を示すものが3byte続く。
その後はクライアントIDやMACアドレス、サーバ固有情報などが含まれ最終的には0xFFをStopperとしている。
Stopperから末尾まではデータが入っているはずがないので0x00で埋めてある。

ここまでがパケット構成となる。
その結果を以下に示す。

========== DHCP Message Debug Print ==========
    op: 1
 htype: 1
  hlen: 6
  hops: 0
   xid: 5abda168 (1522377064)
  secs: 0
 flags: 8000
ciaddr: 0.0.0.0
yiaddr: 0.0.0.0
siaddr: 0.0.0.0
giaddr: 0.0.0.0
chaddr: 08:00:27:0c:a1:27
 sname: 
  file: 
 magic: 63 53 82 63
option[35] 01
option[3d] 01 08 00 27 0c a1 27
option:[ff]
total 300 bytes (padding 47 bytes)

次に構築したパケットを送信、レスポンスを受信する処理を追加した。
ここで使用したものはmicropsに始めから搭載されていたudp関連のsocketライクなAPI

レスポンスデータは形式こそオプション以外では変化が無いもののオペコードは必ず2になって(送信時は1を指定)返ってくる。
またオプション部分でマジックコードの次もOFFERパケットを示すものに変わっている。

以下が受信したOFFERパケットの情報となる。

========== DHCP Message Debug Print ==========
    op: 2
 htype: 1
  hlen: 6
  hops: 0
   xid: 5abda168 (1522377064)
  secs: 0
 flags: 8000
ciaddr: 0.0.0.0
yiaddr: 192.168.0.102
siaddr: 192.168.0.1
giaddr: 0.0.0.0
chaddr: 08:00:27:0c:a1:27
 sname: 
  file: 
 magic: 63 53 82 63
option[35] 02
option[36] c0 a8 00 01
option[33] 00 00 02 58
option[01] ff ff ff 00
option[03] c0 a8 00 01
option[06] c0 a8 00 01
option[0f] 55 62 75 6e 74 75 53 65 72 76 65 72 44 48 43 50 53 65 72 76 65 72 2e 6c 6f 63 61 6c
option:[ff]
total 304 bytes (padding 0 bytes)

二日目はここで終了。

インターン(3日目)

3日目はOFFERパケットから必要な情報を取り出し、REQUESTパケットを構築し送信
返ってくるACK(or NACK)を解析して、micropsを使用したEchoサーバの設定に必要な情報を組み込むところまでが目標。

まずは、yiaddrから使えるIPを取り出してREQUESTパケットとして構築。
構築したパケット自体は2日目に行ったものとほとんど同様なので概要は省略。

========== DHCP Message Debug Print ==========
    op: 1
 htype: 1
  hlen: 6
  hops: 0
   xid: 5abda168 (1522377064)
  secs: 0
 flags: 8000
ciaddr: 0.0.0.0
yiaddr: 192.168.0.102
siaddr: 192.168.0.1
giaddr: 0.0.0.0
chaddr: 08:00:27:0c:a1:27
 sname: 
  file: 
 magic: 63 53 82 63
option[35] 03
option[3d] 01 08 00 27 0c a1 27
option[32] c0 a8 00 66
option:[ff]
total 300 bytes (padding 41 bytes)

次にACK(or NACK)を受け取る。
これもOFFERパケットの受信と同様に行った。

========== DHCP Message Debug Print ==========
    op: 2
 htype: 1
  hlen: 6
  hops: 0
   xid: 5abda168 (1522377064)
  secs: 0
 flags: 8000
ciaddr: 0.0.0.0
yiaddr: 192.168.0.102
siaddr: 192.168.0.1
giaddr: 0.0.0.0
chaddr: 08:00:27:0c:a1:27
 sname: 
  file: 
 magic: 63 53 82 63
option[35] 05
option[36] c0 a8 00 01
option[33] 00 00 02 58
option[01] ff ff ff 00
option[03] c0 a8 00 01
option[06] c0 a8 00 01
option[0f] 55 62 75 6e 74 75 53 65 72 76 65 72 44 48 43 50 53 65 72 76 65 72 2e 6c 6f 63 61 6c
option:[ff]
total 304 bytes (padding 0 bytes)

ここまでは前日に行っていたこととあまり大差なかったのですんなりいった。
問題はこのあと、実際にACKから必要なIPアドレスサブネットマスクデフォルトゲートウェイを取り出して設定として組み込む点だった。
IPアドレスなどはすぐに見つかり、RFCなどにオプション種別も記載されていたので比較的容易に発見できた。

それを組み込む際にip_init 関数が存在するがこいつを単に呼び出すとethernetフレームにipプロトコルが二重に追加されてしまう問題があった。
またルーティングテーブル周りの初期化や、このmicropsを利用する人が任意でDHCPクライアント機能を利用できるようにするなど
やることが山積みだった。

このあたりはメンターの山本さんの力を借りて(もちろんここまで来るのに絶大な支援があった)、実装することができた。

ここで3日目が終了。

インターン(4日目)

この日は、最低限の処理しか追加していなかったコードにオプション解析などを組み込みデータがちゃんと正しいものか、こちらが想定しているものかチェックする処理などを追加
その後はひたすら鬼のペアプロコードリファクタリングを行った。(多分これが一番きつかった)

インターン(5日目)

この日は最終成果発表に向けて資料の準備とPRの作成を行った。
期間中、多大なご迷惑をお掛けした分プルリクがマージされた時はすごく嬉しかったというか、やったぞという気持ちしかなかった。

その後の成果発表も無事に終わり、Klabの方々とご飯を食べに行きこの日は終了。
そしてインターンも終了。

インターン(6日目)

帰りの飛行機は例によって飛行機代を抑えるためにほぼ最終便をとっていた。
そのため、馬鹿みたいに時間があったので大門->スカイツリー->浅草->上野、アキバと観光してきた。
余談ではあるが、人生経験の一環としてメイドカフェに行こうか本気で迷った。

さいごに

というわけで長かったようで短く感じた怒涛の初インターンは終了した。
インターンでやったことあったことを雑にまとめてみたが、実際にはスケジュールがかつかつで8時間はコードを書いている日の連続だった。
技術的な面でも未熟であったことがわかったが、その他にも色々と自分には知らないことが多かった。
成果発表で、色々なエンジニアの方に今回何をやったかということを報告し「わからない事にわからないなりに挑戦し、完成させたという点は評価できる」との評価を頂いたが
やはり実際に作っている時などに、「これはきいたことがあるぞ」「これは知っているかもしれない」と思っていたこともあったがそれをいざ実行しようとなると「お、なんか上手くいかないぞ」といったことが多くあり
わかっている事とできる事は全く別物であると痛感した。
また、自分は今までコードばかりを書いていてそれが楽しかったからというのもあるが
もっと自分が作るものの基盤的な知識を身につけなければいけないということも実感した。

そういった、自身の弱点のようなものを見続けたので今回のインターンはすごく辛く感じる面が多かった。

しかし、それ以上にものすごいレベルのエンジニアの方たちに囲まれて
今回のようにコードを書いて、何かを実現してという環境に身を置けたのはすごく楽しかった。
辛いと感じることもあったが、それでもわからないことをわからないと認めてその上で色々と試行錯誤を繰り返し一つの問題を解決していく。そういった時間を過ごせたことはこの上なく幸せな時間だった。

学ぶべきことが多く分かった反面で、学んだことも非常に多かった。
一週間ではあったが自分自身が以前に比べて成長できたことを感じることができたのでインターンに参加して本当によかったと思った。


最後に、今自分がいる大学ではあまり情報系に熱心な同期がいない。
そして家でもコードを書いて、それを楽しいと5年も言い続けている自分はそんな普通な人からみるとキモいという対象らしい。
そんな人たちに流されて普通の大学生活を送ろうとしていたが、今回のインターンでお昼休みなどに山本さんとキャリアについて話していて
自分のやりたいことがはっきりと見えたので周りを気にしないで残りの学生生活を自分なりに有効活用しようと思う。

本当に今回インターンに参加できてよかったとおもう。
今回、お世話になったKLabの皆さん、そしてなによりメンターとして1週間指導してくださった山本雅也さん
本当にありがとうございました。

無限インターン行きたい!!

プログラマのためのSQL第4版を読んで。〜 データベース VS ファイルシステム 〜

はじめに

訳あって今、カーネルからWebまでという非常に広い範囲を日常的に触っている。
しかし、一日の時間は24時間と決まっており自分にはコードを書く事以外にもしなければならないことがある。
だが、コードを書かないという日はなるべく作りたくない。
我儘を言うなら、限られた時間でC言語で低レイヤーをやったりPHPを使って高レイヤーをいじったり、ScalaJavaを使ってその中間も色々やってみたい。

そう思った時に、その全てを叶えるのがデータベースだと思った。

NoSQLであるRedisはC言語ベースで作られていてソースコードGithub上で公開されている。
PHPではサーバサイドとして実際にDBを操作する時がある。
Scala/Javaにはjdbcなどがある。

つまり、先ほどの我儘を全て叶えてくれる非常に魅力的な領域だった。
というわけで、そんなふわふわした理由のもとにデータベースを勉強してみようと思った。

単にデータベースを勉強するといっても、実際にDBを使うことに始まり
それらを利用するためのドライバやDB自体をソースコードレベルで理解することまでを目標にする。

その上でまずDBの使い方、特にSQLについて深く勉強したいと思ったので「プログラマのためのSQL」を購入した。
ここではその書籍を読んで勉強したことをまとめていく。

データベース VS ファイルシステム

データベース、特にRDBMSにおいては普段利用するプログラミング言語が操作するファイルシステムとはかなり勝手が違う。
データベースを操作するのにはSQLを使用し、SQLプログラミング言語と異なり独自のI/Oシステムを持たずデータの宣言、操作、制御のみを行う。

しかし、プログラミング言語にもSQLにもモデルは存在する。
そのモデルを理解することで対象を理解することが容易になる。
プログラミング言語におけるモデルとは、あまり詳しくないのではっきりとは言えないが「いくつかのプログラミング言語は数学をベースとしているため数学で実際に使用する記号群や構成を使用することができる。」といったことを指す。
SQLにおけるモデルは数学でいうところの集合(Set)としてのデータである。
数学でいう集合は、それに含まれる要素は特定のタイプに属していて順序をもたない。そして、集合に対する操作は一度に全ての要素に対して行われる。

わかりやすい例が参考文献に記述してあった。

正の整数の集合から奇数の部分集合を求める場合、答えはすべての奇数を含む単一の集合として得られる。奇数を1つずつ調べて順番に集合を作っていくようなことはしない。ただ、奇数を「2で割ったときの余りが1になる」という条件を満たす数として定義するだけだ。

このような背景があるために、分類の条件を変更したとしてもテスト可能で、かつ分類そのものも可能である。
それ故、集合を対象とする集合指向モデルは並列処理に向いている。


SQLは3つのサブ言語から構成されている

  • DDL: データ宣言言語(Data Declaration Language)
  • DML: データ操作言語(Data Manipulation Language)
  • DCL: データ制御言語(Data Control Language)

DDLはデータベースに含まれる中身を定義し、そこに含まれるデータの整合性を確保する。
前述のファイルシステムでは整合性やデフォルト値、他のテーブルとの関連性は一切定義されない上に厳密にデータの論理的一貫性を保つ機能はない。
しかしデータベースにおいてはDDLがそれらの役割持ち、DMLとDCLと共に動作するためSQLは統合された全体として扱われファイルの様に分離された一部分とは扱われない。

DMLSQLを一度でも書いたことがある人ならわかる。
SQLにおける次の操作を示す。

  • SELECT
  • INSERT
  • UPDATE
  • DELETE

DCLはここではまとめない。かなり深い概念らしくあまり解説されていなかったから。


ここまでのまとめ

  • SQLは集合をモデルにしている
  • 3つのサブ言語から構成されている
  • データベースにおけるスキーマはファイルの集合体ではなく、関連性を持つ。
  • テーブルはファイルではなく、スキーマの一部である。

エンティティとしてのテーブル

データベース、特にリレーショナルデータベースにおいてエンティティとは属性によって定義される。そしてそのインスタンスはテーブルにおける1行1行を指す。
属性とはテーブルでいうところの列であり、値はそれ以上分解不能な原始的な値であるスカラ値をもつ。
よってエンティティとしての役割をデータベースのテーブルは持つことになる。

完全に同じ構成のテーブルが2つあるとき集合的な観点からみるとそれらは同じ種類の要素を持つ集合でしかない。
これはファイルであれば許されることである。なぜならファイルはそれら自体は物理的に分類された単位であるために同一情報を異なるファイルとして保持することが許容されている。
しかし、前述の内容よりSQLにおいては集合であり区別が付かないのでそのような物は一つにまとめてしまうのがよい。

関連としてのテーブル

これは非常に簡単なことで、テーブルにおける関連とは列が一つ以上のエンティティテーブルを参照することによって成立している。
これはファイルとフィールドにはない特性になっている。

行 VS レコード

行はレコードではない。レコードはそれを読み込むアプリケーション側で定義されるものであって、それ自体はスキーマで定義される。
フィールドの名前はアプリケーション側で定義されるのに対して、行はデータベーススキーマ中で定義されるということになる。


ここで空のファイルを考える。
空のファイルは0バイト長という状態であるが、空のテーブルに関しては行は空としても列を持っていて理論的制約などを保持している。
つまり数学的意味合いの集合における空集合とは少し異なり、上記の構造をとるためたとえ空であっても別の集合として扱うべきである。

次の行にある特徴としてはテーブル内の全ての行は構造上同一な形式を取る点であり、これはファイルにはない。
ファイルであれば内部に含まれるデータの構造などは全て一致しているわけでもなく文字列であったり数値であったり情報もサイズもバラバラにしてしまうこともできるからだ。

列 VS フィールド

レコード内のフィールドはそれを読み込むアプリケーション側で定義されるが行における列はデータベーススキーマで定義される。
そして列が保持するデータ型は前述の通りスカラ値を持つ。

そして、SQLでは列は列名によってのみアクセスされる。
厳密に列名によってのみというわけではなく

SELECT *
INSERT INTO <table name>

の様な省略形は存在する。しかしこれは列名のリストを単にテーブル定義で列が定義された物理順序で展開しているだけにすぎないので内部的には列名でのみアクセスされる。
SQLにおけるNULLの使用もこれに近いものとなっている。

列があるが故に、ファイルとの違いがある。
ファイルは受け入れる内容や吐き出す内容に特に制約は無いがSQLにはその制約が存在する。
それにより、SQLが扱うものには一定の整合性が保証される。

つまりはそれが指す関係の整合性を示すことになる。
これはファイルごとに独立しているファイルシステムには無い特徴となっている。

おわりに

SQLとしての基盤的な事象をファイルシステムと比較してまとめてあったのはSQLを理解する上で非常に大事な情報であると感じた。
次はDBMSが内部的に行っているトランザクションと同時実行制御に関する章を読んでまとめていきたい。

参考文献