20191222のPHPに関する記事は18件です。

BEAR.SundayのチュートリアルでDIが面白いほど理解できた話

はじめに

BEAR.Sunday アドベントカレンダー 23日目の記事です。

長い間、DIに対する理解がふわっとしていたのですが、BEAR.Sundayのチュートリアルをやってみたところ、今まで喉に引っかかっていた小骨な取れたかのように飲み込めたような気がしたので、BEAR.SundayでDIを実践してみたことで理解できたことを記事にしてみようと思った次第です。

DI とは

DI とは、Dependency Injection の略で日本語では依存性の注入という意味です。

依存性をオブジェクトのことだと捉えると、オブジェクトを他のオブジェクトに渡してあげるということになります。

あるクラスの中で他のクラスをNewで生成したりせずに、引数として外部から渡してもらって利用することをDIになります。

DIではない例

<?php
namespace src;

class Car {

    private $tire;

    public function __construct()
    {
        $this->tire = new Tire();
    }

    public function viewCar()
    {
        settingCar($this->tire);
        return $this;
    }
}

Carクラスのコンストラクタ内でTireオブジェクトを生成しています。

DIの例

<?php

namespace src;

class Car {

    private $tire;

    public function __construct(Tire $tire)
    {
        $this->tire = $tire;
    }

    public function viewCar()
    {
        settingCar($this->tire);
        return $this;
    }
}

今度は、コンストラクタの引数にTireオブジェクトを渡してあげるようにしました。

このようにクラス内で他のクラスを生成しないように、外部から注入してあげることがDIになります。

DIには、以下のようなパターンがあります。

  • コンストラクターインジェクション
    • コンストラクタの引数にオブジェクトを渡す
  • セッターインジェクション
    • セッターにオブジェクトを渡してプロパティを設定する
  • メソッドインジェクション
    • メソッドにオブジェクトを渡す
      • セッターインジェクションと似ていますが、注入されたオブジェクトのメソッドを利用することを目的としている場合がこちらに該当するのかなと認識しています

ちなみに、上記の例はコンストラクターインジェクションの例になります。

DI すると何が嬉しいのか

DIのメリットとしてよく説明されるのは

  • 粗結合になり柔軟性が上がる
  • テストしやすくなる

という点だと思います。

例えば、クラスAの中でクラスBを生成している実装の場合、クラスBのオブジェクトを生成する際のパラメータに変更があった場合、クラスA側も修正しなければなりません。

しかも、クラスBを廃止してクラスCに置き換えたくなった場合、クラスBからオブジェクトを生成していた箇所すべてを修正しなければいけません。

現実世界を例に説明してみる

例えば、車工場でタイヤを作っていたら、製造した車に適したタイヤかどうかを組み立てる段階で確認しなければなりません。

ですが、車工場はタイヤ工場が作ったタイヤをただ組み立てるだけにしておけば、車の製造のみに集中することができます。

タイヤ工場も、車工場のことを気にせず、「自分たちの工場でつくるタイヤはこういう規格なんだ!」ということを明確にしておくだけで良くなります。

このように、オブジェクト同士を小さい単位で切り分けられると、他の部品に取り替えることも容易になり、部品交換による影響も小さくすることができます。

BEAR.Sunday(Ray.DI)でDIを試してみる

それでは、具体的な実装の場面でDIを活用するとどうなるかをBEAR.Sundayを利用しながら確認していきたいと思います。

まずは、与えられた正の整数までの正の整数をランダムに生成してレスポンスする処理を作成してみます。

<?php
namespace MasaKuVendor\DiSampleApp\Resource\App;

use BEAR\Resource\ResourceObject;

class Number extends ResourceObject
{
    public function onGet(int $maxNum) : ResourceObject
    {
        $this->body = ['rando' => rand(1, $maxNum)];
        return $this;
    }
}

このコードに指定された値が正の整数かを判定するバリデーションクラスをDIしてみます。

まずはバリデーションクラスのインターフェースを作成します。

<?php
namespace MasaKuVendor\DiSampleApp;

interface CheckNumberInterface
{
    public function checkPositiveInteger(int $num) : bool;
}

次は先程の整数を生成するメソッドに上記のバリデーションクラスをDIする処理を追加します。

<?php
namespace MasaKuVendor\DiSampleApp\Resource\App;

use BEAR\Resource\ResourceObject;
use MasaKuVendor\DiSampleApp\CheckNumberInterface;

class Number extends ResourceObject
{
    /**
     * @var CheckNumberInterface
     */
    private $checkNum;

    public function __construct(CheckNumberInterface $checkNum)
    {
        $this->checkNum = $checkNum;
    }

    public function onGet(int $maxNum) : ResourceObject
    {
        if($this->checkNum->checkPositiveInteger($maxNum)){
            $this->body = ['rando' => rand(1, $maxNum)];
        }else{
            $this->body = ['message' => `正の整数を入力してください`];
        }

        return $this;
    }
}

コンストラクタでバリデーションクラスのオブジェクトを注入してクラス内でバリデーションメソッドが利用できるようにしました。

次に具体的なバリデーション処理を記載していきましょう。

<?php
namespace MasaKuVendor\DiSampleApp;

class CheckNumber implements CheckNumberInterface
{
    public function checkPositiveInteger(int $num) : bool
    {
        if($num > 0){
            return true;
        }else{
            return false;
        }
    }
}

すごく単純なチェック処理ですが、正の整数かをチェックする処理を追加しました。

最後に、アプリケーション内でDIできるようにバリデーションチェッククラスを束縛します。

<?php
namespace MasaKuVendor\DiSampleApp\Module;

use BEAR\Package\AbstractAppModule;
use BEAR\Package\PackageModule;
use MasaKuVendor\DiSampleApp\CheckNumber;
use MasaKuVendor\DiSampleApp\CheckNumberInterface;

class AppModule extends AbstractAppModule
{
    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $appDir = $this->appMeta->appDir;
        require_once $appDir . '/env.php';
        $this->bind(CheckNumberInterface::class)->to(CheckNumber::class); // この行でCheckNumberInterfaceが引数の型に指定された際にCheckNumberが渡されるようになります
        $this->install(new PackageModule);
    }
}

これで、各アプリケーション内でインターフェースを指定してメソッドを呼び出した際に、自動的にオブジェクトを注入してもらえるようになりました。

おわりに

こうしてBEAR.Sundayのチュートリアルを進めながら実際に動かしてみることで、自分として引っかかっていたポイントがするっと理解することができました。

というのも、もともとLaravelアプリケーションで「他の処理でも利用したい共通の処理をサービスクラスとして切り出して、必要な箇所で呼び出しながら利用できるようにしたい」と思っていました。

そこで、いろいろと調べているうちに、DIやファサードという手段があることを知ったのですが、ある処理にクラスを外部から注入する、ということがどうしても理解できず、何となく見様見真似で利用していた状態でした。

BEAR.Sundayでは、あるリクエストを処理する上で必要となるオブジェクト(依存性)だけを注入して処理するということが一連の処理の中で明確になっていたのでチュートリアルを進めるだけでも「なるほど、こういうことか!」と理解することができました。

ツールや手法を実践の中で覚えていくことも大事ですが、意味を理解しながら覚えていくことで、自分が実装しているコードにも自信がついてくると思うので、自分が書いているコードでは何を実現しようとしているのかを意識してコードを書いていきたいですね。

参考サイト

チュートリアル | BEAR.Sunday
[https://bearsunday.github.io/manuals/1.0/ja/tutorial.html]

DI | BEAR.Sunday
https://bearsunday.github.io/manuals/1.0/ja/di.html

PHPでDIする
https://www.slideshare.net/OhasiYuki/phpdi-53487642

「なぜDI(依存性注入)が必要なのか?」についてGoogleが解説しているページを翻訳した
https://qiita.com/mizunowanko/items/53eed059fc044c5aa5dc

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPで「大石・泉・すき」をランダムに表示して「大石泉すき」が完成したら大石泉すき

この記事について

本記事は、「大石泉すき」 Advent Calendar 2019の23日目です。

プログラム言語「PHP(PHP: Hypertext Preprocessor)」を使い、「大石泉すき」が完成するまで「大石・泉・すき」をランダム表示するプログラムを作成します。

大石泉とは

「大石泉」(おおいし いずみ)とは、Mobageにて配信されているバンダイナムコエンターテインメントのソーシャルゲーム『アイドルマスター シンデレラガールズ』に登場するロジックアイドルである。

(引用:ニコニコ大百科より)

私の担当アイドルである二宮飛鳥とは出身地が同じ(静岡県)で歳が近いということでいつか公式でユニット組んでくれないかなーと思ってるんですが、中々その機会が訪れなくて泣けますね。

PHPで「大石泉すき」を表示するには

まず最初に、PHPで「大石泉すき」と単純に表示するだけのプログラムを作る場合について考えます。
PHPでは以下のように記述することで、「大石泉すき」と表示することができます。

大石泉すき

冗談抜きでこれで終わりです。
PHPは、以下のようにHTMLの中に埋め込んで使うことができる言語です。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title><?php echo $title; ?></title>
</head>
<body>
    <?php
        // <?php ~ ?> で囲った部分のみがPHPの記述として解釈される
        echo $content;
    ?>
</body>
</html>

この場合、「<?php ~ ?>」で囲われていない部分はHTMLとして普通に表示されます。
なので、一番最初の例のように単純に文字列を表示したい場合、PHPのタグを記述せず表示したい文字列を書けばそれで終わりなのです。
ね、簡単でしょう?

勿論、以下のようにプログラムっぽく書くことも可能です。

<?php
    echo '大石泉すき';

これで終わるとあまりにもあっさりしすぎなので、ちょっと捻ったことをやってみましょう。

#「大石泉すき」が完成するまで「大石・泉・すき」をランダム表示する #って何?

「大石泉すき」が完成するまで「大石・泉・すき」をランダム表示します。

何年か前に流行った「「進捗・どう・です・か」をランダムに表示し「進捗どうですか」が完成したら煽ってくる」を「大石泉すき」でやろうという試みです。

これをPHPで書くと、こんな感じです。

<?php
// 「大石泉すき」を少しずつ区切って配列に格納する
// 「大石泉すき」以外の文字列が紛れ込んでるって? さあ、何の事ですかね…………
$ooishiizumisuki = [
    '大石',
    '泉',
    '二宮',
    '飛鳥',
    'すき',
];

// 探索対象の文字列リスト
$search_list = [
    '大石泉すき',
    '二宮飛鳥すき',
];

// count関数は配列の要素数を取得する
// そこから1を引くことで当該配列における最大の添字を求めることができる
$max_index = count($ooishiizumisuki) - 1;

// PHPの配列は添字が0から始まる
$min_index = 0;

$text = '';
$appeared_text = '';

while (1) {
    // $ooishiizumisukiの中からランダムに文字列を取得して、文字列結合していく
    $index = random_int($min_index, $max_index);
    $text .= $ooishiizumisuki[$index];

    foreach ($search_list as $search_text) {
        // 結合した文字列の中に、探索対象の文字列が含まれていたらループを抜ける
        if (strpos($text, $search_text) !== false) {
            $appeared_text = $search_text;
            break 2;
        }
    }

}

// 探索対象の文字列が出現するまでに何文字を要したかを記録
$text_length = mb_strlen($text);

?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title><?=$appeared_text;?></title>
</head>
<body>
    <div><?=$text;?></div>
    <div><?=$text_length;?>文字で「<?=$appeared_text;?>」が出現しました。<?=$appeared_text;?></div>
</body>
</html>

実際に実行すると、こんな感じになります。

すき二宮泉すきすき大石すき飛鳥すき飛鳥泉すき泉大石大石飛鳥飛鳥泉飛鳥すきすき二宮泉飛鳥すき飛鳥泉飛鳥二宮大石二宮大石泉すき
61文字で「大石泉すき」が出現しました。大石泉すき

運がよければ以下のように最短で「大石泉すき」できます。

大石泉すき
5文字で「大石泉すき」が出現しました。大石泉すき

ところで「大石飛鳥」、「二宮泉」ってすごくときめく文字列じゃありませんか?
バンナ○ならびにサイゲ○ムスはいずあすを公式化してね! しろよ☆

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Visual Studio Code - Remote Development を使用して PHP Xdebug を使ったデバッグを行う

要約

  • VS Code の Remote Development で Xdebug する時は、remote_host の向き先を ローカルホストにする。
  • Remote Development の接続先の VS Code に PHP Debug を導入する。
  • .vscode/launch.json で Xdebug からの情報を Listen する。

はじめに

VS Code の 拡張機能 「 Remote Development 」は、これまでもどかしく感じていたローカル開発環境の構成を改善してくれそうで色々と試しています。

前回の記事 では、Vagrant で構築した仮想環境の上にある Docker コンテナに対して、 Remote Development で接続する方法を記載しました。

今回は、前回構築した環境に追加する形で、 PHP の Xdebug を用いた開発が出来るように設定方法をまとめてみます。

概要

書くこと

  • Docker コンテナの起動時の Xdebug の設定方法
  • VS Code Server で Xdebug のデータを Listen する方法

書かないこと

  • Vagrant で構築する仮想環境に Docker や docker-compose を導入する方法。

想定環境

この記事で実現するシステムの構成は下図の通りです。

image.png

Remote Development の有無による違い

Remote Development を使用しない場合

Remote Development を使用せず、 Xdebug のデータを HostOS 上の VS Code で Listen する場合は以下のような構成でした。

image.png

この構成を実現するためには、 Xdebug の設定ファイルで remote_host の IP アドレスを指定する必要がありました。

Remote Development を使用する場合

Remote Development を使用する場合の構成は以下の構成になります。

image.png

Xdebug の remote_host は localhost を指定すればよく、送信先の IP をいちいち気にする必要がありません。

設定手順

0. 基本設的な設定手順

この記事で実現している構成の基本的な要素は以下の記事で解説しています。

1. XDebug が有効化された Docker コンテナの構築

Docker コンテナの構築に必要なリソースは以下の通りです。

command(bash@GuestOS)
## Dockerfile
## authorized_keys   : 公開鍵認証に使用する公開鍵。Dockerコンテナ内に配置する。
## php/20-xdebug.ini : Xdebug の設定ファイル。Dockerコンテナ内に配置する。
$ tree
.
├── Dockerfile
├── authorized_keys
└── php
    └── 20-xdebug.ini

Dockerfile は次のように記載します。

Dockerfile
# image
FROM php:7.0.15-fpm

ENV LANG C.UTF-8

RUN apt-get update -qq && \
    apt-get install -y \
        zlib1g-dev \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libpng-dev \
        && docker-php-ext-install zip \
        && yes "" | pecl install xdebug \
        && docker-php-ext-enable xdebug \
        && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
        && docker-php-ext-install -j$(nproc) gd

RUN apt-get update \
    && apt-get install -y libpq-dev \
    && docker-php-ext-install pdo_mysql pdo_pgsql

RUN apt-get update \
    && apt-get install -my wget gnupg

WORKDIR /opt/

# ここから最後までがポイント
RUN apt-get update && apt-get install -y openssh-server
RUN mkdir /var/run/sshd

RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config

# SSH login fix. Otherwise user is kicked off after login
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd

ENV NOTVISIBLE "in users profile"
RUN echo "export VISIBLE=now" >> /etc/profile

# 手元の公開鍵をコピー
COPY authorized_keys /root/authorized_keys

EXPOSE 22

# 公開鍵を使えるようにする (パーミッション変更など)
CMD mkdir ~/.ssh && \
    mv ~/authorized_keys ~/.ssh/authorized_keys && \
    chmod 0600 ~/.ssh/authorized_keys &&  \
    # 最後に ssh を起動
    /usr/sbin/sshd -D

# Xdebug の設定ファイルを Docker コンテナに配置
COPY php/20-xdebug.ini /usr/local/etc/php/conf.d/20-xdebug.ini

XDebug の設定を以下のようにします。

php/20-xdebug.ini
xdebug.remote_enable = 1
xdebug.remote_autostart = 1
xdebug.remote_connect_back = 1
xdebug.remote_host= "localhost"
xdebug.remote_port = 9001
xdebug.remote_log = /var/log/xdebug.log

Docker コンテナを以下のコマンドで構築します。

command(bash@GuestOS)
$ pwd
/vagrant/docker-sample

$ tree
.
├── Dockerfile
├── authorized_keys
└── php
    └── 20-xdebug.ini

# build
$ docker build ./ -t example

# Docker Container を起動
# Port:10000 を Port:22 に転送
$ docker run -d -p 10000:22 example

# Docker Container に SSH 接続
$ ssh root@127.0.0.1 -p 10000 -i ~/.ssh/private_key

2. .ssh/config の設定

Host OS から 仮想環境(GuestOS、Dockerコンテナ)に SSH接続するために、以下の設定を行います。

.ssh/config(Windows10)
# Vagrant で起動した仮想環境への SSH接続設定
Host vagrant-os
  HostName 127.0.0.1
  User vagrant
  Port 2222
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile C:/Users/<username>/work/vagrant/centos-7-docker/.vagrant/machines/default/virtualbox/private_key
  IdentitiesOnly yes
  LogLevel FATAL

# Docker で起動した仮想環境への SSH接続設定
Host docker-os
  Hostname 127.0.0.1
  User root
  Port 10000
  ProxyCommand C:\Windows\System32\OpenSSH\ssh.exe -W %h:%p vagrant-os
  IdentityFile C:/Users/<username>/work/vagrant/centos-7-docker/.vagrant/machines/default/virtualbox/private_key

3. Visual Studio Code の設定

1.) と 2.) の設定をすることで、 VS Code の Remote Development の SSH Target には以下のように接続先候補が表示されるようになります。

image.png

上図のうち、docker-os の方にアクセスした際の画面が下図です。
ここで、 .vscode/launch.json を作成して、 Xdebug からのデータを Listen 出来るよう設定しています。

image.png

.vscode/launch.json の設定は以下の通りです。
php/20-xdebug.inixdebug.remote_port で指定した Port と番号を合わせて設定します。
Docker コンテナ上の VSCode に PHPDebug 機能拡張 をインストールすることも忘れず行います。

.vscode/launch.json
{
    // IntelliSense を使用して利用可能な属性を学べます。
    // 既存の属性の説明をホバーして表示します。
    // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Listen for XDebug",
            "type": "php",
            "request": "launch",
            "port": 9001
        },
        {
            "name": "Launch currently open script",
            "type": "php",
            "request": "launch",
            "program": "${file}",
            "cwd": "${fileDirname}",
            "port": 9001
        }
    ]
}

4. 動作確認

3.) までの手順で設定した内容が正しく設定されているか確認してみます。
Docker コンテナ内で適当な PHP プログラムを作成します。

test.php
<?php

echo "test";

PHPDebug で Listen しながら、適当な箇所でブレイクポイントを設定して テストプログラムを実行します。

command(bash@DockerContainer)
$ pwd
/root

$ ls
test.php

# テストプログラムの実行
$ php test.php
test

実行結果は以下のようになりました。

image.png

おわりに

書いたあとで見返してみると、なんのことは無い、普通の Xdebug の設定でした。

リモートの仮想環境に VS Code Server があり、そこと HostOS の VSCode がやり取りすることによって、リッチな VSCode の UI を使用して仮想環境上のリソースが編集出来るので、ひじょ~~~に便利です。

参考

今回の記事を作成するにあたって参考した記事です。

Remote Development

Visual Studio Code - "Remote Development" を使って Docker Container on "Vagrant + VirtualBox" のファイルを編集する #Qiita

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPでpost_max_sizeやupload_max_filesizeの設定がini_set()で反映されない場合

サーバー上でPHPを利用していると、PHPの設定をphp.iniを書き換えることなく、
特定のファイルや、ディレクトリないだけで変更したい場合がたまにあります。

つい先日も、自作のPHPファイルだけで有効になるよう、
post_max_sizeupload_max_filesizeの値をini_setで値を変更したところ、値が反映されていない・・

php_info()で設定値を確認しても、やっぱり変わっていなかったので、
ちょっと調べたところPHPマニュアルにがっつり書いていました。

PHPの各ディレクティブ(設定値)は、そのディレクティブのモードによって、
何処で設定出来るかが決まっているそうです。
PHPディレクティブとモードの一覧はこちら

post_max_sizeupload_max_filesizeは、
モードがPHP_INI_PERDIRだったので、ini_setでの設定変更が不可だったようです。

以下はそのモードについて。

PHP_INI_USER

PHPマニュアルを確認しても該当するディレクティブがないため気にしなくて良さそうなモード
PHP5以前は使われていたらしく、以下の3つのみ設定変更可能なモード
・自作のphpファイルないでini_setコマンドを使って設定変更
Windowsレジストリから設定変更
・.user.iniから設定変更(PHP5.3以降の場合)

PHP_INI_PERDIR

php.ini、.htaccess、httpd.conf、.user.iniでのみ設定変更

PHP_INI_SYSTEM

php.ini、httpd.confでのみ設定変更可能

PHP_INI_ALL

自作PHPファイルでini_set、.user.ini、.htaccess、php.ini、httpd.confのどれでもも設定可能

今回の場合、post_max_sizeupload_max_filesizeを変更するようの
.htaccessを作成して、自作PHPファイルが格納されているディレクトリに配置したところ、
そのディレクトリだけ設定が反映されてることが確認できました。

まとめ

昔、同じ理由で変更出来なかった事をすっかり忘れて、またハマってしまったため、
自分への戒めとして記事に残しときます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP を JVM 言語にするには?

はじめに

みなさんは JVM を知っていますか?そして JVM を実装したことがありますか?さらに、 JVM 言語そのものを実装したことがありますか?
このアドベントカレンダーは PHP アドベントカレンダー並びに言語実装のアドベントカレンダー 25 日目になっており、記事の内容は PHP を JVM 言語そのものにするといった内容になっています。
Java は JDK 9 から構成されるバイナリが大きく変わっており書籍に載せたいくらいの規模になるので、今回については JDK 8 として書かせていただきます。

そもそもなんで JVM なのか?

JVM は Java Virtual Machine の略で、 Oracle から公式に Java Virtual Machine Specification という名前のドキュメントが出ています。

そのため、書いてあることがどのような方法でも理解できれば、誰もが作りやすい物といっても過言ではないかと思います。

関連する登壇

過去に PHP と JVM を絡めた登壇をいくつかしているので、興味のある方は見てみてください。

プロジェクトについて

100% PHP で JVM の実装及び PHP そのものを JVM 言語化する実装を php-java というリポジトリで行っています。
こちらも興味がある方は見てみてください。そして、コントリビュートしてくれるとうれしいです⭐

PHP そのものを JVM 言語にしてく

さて本題に入り PHP を JVM 言語にしていきます。

PHP のコードを AST (Abstract Syntax Tree, 抽象構文木) に変換する

AST (Abstract Syntax Tree) とは抽象構文木のことを指しますが、正直初見で AST や抽象構文木この単語を見ても何を言っているんだ?となるので、どういうものかを軽く説明します。

Wikipedia によると

抽象構文木(英: abstract syntax tree、AST)は、通常の構文木(具象構文木あるいは解析木とも言う)から、
言語の意味に関係ない情報を取り除き、意味に関係ある情報のみを取り出した(抽象した)木構造の木である。

と書いてありますが、よくわからない。これをとてつもなく簡単に言い換えると、PHP 限定で言えば スペース だとか ブレース ({, }) とかを取り除いて、ノード上、つまりそのステートメントを階層とオブジェクトで表現したものです。

例えば

<?php
namespace A;

class SomethingClass
{
}

というコードがあったときに、これを下記のようにツリー状にしたものを AST とだと思えば大丈夫です。

 + Namespace
    + Name
        + A
    + Statements
        + Class
            + Name
                + SomethingClass
            + Statements
                + ...

上記のような状態にしたもので、PHP 言語をパースする方法としては nikic/php-parser を使います。

ちなみに余談ですが nikic/php-parser は PHP そのものの静的解析などに有用です。

nikic/php-parser を composer でインストールします。

$ composer require nikic/php-parser

そして、 Hello World! を AST にしてみましょう

HelloWorld.php
<?php
echo "Hello World!";
ast.php
<?php
require __DIR__ . '/vendor/autoload.php';

use PhpParser\ParserFactory;

var_dump(
    (new ParserFactory())
        ->create(
            ParserFactory::PREFER_PHP7
        )
        ->parse(
            file_get_contents(
                __DIR__ . '/HelloWorld.php'
            )
        )
);

ast.php を実行すると下記のように出力されます。

array(1) {
  [0]=>
  object(PhpParser\Node\Stmt\Echo_)#1100 (2) {
    ["exprs"]=>
    array(1) {
      [0]=>
      object(PhpParser\Node\Scalar\String_)#1099 (2) {
        ["value"]=>
        string(12) "Hello World!"
        ["attributes":protected]=>
        array(3) {
          ["startLine"]=>
          int(2)
          ["endLine"]=>
          int(2)
          ["kind"]=>
          int(2)
        }
      }
    }
    ["attributes":protected]=>
    array(2) {
      ["startLine"]=>
      int(2)
      ["endLine"]=>
      int(2)
    }
  }
}

これが AST になります。 exprs は expressions, つまり式の複数形です。式を 1 つしか受け取れないものは expr になります。
そして、またここでも余談ですが echo が exprs である理由は echo はカンマ区切りで複数の式を受け取ることができるのです。そのため複数の式を受け取る形になっています。 print は echo とは違い複数の式ではなく一つの式のみしか受け取れず、また常に 1 を返す仕様です。
なお、どちらも言語構造ですが AST に分解した際に echo はステートメントとして扱われ、 print は式として扱われます。

構成するクラスファイルのバイナリを理解する

バイナリファイルにコンパイルする際に AST と同様で重要なのが JVM のクラスファイルの仕様そのものです。
JVM におけるクラスファイルの仕様は Java Virtual Machine Specification の Chapter 4. The class File Format にかかれています。

さて、上記に書かれているクラスファイルの仕様は下記のとおりです。

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

これらの意味そのものについては PHP または JavaScript での実装方法を Qiita にまとめているので詳しく知りたい方は読んでみてください。

AST を上記のフォーマットに則りバイナリファイルに書き出していく必要があります。

構成される Constant Pool を理解する

Constant Pool は文字列やメソッドなどのシンボルテーブルのような役割を担っているとても重要なものです。この Constant Pool からメソッド名やクラス名、出力する文字列などを探し出す必要があります。
今回の出力は HelloWorld を中心にしていきたいと思います。クラスファイルの仕様も踏まえて、Hello World で必要な Constant Pool のエントリは下記になります。

  • 親のクラス名
    • java.lang.Object
  • 現在のクラス名
    • ここでは HelloWorld としておきます。
  • System.out.print
    • echo を Java に置き換えると System.out.print が適任であるため。
  • java.io.PrintStream
    • System.out は java.io.PrintStream であるため。
  • Hello World!
    • 出力される文字列
  • main
    • エントリーポイントになるため
  • <init>, <clinit>
    • あってもなくてもどっちでも動く。動的、静的コンストラクタの役割を担っている。今回は使わない。
  • Code
    • Code Attribute を識別するための文字列

これ以外にも Constant Pool はそのメソッドと対になる引数の情報や返り値の情報が入っているディスクリプタ(Descriptor)と呼ばれるエントリも必要です。これによりメソッド・フィールドの引数の型を理解することができます。
そしてそれを紐付けるために NameAndType と呼ばれるエントリも必要になります。

Code Attribute において生成されるべきオペコードとそれに付随するオペランドを理解する

そもそも、 Code Attribute とは何か?というところですが、Code Attribute はメソッドに付随する属性の一つで、そのメソッドのオペレーションコードが書かれています。

Hello World を出力するにあたり、オペレーションコードの理解をする必要があります。オペレーションコードとは何か?というと 命令 のことを指します。略されてオペコードとも呼ばれたりします。一般的なオペレーションコードはアセンブリでもそうですが、1 バイトの数字で表されることが多いです。命令はそれぞれ異なる動きをし、引数を持つものもあります。その引数のことを オペランド といいます。
そして、これらを人間に見やすい形にしたものを総じて ニーモニック と呼びます。

例えば、JVM の命令の一つとして、 invokevirtual などがありますが、これはニーモニックです。そして、 invokevirtual は indexbyte1, indexbyte2 の 2 つのオペランドを必要とします。
また、invokevirtual のオペレーションコードは 0xB6 です。

そして、 JVM にはもう一つ重要な単語としてオペランドスタックというものがあります。これは何かというと、オペレーションコードを実行したにあたって、オペレーションコードにより返された値を保持する場所と認識していただければ大丈夫です。一般的にはプッシュと言います。また、返り値は積まれていく一方でそれを使用することもあります。それをポップといいます。

さて、 本題ですが Hello World を出力するにあたって、必要なオペレーションコードは何か?というと、JVM においては概ね下記のようになります。

  • System.out をロードする(オペランドスタックに積む)
    • static なフィールドから値をロードするには getstatic を使用する
  • Hello World をロードする(オペランドスタックに積む)
    • 定数文字列を Constant Pool からロードするには ldcldc_w を使用する
  • print をコールする (PHP の echo は終端に改行コードが入らないため)
    • 通常のメソッドの呼び出しには invokevirtual を使用する。
  • return (オペレーションの終了として)

これをオペコードで表すと下記です

getstatic #indexbyte1 #indexbyte2 // System.out の Constant Pool の位置
ldc #index // Hello World! の Constant Pool の位置
invokevirtual #indexbyte1 #indexbyte2 // NameAndType のもの
return

バイナリファイルを構成する

前菜は終わり、ここからが本題です。とても長くなります。

PHP でバイナリを書くには?

PHP でバイナリを書くには、 pack を使用するのがベターです。特に JVM だと符号付き、符号なしの場合や、 2 byte 整数値、 4 byte 整数値などを使用することが多々あります。

pack 関数を使用することにより、 PHP の数値リテラルから整数値を割り出すひと手間を省く事が可能です。

例えば 1234 という数値リテラルがあった場合、 ビッグエンディアンの 2 バイトになおす場合、

<?php
$integerLiteral = 1234;
echo chr(($integerLiteral >> 8) & 0xff) . chr($integerLiteral & 0xff)

($integerLiteral >> 8) & 0xff は何をしているのかというと、数値リテラルを2進数に直したときに8ビット分右にシフトさせていて、 0xff, つまり 255 の範囲内で数値が収まるようにマスクをしています。
1234 を 2 進数に治すと下記です。

0100 1101 0010

これを右に 8bit シフトさせます。

0100

0100, つまり 4 となります。そして、これに 0xff をマスクさせています。

  0000 0000 0000 0100
& 1111 1111 1111 1111
----------------------
  0000 0000 0000 0100

なぜマスクをする必要があるのかというと 4 という数字が 255 を超えていた場合、値がオーバーフローすることになります。
それを防ぐためです。なぜ防げるのかというと、 & オペレータは左右のオペランド(両辺)のビットが立っていない場合は 0 となるからです。つまり、 255 以上の値は必然的に 0 になるので、その値を超えることはないのです。

話に戻りますが、4バイトである場合は下記のようになるでしょう。

<?php
$integerLiteral = 1234;
echo chr(($integerLiteral >> 32) & 0xff) . chr(($integerLiteral >> 16) & 0xff) . chr(($integerLiteral >> 8) & 0xff) . chr(($integerLiteral) & 0xff)

そして、こんな面倒くさいことを関数一つ叩けばしてくれるのが pack です。
例えば 2 バイトのビッグエンディアンである場合は下記のようになります。

<?php
echo pack('n', 1234);

4 バイトの場合は nN になります。

メタ情報を書き込む

PHP でバイナリを書く方法を説明しました。では、実際にどういう値を書いていけばいいのでしょうか。
ずばり ClassFile Structure の通りに書いていきます。

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

まず、 magic, minor_version, major_version のメタデータを書き込んでいきます。
magic はファイルを識別するためのマジックバイトのことで、 Java のクラスファイルの場合は 0xCAFEBABE となります。

minor_version および major_version は JDK 8 である場合、下記の表より、 minor_version は 0, major_version は 52 となります。

Java SE class file format version range
1.0.2 45.0 ≤ v ≤ 45.3
1.1 45.0 ≤ v ≤ 45.65535
1.2 45.0 ≤ v ≤ 46.0
1.3 45.0 ≤ v ≤ 47.0
1.4 45.0 ≤ v ≤ 48.0
5.0 45.0 ≤ v ≤ 49.0
6 45.0 ≤ v ≤ 50.0
7 45.0 ≤ v ≤ 51.0
8 45.0 ≤ v ≤ 52.0
9 45.0 ≤ v ≤ 53.0
10 45.0 ≤ v ≤ 54.0
11 45.0 ≤ v ≤ 55.0

この対応表、 SE 8 のページではなく SE 11 のページに載っています。

<?php
$className = 'HelloWorld';
$handle = fopen($className . '.class', 'w+');

// unsigned short, unsigned int で書き込むための関数を用意する
function writeUnsignedByte(int $number)
{
    global $handle;
    fwrite($handle, pack('C', $number));
}

function writeUnsignedShort(int $number)
{
    global $handle;
    fwrite($handle, pack('n', $number));
}

function writeUnsignedInt(int $number)
{
    global $handle;
    fwrite($handle, pack('N', $number));
}

// magic
writeUnsignedInt(0xCAFEBABE);

// minor_version
writeUnsignedShort(0);

// major_version
writeUnsignedShort(52);

AST にした PHP を解釈して Constant Pool を書き込む

次に constant_pool_count, constant_pool の書き込みを行います。
Constant Pool にはエントリを識別するためのタグがあり、JDK 8 である場合、下記のとおりとなります。

Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

今回使うのは Class, Fieldref, Methodref, String, NameAndType, Utf8 の 6 つとなります。
ConstantPool のエントリ番号は 0 からではなく 1 からスタートする仕様であるため、基本的に 0 から採番されないように 0 番目の配列の要素には null を入れておくと良いです。

これらを書きやすくするため予めルーチン化しておきます。

function writeConstantPoolEntry(int $tag, $value): int
{
    global $constantPool;
    $constantPool[] = [$tag, $value];
    return count($constantPool);
}

function writeConstantPoolClass($value): int
{
    return writeConstantPoolEntry(7, $value);
}

function writeConstantPoolFieldref($value): int
{
    return writeConstantPoolEntry(9, $value);
}

function writeConstantPoolMethodref($value): int
{
    return writeConstantPoolEntry(10, $value);
}

function writeConstantPoolString($value): int
{
    return writeConstantPoolEntry(8, $value);
}

function writeConstantPoolNameAndType($value): int
{
    return writeConstantPoolEntry(12, $value);
}

function writeConstantPoolUtf8($value): int
{
    return writeConstantPoolEntry(1, $value);
}

また、 Constant Pool は以後、検索してエントリの番号を知る必要があるため、検索を簡易に行えるようこれについても予めルーチン化しておきます。

function findEntryIndex(int $tag, $value)
{
    global $constantPool;
    foreach ($constantPool as $index => $element) {
        if ($element === null) {
            continue;
        }
        [$entryTag, $entryValue] = $element;
        if ($tag === $entryTag && $value === $entryValue) {
            return $index;
        }
    }
    throw new RuntimeException(
        'Could not found on the ConstantPool'
    );
}

そして、上の方で説明した Constant Pool の値を書き込んでいきます。

  • 親のクラス名
    • java.lang.Object
  • 現在のクラス名
    • ここでは HelloWorld としておきます。
  • System.out.print
    • echo を Java に置き換えると System.out.print が適任であるため。
  • java.io.PrintStream
    • System.out は java.io.PrintStream であるため。
  • Hello World!
    • 出力される文字列
  • main
    • エントリーポイントになるため
  • <init>, <clinit>
    • あってもなくてもどっちでも動く。動的、静的コンストラクタの役割を担っている。今回は使わない。
  • Code
    • Code Attribute を識別するための文字列

上記から、書き込む文字列は下記のとおりになります。

// 親クラス名を書き込む
writeConstantPoolUtf8('java/lang/Object');

// クラス名を書き込む
writeConstantPoolUtf8($className);

// java/lang/System を書き込む
writeConstantPoolUtf8('java/lang/System');

// out を書き込む
writeConstantPoolUtf8('out');

// print を書き込む
writeConstantPoolUtf8('print');

// java/io/PrintStream を書き込む
writeConstantPoolUtf8('java/io/PrintStream');

// print の引数
writeConstantPoolUtf8('(Ljava/lang/String;)V');

// main メソッドの名称を書き込む
writeConstantPoolUtf8('main');

// main の引数
writeConstantPoolUtf8('([Ljava/lang/String;)V');

// Code Attribute の識別子である Code を書き込む
writeConstantPoolUtf8('Code');

Java における名前空間はコード上では . で表現されますが、 Constant Pool では / になります。これはディレクトリであることを示しているからなのだと勝手に推測しています(要出典)

そして、メソッドに対する引数の情報を書き込む必要があるため、少し解説をします。
JVM の引数は基本的には 1 文字の識別子からその種類が何であるかを示します。例えば I であれば Integer です。また、オブジェクトは少し少々特殊で L のあとに ; が現れるまでの間の文字列をオブジェクト名として認識します。例えば Ljava/lang/String; です。そして、配列は [ の文字数で次元数を表します。これらの組み合わせも Constant Pool に書く必要があります。

最終的な書式については ([Ljava/lang/String;)V のようになります。( から ) の間が引数であり、その後ろのシグネチャが返り値となります。

したがって、下記を書き込む必要があります。

// main における引数を書き込む
writeConstantPoolUtf8('([Ljava/lang/String;)V');

さらに、これらをエントリの採番から紐付ける必要があります。例えばクラスであることを示すために Class を使用したり、メソッド名と引数を紐付けるために NameAndType を使用したりします。

今回のケースで必要なのは親クラス, カレントクラス, java.lang.System, main, System.out.print です。
まず、親クラスとカレントクラスを書き込みます。

// カレントクラス
$currentClassEntryIndex = writeConstantPoolClass(findEntryIndex(1, $className));

// 親クラス
$parentClassEntryIndex = writeConstantPoolClass(findEntryIndex(1, 'java/lang/Object'));

// java.lang.System
$javaLangSystemClassEntryIndex = writeConstantPoolClass(findEntryIndex(1, 'java/lang/System'));

// print を包括するクラス
$printStreamClassEntryIndex = writeConstantPoolClass(writeConstantPoolUtf8('java/io/PrintStream'));

そして、 main と引数の情報が対になったものを書き込みます。対であることを示すのは NameAndTypeです。さらに、そのメソッドがどこのクラスのものであるかを示す必要があり、 Methodref を使用する必要があります。

コードで表すと下記のようになります。

// main と引数を対にする
$mainAndArgumentsEntryIndex = writeConstantPoolNameAndType([
    findEntryIndex(1, 'main'),
    findEntryIndex(1, '([Ljava/lang/String;)V'),
]);

// Methodref を書き込む
$mainMethodrefEntryIndex = writeConstantPoolMethodref([
    $currentClassEntryIndex,
    $mainAndArgumentsEntryIndex
]);

次に System.out フィールドの参照を書き込みます。フィールドは引数はなく返り値のみなので、 () を省略して、書き込みます。

writeConstantPoolUtf8('Ljava/io/PrintStream;');

そして、これを out に紐付け、更にその値を java.lang.System に紐付けます。

// System.out を書き込む
$outAndPrintStreamEntryIndex = writeConstantPoolNameAndType([
    findEntryIndex(1, 'out'),
    findEntryIndex(1, 'Ljava/io/PrintStream;'),
]);

// java.lang.System に紐付ける
$systemFieldrefEntryIndex = writeConstantPoolFieldref([
    $javaLangSystemClassEntryIndex,
    $outAndPrintStreamEntryIndex,
]);

そして、 print 自体の情報を ConstantPool に登録する必要があるため、下記を書き込みます。

// print を紐付ける
$printmethodNameAndTypeEntryIndex = writeConstantPoolNameAndType([
    findEntryIndex(1, 'print'),
    findEntryIndex(1, '(Ljava/lang/String;)V'),
]);

$printMethodrefEntryIndex = writeConstantPoolMethodref([
    $currentClassEntryIndex,
    $printmethodNameAndTypeEntryIndex
]);

print を使えるようにクラスに ConstantPool を登録しておきます。

$printMethodrefEntryIndex = writeConstantPoolMethodref([
    $printStreamClassEntryIndex,
    $printmethodNameAndTypeEntryIndex,
]);

さらに予め PHP の AST から文字列を抜き出し Constant Pool に追加する処理を行います。
私が行っている php-java というプロジェクトでは動的に追加 (Constant Pool に入れる必要のある文字列が出現したタイミングで追加)をするようにしていますが、ここでは話の流れとして、追加するようにしています。

function registerConstantsRecursive(\PhpParser\Node ...$nodes): void
{
    foreach ($nodes as $node) {
        if (property_exists($node, 'exprs')) {
            registerConstantsRecursive(...$node->exprs);
            continue;
        }
        if (property_exists($node, 'expr')) {
            registerConstantsRecursive(...[$node->expr]);
            continue;
        }
        if (property_exists($node, 'value')) {
            $utf8Index = writeConstantPoolUtf8($node->value);
            writeConstantPoolString($utf8Index);
        }
    }
}

registerConstantsRecursive(...$ast);

(アドベントカレンダーということもあり、だいぶ雑実装ですが、 php-java だともっとまともに書いています。)

最後に、 $constantPool へ追加された値をバイナリへ書き込んでいきます。

foreach ($constantPool as $element) {
    if ($element === null) {
        continue;
    }
    [$tag, $value] = $element;

    writeUnsignedByte($tag);

    switch ($tag) {
        case 7: // Class
        case 8: // String
            writeUnsignedShort($value);
            break;
        case 9: // Fieldref
        case 10: // Methodref
        case 12: // NameAndType
            writeUnsignedShort($value[0]);
            writeUnsignedShort($value[1]);
            break;
        case 1: // Utf8
            fwrite($handle, $value);
            break;
        default:
            throw new RuntimeException(
                'The kind tag is not implemented.'
            );
    }
}

access_flags を書き込む

access_flags は、final であったり public のようなアクセス権限を設定できます。
今回の例だと、コード上においては継承していない(が、実態は java.lang.Object を継承している)ため、 publicsuper を設定します。

access_flags は下記に従って設定します。

Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_FINAL 0x0010 Declared final; no subclasses allowed.
ACC_SUPER 0x0020 Treat superclass methods specially when invoked by the invokespecial instruction.
ACC_INTERFACE 0x0200 Is an interface, not a class.
ACC_ABSTRACT 0x0400 Declared abstract; must not be instantiated.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ANNOTATION 0x2000 Declared as an annotation type.
ACC_ENUM 0x4000 Declared as an enum type.

ACC_PUBLIC 及び ACC_SUPER が該当します。この値を unsigned short で書き込みます。

writeUnsignedShort(0x0001 | 0x0020);

this_class, super_class を書き込む

次に this_class と super_class を書き込みます。
これは予め ConstantPool に書き込んだである Class を使用し、それらを unsigned short で書き込みます。

writeUnsignedShort($currentClassEntryIndex);
writeUnsignedShort($parentClassEntryIndex);

field_count, interface_count を書き込む

HelloWorld クラスにはフィールド及びインタフェースは存在しないため、 2 つとも 0 と書き込みます。

writeUnsignedShort(0);
writeUnsignedShort(0);

AST にした PHP を解釈してメソッドを書き込む

今回のアドベントカレンダーの趣旨の話をします。 しかし残念ながら、AST にふれるのはここだけで、あっという間に終わってしまいます。まるで、作るのは手間暇がかかるけど、食べるのは一瞬である北京ダックのようです。ちなみに北京ダックは好きです。

まず、メソッドの数を書き込みます。本来は <init> も必要ですが、省略しても動くので省略します。
つまり必要なメソッド数は 1 つです。ということで 1 を書き込みます。

writeUnsignedShort(1);

次に、PHP で AST された値を再度確認してみます。

array(1) {
  [0]=>
  object(PhpParser\Node\Stmt\Echo_)#1100 (2) {
    ["exprs"]=>
    array(1) {
      [0]=>
      object(PhpParser\Node\Scalar\String_)#1099 (2) {
        ["value"]=>
        string(12) "Hello World!"
        ["attributes":protected]=>
        array(3) {
          ["startLine"]=>
          int(2)
          ["endLine"]=>
          int(2)
          ["kind"]=>
          int(2)
        }
      }
    }
    ["attributes":protected]=>
    array(2) {
      ["startLine"]=>
      int(2)
      ["endLine"]=>
      int(2)
    }
  }
}

通常トップレベルなものは配列であるので、配列を回しつつ処理を分けていく形になります。
余談ですが、php-java は下記のように分けています。

今回の場合下記のようなコードになるでしょう。

$operations = '';
foreach ($ast as $stmt) {
    switch ($type = get_class($stmt)) {
        case \PhpParser\Node\Stmt\Echo_::class:
            // echo は PHP だとカンマで区切って複数出力できるため複数形の exprs になっているが、
            // 出力は一つのみとして考慮する。
            $expr = $stmt->exprs[0];
            $reference = null;
            switch ($expressionType = get_class($expr)) {
                case \PhpParser\Node\Scalar\String_::class:
                    /**
                     * @var \PhpParser\Node\Scalar\String_ $expr
                     */
                    $reference = findEntryIndex(8, $expr->value);
                    break;
                // ...
                default:
                    throw new RuntimeException(
                        $expressionType . 'is not implemented yet.'
                    );

                // ここの解説をします。
            }
            break;
        // ...
        default:
            throw new RuntimeException(
                $type . 'is not implemented yet.'
            );
    }
}

このように取得した Node ごとに処理をの役割を分離していく必要があります。// ... と書かれている箇所に分岐処理が大量に入ってきます。楽しいね!

さて、 // ここの解説をします。 の部分について解説をします。ここにはずばり PHP における echo そのものの実装が入り、それらをすべてオペレーションコードで表す必要があります。

では、一体必要なオペレーションコードは何かを考えたときに、上記でも説明した通りのものになります。

  • System.out をロードする(オペランドスタックに積む)
    • static なフィールドから値をロードするには getstatic を使用する
  • Hello World をロードする(オペランドスタックに積む)
    • 定数文字列を Constant Pool からロードするには ldcldc_w を使用する
  • print をコールする (PHP の echo は終端に改行コードが入らないため)
    • 通常のメソッドの呼び出しには invokevirtual を使用する。
  • return (オペレーションの終了として)

上記はニーモニックですから、オペレーションコードが必要になります。それぞれのペアは JVM Specification にも書かれていますが、下記のとおりです。

ニーモニック オペコード
getstatic 0xb2
ldc 0x12
invokevirtual 0xb6
return 0xb1

これらを $operations に書いていきます。

// getstatic
$operations .= pack('Cn', 0xb2, $systemFieldrefEntryIndex);

// ldc
$operations .= pack('CC', 0x12, $reference);

// invokevirtual
$operations .= pack('Cn', 0xb6, $printMethodrefEntryIndex);

// return
$operations .= pack('C', 0xb1);

上記が実行するオペレーションコードになります。そしてこれは Code Attribute という属性に分類され、これを紐付ける先であるメソッドのエントリを構築します。

メソッドのエントリは JVM Specification より下記です。

method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

書き込むエントリはずばり main メソッドです。まず access_flags を書き込みます。
access_flags は クラスとちょっと異なります。

Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_PRIVATE 0x0002 Declared private; accessible only within the defining class.
ACC_PROTECTED 0x0004 Declared protected; may be accessed within subclasses.
ACC_STATIC 0x0008 Declared static.
ACC_FINAL 0x0010 Declared final; must not be overridden (§5.4.5).
ACC_SYNCHRONIZED 0x0020 Declared synchronized; invocation is wrapped by a monitor use.
ACC_BRIDGE 0x0040 A bridge method, generated by the compiler.
ACC_VARARGS 0x0080 Declared with variable number of arguments.
ACC_NATIVE 0x0100 Declared native; implemented in a language other than Java.
ACC_ABSTRACT 0x0400 Declared abstract; no implementation is provided.
ACC_STRICT 0x0800 Declared strictfp; floating-point mode is FP-strict.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.

上記になります。そして main メソッドは publicstatic を使用します。

writeUnsignedShort(0x0001 | 0x0008);

次に、 name_index および descriptor_index ですが、これはメソッド名と引数・返り値情報の文字列のエントリー番号になります。

writeUnsignedShort(findEntryIndex(1, 'main'));
writeUnsignedShort(findEntryIndex(1, '([Ljava/lang/String;)V'));

上記のようになります。次に attribute_count ですが、 CodeAttribute が入ってくるため 1 となります。

writeUnsignedShort(1);

次に Code Attribute を書き込んでいきます。 Code Attribute は JVM Specification により下記です。

Code_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length;
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

まず、 attribute_name_index は何の属性であるかを示したものが入ります。今回の場合は Code になります。次に attribute_length は属性自体の長さです。この値は予め、それ以外の値及び長さを知っている必要があります。

max_stack 及び max_locals はオペランドスタックに積まれる最大のスタック数及び、パラメータの数を含めたローカル変数の最大値となります。ぶっちゃけ、足りなくならなければ何でもいいので、両方とも 10 とか雑な値で大丈夫です。

php-java はオペレーションコードを見て、サイズを見るようにしています。これには実際にエミュレート的な仕様が必要で、オペレーションコードが走って初めてオペランドスタックの最大の大きさがわかったりするのですが、だいぶ面倒くさいということもあり、説明を省略します。
ちなみに、話が脱線しますが、 if 文の実装にも実はこのエミュレートっぽい処理は必要(もしかしたら別手段はあるかもしれない)、 StackMap テーブルと呼ばれる if 文のブランチ先を定義するものが必要になります。

そして、 code_length はすべてのオペレーションコード及びオペランドのサイズが入ります。
次に、code にはそのまま code が入ります。

exception_table_length 及び exception_table は例外処理を適用するオペレーションコードの範囲を定義するものですが、今回に至っては例外処理はないので exception_table_length は 0 になります。

最後に attributes_count 及び attributes は存在しないため、 attribute_length は 0 になります。それを踏まえると下記のようなコードになります。

$attribute = '';

// max_stack
$attribute .= pack('n', 10);

// max_locals
$attribute .= pack('n', 10);

// code_length
$attribute .= pack('N', strlen($operations));

// code
$attribute .= $operations;

// exception_table_length
$attribute .= pack('n', 0);

// attributes_count
$attribute .= pack('n', 0);

// 書き込む
writeUnsignedShort(findEntryIndex(1, 'Code'));
writeUnsignedInt(strlen($attribute));
fwrite($handle, $attribute);

あと処理

そして、最後にクラスファイル自体の attributes_count を書き込みます。特に付ける必要はないので 0 です。

writeUnsignedShort(0);

少し長くなってしまいましたが、以上で JVM 言語の開発ができるようになりました。めでたい?

実際に動かしてみる

java HelloWorld

上記を実行し Hello World! が出れば成功です。

コードのまとめ

バイナリはどうなっているかというと、下記みたいな形になります。

00000000: CA FE BA BE 00 00 00 34 00 19 01 00 10 6A 61 76  .......4.....jav
00000010: 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 01 00 0A  a/lang/Object...
00000020: 48 65 6C 6C 6F 57 6F 72 6C 64 01 00 10 6A 61 76  HelloWorld...jav
00000030: 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D 01 00 04  a/lang/System...
00000040: 6D 61 69 6E 01 00 04 43 6F 64 65 01 00 16 28 5B  main...Code...([
00000050: 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E  Ljava/lang/Strin
00000060: 67 3B 29 56 01 00 15 28 4C 6A 61 76 61 2F 6C 61  g;)V...(Ljava/la
00000070: 6E 67 2F 53 74 72 69 6E 67 3B 29 56 01 00 15 4C  ng/String;)V...L
00000080: 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72  java/io/PrintStr
00000090: 65 61 6D 3B 01 00 03 6F 75 74 01 00 05 70 72 69  eam;...out...pri
000000a0: 6E 74 01 00 13 6A 61 76 61 2F 69 6F 2F 50 72 69  nt...java/io/Pri
000000b0: 6E 74 53 74 72 65 61 6D 07 00 02 07 00 01 07 00  ntStream........
000000c0: 03 01 00 13 6A 61 76 61 2F 69 6F 2F 50 72 69 6E  ....java/io/Prin
000000d0: 74 53 74 72 65 61 6D 07 00 0F 0C 00 04 00 06 0A  tStream.........
000000e0: 00 0C 00 11 0C 00 09 00 08 0C 00 0A 00 07 0A 00  ................
000000f0: 10 00 14 09 00 0E 00 13 01 00 0C 48 65 6C 6C 6F  ...........Hello
00000100: 20 57 6F 72 6C 64 21 08 00 17 00 21 00 0C 00 0D   World!....!....
00000110: 00 00 00 00 00 01 00 09 00 04 00 06 00 01 00 05  ................
00000120: 00 00 00 15 00 0A 00 0A 00 00 00 09 B2 00 16 12  ................
00000130: 18 B6 00 15 B1 00 00 00 00 00 00                 ...........

このバイナリは普通に Hello World! と書かれた Java ファイルを javac でコンパイルしたクラスファイルより余分な情報がないので、短い形で済み、かつ java コマンドを使用しても動きます。

<?php
require __DIR__ . '/../../vendor/autoload.php';

use PhpParser\ParserFactory;

$className = 'HelloWorld3';
$handle = fopen($className . '.class', 'w+');

// unsigned short, unsigned int で書き込むための関数を用意する
function writeUnsignedByte(int $number)
{
    global $handle;
    fwrite($handle, pack('C', $number));
}

function writeUnsignedShort(int $number)
{
    global $handle;
    fwrite($handle, pack('n', $number));
}

function writeUnsignedInt(int $number)
{
    global $handle;
    fwrite($handle, pack('N', $number));
}

// magic
writeUnsignedInt(0xCAFEBABE);

// minor_version
writeUnsignedShort(0);

// major_version
writeUnsignedShort(52);

$constantPool = [null];

function writeConstantPoolEntry(int $tag, $value): int
{
    global $constantPool;
    $constantPool[] = [$tag, $value];
    return count($constantPool) - 1;
}

function writeConstantPoolClass($value): int
{
    return writeConstantPoolEntry(7, $value);
}

function writeConstantPoolFieldref($value): int
{
    return writeConstantPoolEntry(9, $value);
}

function writeConstantPoolMethodref($value): int
{
    return writeConstantPoolEntry(10, $value);
}

function writeConstantPoolString($value): int
{
    return writeConstantPoolEntry(8, $value);
}

function writeConstantPoolNameAndType($value): int
{
    return writeConstantPoolEntry(12, $value);
}

function writeConstantPoolUtf8($value): int
{
    return writeConstantPoolEntry(1, $value);
}

function findEntryIndex(int $tag, $value)
{
    global $constantPool;
    foreach ($constantPool as $index => $element) {
        if ($element === null) {
            continue;
        }
        [$entryTag, $entryValue] = $element;
        if ($tag === $entryTag && $value === $entryValue) {
            return $index;
        }
    }
    throw new RuntimeException(
        'Could not found on the ConstantPool'
    );
}

// 親クラス名を書き込む
writeConstantPoolUtf8('java/lang/Object');

// クラス名を書き込む
writeConstantPoolUtf8($className);

// java/lang/System を書き込む

writeConstantPoolUtf8('java/lang/System');

// main メソッドの名称を書き込む
writeConstantPoolUtf8('main');

// Code Attribute の識別子である Code を書き込む
writeConstantPoolUtf8('Code');

// main における引数を書き込む
writeConstantPoolUtf8('([Ljava/lang/String;)V');

writeConstantPoolUtf8('(Ljava/lang/String;)V');

writeConstantPoolUtf8('Ljava/io/PrintStream;');

writeConstantPoolUtf8('out');

writeConstantPoolUtf8('print');

// java/io/PrintStream を書き込む
writeConstantPoolUtf8('java/io/PrintStream');


// 親クラス
$currentClassEntryIndex = writeConstantPoolClass(findEntryIndex(1, $className));

// カレントクラス
$parentClassEntryIndex = writeConstantPoolClass(findEntryIndex(1, 'java/lang/Object'));

$javaLangSystemClassEntryIndex = writeConstantPoolClass(findEntryIndex(1, 'java/lang/System'));

$printStreamClassEntryIndex = writeConstantPoolClass(writeConstantPoolUtf8('java/io/PrintStream'));

// main と引数を対にする
$mainAndArgumentsEntryIndex = writeConstantPoolNameAndType([
    findEntryIndex(1, 'main'),
    findEntryIndex(1, '([Ljava/lang/String;)V'),
]);

// Methodref を書き込む
$mainMethodrefEntryIndex = writeConstantPoolMethodref([
    $currentClassEntryIndex,
    $mainAndArgumentsEntryIndex,
]);


// System.out を書き込む
$outAndPrintStreamEntryIndex = writeConstantPoolNameAndType([
    findEntryIndex(1, 'out'),
    findEntryIndex(1, 'Ljava/io/PrintStream;'),
]);

// print を紐付ける
$printmethodNameAndTypeEntryIndex = writeConstantPoolNameAndType([
    findEntryIndex(1, 'print'),
    findEntryIndex(1, '(Ljava/lang/String;)V'),
]);

$printMethodrefEntryIndex = writeConstantPoolMethodref([
    $printStreamClassEntryIndex,
    $printmethodNameAndTypeEntryIndex,
]);

$systemFieldrefEntryIndex = writeConstantPoolFieldref([
    $javaLangSystemClassEntryIndex,
    $outAndPrintStreamEntryIndex,
]);

$ast = (new ParserFactory())
->create(
    ParserFactory::PREFER_PHP7
)
->parse(
    file_get_contents(
        __DIR__ . '/test.php'
    )
);

function registerConstantsRecursive(\PhpParser\Node ...$nodes): void
{
    foreach ($nodes as $node) {
        if (property_exists($node, 'exprs')) {
            registerConstantsRecursive(...$node->exprs);
            continue;
        }
        if (property_exists($node, 'expr')) {
            registerConstantsRecursive(...[$node->expr]);
            continue;
        }
        if (property_exists($node, 'value')) {
            $utf8Index = writeConstantPoolUtf8($node->value);
            writeConstantPoolString($utf8Index);
        }
    }
}

registerConstantsRecursive(...$ast);

writeUnsignedShort(count($constantPool));

foreach ($constantPool as $element) {
    if ($element === null) {
        continue;
    }
    [$tag, $value] = $element;

    writeUnsignedByte($tag);

    switch ($tag) {
        case 7: // Class
        case 8: // String
            writeUnsignedShort($value);
            break;
        case 9: // Fieldref
        case 10: // Methodref
        case 12: // NameAndType
            writeUnsignedShort($value[0]);
            writeUnsignedShort($value[1]);
            break;
        case 1: // Utf8
            writeUnsignedShort(strlen($value));
            fwrite($handle, $value);
            break;
        default:
            throw new RuntimeException(
                'The kind tag is not implemented.'
            );
    }
}

writeUnsignedShort(0x0001 | 0x0020);

writeUnsignedShort($currentClassEntryIndex);
writeUnsignedShort($parentClassEntryIndex);

writeUnsignedShort(0);
writeUnsignedShort(0);

$operations = '';
foreach ($ast as $stmt) {
    switch ($type = get_class($stmt)) {
        case \PhpParser\Node\Stmt\Echo_::class:
            // echo は PHP だとカンマで区切って複数出力できるため複数形の exprs になっているが、
            // 出力は一つのみとして考慮する。
            $expr = $stmt->exprs[0];
            $reference = null;
            switch ($expressionType = get_class($expr)) {
                case \PhpParser\Node\Scalar\String_::class:
                    /**
                     * @var \PhpParser\Node\Scalar\String_ $expr
                     */
                    $utf8pos = findEntryIndex(1, $expr->value);
                    $reference = findEntryIndex(8, $utf8pos);
                    break;
                // ...
                default:
                    throw new RuntimeException(
                        $expressionType . 'is not implemented yet.'
                    );
            }

            // getstatic
            $operations .= pack('Cn', 0xb2, $systemFieldrefEntryIndex);

            // ldc
            $operations .= pack('CC', 0x12, $reference);


            // invokevirtual
            $operations .= pack('Cn', 0xb6, $printMethodrefEntryIndex);

            // return
            $operations .= pack('C', 0xb1);

            break;
        // ...
        default:
            throw new RuntimeException(
                $type . 'is not implemented yet.'
            );
    }
}


writeUnsignedShort(1);

writeUnsignedShort(0x0001 | 0x0008);
writeUnsignedShort(findEntryIndex(1, 'main'));
writeUnsignedShort(findEntryIndex(1, '([Ljava/lang/String;)V'));
writeUnsignedShort(1);

$attribute = '';

// max_stack
$attribute .= pack('n', 10);

// max_locals
$attribute .= pack('n', 10);

// code_length
$attribute .= pack('N', strlen($operations));

// code
$attribute .= $operations;

// exception_table_length
$attribute .= pack('n', 0);

// attributes_count
$attribute .= pack('n', 0);

writeUnsignedShort(findEntryIndex(1, 'Code'));
writeUnsignedInt(strlen($attribute));
fwrite($handle, $attribute);

// attributes_count
writeUnsignedShort(0);

※ 上記は本アドベントカレンダーを解説するために作った雑コードであるため、一部説明している手順と異なっている可能性があります?

おわりに

PHP で JVM を作るのは、 JVM Specification に則れば作ることが可能であることがわかりました。
本アドベントカレンダーでは JVM を題材にしていますが、 AST にすることにより別言語を作ることも可能であり、アセンブリに直し、実行ファイルを生成することもぶっちゃけ可能です。
さらに PHP で説明をしていますが、こういった処理ができるのであればどんな言語でも実装可能なのです。夢が広がりますよね。日頃のプロダクションコードとは少し離れて、プログラムと戯れるのもまたありだと思います。ぜひお試しください。

そして、 JVM 言語の実装ではなく、 PHP で JVM を実装する方法を以前何回か登壇しています。 JVM の実装そのものに興味があるかたはぜひ見てみてください。

そして、最後になりますが、本を書いていたりするので買ってね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】boolean型へのキャスト

boolean型(理論型)とは

phpのマニュアルによれば、「boolean は、真偽の値を表します。 この値は、TRUE または FALSE のどちらかになります。」とのこと。

つまりboolean型には「TRUE」と「FALSE」の2種類の値にしかならない。

型をキャスト(変換)すると

boolean型へのキャストを行うと次の値は「FALSE」と判定される。

・boolean の FALSE
・integerの0
・floatの0.0
・空の文字列、および文字列の "0"
・要素の数がゼロである配列
・NULL
・空のタグから作成された SimpleXML オブジェクト

var_dump((bool) "false");  // bool(false)
var_dump((bool) 0);        // bool(false)
var_dump((bool) 0.0);      // bool(false)   
var_dump((bool) "");       // bool(false)
var_dump((bool) "0");      // bool(false)
var_dump((bool) []);       // bool(false)
var_dump((bool) NULL);     // bool(false)

これら以外の値はTRUEとなる

判定処理への利用

DBテーブル内にその値があるかどうかの判定
例)指定したIDのユーザが存在するかどうか

User.php
return (boolean) $this->where("id",$user->id)->first();

first()で取り出したデータはオブジェクト型のデータとなるが、存在しない場合NULL値となるためFALSEを返す。

これだったらis_null()やempty()でいいかも。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自分が調べたPHPのマジックメソッドの機能についてまとめてみた

はじめに 

Laravelの仕組みを調べているときに調べたマジックメソッドの機能について、自分の備忘録を兼ねてここにまとめました。
この投稿について疑問や指摘があれば、ぜひコメントお願いします。

本題

property_exists メソッド

PHPマニュアル(https://www.php.net/manual/ja/function.property-exists.php)

機能 オブジェクトもしくはクラスにプロパティが存在するかどうかを調べる。
説明 property_exists ( mixed $class , string $property ) : bool
パラメータ class:確認するクラス、もしくはクラスのオブジェクトを指定します。 property:プロパティ名を指定します。

試し書き

PropertyExists.php
//基本型
class Pikotaro
{
  public $pen;
  protected $pineapple;
  private $apple;
}
var_dump(property_exists('Pikotaro', 'pen')); //bool(true)
var_dump(property_exists(new Pikotaro, 'pineapple')); //bool(true)
var_dump(property_exists(new Pikotaro, 'apple')); //bool(true)
var_dump(property_exists('Pikotaro', 'applepen')); //bool(false)
PulsPropertyExists.php
<?php
//同じ名前空間で使うとき
namespace Kosakadaimao;

class Pikotaro
{
  public $pen;
  protected $pineapple;
  private $apple;
}
var_dump(property_exists('\\Kosakadaimao\\Pikotaro', 'pen')); //bool(true)
var_dump(property_exists(new Pikotaro, 'pineapple')); //bool(true)


//異なる名前空間で使うとき
namespace Kosaka;

use Kosakadaimao;

var_dump(property_exists('\\Kosakadaimao\\Pikotaro', 'pen')); //bool(true)
var_dump(property_exists(new Kosakadaimao\Pikotaro, 'apple')); //bool(true)
?>

追記)名前空間の特定クラスのプロパティーを調べたい場合は上記のようにすればできます。

end メソッド

PHPマニュアル(https://www.php.net/manual/ja/function.end.php)

機能 配列の内部ポインタを最終要素にセットする。
説明 end ( array &$array ) : mixed
パラメータ array:配列。この配列は参照渡しとなります。関数内で配列の中身を変更するからです。 つまり、ここには配列そのものを渡さなければならず、 配列を返す関数を指定することはできません。 参照渡しできるのは、実際の変数だけだからです。

試し書き

End.php
//基本型
$kosaka = ['Pikotaro', 'Kosakadaimao', 'Kazuhito'];

var_dump($kosaka); //array(3) { [0]=> string(8) "Pikotaro" [1]=> string(12) "Kosakadaimao" [2]=> string(8) "Kazuhito" } 
echo end($kosaka); //Kazuhiro
PlusEnd.php
//User Contributed Notes から
$kosaka[2] = 'Pikotaro';
$kosaka[1] = 'Kosakadaimao';
$kosaka[0] = 'Kazuhito';

var_dump($kosaka); //array(3) { [2]=> string(8) "Pikotaro" [1]=> string(12) "Kosakadaimao" [0]=> string(8) "Kazuhito" }
echo end($kosaka); //Kazuhito

var_dump()の結果からわかりますが、連想配列のキーを数字にしても、デフォルトのキーのように昇順で並んでくれないようです。

PlusPlusEnd.php
$kosaka = ['Pikotaro', 'Kosakadaimao', 'Kazuhito'];
$kosaka[2] = 'Pikotaro';
$kosaka[1] = 'Kosakadaimao';
$kosaka[0] = 'Kazuhito';

var_dump($kosaka); //array(3) { [0]=> string(8) "Kazuhito" [1]=> string(12) "Kosakadaimao" [2]=> string(8) "Pikotaro" } 
echo end($kosaka); //Pikotaro

しかし、デフォルトのキーの値を上書きする形であれば、デフォルトのキーの順番通りに並ぶようです。

終わりに

もっといろんなマジックメソッドの機能を上げようと思ったのですが、疲れたのでここで終わります。
いつかまたこの続きをやりたいなと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Web開発勉強のためWordpressでポートフォリオサイトを開設するまで③〜XAMPPローカル環境構築失敗編〜

XAMPPでローカル環境を構築できたといったな。あれは嘘だ。

いや、嘘ではないんですけど、うまくいったのはインストールまででした。
講座の動画に従って投稿や固定ページの更新をしたところ、
「更新に失敗しました。 エラーメッセージ: 返答が正しい JSON レスポンスではありません。」
というエラーメッセージが表示され、なにもできない状態でした。

いまだにデータ交換をテキストやらCSVでやっている仕事ですので、XMLはおろかJSONとは、みたいな状態で、ググってもいまいち解決方法がピンとこない・・・
UdemyのQ&Aを覗いても同様の質問がなかったため、思いきって自ら質問してみました。
すると、すぐに先生自ら回答をくださいました!

php.iniのmax_execution_timeをデフォルト設定から300に変更して、Apacheを再起動してみてください。

とのこと。ちなみにこの日は有給予定だったので平日朝の7時ぐらいに書きこみました。先生ヒマなのかしら。
というわけで、php.iniを探してパラメーターを上書き。
念のためPCも再起動しましたが・・・残念ながら改善せず。
どうするかなあと悩みましたが、この時点でもうローカル環境は諦めるつもりでした。
Railsチュートリアルやろうとした時のAWSアカウント、まだ無料枠残ってるからそれ使って環境構築すればええやん、と。
程度にもよりますが、初心者向けの講座もろもろいじって無料枠超えても1000円は取られないらしいので、今後のAWSの勉強も兼ねてAWSで環境構築することにしました。

というわけで、先生にはうまくいかなかったことと、AWSで環境構築する旨をお伝えしました。
先生からも、

XAMPP とWordPressのいずれが原因かどうか切り分けるために、AWSやVPSなどでApacheやNginxで試してみるのがよいと思います。

とのご回答が。めんどくさくなったんですね、わかります。

ちなみに先生からlogsフォルダにエラーログ出てない?と聞かれたんですが、残念ながらエラーログは見当たりませんでした。
これでAWSの環境構築を試してうまくいかなかったら、いよいよMAC買うしかないな、と思いました。

そんな背水の覚悟で臨んだAWSのWordpress環境構築ですが、下記のサイトを参考にしました。
https://skillhub.jp/blogs/197

結論から言うと、ようやくうまくいきました。
ターミナルの開き方とかAWSの操作面で不安がありましたが、それほど難しくないと思います。
投稿や固定ページの更新も問題なくオーケー。
ようやくスタートラインに立てたかなという感じです。
次回からは、サイトを作っていく中でつまづいた点などを書いていきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel-Excelでセル結合した複数シートを出力する

この記事は、Fusic その2 Advent Calendar 2019 22日目の記事です。

はじめに

LaravelでExcel出力するなら Laravel-Excel を使っている方が多いかと思います。

ただこのライブラリ、PHPSpreadSheet の完全なラッパーではありません。
それ故に少々詰まったので、書き残しておきます。

環境

PHP 7.4.1
Laravel 6.9.0
Laravel-Excel 3.1

クラス構成

Userモデルを介して取得した全データを、
UsersExportクラスにDIし、各ユーザごとにUsersSheetクラスを作成します。

├── app
│   ├── Exports
│   │   ├── UsersExport.php
│   │   └── UsersSheet.php
│   └── User.php

最終的には各ユーザごとにレイアウトが変動する複雑なExcelファイルの出力が必要だったのですが、
今回はその前段階として、結合セルのある複数シートの出力を目指します。
image.png

セル結合

Laravel-Excel では、PHPSpreadsheet の mergeCells() がラッパーされていません。
即ちセル結合をする際は、ライフサイクルの中で PHPOffice\PHPSpreadSheet\WorkSheet\WorkSheet インスタンスを取得する必要があります。

Laravel-Excelのライフサイクル

以下4つのイベントが順に発火します。

  • BeforeExport
  • BeforeSheet
  • AfterSheet
  • BeforeWriting

公式ドキュメントの記述によると、 PHPOffice\PHPSpreadSheet\WorkSheet\WorkSheetが生成されるのはBeforeSheetイベントの直前です。
しかし、肝心の取得方法は記載されていません。

PHPOffice\PHPSpreadSheet\WorkSheet\WorkSheetの取得

以下のようにして、BeforeSheetイベント内で取得できました。
あとはPHPSpreadSheetのドキュメントに従うだけです。
$event->getSheet()で取得されるのが Maatwebsite\Excel\Sheet なのはややこしいですね。

namespace App\Exports;

use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Events\BeforeSheet;

class UsersExport implements WithEvents
{
    /**
     * @return array
     */
    public function registerEvents(): array
    {
        return [
            BeforeSheet::class => function (BeforeSheet $event) {
                $sheet = $event->getSheet()->getDelegate();
                // セル結合
                $sheet->mergeCells('A2:D2');
            },
        ];
    }
}

複数シートの出力

続いて、複数シートの出力を行います。

失敗例

Laravel-Excel # Multiple Sheets を参考に、
先ほどのコードに WithMultipleSheetssheets() メソッドを追加してみました。

namespace App\Exports;

use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Events\BeforeSheet;

class UsersExport implements WithEvents, WithMultipleSheets
{
    protected $users;

    public function __construct($users)
    {
        $this->users = $users;
    }

    /**
     * @return array
     */
    public function registerEvents(): array
    {
        return [
            BeforeSheet::class => function (BeforeSheet $event) {
                $user = $event->getSheet()->user;
                $sheet = $event->getSheet()->getDelegate();
                // セル結合
                $sheet->mergeCells('A2:D2');
                $sheet->setCellValue('A2', $user->name);
            },
        ];
    }

    /**
     * @return array
     */
    public function sheets(): array
    {
        $sheets = [];
        foreach ($this->users as $user) {
            $sheets[] = new UsersSheet($user);
        }
        return $sheets;
    }
}
<?php

namespace App\Exports;

use App\User;

class UsersSheet
{
    public $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }
}

これを実行すると…

image.png

まっさら!!!

ライフサイクルの確認

各イベントが発火しているのか確認します。

<?php

namespace App\Exports;

use App\User;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Maatwebsite\Excel\Events\BeforeExport;
use Maatwebsite\Excel\Events\BeforeWriting;
use Maatwebsite\Excel\Events\BeforeSheet;
use Maatwebsite\Excel\Events\AfterSheet;

class UsersExport implements WithEvents, WithMultipleSheets
{

    protected $users;

    public function __construct($users)
    {
        $this->users = $users;
    }

    /**
     * @return array
     */
    public function registerEvents(): array
    {
        return [
            BeforeExport::class => function (BeforeExport $event) {
                \Log::debug('===========================================');
                \Log::debug('BeforeExport');
                \Log::debug('===========================================');
            },
            BeforeSheet::class => function (BeforeSheet $event) {
                \Log::debug('===========================================');
                \Log::debug('BeforeSheet');
                \Log::debug('===========================================');
            },
            AfterSheet::class => function (AfterSheet $event) {
                \Log::debug('===========================================');
                \Log::debug('AfterSheet');
                \Log::debug('===========================================');
            },
            BeforeWriting::class => function (BeforeWriting $event) {
                \Log::debug('===========================================');
                \Log::debug('BeforeWriting');
                \Log::debug('===========================================');
            },
        ];
    }


    /**
     * @return array
     */
    public function sheets(): array
    {
        $sheets = [];
        foreach ($this->users as $user) {
            $sheets[] = new UsersSheet($user);
        }
        return $sheets;
    }
}
[2019-12-22 03:42:15] local.DEBUG: ===========================================  
[2019-12-22 03:42:15] local.DEBUG: BeforeExport  
[2019-12-22 03:42:15] local.DEBUG: ===========================================  
[2019-12-22 03:42:15] local.DEBUG: ===========================================  
[2019-12-22 03:42:15] local.DEBUG: BeforeWriting  
[2019-12-22 03:42:15] local.DEBUG: ===========================================  

BeforeSheet, AfterSheet イベントが発火していませんね。

UsersSheet のほうにもデバッグコードを追加してみます。

class UsersSheet implements WithEvents
{
    /**
     * @return array
     */
    public function registerEvents(): array
    {
        return [
            BeforeExport::class => function (BeforeExport $event) {
                \Log::debug('===========================================');
                \Log::debug('BeforeExport -- Sheet');
                \Log::debug('===========================================');
            },
            BeforeSheet::class => function (BeforeSheet $event) {
                \Log::debug('===========================================');
                \Log::debug('BeforeSheet -- Sheet');
                \Log::debug('===========================================');
            },
            AfterSheet::class => function (AfterSheet $event) {
                \Log::debug('===========================================');
                \Log::debug('AfterSheet -- Sheet');
                \Log::debug('===========================================');
            },
            BeforeWriting::class => function (BeforeWriting $event) {
                \Log::debug('===========================================');
                \Log::debug('BeforeWriting -- Sheet');
                \Log::debug('===========================================');
            },
        ];
    }
}
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: BeforeExport  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: BeforeSheet -- Sheet  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: AfterSheet -- Sheet  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: BeforeSheet -- Sheet  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: AfterSheet -- Sheet  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: BeforeSheet -- Sheet  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: AfterSheet -- Sheet  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: BeforeSheet -- Sheet  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: AfterSheet -- Sheet  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: BeforeWriting  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  

BeforeSheet, AfterSheet はどちらも UsersSheet 内での発火になっています。
Sheetイベントなので、確かに各Sheetクラスに委任するのが自然ですね。

最終コード

以下のコードで、望んでいたExcelが出力できました。

<?php

namespace App\Exports;

use App\User;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;

class UsersExport implements WithMultipleSheets
{
    protected $users;

    public function __construct($users)
    {
        $this->users = $users;
    }

    /**
     * @return array
     */
    public function sheets(): array
    {
        $sheets = [];
        foreach ($this->users as $user) {
            $sheets[] = new UsersSheet($user);
        }
        return $sheets;
    }
}
<?php

namespace App\Exports;

use App\User;
use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Maatwebsite\Excel\Events\BeforeSheet;

class UsersSheet implements WithEvents, WithTitle
{
    private $user;

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

    /**
     * @return array
     */
    public function registerEvents(): array
    {
        return [
            BeforeSheet::class => function (BeforeSheet $event) {
                $sheet = $event->getSheet()->getDelegate();
                $sheet->mergeCells('A2:D2');
                $sheet->setCellValue('A2', $this->user->name);
            },
        ];
    }

    public function title(): string
    {
        return $this->user->name;
    }
}

まとめ

今回はセル結合のみを行いましたが、
PHPOffice\PHPSpreadSheet\WorkSheet\WorkSheetインスタンスの取得方法がわかったので、ほかにもいろいろできそうです。
BeforeSheet以外のイベントも上手く使っていきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

データクラスを活用しよう!

はじめに

この記事は、僕が最近同期(めちゃ仕事できるマン)と仕事をしていてコードレビューで指摘をされて「なるほど!」と感動したので
その感動を忘れないようにするための備忘録みたいなものです。

奇数と偶数に分ける処理を例に考えてみる

今回は例として、奇数と偶数を分ける処理を書いてみます。
やり方としては…

  • 奇数と偶数を分けた結果を同じ配列の異なるキーに格納する

呼び出し側はこんな感じです。

DivideTask.php
<?php
require_once "./divider.php";

class DivideTask
{
    public function execDivide() {
        $number = array(1, 2, 3, 4, 5);
        $divider = $this->getDivider();
        $divided_array = $divider->divide($number);
        if (!empty($divided_array["even"])) {
            echo "-------偶数--------\n";
            foreach ($divided_array["even"] as $even) {
                echo $even . "\n";
            }
        }
        if (!empty($divided_array["odd"])) {
            echo "-------奇数--------\n";
            foreach ($divided_array["odd"] as $odd) {
                echo $odd . "\n";
            }
        }
    }

    private function getDivider() {
        return new Divider();
    }
}

$divideTask = new DivideTask();
$divideTask->execDivide();

実際に偶数と奇数に分ける処理はこんな感じです。

Divider.php
<?php
class Divider
{
    /**
     * 受け取った数字を偶数と奇数に分ける処理
     * @param array $numbers 偶数と奇数の数字が混合している配列
     * @return array $result 振り分けが完了した配列
     * ex) ["even"=>2, "odd"=>3]
     */
    public function divide(array $numbers) {
        $result = array();
        foreach($numbers as $number) {
            if($number % 2 === 0) {
                $result["even"][] = $number;
            } else {
                $result["odd"][] = $number;
            }
        }
        return $result;
    }
}
-------偶数--------
2
4
-------奇数--------
1
3
5

動作としてはこれで全然問題は無いのですが、以下の点において問題があります。

  • divide()メソッドを呼び出す側は、返ってくる配列の中にevenoddの2つのキーがある前提で呼んでいる
  • 「素数も追加してよ」と言われた時は新しく配列にキーを追加する必要があり、呼び出し元も呼び出し側も変えなければならない
  • 「奇数いらなくなったし消していいよ」と言われた瞬間にoddキーが一瞬でレガシーになる

まあともかく、今のままだと仕様の変更に脆くレガシーになりやすいということです。

そこで今回はデータクラスを活用してみたいと思います。

リファクタリング

データクラスを作成します。
偶数と奇数をそれぞれクラスプロパティとして定義をし、メソッドはセッターとゲッターだけです。

Number.php
<?php
class Number
{
    public evenNumber = array();
    public oddNumber  = array();

    public function getEvenNumber() {
        return $this->evenNumber;
    }

    public function getOddNumber() {
        return $this->oddNumber;
    }

    public function setEvenNumber($number) {
        $this->evenNumber[] = $number;
    }

    public function setOddNumber($number) {
        $this->oddNumber[] = $number;
    }
}

DividerクラスでこのNumberクラスのインスタンスを作成し、偶数と奇数をそれぞれプロパティに追加をします。
追加が終わった段階で、オブジェクトをそのままDivideTaskクラスにreturnをしてあげます。

Dividerクラスをリファクタします。
主な変更点としては以下の通りです。

  • 結果を配列に追加するのではなく、オブジェクトに持たせる
  • 返却する結果もそのままオブジェクトで返す
Divider.php
<?php
require_once "./Number.php";
class Divider
{
    /**
     * 受け取った数字を偶数と奇数に分ける処理
     * @param array $numbers 偶数と奇数の数字が混合している配列
     * @return Object Number
     */
    public function divideOddOrEven(array $numbers) {

        /* ①結果を配列ではなくオブジェクトに持たせる */
        //$result = array();
        $numberObject = $this->getNumberObject();

        foreach($numbers as $number) {
            if($number % 2 === 0) {
                /* ②数字をSetterを使ってプロパティに格納していく */
                //$result["even"][] = $number;
                $numberObject->setEvenNumber($number);
            } else {
                /* ②数字をSetterを使ってプロパティに格納していく */
                //$result["odd"][] = $number;
                $numberObject->setOddNumber($number);
            }
        }
        /* ③返却する結果もオブジェクトをそのまま返す */
        //return $result;
        return $numberObject;
    }

    /**
     * @return Object Number
     */
    private function getNumberObject() {
        return new Number();
    }
}

処理を呼ぶ側もリファクタしましょう。

DivideTask.php
<?php
require_once "./divider.php";

class DivideTask
{
    public function execDivide() {
        $number = array(1, 2, 3, 4, 5);
        $divider = $this->getDivider();

        /* ①返ってくる結果はObjectなので、それをメソッド内で活用する */
        //$divided_array = $divider->divideOddOrEven($number);
        $numberObject = $divider->divideOddOrEven($number);

        /* ②Getterメソッドを使って現在のNumberクラスのプロパティを取得する */
        $evenNumber = $numberObject->getEvenNumber();
        $oddNumber  = $numberObject->getOddNumber();
        if (!empty($evenNumber)) {
            echo "-------偶数--------\n";
            foreach ($evenNumber as $even) {
                echo $even . "\n";
            }
        }
        if (!empty($oddNumber)) {
            echo "-------奇数--------\n";
            foreach ($oddNumber as $odd) {
                echo $odd . "\n";
            }
        }
    }

    private function getDivider() {
        return new Divider();
    }
}

$divideTask = new DivideTask();
$divideTask->execDivide();

おわりに

イマサラタウンな話題ですが、ふとした時に忘れがちで油断するとリファクタ前みたいなコード(なんでも配列のキーに追加マン)になってしまうので、改めてこのタイミングでアウトプットをすることで初めて自分のものに出来た感があります。
最後まで読んでくださり、ありがとうございました(`・ω・´)ゞビシッ!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[PHP]データクラスを活用しよう!

はじめに

この記事は、僕が最近同期(めちゃ仕事できるマン)と仕事をしていてコードレビューで指摘をされて「なるほど!」と感動したので
その感動を忘れないようにするための備忘録みたいなものです。

奇数と偶数に分ける処理を例に考えてみる

今回は例として、奇数と偶数を分ける処理を書いてみます。
やり方としては…

  • 奇数と偶数を分けた結果を同じ配列の異なるキーに格納する

呼び出し側はこんな感じです。

DivideTask.php
<?php
require_once "./divider.php";

class DivideTask
{
    public function execDivide() {
        $number = array(1, 2, 3, 4, 5);
        $divider = $this->getDivider();
        $divided_array = $divider->divide($number);
        if (!empty($divided_array["even"])) {
            echo "-------偶数--------\n";
            foreach ($divided_array["even"] as $even) {
                echo $even . "\n";
            }
        }
        if (!empty($divided_array["odd"])) {
            echo "-------奇数--------\n";
            foreach ($divided_array["odd"] as $odd) {
                echo $odd . "\n";
            }
        }
    }

    private function getDivider() {
        return new Divider();
    }
}

$divideTask = new DivideTask();
$divideTask->execDivide();

実際に偶数と奇数に分ける処理はこんな感じです。

Divider.php
<?php
class Divider
{
    /**
     * 受け取った数字を偶数と奇数に分ける処理
     * @param array $numbers 偶数と奇数の数字が混合している配列
     * @return array $result 振り分けが完了した配列
     * ex) ["even"=>2, "odd"=>3]
     */
    public function divide(array $numbers) {
        $result = array();
        foreach($numbers as $number) {
            if($number % 2 === 0) {
                $result["even"][] = $number;
            } else {
                $result["odd"][] = $number;
            }
        }
        return $result;
    }
}
-------偶数--------
2
4
-------奇数--------
1
3
5

動作としてはこれで全然問題は無いのですが、以下の点において問題があります。

  • divide()メソッドを呼び出す側は、返ってくる配列の中にevenoddの2つのキーがある前提で呼んでいる
  • 「素数も追加してよ」と言われた時は新しく配列にキーを追加する必要があり、呼び出し元も呼び出し側も変えなければならない
  • 「奇数いらなくなったし消していいよ」と言われた瞬間にoddキーが一瞬でレガシーになる

まあともかく、今のままだと仕様の変更に脆くレガシーになりやすいということです。

そこで今回はデータクラスを活用してみたいと思います。

リファクタリング

データクラスを作成します。
偶数と奇数をそれぞれクラスプロパティとして定義をし、メソッドはセッターとゲッターだけです。

Number.php
<?php
class Number
{
    public evenNumber = array();
    public oddNumber  = array();

    public function getEvenNumber() {
        return $this->evenNumber;
    }

    public function getOddNumber() {
        return $this->oddNumber;
    }

    public function setEvenNumber($number) {
        $this->evenNumber[] = $number;
    }

    public function setOddNumber($number) {
        $this->oddNumber[] = $number;
    }
}

DividerクラスでこのNumberクラスのインスタンスを作成し、偶数と奇数をそれぞれプロパティに追加をします。
追加が終わった段階で、オブジェクトをそのままDivideTaskクラスにreturnをしてあげます。

Dividerクラスをリファクタします。
主な変更点としては以下の通りです。

  • 結果を配列に追加するのではなく、オブジェクトに持たせる
  • 返却する結果もそのままオブジェクトで返す
Divider.php
<?php
require_once "./Number.php";
class Divider
{
    /**
     * 受け取った数字を偶数と奇数に分ける処理
     * @param array $numbers 偶数と奇数の数字が混合している配列
     * @return Object Number
     */
    public function divideOddOrEven(array $numbers) {

        /* ①結果を配列ではなくオブジェクトに持たせる */
        //$result = array();
        $numberObject = $this->getNumberObject();

        foreach($numbers as $number) {
            if($number % 2 === 0) {
                /* ②数字をSetterを使ってプロパティに格納していく */
                //$result["even"][] = $number;
                $numberObject->setEvenNumber($number);
            } else {
                /* ②数字をSetterを使ってプロパティに格納していく */
                //$result["odd"][] = $number;
                $numberObject->setOddNumber($number);
            }
        }
        /* ③返却する結果もオブジェクトをそのまま返す */
        //return $result;
        return $numberObject;
    }

    /**
     * @return Object Number
     */
    private function getNumberObject() {
        return new Number();
    }
}

処理を呼ぶ側もリファクタしましょう。

DivideTask.php
<?php
require_once "./divider.php";

class DivideTask
{
    public function execDivide() {
        $number = array(1, 2, 3, 4, 5);
        $divider = $this->getDivider();

        /* ①返ってくる結果はObjectなので、それをメソッド内で活用する */
        //$divided_array = $divider->divideOddOrEven($number);
        $numberObject = $divider->divideOddOrEven($number);

        /* ②Getterメソッドを使って現在のNumberクラスのプロパティを取得する */
        $evenNumber = $numberObject->getEvenNumber();
        $oddNumber  = $numberObject->getOddNumber();
        if (!empty($evenNumber)) {
            echo "-------偶数--------\n";
            foreach ($evenNumber as $even) {
                echo $even . "\n";
            }
        }
        if (!empty($oddNumber)) {
            echo "-------奇数--------\n";
            foreach ($oddNumber as $odd) {
                echo $odd . "\n";
            }
        }
    }

    private function getDivider() {
        return new Divider();
    }
}

$divideTask = new DivideTask();
$divideTask->execDivide();

おわりに

イマサラタウンな話題ですが、ふとした時に忘れがちで油断するとリファクタ前みたいなコード(なんでも配列のキーに追加マン)になってしまうので、改めてこのタイミングでアウトプットをすることで初めて自分のものに出来た感があります。
最後まで読んでくださり、ありがとうございました(`・ω・´)ゞビシッ!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Web開発勉強のためWordpressでポートフォリオサイトを開設するまで②〜MAMPローカル環境構築失敗編〜

Wordpressの講座で、ローカルで使用環境を構築するための案内をしていたので、動画に沿って環境構築をしていきました。
PCは動画ではMacを使って案内していましたが、当方Windowsでの環境構築となります。
ちなみに使用PCのスペックは、
・Windows10 HOME バージョン1909
・CPU Cerelon(R) N4000CPU 1.10GHz 1.10GHz
・メモリ 4GB
・ストレージ HDD1TB
です。(ムダにHDD1TBなのがツッコミどころですw)

まずMAMPをインストールするよう案内されました。
https://www.mamp.info/
LAMPをもじったオープンソースで、Webサーバーの環境構築が簡単にできるソフトです。
WindowsだからWAMPだよなーと思いつつ無事にインストールまで完了。

動画の案内通り、MAMPのフォルダ内にあるhtdocs内にダウンロードしたWordpress一式を移動。
MAMPを起動させ、ブラウザが起動したのでパスにWordpressのフォルダ名を指定。
この時点でインストール画面が表示されるはずなのですが・・・うまくいかない。
ネットを調べていたところ、wp-config.phpをパスに指定すればオーケーとのことだったのでその通りに操作。
(参考サイト:MAMP・Wordpressセットアップの詳細な手順はこちらのサイトで丁寧に解説されています。)
 ・https://bazubu.com/how-to-insall-wp-in-mamp-23798.html
 
無事にセットアップ画面が表示されたので、データベースを作成。
Wordpressのセットアップ手順に従い、データベース名やサイト名を入力。
無事にダッシュボード画面が開きました!わーぱちぱちぱち!

え、ローカル環境構築失敗してないじゃないかって?
そう、タイトル詐欺です。ここまでは。
とりあえずその日はそこまでで操作を終え、翌日にMAMPを起動したところ・・・

あれ?Apacheに緑のランプがつかないぞ?
まあいいかと気にせずWordpressを開こうとするもエラーに。そりゃそうだ。
ネットで調べていたらポート番号が別のアプリケーションと被ってダメなことがあるらしい。
ならばとポート番号をいろいろ変更するも・・・やはりApacheのグリーンライトは点灯せず。
うーむ、困ったなあ・・・と思っていたところ、そういえばUdemyには講座ごとにQ&Aが存在していたことを思い出す。
Q&Aを眺めていたところ、同様の質問が何件か。やはりけっこうある現象なのね。
講師の先生が自ら原因を探っていくも、最終的にはXAMPPを使っては?という回答に大半が落ち着いている。めんどくさくなったんですね、わかります。

ということで、自分もXAMPPをインストールしました。
https://www.apachefriends.org/jp/index.html
インストール手順は下記のサイトを参考にしました。
https://bazubu.com/xampp-wordpress-23795.html

MAMPよりボタンとか項目が多いので戸惑うかもしれませんが、基本の流れは一緒なのですんなりインストールできました。
念のためPCを再起動しても問題ないか確認したところ、無事にWordpressのダッシュボード画面を開くことができました!

というわけで、MAMPでうまくいかなかった皆様、その時はXAMPPをインストールしましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【入門】Laravel×Vue.js②〜実装編〜

はじめに

PHPのフレームワークであるLaravelで作成したアプリケーションに
JavaScriptのフレームワークであるVue.jsを連携させる方法について説明します。
簡単な機能を実装します。自分のコンポーネントを登録する方法について説明します
実装する機能
  メッセージを表示して、ボタンを押すと、メッセージを反転する

コンポーネントの作成

まず、resources/js/components/内にHelloComponent.vueを作成してください
そして、内容を以下のようにしてください

resources/js/components/HelloComponent.vue
<template>
    <div id="app">
      <p>{{ message }}</p>
      <button v-on:click="reverseMessage">Reverse Message</button>
    </div>
</template>
<script>
export default {
      data:function(){
          return {
            message: 'Hello Vue!',
          };
      },
      methods: {
        reverseMessage: function () {
          this.message = this.message.split('').reverse().join('')
        }
      }
}
</script>

コンポーネントの登録

上で作成したコンポーネントを登録しましょう

resource/js/app.js
require('./bootstrap');

window.Vue = require('vue');

Vue.component('example-component', require('./components/ExampleComponent.vue').default);
Vue.component('hello-component', require('./components/HelloComponent.vue').default);


ビューファイルへの組み込み

ビューファイルの追加したい時に下記を記述してください

hello/index.blade.php
<div id="app">
  <hello-component></hello-component>
</div>

以上で実装完了です。

疑問、気になるところがございましたら、質問、コメントよろしくお願いします!!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

phpでcookieやurlを使わずにsession_start()でセッション管理する

phpでcookieを使わずに、session idを手動で設定し、session_start()する方法。

こんな方向けの話

  • アクセス元からsessionを表す or 作ることができる文字列がpostされる(alexaスキルや、google assistatアプリ、line botなどの受け側をphpで作る場合など)
  • セッション管理だけの為にRDBMS、nosql系を使いたくない → Webサーバのsession機能を使いたい。

制限

セッションIDとして使えるのは「英数字」「カンマ」「ハイフン」のみです。

コード

<?php
ini_set( 'session.use_cookies', 0 );
ini_set( 'session.use_only_cookies', 0 );
$session_id = "session-id-text"; //この文字列がセッションidとなる
session_id($session_id);
session_start();
?>

サンプル

test1.php

<?php
ini_set( 'session.use_cookies', 0 );
ini_set( 'session.use_only_cookies', 0 );
$session_id = "session-id-text"; //この文字列がセッションidとなる
session_id($session_id);
session_start();
$_SESSION['test'] = "yamada"; //セッションに保存してみる
?>

test2.php

<?php
ini_set( 'session.use_cookies', 0 );
ini_set( 'session.use_only_cookies', 0 );
$session_id = "session-id-text"; //この文字列がセッションidとなる
session_id($session_id);
session_start();
var_dump($_SESSION);
?>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

foreachをlistを使ってかっこよく取得する

よく使う使い方

$fruitList = [
 '1'  => 'リンゴ',
 '2'  => 'レモン',
 '3'  => 'バナナ'
];

foreach($fruitList as $index => $name){
 dd($index. ':'. $name);
}

Screenshot_1.png

1:リンゴと出力されました。

一般的な使い方として、$key => $valueの形で使用します。

これが例えば、$valueのほうが配列になった場合を考えてみましょう。

valueが配列の場合

添字配列の場合

$fruitInfoList = [
 '1'  => ['リンゴ', '100円'],
 '2'  => ['レモン', '200円'],
 '3'  => ['バナナ', '300円']
];

foreach($fruitInfoList as $index => $fruitInfo){
 dd($index. ':'. $fruitInfo[0]. ':'. $fruitInfo[1]);
}

Screenshot_2.png

これでは、0と1という添字を書いて、取得するのはちょっとみにくいですね。
連想配列にしてみます。

連想配列の場合

$fruitInfoList = [
 '1'  => [
    'name'  => 'リンゴ',
    'price' => '100円'
 ],
 '2'  => [
    'name'  => 'レモン',
    'price' => '200円'
 ],
 '3'  => [
    'name'  => 'バナナ',
    'price' => '300円'
 ],
];

foreach($fruitInfoList as $index => $fruitInfo){
 dd($index. ':'. $fruitInfo['name']. ':'. $fruitInfo['price']);
}

Screenshot_2.png

取得の際は、nameとpriceと意味付けができて見やすくなりましたが、宣言する際が少し大変で、長いし冗長ですね。
list関数を使ってみましょう。

listを使用した際

listは、定義された変数の順番に配列の要素が代入されます。さっそく見てみましょう
https://www.sejuku.net/blog/24406

$fruitList = [
 '1'  => ['リンゴ', '100円'],
 '2'  => ['レモン', '200円'],
 '3'  => ['バナナ', '300円'],
];

foreach($fruitList as $index => list($name, $price)){
 dd($index. ':'. $name. ':'. $price);
}

Screenshot_2.png
だいぶすっきりしました。
foreachの中で、$fruitInfoと命名していたものが消えました。(変数を命名するのは難しいので、避けられるなら避けたほうが良いときもある)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ランサーズ創業時から今まで12年分のコミットをビジュアライズしてみた

はじめに

ランサーズ AdventCalendar 21日目担当の @sayanet です。

ふだんはtoBの LancersEnterprise プロダクトの開発チームリーダーとして、プロジェクトマネジメントを行っています。最近プロダクトマネジメントもはじめました。

ハイ、さっそく本題にはいります!
私がランサーズに入社したのは2014年4月で、その頃はまだメンバーは50名に満たない規模でした。
今回、上場という節目においてここまでの軌跡をなにかの形で残しておきたいなと思い、主に Lancers の開発を行っているリポジトリで創業から今までの約12年(2008-2019)にわたるすべてのコミットをビジュアライズしてみました。
今回は公開用として、創業者兄弟である秋好社長&聡さんと著者以外はイニシャルにしています。

つくりかた

1. コミットログの取得

git shortlogを利用してユーザーごとのコミット数を取得します。

例えば今月のコミットをユーザーごとにリストアップしたい場合はこんなかんじです。

git shortlog -sn --no-merges --since='date +%Y/%m/01' --until='date +%Y/%m/%d'

オプション説明
-s: ユーザー毎にサマライズ
-n: コミット数多い順にソート
--no-merges: マージコミットは除外
--since: 対象期間いつから
--until: 対象期間いつまで

出力結果
コミット数合計とユーザー名が表示されます
image.png

さてさて、今回は年ごとにサマった述べコミット数がほしいので、創業から今年まで年単位でループをまわします。
それぞれの年に過去合計値をセットしたいのでsinceオプションは2008年固定にしておくのがミソ。

for ($year=2008; $year<=2019; $year++) {
    echo $year . "\n";
    $command = sprintf("git shortlog -sn --no-merges --since='2008/01/01' --until='%d/12/31'", $year, $year);
    system($command);
}

2. データを整形する

1で取得したユーザーごとのコミット数をスプレッドシートに貼り付けて整形。年とユーザー名をマトリクス表で表現していきます。

こちらがベースとなるデータ。年とユーザー名を結合させて下ごしらえ。
※ yosuke->秋好社長、satoshi->聡さん です。他の方はマスキング
image.png

年とユーザー名をもとにVLOOKUP使ってマトリクス表のシートにプロット。

image.png
ここで予期せぬ事態。同一人物でもコミット時のユーザー名が異なることが多々あり、名寄せするのが大変でした。仕組み化するならメアドを使って名寄せするとかうまいことやりたい。

さて、ここまでできればデータ整形完了!
このタイミングでユーザー名をイニシャル化にしています。

3. ビジュアライズする

ビジュアライズにはFlourishのBar chart raceを使いました。
https://app.flourish.studio/@flourish/bar-chart-race/9

Dataにスプレッドシートの内容をそのまま貼ればOKというお手軽さ。職種で色分けしたかったのでカテゴリー追加しています。

Previewモードで件数や速さなど表示の設定ができます。
動きを確認しながら調整できてトテモ便利 :angel_tone2:

image.png

デキアガリ\(^o^)/

image.png

APIも公開されていて捗りそうです :bar_chart:
https://app.flourish.studio/@flourish/bar-chart-race/9#api

まとめ

後半のデザイナーT.Tさんの伸びがすごかったです。コミット数が多い方は総じて仕事のスピードが早いなという印象。ちなみにT.Tさんからはプログラマーはプログラムを書くな! 有能なプログラマーはプログラムを書かないリンクをそっと渡されましたがそれもまたひとつの謙虚スタイル。リスペクト。

エンジニア出身の社長が今でもTOP17に残っているのを見て、ほんとうに創業時はめちゃくちゃエンジニアフルコミットだったんだなと思ったり、私が入社した2015年頃にメンバーが一気に増えてベンチャーの勢いを感じたり、最近入社したメンバーがガンガンコミット伸ばしていることに気づけたりとなかなかの感慨深さがありました。

もちろん、コミットの単位は時期や実装者・チームによって異なるし、fix:typoなどもあるため多いからよいとは一概にいえませんが、純粋にこれだけコミットしたのすごいな〜という意味合いで見ています。

ソースコードは日々刷新されますがコミットに込めた気持ちとビジョンが受け継がれて今のランサーズがあるんだなと再認識。日々の積み重ねって偉大ですね。

年末なので、一年の振り返りとして月ごとに出してみても面白そうだし、毎日眺めてがんばったなーとムフフするのも楽しそう。

それではみなさん、良いエンジニアライフを!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【IT専門学生の就活】僕がこれからやること

自己紹介

僕はIT系の専門学生1年で、現在はWeb系の企業を目指して学習を進めています。今回この記事を書こうと思った理由は、就活が迫ってきたのですが学校にはWeb系企業の求人がなく、ポートフォリオを作成し、技術を証明する必要があると考えたからです。

今までやったこと

僕がWeb開発を始めたのは大学受験に落ちた日からです。もちろん最初は Progateさんから始めました:)

そしていつの間にかIT系の専門学校に

独学

  • ポートフォリオページ作成(HTML&CSS)
  • Ruby
  • Ruby on Rails
  • AWS(EC2)
  • adobeXD
  • PaizaスキルチェックBランク(Java,Ruby)

学校

  • MySQL
  • CentOS
  • GUI電卓作成(JavaFX)
  • GUIポーカー作成(JavaFX)
  • シューティングゲーム作成(JavaFX)
  • 基本情報技術者試験合格
  • 成績全てA(1年前期)

書き途中に気付いたのですが、自主学習時間の割に形になっているものが少ないなと思ったので、作成物や資格合格を目標として取り組むことにします。

これからやること

自分でやることで何をすればよいかを整理するために書くので、学校のみで勉強するものは省いています。

就活までにやること(専門1年)

先頭の番号は優先順位です

  1. Webアプリ作成(Rails&HTML&CSS&BT)  [独学]
  2. PaizaスキルチェックAランク(Python) [独学]
  3. ポートフォリオサイト作成(HTML&CSS&BS) [独学]
  4. 成績全てA [自宅+学校]
  5. 応用情報技術者試験合格 [自宅+学校]

プラスしてやること(専門2年)

まだ具体的に定まっていないため優先順位はつけられないのでつけません。

  • Webアプリ作成(Laravel)
  • Webアプリ作成(Java Spring Framework)
  • TOEIC600点?(受けたことないからわからない)

ここからは趣味でやりたいことかもしれません。余裕があればやります

  • 高校数学復習+大学数学
  • 機械学習・人工知能
  • Pythonでいろんな分野で遊ぶ

タスクに分解

やることを明確にするため、目標をタスクに分解していきます。

1.Webアプリ作成(Rails)

  • Ruby
  • Rails
  • HTML・CSS
  • Git

主役はRailsですがもちろん基礎文法としてRuby、デザインとしてHTML・CSS、バージョン管理としてGitをやる必要があります。デプロイはHerokuで行います(デプロイについてはRailsチュートリアルで学習できます↓)

タスク

【Ruby】
・[完了]Progate Rubyコース
・[完了]Paiza Rubyコース
・[完了]プロを目指す人のためのRuby

【Rails】
・[完了]Progate Railsコース
Railsチュートリアル

【HTML・CSS】
・[完了]Progate HTML&CSS

【Git】
Udemy Git:はじめてのGitとGitHub

2.PaizaスキルチェックAランク

  • Python
  • アルゴリズム

Pythonは応用情報と組み合わさった良い講座があったのでそれを見ることにします。アルゴリズムは演習もやりながらできるものがQiitaにたくさんあったのでそれをやることにします。

タスク

【Python】
基本情報技術者試験+応用情報技術者試験+Python+SQL

【アルゴリズム】
AtCoder に登録したら次にやること 過去問精選10問
AtCoder 版!蟻本 (初級編)

3.ポートフォリオサイト作成(HTML&CSS&BS)

サーバーサイドを見せるためのポートフォリオ程度のHTML・CSSで新たに学習することはないので適当に調べながらやることにします。

4.成績全てA

テスト範囲が発表されてから考えます

5.応用情報技術者試験合格

基本情報で基本的なことは覚えたのでやるべきことは過去問演習だけです

タスク

・過去問演習

おわりに

応用情報合格したいな...?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Moodle 3.8 マニュアル - Ubuntu のための step-by-step インストールガイド

原文

Ubuntu のための step-by-step インストールガイド

注:このドキュメントは、PHP 7.2 をもつ Ubuntu 18.04 に Moodle 3.7 をインストールすることについてのものです。

内容

1 始める前に
2 ステップ 1:Ubuntu をインストールする
2.1 なぜ我々は Ubuntu desktop よりも Ubuntu を好むのか
2.2 なぜ我々は Ubuntu 14.04 よりも Ubuntu 16.04 よりも Ubuntu 18.04 を好む(あるいは好まない)のか
2.3 手順
3 ステップ 2:Apache/MySQL/PHP をインストールする
4 ステップ 3:追加のソフトウェアをインストールする
5 ステップ 4:Moodle をダウンロードする
6 ステップ 5:ローカルリポジトリを /var/www/html/ にコピーする
7 ステップ 6:MySQL サーバをセットアップする
8 ステップ 7:完全なセットアップ
8.1 moodledata のパスを変更する
8.2 データベースのタイプ
8.3 データベースの設定
8.4 環境のチェック
8.5 次 次 次
8.6 サイト管理者のアカウントを作成する
8.7 インストールの完了
8.8 忘れないで
9 インストール後のシステムのパス
10 提案:Zend OpCache/Change Document Root を有効にする
11 moodle のために config.php を編集する
12 一台の Ubuntu サーバにおいて複数の Moodle ブランチをホストする
12.1 例 1
12.2 例 2
13 関連項目

1 始める前に

パスワード(とユーザ名)を書き留めておくのはいい考えです。Ubuntu と Moodle に使うために必要となるでしょう。

  • Ubuntu の root パスワード
  • Moodle が使用するであろう MySQL のユーザ名とパスワード
  • Moodle のメインのユーザ名とパスワード
  • 追加の管理者のユーザ名とパスワード

2 ステップ 1:Ubuntu をインストールする

2.1 なぜ我々は Ubuntu desktop よりも Ubuntu を好むのか

  • ほとんどの IT プロフェッショナルは、安全でありハッキングされにくいためにコマンドラインインターフェース(CLI)サーバを使用することを好みます。
  • アマチュアユーザはグラフィカルインターフェース(デスクトップ)を使用するのを簡単だと思うでしょう。
  • Moodle サーバを実験のためにローカルで使用したいだけならば、Ubuntu のデスクトップバージョン(64 ビットが好まれます)をインストールするのを好むかもしれません。
  • CLI だけのサーバをインストールしてあとで後悔するならば、単にグラフィカルデスクトップを簡単に追加するだけです。

注:ほとんどのエキスパートには推奨されませんが、'Ubuntu desktop' をインストールするために、'sudo tasksel' または 'sudo apt install ubuntu-desktop' コマンドを発行してグラフィカルユーザインターフェース(デスクトップ)をインストールできるでしょう。しかし注意して使用してください:GUI は予想するようには現れず、CLI すら使えなくなるかもしれません。まずテストマシンで試してください。

2.2 なぜ我々は Ubuntu 14.04 よりも Ubuntu 16.04 よりも Ubuntu 18.04 を好む(あるいは好まない)のか

  • All three are LTS (Long Term Service) releases. amd64 edition preferred. http://www.ubuntu.com/download
  • Ubuntu Server 18.04 has all the required packages (comes default with Apache2, PHP 7.2, MySQL 5.7). Ubuntu Server 18.04 has some minor differences from 16.04 and 14.04, mainly PHP. This document has been updated to reflect those changes.
  • Moodle 3.0.1 onwards can use PHP 7.0. Moodle 3.7 requries PHP 7.1.
  • But beware that if you are using external authentication or enrollment plug-ins, please note that at the moment of Moodle 3.0.1 release (December 2015) neither MSSQL nor SQLSRV are available under php7. So, anybody using SQL*Server as primary database or remote auth/enrol plugin should not move to PHP 7 at all ! Also, depending of your configuration, some extensions (memcached can be installed, redis, mongodb, xmlrpc...) maybe missing or work in progress for your distribution, triple check the exact availability for your OS.

2.3 手順

  • Ubuntu has a well known issue with its automatic updates filling up the /boot directory until automated updates start to fail and automated removal of old kernel files from /boot is impossible. Because of this you should consider installing Ubuntu with a /boot directory of around 5Gb and putting some automated clean up in place. More info can be found here - [1]

  • You can use either VI (lightweight editor) or VIM (heavyweight editor), however, if you wish to use VIM you will need to install it

sudo apt-get install vim

  • VI or VIM Commands

To edit a file press "Insert" Key
To finish editing press "Esc" Key
To write the file press ":w"
To Exit the editor press ":q"
You can also write and quit ":wq"

  • In Ubuntu, the standard user, the account you created during the install, does not have rights to install/write to many of the directories. In the below tutorial we will be using the term "sudo" which stands for "super user do" before most of the commands.

3 ステップ 2:Apache/MySQL/PHP をインストールする

Note: Moodle 3.0.1 introduced support for PHP 7.0 and we will be using PHP 7.2 in this tutorial

Open up Terminal and install the following;

sudo apt install apache2 mysql-client mysql-server php libapache2-mod-php

Run 'sudo mysql_secure_installation' to set the root password for mysql - please, please my dear friends, WRITE IT DOWN and spare yourself some grief, you will need it in step 6.

4 ステップ 3:追加のソフトウェアをインストールする

sudo apt install graphviz aspell ghostscript clamav php7.2-pspell php7.2-curl php7.2-gd php7.2-intl php7.2-mysql php7.2-xml php7.2-xmlrpc php7.2-ldap php7.2-zip php7.2-soap php7.2-mbstring

Restart Apache so that the modules are loaded correctly

sudo service apache2 restart

We will be using Git to install/update the Moodle Core Application

sudo apt install git

5 ステップ 4:Moodle をダウンロードする

Setup your local repository and download Moodle, We will use /opt for this installation.

  • Git is what is called a "version control system". By using git it will much easier down the road to update the moodle core application. Within Step 5 there is a little more detail on why we put the moodle core application code in the /opt directory.

cd /opt

Download the Moodle Code and Index

sudo git clone git://git.moodle.org/moodle.git

Change directory into the downloaded Moodle folder

cd moodle

Retrieve a list of each branch available

sudo git branch -a

Tell git which branch to track or use

sudo git branch --track MOODLE_37_STABLE origin/MOODLE_37_STABLE

Finally, Check out the Moodle version specified

sudo git checkout MOODLE_37_STABLE

6 ステップ 5:ローカルリポジトリを /var/www/html/ にコピーする

sudo cp -R /opt/moodle /var/www/html/

sudo mkdir /var/moodledata

sudo chown -R www-data /var/moodledata

sudo chmod -R 777 /var/moodledata

sudo chmod -R 0755 /var/www/html/moodle

  • Explanation:

Since we setup a local repository in the previous step, you will copy it to your webroot after any updates and making changes. Having your local repository outside of the webroot, like we have in /opt, you will be able to prepare and stage your upgrades in a more efficient manner. For example, you want to make some changes or add some plug-ins, you would download the plugin and copy it to your local moodle repository. After you have added the plug-in and any other changes you might have made you will need to edit the file located in /opt/moodle/.git/info/exclude. Within that file you want to tell git which files/folders to exclude when it pulls down the updates when you run your next "sudo git pull". An example entry would be the certificate mod located in /opt/moodle/mod/certificate so within the exclude file you want to add "/mod/certificate" below the last comments. You would add additional entries, 1 per line, for each plug-in or file you might have changed. If I were to change the favicon.ico file you would just add "favicon.ico" to the exclude file. Now when you run "sudo git pull" to update moodle to the latest version it will ignore those files and directories and just update the core moodle code. Before copying to your webroot to upgrade you want to make sure and download and copy over the latest versions of the plug-ins you might have added.

7 ステップ 6:MySQL サーバをセットアップする

First we need to change the default storage engine to innodb and change the default file format to Barracuda, this is a new setting compared to previous versions. You also need to set innodb_file_per_table in order for Barracuda to work properly. Ref: https://dev.mysql.com/doc/refman/5.6/en/innodb-compression-usage.html

  • You should not need to make innodb the default storage engine anymore, the latest version of Moodle will select it automatically during install. It is always a good idea to make it default anyway. You do however need to set the default file format!

  • If you chose to use VIM instead please substitute vi for vim

sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf

Scroll down to the [mysqld] section and under Basic Settings add the following line under the last statement. if you want to add you have to press the "insert" button on your keyboard. this is usually above the "delete" button. this allows you to add some text.

default_storage_engine = innodb

innodb_file_per_table = 1

innodb_file_format = Barracuda

In order to save my.cnf using the editor, press the Esc (Escape) key, type the following in sequence which will save :w then close the editor :q

:w

:q

Restart MySQL Server for changes to take affect

sudo service mysql restart

Now we need to create the Moodle database and the Moodle MySQL User with the correct permissions

Use the password you created in step 1

sudo mysql -u root -p

mysql>

CREATE DATABASE moodle DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

Note: Use 'utf8mb4' for full range (4-byte) support of UTF-8, including Emoji ('utf8' only supports 3-byte). You will be compliant by Moodle admin page if you don't use 'utf8mb4' here.

Where it says "moodledude" and "passwordformoodledude" you should change to the username and password of your choosing.
mysql>

create user 'moodledude'@'localhost' IDENTIFIED BY 'passwordformoodledude';

mysql>

GRANT SELECT,INSERT,UPDATE,DELETE,CREATE,CREATE TEMPORARY TABLES,DROP,INDEX,ALTER ON moodle.* TO moodledude@localhost IDENTIFIED BY 'passwordformoodledude';

mysql>

quit;

Note - If you are using MySQL 5.6+ and when you issue the create user and get an error about the password hash you need to adjust the password to use the hash value

You can get this by following the below
mysql>

SELECT password('passwordformoodledude');

This will print the hash of the password like *AD51BAFB2GD003D3480BCED0DH81AB0BG1712535, you will want to use this in the IDENTIFIED BY ' part

8 ステップ 7:完全なセットアップ

  • Note - If you are not comfortable using terminal to create the config.php file that needs to be created when going through the installer, you should temporarily make the webroot writable by doing the following:

sudo chmod -R 777 /var/www/html/moodle

After you have ran the installer and you have moodle setup, you NEED to revert permissions so that it is no longer writable using the below command.

sudo chmod -R 0755 /var/www/html/moodle

Open your browser and go to http://IP.ADDRESS.OF.SERVER/moodle

Follow the prompts:

8.1 moodledata のパスを変更する

/var/moodledata

8.2 データベースのタイプ

Choose: mysqli

8.3 データベースの設定

Host server: localhost

Database: moodle

User: moodledude (the user you created when setting up the database)

Password: passwordformoodledude (the password for the user you created)

Tables Prefix: mdl_

8.4 環境のチェック

This will indicate if any elements required to run moodle haven't been installed.

8.5 次 次 次

follow prompts and confirm installation

8.6 サイト管理者のアカウントを作成する

Create your moodle user account which will have site administrator permissions.

The password you select has to meet certain security requirements.

8.7 インストールの完了

Congrats! You can now start using Moodle!

8.8 忘れないで

If you made the webroot writable, revert permissions

sudo chmod -R 0755 /var/www/html/moodle

9 インストール後のシステムのパス

After installing Moodle you should set the system paths, this will provide better performance VS not setting them. Each entry in Moodle will have it's explanation.

Navigate, on the moodle webpage, to Site Administration > Server > System Paths

Input the following;

Path to du: /usr/bin/du

Path to apsell: /usr/bin/aspell

Path to dot: /usr/bin/dot

Save Changes

  • Optional if you do not already have an AntiVirus Solution

We also installed ClamAV in Step 3 so we need to set the path in Moodle

1st Create the Quarantine Directory

sudo mkdir /var/quarantine

Change Ownership

sudo chown -R www-data /var/quarantine

Navigate to Site Administration > Plugins > Antivirus plugins > Manage antivirus plugins

Enable ClamAV antivirus

Click on Settings

Set the proper settings

Save changes

In previous Moodle branches: Check "Use ClamAV on uploaded files" ClamAV Path : /usr/bin/clamscan Quarantine Directory : /var/quarantine

Save Changes

10 提案:Zend OpCache/Change Document Root を有効にする

Within the link above, https://docs.moodle.org/26/en/OPcache add the recommended settings to your 05-opcache.ini file. Again, substitute vi with vim and remember to use the correct key squences from the introduction.

sudo vi /etc/php5/apache2/conf.d/05-opcache.ini

NOTE: In Ubuntu 16.04 opcache.ini is located in:

/etc/php/7.0/mods-available/opcache.ini

Restart Apache for changes to take affect.

sudo service apache2 restart

That's it for the Zend OpCache!

You can also install a GUI to view the status of your Zend OpCache, not recommended on production servers.

cd /var/www/html/moodle/

Download the PHP Script to your Moodle directory, you should also add this file to /opt/moodle/.git/info/exclude file so it does not get removed when upgrading your installation.

sudo wget https://github.com/rlerdorf/opcache-status/blob/master/opcache.php

Visit http://ip.address.of.server/moodle/opcache.php

If you do not want your end users to type http://yourserver/moodle and just want them to navigate to http://youserver you will need to edit the site configuration for Apache which will tell Apache to use the /var/www/html/moodle as the root directory and not /var/www/html

Open up the Apache sites config and change the document root

sudo vi /etc/apache2/sites-available/000-default.conf

On the line where DocumentRoot is;

Change From: DocumentRoot /var/www/html

Change To: DocumentRoot /var/www/html/moodle

:w

:q

Restart Apache for changes to take affect.

sudo service apache2 restart

Important note!

If you have already installed Moodle then you should make the below changes.

11 moodle のために config.php を編集する

In the installation instructions, one of the suggested settings for 'webroot' is 'localhost'. This is fine if all you want to do is some local testing of your new Moodle installation. If, however, you want to view your new installation from another machine on the same local area network, or view your site on the Internet, you will have to change this setting:

For local testing, 'localhost' is fine for the webroot ($CFG->wwwroot in config.php). If you want to test your site from other machines on the same local area network (LAN), then you will have to use the private ip address of the serving machine, (e.g. 192.168.1.2/moodle) or the network name of the serving computer (e.g. network_name_of_serving_machine/moodle) as the web root. Depending on your LAN setup, it may be better to use the network name of the computer rather than its (private) ip address, because the ip address can and will change from time to time. If you don't want to use the network name, then you will have to speak to your network administrator and have them assign a permanent ip address to the serving machine. Finally, if you want to test your new installation across the internet, you will have to use either a domain name or a permanent (public) ip address/moodle as your web root. To handle both types of access, see masquerading.

Edit config.php for Moodle

cd /var/www/html/moodle sudo vim config.php

Hit the "insert" button on your keyboard, make then changes you need to make. Then press "escape" and type the following in to quit and to save changes (excluding quotation marks): ":wq"

Under $CFG->wwwroot change to http://ip.address.of.server instead of http://ip.address.of.server/moodle

12 一台の Ubuntu サーバにおいて複数の Moodle ブランチをホストする

  • This is very useful for the language pack maintainers to test translations in several Moodle branches.
  • It is also very useful for developers to test their plugins in different Moodle branches.
  • Just create a folder for each instance inside the web folder and that would be enough.
  • To access the sites you only need to add the folder to localhost URL: http://localhost/moodle31
  • You can have an instance for each version from 1.9 to 3.1

  • You do need a separate data folder for each instance and a separate database (You can use phpmyadmin to set your database, but that's not necessary), add each instance in its own folder, and carry on as above. You can also host another service (eg, Mahara) in it's separate folder.

12.1 例 1

  • So, one example folder tree on one Linux laptop (an actual server would be more) may look something like:

var

--www

----maharadata
----moodlecleandata
----moodlestabledata
----moodlemasterdata
----moodletestingdata
----uswmoodledata
----html
------mahara
------moodleclean
------moodlestable
------moodlemaster
------moodletesting
------uswmoodle

12.2 例 2

  • Have several sandboxed Moodles on a single (CentOS X) server all of different versions .. only the ones that are supported for security fixes and above - 2.7,2.8,2.9,3.0, and now a 3.1. Pretty much 'stock' Moodles with only occasional addons, etc. for testing.
  • All have their separate code and data directories as well as their separate DB's.

  • Hint: install and maintain them all with git ... even if you don't prefer/like command line, that is by far the most efficient way to update and/or upgrade a site.

/var/www/html/moodle27/version.php:$release = '2.7.14 (Build: 20160509)'
/var/www/html/moodle28/version.php:$release = '2.8.12 (Build: 20160509)'
/var/www/html/moodle29/version.php:$release = '2.9.6+ (Build: 20160520)'
/var/www/html/moodle30/version.php:$release = '3.0.4+ (Build: 20160603)'
/var/www/html/moodle31/version.php:$release = '3.1+ (Build: 20160603)'

  • The git -b command locks a site into the version provided with the rest of the git command ... for example, installing the 3.1, which is a long term support version, installed with git -b option. Don't plan on upgrading nor testing upgrades with that one.

git clone -b MOODLE_31_STABLE git://git.moodle.org/moodle.git moodle31

  • All the other moodles I have on that server have been installed via git

git clone git://git.moodle.org/moodle.git [nameofdir]

  • then from nameofdir

git branch --track MOODLE_2#STABLE origin/MOODLE_2#_STABLE
git checkout MOODLE
2#_STABLE

  • 2# is the version number.

  • That allows one to march that moodle upwards ... higher branch(es). So one can test an upgrade (as opposed to an 'update').

  • This second method 'gits' more code and backups will range in the 5+ Meg range due to all the older version git stuff The 3.1 much less (restricted to 3.1 branch):

  • 545M ./moodle296-code-20160604145012.tar

  • 193M ./moodle31+-code-2016060883737.tar

13 関連項目

Installation on Ubuntu using Git

カテゴリ:インストール

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む