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

まだJSで消耗してるの?LaravelのLivewireでSPA開発

タイトルで釣りました(1年ぶり2度目の犯行

煽ってごめんなさい
JavaScript大好きだよ(ホントダヨ)

はじめに

この記事は 弁護士ドットコム Advent Calendar 2020 の記念すべき1日目の記事です?

Livewireとは

こんなことを思った事はないだろうか、、
なぜ簡単なWebアプリケーションを作りたいだけなのにVueやReactを別で用意してJSを書かないといけないのか...
ワイはPHPerなんや...JSなんて書きたくないんや....

そんな贅沢な悩みを持つPHPerを解決に導くツールこそ Livewire なのだ!!

それっぽい説明

LivewireとはBlade(Laravelの標準Viewテンプレート)を用いて、JavaScriptを記述せずにSPAを実現できるライブラリのことです。
これまでの様にフロントエンドにVueやReactなどフレームワークを選定する必要がなくなり、
バックエンドはもちろんフロントエンドもLaravelだけで開発が可能になりました。

LivewireはLaravelの標準ライブラリではありません。
Laravelで認証機能を担う Jetstream を使用する際に、
View部分をLivewireかInertiaを選択する必要があるのでその際にLivewireを選択する。
または直接インストールして使用する方法があります。

自分で書いといてアレだけどピンとこないと思う

とはいえ実装方法

上記のどこにでも落ちてそうな説明ではいまいちイメージ出来ないと思うので、
公式のデモコードで実装方法を確認してみます。

デモコードの内容は 要素に名前を入力して一致した名前をリアルタイムで下に表示するというよくあるやつ

1l6nc-5wawf.gif

Userのテストデータが用意されている前提で進めます!

コンポーネント(ロジック部分)

従来の Controller の様に使用します。
Laravel知ってる人なら分かると思うけど、この辺は記述に変わったところはなく、
プロパティの $search の値を検索条件として、Usersテーブルの name に一致するデータをViewに返しているだけです。

App\Http\Livewire\SearchUsers.php
<?php

namespace App\Http\Livewire;

use Livewire\Component;
use App\Models\User;

class SearchUsers extends Component
{
    public $search = '';

    public function render()
    {
        return view('livewire.search-users', [
            'users' => User::where('name', $this->search)->get(),
        ]);
    }
}

コンポーネント(View部分)

View部分は 要素に wire:model="search" という記述が以外変わったところはなさそう
ロジック部分から受け取った値 users から name のみを表示している。

resources/views/livewire/search-users.blade.php
<div>
    <input wire:model="search" type="text" placeholder="Search users..."/>

    <ul>
        @foreach($users as $user)
            <li>{{ $user->name }}</li>
        @endforeach
    </ul>
</div>

コンポーネントを呼び出す

最初から用意されている welcome.blade.php にて先ほど作成したコンポーネントを呼び出してみます! @livewire('search-users')の部分が該当部分です。

welcome.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Livewire</title>
    @livewireStyles
</head>
<body>
    @livewire('search-users')
    @livewireScripts
</body>
</html>

なんとこれだけで終わりです!
Laravel知ってる人なら簡単ですよね?

Livewireを使えばこんな事も簡単

welcome.blade.phpでコンポーネントを呼び出す前提でデモを実装しています。

1. ファイルアップロード

リアルタイムでファイルアップロード機能を実装したいなぁ
だけdなんだかんだでJSでAjax書いたりと色々大変だしなぁ...

Livewire なら WithFileUploads をuseしてstore()で保存するだけ!

chthl-cy304.gif

ロジック部分

UploadPhoto.php
<?php

namespace App\Http\Livewire;

use Livewire\Component;
use Livewire\WithFileUploads;

class UploadPhoto extends Component
{
    use WithFileUploads;

    public $photo;

    public function save()
    {
        $this->photo->store('photos');
    }
}

View部分

upload-photo.blade.php
<div>
    <input type="file" wire:model="photo">
    <button>Save Photo</button>
</div>

2. もっと読み込む

もっと読み込むってボタンを押したら非同期で一覧データを読み込むの作りたいなぁ
でもJS書くのめんどくさいなぁ...

Livewire なら wire:click でイベントを受け取って取得処理するだけで簡単

1wldn-0gui9.gif

ロジック部分

UploadPhoto.php
<?php

namespace App\Http\Livewire;

use Livewire\Component;
use Livewire\WithFileUploads;

class UploadPhoto extends Component
{
    use WithFileUploads;

    public $photo;

    public function save()
    {
        $this->photo->store('photos');
    }
}

View部分

upload-photo.blade.php
<div>
    <input type="file" wire:model="photo">
    <button>Save Photo</button>
</div>

最初のデモと処理的な物はほとんど一緒なので詳細は省く

メリデメ

Livewireを使うメリット

  • 普段からLarvel使ってる人なら低い学習コストで実装できる
  • 簡単なwebアプリケーションの実装ならJSよりこっちの方が楽(な気がする)
  • JSのこと気にしないで書けるのは割とストレスフリー
  • マスコットが可愛い

Livewireを使うデメリット

  • Laravel知らない人には学習コストが高い
  • Laravel依存ライブラリなので改修する時のコストが高そう
  • JSのフレームワークの様に知識的な汎用性に欠ける
  • 記事が少ないので詰まる ≒ 死

所感

多分複雑なことをやろうとすると結局は内部で使用している Alpine.js を使うことになると思うの...
PHPerの理想郷はまだ先なのかもしれない...。

最後に

弁護士ドットコムでは Livewire を使う予定は皆無だけど
一緒に働く仲間を募集しています?‍?

弁護士ドットコム株式会社 エンジニア採用情報

本当に最後に

明日 12/02 はReactといえばの「プログラミングをするパンダ」さんの担当日です!
楽しみ!!!?

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

【SQL】SQL大量発行 パフォーマンス向上

実現したいこと

今回は、SQLで1日毎30日間のデータを取得した時に、大量のクエリが発行された(30個程)ため、パフォーマンスが下がってしまった。可能な限りクエリの発行数を少なくしていきたいと思います。

なぜ大量にクエリ発行されるとパフォーマンスがよくないのか

SQLが大量に発行されるということは、例えば、レジでガムを10個買うときに、1個ずつレジで会計するくらい効率が悪いです。

現状

下記に示したコードより、for文で30回回しているため、パフォーマンスはかなり悪いです。一回毎にSQLが発行される仕組みになっています。計測したところ0.744秒でした。

 $time_start = microtime(true);
        for ($i = 0; $i < 30; $i++) {
            $daily = date('Y/m/d', strtotime("-{$i} day"));
         $projectPerDayCount = Project::whereDate('created_at', '=', $daily)->count();
        }
 dd(microtime(true) - $time_start);

そこで、パフォーマンス向上のため2つの方法を試してみました。

パート1 クエリービルダーで実行

クエリービルダーを用いて一括で取得する方法。
whereYear、whereMonthなど便利なメソッドを用いて、更にはcreated_atでは時間指定しなければいけないため、
Carbon::createFromFormaでformatをY-m-d H:i:s->Y-m-dに変換し、mapメソッドでその日のデータをカウントするなどの方法を使い実現しました。結果は、0.193秒の速さで、クエリー数は元の30->1回になりました。

  $time_start = microtime(true);
        $year = date('Y');
        $month = date('m');
        $projectPerDayCount = Project::whereYear('created_at', $year)
        ->whereMonth('created_at', $month)
        ->orderBy('created_at')
        ->get()
        ->groupBy(function ($row) {
            return Carbon::createFromFormat('Y-m-d H:i:s', $row->created_at)->format('Y-m-d');
        })
        ->map(function ($day) {
            return $day->count();
        });
    dd(microtime(true) - $time_start);


パート2 直接SQLを書いて実行


直接SQLを書くことで、クエリーは1回に発行で、処理の速さはなんと0.036秒になりました。20倍近く処理が早くなりました。
     $sql = "SELECT DATE_FORMAT(created_at, '%Y/%m/%d') as date, count(*) as count ";
      $sql .= " FROM projects";
      $sql .= " WHERE YEAR(created_at) = ? AND MONTH(created_at) = ?";
      $sql .= " group by DATE_FORMAT(created_at, '%Y/%m/%d')";
      $result = DB::select($sql, [$year,$month]);
      $projectCountPerDay = [];
      foreach($result as $row) {
        $projectCountPerDay[$row->date] = $row->count;
      }
       return $projectCountPerDay;

まとめ

今回のように、ループでSQLを書いてしまうとパフォーマンスが落ちてしまいます。
「N+1問題」と検索をかけるとSQLの大量クエリ発行について詳しく調べることができます。
今回の検証では直接SQLを書くことで、20倍近く処理が速くなることがわかりました。

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

[2020] HHVM/Hackの始め方 導入編

HHVM/Hack

今回はHHVM/Hackについて、
2018年のアドベントカレンダーからアップデートした内容をお届けします。
まずはHHVMについて、以前は下記の印象をお持ちの方が多いと思います。

  • HHVMってPHPのちょっと早いやつだよね
  • PHP7で早くなったし、HHVMってもう使わないよね

これはあくまでHHVM3系までの話であり、
2019年1月にリリースされた v4.0.0以降でPHPを動かすことはできません。

ネット上にある日本語の記事のほとんどが古いバージョンのインストール記事が多く、
最近のものでもHHVM3系、または2系をインストールしている物はほぼ動かないものと思ってください。

HHVMの今

先日PHP8がリリースされ、Hackに実装されていたJITコンパイラや、
nullsafe operator、Attributes、Constructor property promotionなどが追加されました。

そんな中でHackを使う利点ってあるの。。? と思う方も多いのかもしれません。

現時点のHackは、PHPとは全く別の方向を歩んでおり、
元はPHP5系ではありましたが、それからLL言語っぽくない進化を遂げています。
そんな中でもHackの大きなアドバンテージは厳格な型チェックがあります。

PHPではPhan/PHPStan/Psalmといったライブラリを導入しなければ静的解析ができませんが、
HackではTyepCheckerが、IDEと結びつき、
LL言語でありながらも静的型付け言語に近い解析、コンパイラライクなツールが用意されています。
PHPでは開発を行うまでにいくつか用意するものがあり、
安全な開発までにステップが必要です。
またより良いライブラリが登場すると、それへの移行コストが付き纏います。

これが言語レベルでサポートされていればどんなに幸せでしょうか?
HHVM/Hackではだれでも簡単に(何もしなくても)コンパイラ並の静的解析がリアルタイムで実行されます。
これはPHPと大きく異なる点ともいえるでしょう!
もちろんPHPでは実装されていないEnumsとかGenerics、PHPよりも進化しているAttributeや、
シングルバイナリ実行など、
実際に利用すると静的片付け言語の様な機能が多く実装されているのがわかります。

そんなHHVM/Hackを始めるためにどうすればいいのか解説していきましょう。

IDE

IDEは開発する上で一番大事なもの。

現在でもNuclide のイメージが強い方が多いと思いますが、

現在は Visual Studio Code
Hack for Visual Studio Code も用意されているので、
Visual Studio CodeにHack for Visual Studio Codeを組み合わせるのが鉄板です。

コードの書き方

以前はhhタグにstrictを指定するのが一般的でしたが、
現在はhhタグはあまり使わない方がいいでしょう。
hhタグを使わない代わり、ファイル拡張子に.hackを使うことで自動でstrictモードになるように変わっています。

sample.hack
final class StrictClass {

  public function __construct(
    private vec<string> $v = vec['Hack', 'PHP'],
    private (function(): string) $func = () ==> "HHVM"
  ) {}

  public function CollectableArray(): vec<string> {
    return $this->v;
  }
}

hhタグを書かずにいきなりコードを書くように変更されていますので、
PHPと併用する方は書き方に気をつけましょう!

IDEを使っても補完とかあまりしてくれないんでしょう?

確かにPHPStormの様に自動で色々やってくれるわけではありませんが、
普通の補完については不自由なく対応してくれます。

スクリーンショット 2020-11-30 22.05.10.png

開発時には上記のようにクラスの内容なども教えてくれますので、
とくに不自由はないでしょう。
当然PHPと共通している関数や、Hack独自の関数なども教えてくれます。

開発を快適にするために

開発を快適にするためには次にのべるものを導入しましょう。

Composer

以前HHVM上での動作がドロップされましたが、
通常通りHackのライブラリ導入にはそのまま使える様になっています。
(2.0で全く問題なく動作します)

$ composer update

これまでは $ hhvm $(which composer) update などとしていましたが、
利用できなくなりました。
その代わりにPHPと同じ様にcomposerコマンドを実行すると、
HHVM環境に合わせてHackのライブラリなどを持ってきてくれます。

yarnを拡張してFacebook関連の言語やライブラリを提供するパッケージマネージャ開発話がありましたが、
現在はなくなっています。

hhvm-autoload

Hackではcomposerのautoloaderを使わずに、Hack専用のものを使う必要があります。
hhvm/hhvm-autoload

現在Hackで実装する場合は ほとんど必須 のライブラリです。
利用方法は簡単で、composerのvendor/autoload.phpを記述している部分を
vendor/hh_autoload.php、またはvendor/autoload.hackへ変更します。

それに加えてhh_autoload.jsonをプロジェクトルートに作成し、
autoloaderにHackのクラスファイルや、
hhiファイル、enumsやtype aliasなどを記載したファイルがどのディレクトリにあるかなどを記述します。
記述方法はいくつかありますが、もっとも標準的な指定は以下のようなものになるかと思います。

hh_autoload.json
{
  "roots": [
    "src/"
  ],
  "devRoots": [
    "tests/"
  ]
}

これを記述しておくことで、composer dump-autoload時にHack用のautoloaderに取り込まれ、
IDEへ反映させたり、様々なファイルのオートロードが可能になります。
名前空間などはそのままcomposer.jsonを利用しましょう。

公開されているHackのライブラリで、
この辺りに対応しているライブラリはほぼ保守されているものだと思っていいです。
逆にこのライブラリを導入していないものは保守されていないものとなりますので、
判断しやすいでしょう!

hhast

HackのASTライブラリで、Linterや、HHVMのバージョンによる記述方法の差分などを解決してくれるライブラリです。
Hack for Visual Studio Codeなどと組み合わせて利用することで、
実装中に、型チェック以外にcheck style的な動作をしてくれるわけです。

hhvm/hhast

Hack専用ですので、特別に理由がない限りは入れておくと良いでしょう。
これを利用するときは、プロジェクトルートに hhast-lint.json を作成してどのディレクトリを対象にするかを記述します。

hhast-lint.json
{
  "roots":[
    "src/",
    "tests/"
  ]
}

これも指定方法はいくつかありますが、ほとんど上記の指定で良いでしょう。
アプリケーションに合わせて記述してください。

特別な指定をしたい場合は、overridesを使って以下のように記述できます。

  "overrides": [
    {
      "patterns": [ "codegen/*", "tests/examples/*" ],
      "disabledLinters": [
        "Facebook\\HHAST\\UnusedUseClauseLinter",
        "Facebook\\HHAST\\LicenseHeaderLinter"
      ],
      "disableAllAutoFixes": true
    },
    {
      "patterns": [ "src/__Private/Wrap/*" ],
      "disabledLinters": [
        "Facebook\\HHAST\\CamelCasedMethodsUnderscoredFunctionsLinter"
      ]
    },
    {
      "patterns": ["tests/*"],
      "extraLinters": [
        "Facebook\\HHAST\\DataProviderTypesLinter"
      ]
    }
  ]

当然ですがPHP-CS-Fixerなどは利用せずに、Hack専用のものを使いましょう!

hsl

hslは Hack Standard Libraryというもので、
重宝することが多い処理や、Hackならではの実装が多く用意されているライブラリで、
Hackで実装するときには最初の学習に利用するといいでしょう。

hhvm/hsl

サンプルにあるコードを例にしてみましょう。

use namespace HH\Lib\Vec;

function sample(vec<?int> $foo): vec<string> {
  return $foo
    |> Vec\filter_nulls($$)
    |> Vec\map($$, $it ==> (string) $it);
}

上記の関数は、intのみを要素とするvec配列で、vecの中身にnullがあっても構わない となります。
渡されたvecはPipe Operator
順番に処理されてreturnされます。

Vec\filter_nullsは、vec配列の要素でnullではないものを対象として新たにvec配列を作成します。
Vec\mapは第一引数に渡された配列をMapとし、
第二引数は、第一引数に渡されたMapの要素がそれぞれcallbackで渡されます。
上記のサンプル関数を利用する場合は、次の通りです。

require_once __DIR__ . '/vendor/autoload.hack';

<<__EntryPoint>>
function main(): void {
  var_dump(sample(vec[]));
  var_dump(sample(vec[1,2,3]));
  var_dump(sample(vec[1, null, 400]));
}

簡単です。

.hhconfig

Hackとして動作させるには、.hhconfigファイルをプロジェクトルートに設置しなければいけません。
この.hhconfigファイルには様々な指定を記述することができます。
ちょっと前のHackを触った方はとりあえず作れば良いだけのファイル、として認識しているかもしれません。
が、現在はそうではありません。
エラーの内容の精査やさまざまな挙動の変更を指示することができます。

実装する場合は、次の記述をするといいでしょう

assume_php=false
ignored_paths = [ "vendor/.+/tests/.+" ]
disallow_elvis_space=true
disallow_non_arraykey_keys=true
disallow_unsafe_comparisons=true
decl_override_require_hint=true
enable_experimental_tc_features=shape_field_check,sealed_classes
disable_primitive_refinement=true
disable_static_local_variables = true
disallow_array_literal = true
allowed_decl_fixme_codes=1002,2053,4045,4047
allowed_fixme_codes_strict=1002,2011,2049,2050,2053,4007,4027,4045,4047,4053,4104,4106,4107,4108,4110,4128,4135,4188,4240,4323

いくつかのライブラリではcomposer install時にtestsが含まれる場合もありますので、
念のためにignored_pathsでtype checkerの対象から外します。
disallow_*はHackらしく、通常のものからより厳格に書くためのものです。
PHP系の書き方を排除することができます。
disable_系も同じく、古き良きPHPライクな書き方を排除できます。
現在も動くには動く、古き良きPHPの記述法は将来のバージョンアップで削除される可能性が非常に高いため、
HHVMのバージョンアップ情報をブログでキャッチして、対応するといいでしょう。

HHVM blog

余談ですが、Hack/HHVMのドキュメントサイトは当然Hackでできています。

動作環境について

開発するための事前知識がついたら実際に開発環境を、となりますが、
下記にある通りの環境で動作させることが望ましいです。
Installation

MacOSの場合は、brewでインストールできます

$ brew tap hhvm/hhvm
$ brew install hhvm

Linux環境ではUbuntuのみサポートされています。
インターネット上にはCentOSにインストールする、というようなエントリがありますが、
100%古い記事なので、注意しておきましょう。
現在はDockerが普及していますので、特に都合が悪いということはないでしょう。

Docker

Dockerを利用する場合は公式から提供されていますので、それを利用しましょう。
Docker Hub/hhvm-proxygen
Docker Hub/hhvm

コンテナを使って運用をする場合は、
アプリケーションをコンパイルして、シングルバイナリで動かすことができるhhvm-proxygenを利用することを強くお勧めします。

docker-composeを利用する場合は、下記のようにすると簡単に利用できます。

version: '3'
services:
  nginx:
    image: nginx:1.19-alpine
    ports:
      - "80:80"
    volumes:
      - .:/var/www/html
      - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./docker/nginx/conf.d:/etc/nginx/conf.d
    container_name: web-server
    tty: true
  hhvm:
    build:
      context: ./docker/hhvm
    volumes:
      - .:/var/www/html
      - ./docker/hhvm/hh.conf:/etc/hh.conf
      - ./docker/hhvm/php.ini:/etc/hhvm/php.ini
      - ./docker/hhvm/server.ini:/etc/hhvm/server.ini
    command: hhvm --mode server -vServer.AllowRunAsRoot=1
    restart: always
    tty: true
    container_name: hhvm
    ports:
      - 19001:19001

composerを利用するには、hhvmコマンドを利用する必要がありますので、
hhvmのコンテナに含めてしまう方が簡単かもしれません。

/docker/hhvm/Dockerfile
FROM hhvm/hhvm-proxygen:latest AS dev

RUN apt-get update
RUN DEBIAN_FRONTEND=noninteractive
RUN apt install -y dnsutils iputils-ping net-tools

RUN hhvm --version && php --version
RUN cd $(mktemp -d) \ 
 && curl https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

hhvmの開発向けphp.iniなどは環境に合わせて記述してください。
proxygenで動かす場合は下記の記述にしておくといいでしょう。

php.ini
xdebug.enable = 1
date.timezone = Asia/Tokyo

hhvm.log.header = true
hhvm.debug.server_error_message = true
display_errors = On
html_errors = On
error_reporting = 22527

hhvm.server.fix_path_info = true
hhvm.server.type = proxygen
hhvm.server.port = 18080
hhvm.log.use_log_file = true
hhvm.server.source_root = /var/www/public

hhvm.php_file.extensions[hack]=1 
hhvm.jit=1

hhvm.server.default_document = "index.hack"
hhvm.server.error_document404 = "index.hack"
hhvm.server.utf8ize_replace = true
hhvm.log.file=stderr

hhvm.admin_server.port=19001
hhvm.admin_server.password=SomePassword

環境構築ができたらIDE環境と合わせて整えてみましょう。

Hackを運用するためのコンテナ環境、コンパイルについては次回以降に紹介します。
乞うご期待!

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

超基本的なプログラミング用語の英単語メモ

日本語 ☞ 英語

変数 → variable
定数 → constant
配列 → array
オブジェクト指向プログラミング → object‐oriented programming(OOP)
演算子 → operator
識別子 → identifier
代入 → assignment
属性 → attribute
キャッシュ → cache (cashまたはcasheではない)

今日の名言

Done is better than perfect.
(完璧であることよりも終わらせることの方が重要)

-マーク・ザッカーバーグ

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

【環境構築メモ】docker × Laravel 開発環境構築(Laradock使用)

はじめに

Laravelでの開発環境を構築する際の手順をまとめました。
今回は、laradockを使ってみたかったので、laradockを使用して環境構築します。

※ laradockとは? http://laradock.io/

  1. Laravel(PHP)のプロジェクトをDocker上で動作させるためのワンダフルな環境
  2. Dockerを用いてlaravelの環境を作るために必要なものがほぼ全てパッケージ化されている
  3. PHP, nginx, MySQLやミドルウェア等、Laravelの実行に必要な環境が整っている。

前提環境

  • Windows 10 Professional
  • PHP7.3
  • Dockerインストール済み
  • gitインストール済み

ゴール

  • Laradockを使用して、Laravelの開発環境を構築する。
  • 開発環境としては、以下のような構成にします。
    • Laravel(Webアプリケーションフレームワーク)
    • mysql(DB)
    • nginx(Webサーバ)

手順

1. プロジェクトディレクトリを作成

  • 任意の場所にプロジェクトディレクトリを作成します。どこでもOK。
    $ mkdir todo-list

2. プロジェクトディレクトリに移動し、laradockをプロジェクトディレクトリ直下にclone

  • Githubからlaradockをcloneしてきます。cloneが終わると、プロジェクトディレクトリ直下に「laradock」というディレクトリが作成されます。
    $ git clone https://github.com/laradock/laradock.git

  • clone直後のプロジェクトディレクトリ階層: laradockディレクトリ下に色々入ってます

3. laradockディレクトリに移動し、「.env」ファイルを作成、編集

  • laradockディレクトリ直下の「env-example」ファイルをリネームして作成する
    • .envファイル = 環境設定ファイル
    • 設定ファイルのサンプルをベースに置き換える為、設定ファイルをコピーしておく。
      $ cd laradock/ $ cp env-example .env

3-1. envファイルの設定

  • .envファイル内の値を編集していきます。

【設定詳細】

I. envファイル内の「APP_CODE_PATH_HOST」の値を変更

  5 ### Paths #################################################
  6
  7 # Point to the path of your applications code on your host
  8 APP_CODE_PATH_HOST=../todo-app
  9

↑上記記述で、「todo-list/todo-app/」下の階層がアプリケーションのコードを格納するディレクトリとみなされるようになる。

  • 2020年11月時点では.envファイルに書いてあるPHPのバージョンは「7.3」でした。

    • PHP_VERSION=7.3
  • mysqlは「その時点の最新版」

    • MYSQL_VERSION=latest

4. docker-composeでコンテナを立ち上げる:nginx と mysqlを立ち上げ

  • laradockディレクトリに移動し、コマンドを実行。コンテナを起動します。
    $ docker-compose up -d nginx mysql
    ※初回起動は時間がかなり!!かかります。気長に待ちましょう。10分以上は間違いなくかかる!

  • docker-compose up でエラーが出たら、下記参考URLの内容を参照ください。
    https://qiita.com/blueray777/items/44e25dc04ed1ab5c4cc8

    ・Laradockのdocker-compose upで出会ったエラー達

  • コンテナの確認

$ docker-compose ps -a
           Name                          Command               State                                                                      Ports
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
laradock_docker-in-docker_1   dockerd-entrypoint.sh            Up      2375/tcp, 2376/tcp
laradock_mysql_1              docker-entrypoint.sh mysqld      Up      0.0.0.0:3306->3306/tcp, 33060/tcp
laradock_nginx_1              /docker-entrypoint.sh /bin ...   Up      0.0.0.0:443->443/tcp, 0.0.0.0:80->80/tcp, 0.0.0.0:81->81/tcp
laradock_php-fpm_1            docker-php-entrypoint php-fpm    Up      9000/tcp
laradock_workspace_1          /sbin/my_init                    Up      0.0.0.0:2222->22/tcp, 0.0.0.0:3000->3000/tcp, 0.0.0.0:3001->3001/tcp, 0.0.0.0:4200->4200/tcp, 0.0.0.0:8001->8000/tcp, 0.0.0.0:8080->8080/tcp

5. Laravelプロジェクトの作成

  • コンテナ起動後は、コンテナ内に入ってLaravelプロジェクトを生成します。
    laradockはworkspaceという全ての操作が可能なコンテナが用意されるので、ここから操作していきます。

  • コンテナ内に入る(laradockユーザー/rootユーザー)
    $ winpty docker-compose exec --user=laradock workspace bash
    $ winpty docker-compose exec --user=root workspace bash

    - Laradockに入れた時の表示
    laradock@fb36cc3540cd:/var/www$

  • Laravelプロジェクトのダウンロード
    • コンテナに入ったら/var/www配下にいますので、composerを使用してプロジェクトを生成します。
      laradock@fb36cc3540cd:/var/www$ composer create-project laravel/laravel todo-app ※ダウンロードに時間かかる

6. プロジェクトの生成確認

  • http://localhost/ にブランチからアクセス確認

    • Laravelのデフォルト画面が出ることを確認
  • 404 Not Foundが表示された場合

    • 3-1. envファイルの設定 で、.envファイルの「APP_CODE_PATH_HOST」のみ変更しましたが、「APP_CODE_PATH_CONTAINER」のパスも変更する必要がありました。(共有ディレクトリのマウント設定)
  5 ### Paths #################################################
  6
  7 # Point to the path of your applications code on your host
  8 APP_CODE_PATH_HOST=../todo-app
  9
 10 # Point to where the `APP_CODE_PATH_HOST` should be in the container
 11 APP_CODE_PATH_CONTAINER=/var/www/todo-app
  • nginxのroot変更
    • nginxの設定を変更します。ドキュメントルートを変更(13行目:root 以下のパスを変更)
$ vi nginx/sites/default.conf

  1 server {
  2
  3     listen 80 default_server;
  4     listen [::]:80 default_server ipv6only=on;
  5
  6     # For https
  7     # listen 443 ssl default_server;
  8     # listen [::]:443 ssl default_server ipv6only=on;
  9     # ssl_certificate /etc/nginx/ssl/default.crt;
 10     # ssl_certificate_key /etc/nginx/ssl/default.key;
 11
 12     server_name localhost;
 13     root /var/www/todo-app/public;
 14     index index.php index.html index.htm;
  • envファイル変更後は、コンテナを再起動してください。
    $ docker-compose stop $ docker-compose up -d nginx mysql

まとめ

  • ここまでで、laradock環境で構築したLaravelの初期画面表示ができるようになりました。 プロジェクトディレクトリの階層はこんな感じです。
  todo-list/
      | todo-app/
      └ laradock/

todo-appディレクトリ配下には、Laravelの各種ソースが格納されています。
dockerの知識や、Nginx等のインフラ知識がなくても、ここまではすんなりとこれるのではないかと思います。次の記事では、今回構築したLaravel開発環境をベースに、簡単なWebアプリケーションを構築して実行してみたいと思います。

次回!

  • DB設定
  • テーブルを作成、データ投入→ DBのデータを画面(ビュー)で一覧表示させてみる。

参考にしたサイト

Laravelで簡単なページを作成するまで https://qiita.com/tasogarei/items/1e0a3577a9fae7e519ba

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

LaravelとPHPWordを使って、年賀状の宛名面を出力してみた。

PHP(Laravel)を使って、Wordファイルを出力する記事です。

わざわざLaravelとphpwordを使ってはがき宛名面の作成は実用性低いですが、
この手順は報告書の自動作成などに応用できます。

ちなみに、年賀状の宛名面がテーマなのはただの思いつきで決めました。

動作環境
・Windows10
・XAMPP
・Word 2016

開発環境
・Laravel 6

1.Laravelインストール

Composerを利用してLaravelのインストールを行います。
プロジェクトのフォルダに移動して、下記コマンドを実行します。


$ composer create-project "laravel/laravel=6.0.*" project

Composerを利用して、phpwordをインストールします。

$ composer require phpoffice/phpword

2.マイグレーションファイルを作成します。

$ php artisan make:migration create_addresses_table --create=addresses
"2020_11_09_134553_create_addresses_table.php"
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateAddressesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('addresses', function (Blueprint $table) {
            $table->increments('id');
            $table->timestamps();
            $table->string('zipcode', 7);
            $table->string('name', 50);
            $table->string('prefecture', 30);
            $table->string('city', 30);
            $table->string('town', 100);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('addresses');
    }
}

3.モデルを作成します。

$ php artisan make:model Address

4.コントローラーを作成します。

$ php artisan make:controller TestController

5.シーダーを作成します。

$ php artisan make:seeder AddressTableSeeder
”AddressTableSeeder.php”
<?php

use Illuminate\Database\Seeder;

class AddressTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('addresses')->insert([
            [
                'zipcode' => '1430002',     //郵便番号
                'prefecture' => '東京都',   //都道府県
                'city' => '大田区',         //市区町村
                'town' => '城南島六ー二ー一 カーム城南島 一〇〇六号室',      //町域
                'name' => '高野彦好',       //名前
            ],
            [
                'zipcode' => '1730003',     //郵便番号
                'prefecture' => '東京都',   //都道府県
                'city' => '板橋区',         //市区町村
                'town' => '加賀二ー二ー九',      //町域
                'name' => '米田政広',       //名前
            ],
            [
                'zipcode' => '1940204',     //郵便番号
                'prefecture' => '東京都',   //都道府県
                'city' => '町田市',         //市区町村
                'town' => '小山田桜台一ー一三ー八 アスコットヴィラ小山田桜台 二一二',    //町域
                'name' => '溝口為一郎',     //名前
            ],
            [
                'zipcode' => '1300025',     //郵便番号
                'prefecture' => '東京都',   //都道府県
                'city' => '墨田区',         //市区町村
                'town' => '千歳二ー七ー七五',       //町域
                'name' => '小倉真紀子',     //名前
            ],
            [
                'zipcode' => '2060041',     //郵便番号
                'prefecture' => '東京都',   //都道府県
                'city' => '多摩市',         //市区町村
                'town' => '愛宕五ー二ー九',      //町域
                'name' => '吉井孝市',       //名前
            ]
        ]);
    }
}

6.マイグレーションとシードを同時に実行します。

$ php artisan migrate:fresh --seed

7.Viewのレイアウトファイルを作る

”base.blade.php”
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <title>アプリケーション名</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>

<body>
  <nav class="navbar navbar-expand-lg navbar-light bg-light">
    <a class="navbar-brand" href="#">テスト</a>
  </nav>

@yield('content')

    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>

</html>

8.ルーティングの設定をする

web.php
Route::group(['prefix' => 'test'], function() {
    Route::get('index','TestController@index');
    Route::post('word_inport','TestController@word_inport');
    Route::get('word_inport','TestController@word_inport');
});

9.コントローラーの設定をする。

今回は、phpwordのテンプレートプロセッサを利用してdocxファイル出力をする。

"TestController.php"
<?php

namespace App\Http\Controllers;

use App\Address;

use PhpOffice\PhpWord\PhpWord;
use PhpOffice\PhpWord\IOFactory;
use PhpOffice\PhpWord\TemplateProcessor;

use Illuminate\Http\Request;

class TestController extends Controller
{
    //
    public function index(){
        $addresses = Address::all();

        return view('index',['addresses' => $addresses]);
    }

    public function word_inport(Request $request){
        $addresses = Address::find($request->id);

        $templateprocessor = new TemplateProcessor('./template/template.docx');
        $templateprocessor->setValue('zipcode',$addresses->zipcode);
        $templateprocessor->setValue('prefecture',$addresses->prefecture);
        $templateprocessor->setValue('city',$addresses->city);
        $templateprocessor->setValue('town',$addresses->town);
        $templateprocessor->setValue('name',$addresses->name);

        $templateprocessor->saveAs($addresses->name.'様宛名.docx');
        return response()->download($addresses->name.'様宛名.docx')->deleteFileAfterSend(true);
    }
}

10.一覧画面を作る

"index.blade.php"
@extends('base')
@section('title', 'テスト')

@section('content')
<div class="container">
    <div class="table_position">
        <table class="table table-bordered">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>郵便番号</th>
                    <th>都道府県</th>
                    <th>市区町村</th>
                    <th>町域</th>
                    <th>名前</th>
                    <th></th>
                </tr>
            </thead>
            <tbody class="table">
            @foreach($addresses as $address)
                <tr>
                    <td>{{ $address->id }}</td>
                    <input type="hidden" value="{{ $address->id }}" name="id">
                    <td>{{ $address->zipcode }}</td>
                    <input type="hidden" value="{{ $address->zipcode }}" name="zipcode">
                    <td>{{ $address->prefecture }}</td>
                    <input type="hidden" value="{{ $address->prefecture }}" name="prefecture">
                    <td>{{ $address->city }}</td>
                    <input type="hidden" value="{{ $address->city }}" name="city">
                    <td>{{ $address->town }}</td>
                    <input type="hidden" value="{{ $address->town }}" name="town">
                    <td>{{ $address->name }}</td>
                    <input type="hidden" value="{{ $address->name }}" name="name">
                    <th><a href="{{ action('TestController@word_inport', ['id' => $address->id]) }}" class="btn btn-primary">Word出力</a></th>
                </tr>
            @endforeach
            </tbody>
        </table>
    </div>
</div>
@endsection

11.テンプレートファイルを置く

Wordで、宛名面のテンプレートファイルを使って図のように編集する。
注意:宛名印刷テンプレートが利用できるのは、Windows版のWordのみです。
Mac版の場合は、ご自身でテンプレートファイルを探してください。

スクリーンショット 2020-11-29 201401.PNG

編集したものは、project\public\templateに置く
(ここでは、ファイル名はtemplate.docxになっている。)

動作確認をしてみる。

http://localhost:8000/test/indexを開き、Word出力のボタンを押す。

スクリーンショット 2020-11-29 204843.PNG

ダウンロードされたWordファイルを開く。

スクリーンショット 2020-11-29 204949.PNG

以上で、Laravelとphpwordを用いてのテンプレート出力が完了です。

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

【PHP】MAMP使用でCSSが適用されない時の対処法

https://y-com.info/contents/?p=1165

こちらのページが参考になりました。

実際に私が行った対処法は、

  1. Applications > MAMP > bin > php > php7.4.9 > conf > php.iniを開く
  2. cmd + F ショートカットで OPcache.enable_cli を検索
  3. 2カ所ほどヒットしたが、【OPcache】という欄の   
opcache.enable_cli=1 → opcache.enable_cli=0

と変更。さらに、

opcache.enable=0

を新しく追加しました。

すると、今までstyles.cssに変更を加えても何も変化しませんでしたが、即座に変更が反映されるようになりました。

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

CakePHP コントローラーからjQueryにデータを渡す方法

・コントローラー

$array = ["a","b","c"];

・一旦ビューで

<script>
    let 
array = <?php echo json_encode($array);?>;
</script>

・jQuery

console.log(array);
1 => "a"
2 => "b"
3 => "c"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP Laravel】コントローラへのルーティングが上手くいかない

この記事は、「PHPフレームワーク Laravel入門」を使用して勉強した時につまった所になります。

目次

  • バージョン情報
  • コントローラへのルーティングが上手くいかなかった
    • 解決策1
    • 解決策2
    • 参考サイト

バージョン情報

バージョン
PHP 7.4.9
Laravel 8.16.1

コントローラへのルーティングが上手くいかなかった

書籍に倣って以下のようにコーディングした。

Route::get('hello', 'helloController@index');

実行すると以下のようなエラーが出てきた。

キャプチャ.JPG

以下のような変更をしたら実行できた。

解決策1

use App\Http\Controllers\HelloController;

Route::get('hello', [HelloController::class,'index']);

解決策2

artisanコマンドでRoute情報を確認する。

php artisan route:list

+--------+----------+------------+------+---------------------------------------------+------------+
| Domain | Method   | URI        | Name | Action                                      | Middleware |
+--------+----------+------------+------+---------------------------------------------+------------+
|        | GET|HEAD | /          |      | Closure                                     | web        |
|        | GET|HEAD | api/user   |      | Closure                                     | api        |
|        |          |            |      |                                             | auth:api   |
|        | GET|HEAD | hello      |      | App\Http\Controllers\HelloController@index  | web        |
+--------+----------+------------+------+---------------------------------------------+------------+

Actionの所がApp\Http\Controllers\HelloController@indexとなっていたので、
Route::getを以下のように変更。

Route::get('hello', 'App\Http\Controllers\HelloController@index');

参考サイト

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

continue,breakに関して(PHP)

continue,breakに関して

continueとbreakについてはそれぞれループ処理で使われる。

continueはある特定の条件をスキップして、そのままループ処理を続ける一方で、
breakはある特定の条件でループ処理を中断する。

実際のコードと出力結果については以下の通りとなる。

continue

continueは特定の条件で、ループをスキップして、そのままループを続ける。

<?php
for ($i = 1; $i <= 10; $i++) {
  if ($i % 3 === 0) {
    continue;
  }
  echo $i . PHP_EOL;
}
?>

出力結果

1
2
4
5
7
8
10

iが3の倍数になる条件でスキップしていて、3の倍数以外で、1から10までの数が出力されている。

break

breakの場合はある特定の条件でループ処理が止まる。

<?php

for ($i = 1; $i <= 10; $i++) {
  if ($i === 4) {
    break;
  }
  echo $i . PHP_EOL;
}
?>

出力結果

1
2
3

iが4になった場合にループ処理が止まり、1〜3の数字のみ出力される。

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

[PHP] エラー解決 gyp: No Xcode or CLT version detected!

laravelに必要なnpmをインストールをすると

gyp: No Xcode or CLT version detected!

というエラーが出たのでそれの解決方法を投稿いたします。
今回はXcodeというAPPをアンインストールをして解決した方法になります。
下記はエラー画面のスクショになります。
“スクリーンショット” 2020-11-30 12.03.07.jpg

解説

gyp: No Xcode or CLT version detected!
これは「XcodeとCLTのバージョンがおかしい」というエラー内容でした。
私自身、Xcodeは暫く使うことはないのでアンインストールを実行したいと思います。

//ターミナルにて上から一つずつ実行する
sudo rm -rf /Applications/Xcode.app
sudo rm -rf /Library/Preferences/com.apple.dt.Xcode.plist
sudo rm -rf ~/Library/Preferences/com.apple.dt.Xcode.plist
sudo rm -rf ~/Library/Caches/com.apple.dt.Xcode
sudo rm -rf ~/Library/Application Support/Xcode
sudo rm -rf ~/Library/Developer/Xcode
sudo rm -rf ~/Library/Developer/CoreSimulator

無事にアンインストールはできました。しかし、npm installを実行すると

(省略)
found 2 vulnerabilities (1 low, 1 high)
  run `npm audit fix` to fix them, or `npm audit` for details
(省略)

という脆弱性が見つかりました。次回にて脆弱性が問題なのでこちらも投稿します。

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

iCal週間天気予報を支える技術

この記事は「個人アプリ/サービス開発の進め方と運用、得た学び - 02【PR】 Lenovo Advent Calendar 2020」に参加しています。

iCal週間天気予報とは

「スケジュールを確認するときに合わせて天気も確認できたらな」という人は多いでしょう。
私もその一人です。
そして、これまでは「livedoor天気予報」というサービスがあり、iCal形式で配信される天気予報をカレンダーアプリに登録することで、チェックしていました。
しかし、2020年7月31日をもって、このサービスの終了が発表されたのです。

ここで困った私は、自分で作ろうと思い立ちました。
(裏側の話をすると、以下の本を執筆している最中で、livedoor天気予報について紹介していたのです。)

『ITエンジニアがときめく自動化の魔法』
https://www.amazon.co.jp/dp/4802612486

ところが、サービスが終了してしまうと、本の読者は試せなくなってしまいます。
書いたページを省略する、という判断もあったのですが、自分で似たようなサービスを作ってみることにしました。
そして、自分で作るだけでなく、他の人にも使ってもらおう、と考えたのです。

そして、できあがったのが「iCal週間天気予報」です。
https://weather.masuipeo.com

詳しい使い方はサイトを見ていただければと思います。

どんな技術で動かしているのか

ここはQiitaですので、その裏側について解説します。

iCalとは

今回提供するiCal週間天気予報は名前の通り「iCal形式」で配信するものです。
iCalはiCalendarの略で、その書式はRFC 5545で定められています。

iCloudのカレンダーや、Googleカレンダーなどに登録して使用でき、これらのカレンダーアプリが自動的に更新を取得してくれます。
フォーマットはただのテキスト形式で、以下のような形で作成し、Webサーバーに配置するだけです。

BEGIN:VCALENDAR
省略
END:VCALENDAR

天気情報の取得

天気情報は気象庁が提供しているXMLデータを使うことにしました。
気象庁では、「防災情報XML」というデータを配信しており、無料で使用できます。
http://xml.kishou.go.jp

詳しい情報は上記のサイトを見ていただくとわかるので省略しますが、観測所ごとに毎日の天気予報や週間天気予報などを配信されています。
今回はこの週間天気予報のデータを使用しました。

フォーマットの変換

このサービスを提供するためには、上記の気象庁のサイトから取得したXML形式のデータをiCal形式に変換する必要があります。
今回はレンタルサーバーで実行するため、手軽なPHPを選択しました。
PHPにはsimplexml_load_fileという関数が用意されているので、この関数でXMLファイルを取得し、取得した項目をiCal形式に変換するだけです。

これを、各都道府県のすべてのXMLファイルに対して実行し、iCal形式のテキストファイルとして保存しておきます。

ファイルの提供

変換したファイルをWebサーバーの公開領域に、iCal形式のテキストファイルとして配置します。
逆に言えば、公開しているのはこの単純なテキストファイルだけなので、見た目上はシンプルな静的サイトです。
(1日に1回、上記の変換処理を実行してファイルを上書きするだけ)

開発時間

実際には、上記の変換処理だけなので、開発に要した時間はXMLファイルのフォーマットの理解を含めて2日程度です。
その後、実際に自分だけで1週間ほど運用してみて、特に問題なさそうなので、公開することにしました。

公開前に考えたことと公開後の状況

アクセス数の予測

一般に公開するにあたり、どのくらいのアクセス数が来るのか予測しておく必要があります。
それによって、レンタルサーバーを使うのか、AWSなどのクラウドを使うのか判断しようと考えました。

とはいえ、個人が提供するサービスをどのくらいの人が使ってくれるのか、想像もつきません。
そこで、1つの目安として1万人という数値を考えました。
なんの根拠があるわけでもありませんが、1万人が使うサービスとなれば、それなりの規模だと考えられます。

そして、1万人が1日に3回アクセスする(カレンダーアプリが1日3回更新する)と考えると、1万×3回×30日=月間約90万PVとなります。
ただのテキストファイルだけなので、月間100万PV程度ならレンタルサーバーでも問題ない、というのは私の経験から判断できました。
(過去にそれ以上のものを運営していたため)

もしこれを大きく超えることになれば、大成功だと考えて途中でクラウドに移行することになるかな、と考えていました。
(最初からクラウドという選択肢もありましたが、個人事務所のWebサイトとして契約しているレンタルサーバーなら無料で使えるため)

実際に公開してみて

公開を開始してから、noteに記事を書きました。
https://note.com/masuipeo/n/nf6636e5919b8

公開したのが7月下旬で、少しずつアクセスが増えてきます。
そして、8月に入ったあたりで、Twitter上で多くのつぶやきが出てきます。

「livedoor天気予報が終了してカレンダーの天気が更新されない」

このようなツイートを見つけるたびに、今回のサービスを開始したことを返信で紹介していきました。
これを繰り返していると、着実にアクセス数が増えていきました。

そして2ヶ月が経とうとしている9月末、ついに1日のアクセスが想定していた3万回に到達。
開始前に想定していた「1万人が1日に3回アクセスする」という状況が生まれました。

そして、翌月は1ヶ月で110万PVを超えてきました。
その時の記事がこちらです。
https://note.com/masuipeo/n/nd2dade6f8ce2

ただし、上記の記事を見ていただくとわかるのですが、通常のブログなどと違って特定の日にバズって100万PVという状況ではない、ということです。
常に安定して毎日3万PV〜4万PVくらいを繰り返しています。

今後について

上記の記事からまた約1ヶ月が経ちました。
現在も安定して1日4万PV〜5万PVあたりを継続しています。

特に集中したアクセスがあるわけでもないので現在のところはこのままレンタルサーバーでもいいかな、と考えています。
(これが10倍になると、さすがにレンタルサーバーでは耐えられないような気がしますが)

今回のサービスを作っての学び

最後に、今回のサービスを作って気づいたことを改めて紹介します。

ちょっとしたことでも手を動かすことが大事

今回のプログラムは、XMLファイルをiCal形式に変換するだけのプログラムです。
iCal形式のフォーマットを知っており、提供されているXMLファイルのレイアウトがわかっていれば、1時間もかからずに作成できるでしょう。

しかし、それを実際に作ってみて、公開してみると1万人を超える人に使っていただけるようになりました。
livedoor天気予報の終了というタイミングがあったことも事実ですが、やはり手を動かしてやってみると、iCalを何か他に使えないかな、というアイデアも生まれてきます。

発信することが大事

今回も、ただ公開するだけでは多くの人に使ってもらえるところまでいかなかったと思います。
しかし、Twitterなどで積極的に宣伝(紹介)をすることで、多くの人に届きました。

そして、一部のYouTubeなどで紹介され、確実に利用者が増えていきました。
このQiitaもそうですが、記事を作成することで多くの人の目にとまり、使ってもらえるようになります。

個人でサービスを開発したけれど、なかなか利用者が増えない、という場合は発信力をつけていかないといけないと改めて感じました。

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

composerとは/活用方法

composer = PHPのパッケージ管理システム

大抵の場合は、PHP関数のみではなくフレームワークやライブラリの活用を行う。
その際に、毎回それらをインストールするのは面倒。
composerは使うたびに名前を列挙するだけで呼ぶ出すことができる優れもの。

●⚪︎● 実際の流れ ●⚪︎●

新しいプロジェクトの中で、ログライブラリのmonologを使いたい場合。

1、PHPアプリケーションを作成するディレクトリに、JSONファイルを入れる。

{
    "require": {
        "monolog/monolog": "1.0.*"
    }
}

composer.json は、アプリケーションで利用するパッケージ(ライブラリ)を列挙します。 ここに列挙したパッケージが、Composer による管理の対象になる。

2. composer.json に列挙したパッケージをインストール(ダウンロード)してみます。 composer install コマンドを実行する。

composer install

3.すると、vendor というフォルダが作成される。 この vendor というフォルダの中を表示してみると、プロジェクトで必要とする monolog がダウンロードされたことがわかる。
こんな感じ。

ls -l vendor
-rw-r--r--  1 OSCA staff 183 9 23 15:45 autoload.php
drwxr-xr-x 10 OSCA staff 340 9 23 15:45 composer
drwxr-xr-x  3 OSCA staff 102 9 23 15:45 monolog

4.vendor フォルダの直下には、autoload.php というファイルも生成される。 この PHP ファイルには、 composer.json で列挙したライブラリを読み込む処理が書かれてるので、実際にコーディングを始める際には、つぎのように vendor/autoload.php を読み込めば、全てのライブラリが利用できるようになる。

require_once("vendor/autoload.php");
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP8, Docker】Jupyter と PHP8.0 カーネルのノートブックを Mac やラズパイや Windows の Docker 上で動かす

PHP8.0 でも Jupyter の Notebook を使いたい。macOS や RaspberryPi や Windows で。

でも、いろいろインストールしてローカルを汚したくない。

そんな遊び心満載なユーザー向けなものが Dockerどこか にないものか。

TL; DR

Jupyter-PHP の PHP8 用修正パッチと macOS/Windows10/Raspbian で動く JupyterLab の Dockerfile を作ってみました。

作り方や経緯、「Jupyter?何それ。typoってない?」と言う方は TS; DR をご覧下さい。

PHP8 カーネル入り Dockerfile の URL

使い方

ダウンロードとビルド
$ # Dockerfile 一式のクローン(ダウンロードでも可)
$ git clone https://github.com/KEINOS/Jupyter-PHP8.git
...

$ # Docker イメージのビルド
$ cd Jupyter-PHP8
$ docker build -t jupyter:local .
...(いささか時間がかかります)...
コンテナ(Jupyterサーバ)の起動
$ # Docker イメージからコンテナ(Jupyter サーバ)の起動。
$ # ローカルのポート 8001 をコンテナの 8000 ポート(Jupyter サーバのポート)につなげて起動する
$ docker run --rm -p 8001:8000 -v $(pwd)/data:/workspace jupyter:local
...(途中でアクセストークンが表示されるのでコピーしておく)
[I 12:43:22.853 LabApp] Jupyter Notebook 6.1.5 is running at:
[I 12:43:22.854 LabApp] http://44a5fb18069a:8000/?token=375a1f68224543bde87ffd8d5ce97b89ee707e3591ddcf90
[I 12:43:22.854 LabApp]  or http://127.0.0.1:8000/?token=375a1f68224543bde87ffd8d5ce97b89ee707e3591ddcf90
[I 12:43:22.855 LabApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
...(アクセス・ログが表示されていく。サーバを終了するには Ctrl+C)
  • 上記で Jupyter サーバのコンテナが起動したら、ブラウザからアクセスします。
    • ポートは 8001 です。8001 で都合が悪い場合は、上記の docker run で 8001 を他のポート番号にしてください。
    • ローカル(同一マシン)からアクセスする場合:
      • http://localhost:8001/?token=<アクセストークン>
    • 同じネットワークの別のマシンからアクセスする場合
      • http://<ホストのIPアドレス>:8001/?token=<アクセストークン>

インフォ

  • Intel/AMD および ARM v7l の CPU で動作確認しています。
    • macOS Catalina (OSX 10.15.7, MacBook Pro Early 2015, Docker version 19.03.13)
    • Raspbian Stretch (Debian 9, Raspberry Pi3+, Docker version 19.03.13)
    • Windows 10 Pro 64bit (20H2 ビルド 19042.630, Intel Pentium Silver N5000, Docker v20.10.0-rc1)
  • Docker のベースイメージ: keinos/php8-jit:latest Alpine ベース
  • PHP のバージョン: PHP8.0.0(2020/11/28 時点の master)+ JIT 有効
  • デフォルトで JupyterLab が起動しますが、以下のサービスもインストールされています。
    • jupyter core
    • jupyter-notebook
    • qtconsole
    • ipython
    • ipykernel
    • jupyter client
    • jupyter lab
    • nbconvert
    • pywidgets
    • nbformat
    • traitlets

TS; DR

Jupyter/Jupyter Notebook とは

Jupyter や Jupyter Notebook をご存知でしょうか。私は知りませんでした。

いや、名前や存在自体は知っていたのです。ペチペチ PHPer の私は「Python かぁ... Python かぁ...」と言いながら、完全に理解すらしていない機械学習の記事や本を読んでいると、Jupyter やらノートブックと言う単語にチョイチョイ出くわしていたのです。それが何であるかの説明もなく、当然のごとく話しが進むのです。

しかし、どうもテキスト・エディタや IDE のように見えるため、Win の頃は秀丸エディタ、サクラ・エディタ、Mac に移ってからは Coda2、Atom を経て VSCode に落ち着いた自分としては新たなエディタとして使うには汎用性に欠けると思い込んでおり、避けていました。

すると、先日(2020/11/18)とあるニュースが目に飛び込んできました。

『Microsoft、「Visual Studio Code」の新しい拡張機能「Jupyter」を発表』
「Python」言語拡張と切り離して、それ以外のプログラミング言語でも利用可能に

『Microsoft、「Visual Studio Code」の新しい拡張機能「Jupyter」を発表』@ インプレス Watch より)

「ん? VSCode で Jupyter?どう言うこと?」となりました。「VSCode で秀丸エディタが使える!」と言うような意味不明なニュースに見えたからです。

そこで、偏見や食わず嫌いなのかもしれない、と思い。改めて調べてみました。

プログラムのソースコードに、注釈や補助情報としてコメントを入れることがあると思います。

恐れずに言うなら、Jupyter は「その逆」です。つまり、Jupyter は Markdown のドキュメントにプログラムをコメントとして埋め込んでステップ実行できるものと言えそうです。

そのファイルが「ノートブック」(正しくは「Jupyter ノートブック」)と呼ばれ、JSON 形式の「Jupyter ノートブック・フォーマット」で書かれたものです。拡張子は .ipynbiPython NoteBook の略です。

例えば、以下のような「なんちゃって論文」を Markdown ドキュメントで書きたいとします。

# All about Hello, world

```python3
print("HelloWorld")
```

しかし、コードブロック内の print() をユーザー(読者)が実行して試せるようにしたいので、Jupyter ノートブック形式で作りたいとします。

最終的に以下のような JSON 形式になるのですが、ざっと構成だけみて下さい。「へぇ」と思う反面、手打ちで作成するには結構面倒だと思います。

sample.ipynb
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# All about Hello, world"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "print(\"HelloWorld\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}

上記をみると、Markdown 行のブロックと、コード行のブロックで分かれており、Jupyter のフォーマットでは、この各ブロックを「セル」(cell)と呼んでいます。そして最後に実行環境のメタデータが埋め込まれているのがわかると思います。シンプルですが、柔軟性があるフォーマットだと思います。

つまり、エディタを使ってドキュメントを書く時は以下を繰り返して、積み上げるイメージで作成して行きます。

  1. セルを挿入する
  2. 各々のセルに「markdown」や「code」といった属性を付ける
  3. セルに内容(テキスト)を記入

そして、これらのファイルのエディタやビューアとなるのが Jupyter NotebookJupyterLab と呼ばれる Python で書かれた Web アプリです。

Jupyter Notebook は従来からあるシンプルなエディタ&ビューアーですが、ファイルとしてのJupyter Notebook なのか、アプリとしての Jupyter Notebook なのかがわかりづらいことから、「クラシック」(Classic Jupyter Notebook)とも呼ばれます。

JupyterLabClassic Jupyter Notebook の後続の上位バージョンです。より多機能になっており、IDE に近いアプリになっています。

公式のオンラインのデモが触れるので試してみて下さい。

どちらも画面上部に「▶︎」の再生ボタンが表示されるので、押していって動きをみてください。ステップ実行的に実行されていくのがわかると思います。本記事は、これに PHP8 を動かすための記事ということになります。

Jupyter Notebook で書くと何が良いのか

何回目か分からない人工知能(AI)ブームの昨今、毎日のように機械学習の新しい論文やアイデアが発表されているそうです。そして、どの分野の論文もそうですが査読、つまり「内容の確認と検証」などを経て認知されます。

しかし、論文と同じ環境を用意したり、論文中に書かれたコードが本当に同じ結果を出すのか、写経のごとく別途コードを入力して検証するのも大変です。

Jupyter のノートブック形式であれば、「▶︎」の再生ボタンを押して行けば Markdown 中に埋め込まれた順にコードが実行されていきます。コードエディタや IDE で言うステップ実行と同じ感覚です。

つまり、論文と共に検証実証もセットで配布できる仕組みが研究者に好まれているのだと思います。

さすが Python パイセン。後輩の PHP が、止まることより動き続けることに重きを置くフラジャイル開発に始まり(嘘)、小さな気合と根性の積み重ねが大事なんだ、型々言うなとアジャガイル開発を続け(嘘)、問題を後任へ後任へとパスしながら前へ進むスクラム開発を経て(違)PHP が一世を風靡したのは今や昔(本当)。

体育会系の PHP と違い、教育を念頭に力を入れてきた理系の Python が、ここに来て真価が現れてきたのだと思います。

これは PHPer の私も見習わないと!、と思い Jupyter で PHP は使えないのか調べ始めました。

Jupyter のカーネルとは

木星の Jupiter でなく Jupyter なのは、元々は「Julia、Pythonと R 言語向けだったから」だそうで、それらを組み合わせて Ju-Pyt-Er つまり Jupyter なんだそうです。お洒落ですね。PHP が Programmers Hate PHP なのとは大違い(違)。

その後、「この仕組みが勉強やアイデアの共有には便利」と言うことで他の言語でも使えるようになり、その仲介役、つまり Jupyter から該当するプログラム言語にパースするプログラムを「カーネル」と呼ばれるようになります。

つまり、PHP を Jupyter で動かすためには PHP カーネルが必要になります。

しかし、残念なことに 2020/11/19 現在 PHP カーネルは IPHPJupyter-PHP の2つしか存在していません。

しかも IPHP は開発は中止、Jupyter-PHP を見ろと言いつつも、肝心の Jupyter-PHP は事実上放置されています。

? 【カーネルとは】

OS では Linux や Darwin のカーネルがあったり、機械学習ではカーネル関数があったりと、「カーネル」と言う言葉をあちこちで聞きます。ケンタの白髭のおっさんのカーネル(Colonel)ではなく、Kernel です。

Kernel とは「何かの中核や本質をなすもので、何かに覆われており見つけにくいもの」を言います。語源は中期オランダ語の「cornel」(粗食)から中高ドイツ語の「kornel」(穀物)を経て、corn の語源でもある古英語の「cyrnel」(種や、木を見た時の根っこなど。corn+-nel と同義の cyrnel)から来ています。日本語で「種」「核」「(牡蠣などの)身」的なニュアンスです。

例えば、OS の場合は、シェルなどを通してしかユーザーがアクセスできない OS の中核をなすプログラム群を「カーネル」と言います。機械学習では、一見すると A と B に直線で分けづらい(線引きしづらい) X,Y の2次元データに、もう 1 データ加えて 3 次元にすると面で線引きできる(現れてくる)データを「カーネル」と言います。

ディープラーニングで動画の顔画像の入れ替え実施編&そこから学ぶ機械学習の基礎の基礎 @ Qiita より)

Jupyter-PHP を PHP8 で動かす

公式の Docker の PHP イメージ(php:8.0-rc-alpine)は RaspberryPi Zero で動かなかったり、JIT がデフォルトで有効になっていなかったり、リージョンが日本になっていなかったり、mbstring が有効になっていなかったり、と、すぐに使うには色々と面倒臭いので設定済みのイメージを使います。

  • keinos/php8-jit:latest

これをベースに Dockerfile を構築することにしました。最終的な成果物はこちらです。

さて、問題の PHP カーネルですがゼロから作るよりは既存のカーネルを修正して動くなら、それを使うのがベストと言うもの。IPHPJupyter-PHP で違いを調べてみました。

IPHPIPython が使えれば composer だけで使えていたらしいのですが不具合や限界も多く、Jupyter-PHP は、さらに PHP エクステンションの zmq を必要としています。

しかし、理論上 Jupyter フォーマットが変わらない限り、受け取った PHP コードをパースして PHP ランタイムに渡せば良いだけです。そこで、なんとか Jupyter-PHP を使えないものか奮闘してみました。

Jupyter-PHP のソースコード を見てみると composer.json があるので、composer require でインストールできるのかと思いきや、エラーでできません。

composer.json を覗くと "require":{"php":">=7.0"} と PHP のバージョンを制限していました。そこで --ignore-platform-reqs オプションを付けてみたところ、今度はエラーは出ないものの、やはり動きません。Jupyter にカーネルが現れないのです。どうやら、肝心のカーネルがしかるべきパスに設置されないようです。

つまり、Jupyter-PHP のカーネルをスコープ(許された範囲)外に設置する必要があります。しかし、composer だけでは、どこのパスに Jupyter のカーネル設置先のディレクトリがあるか分からず、検索するにしても OS にも依存するため composer.json 単体では完結できないのです。

そこで、次に README にあるようにインストーラーを実行してみましたが、やはりエラーでインストールできませんでした。

インストーラーの中身を確認してみると、カーネルのインストール先ディレクトリを検索して見つけたのち、内部で composer コマンドを呼び出して composer install していました。

この時に --ignore-platform-reqs オプションが指定されていないため「PHP8 ではバージョンが合わない」とエラーが出ていたのです。

そのため、Jupyter-PHP を PHP8 で動かすためには、主に2カ所の修正が必要です。

  • Jupyter-PHP 本体の composer.json の PHP バージョン指定の削除
  • Jupyter-PHP インストーラーのスクリプト内に --ignore-platform-reqs を加え、依存パッケージに PHP バージョンを無視させる

そして、この修正を行うパッチを作成しました。

また、JupyterLab に表示されるカーネルの選択パネル(アイコン?)の表記が「PHP」だったので、わかりやすいように「PHP8」に変更するパッチも作成しました。Jupyter-PHP インストーラーでインストール後の PHP のカーネル kernel.json を検索して、パッチをあてて下さい。

PHP に関する注意点としては、composer のバージョンと、Jupyter-PHP が依存する PECL パッケージの zmq の PHP エクステンション(PHP 拡張機能)です。

  1. composer のバージョンを v1系にすること。最新の v2 系だと、追随していない Composer パッケージが多いため、エラーがでまくります。
  2. zmq はソースから PECL パッケージをインストールすること。pecl install zmq で入るコンパイル済みパッケージは古すぎるため動きません。また、zmq が依存する zip の PHP エクステンションも一緒に入れておく必要があります。

あとは、JupyterLab や Classic Jupyter Notebook を動かすのに必要な Python3 と、インストールに使う pip を入れます。

  1. Python3 を Alpine Docker に入れる
  2. pip を Alpine に入れる
  3. Jupyter 一式をインストールする

ここまで来て、やっと出来上がったのですが、1つだけ後悔していることがあります。いつものクセで Docker のベースイメージを Alpine にしたことです。

Docker の Alpine Linux ベースのイメージは軽量であるため、複数のコンテナを起動して利用するのには向いています。つまり、1コンテナに1つの機能(サービス)を動かして使う場合です。しかし、軽量である反面、Alpine はクセが強いため、1つのコンテナにアレコレとインストールするのには向いていません。

Alpine にも apk と言うパッケージマネージャーがあるのですが、お世辞にも Ubuntu や Debian などの apt ほど素直にインストールできません。そのため、PHP やら Python やら node.js を入れてアレコレとアプリをインストールするなら、サイズは巨大になるものの Ubuntu や Debian ベースの Docker イメージの方が慣れていない人には楽だったのかもしれないと感じました。

参考文献

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

【PHP8】Jupyter と PHP8.0 カーネルのノートブックを Mac やラズパイや Windows の Docker 上で動かす

PHP8.0 でも Jupyter の Notebook を使いたい。macOS や RaspberryPi や Windows で。

でも、いろいろインストールしてローカルを汚したくない。

そんな遊び心満載なユーザー向けなものが Dockerどこか にないものか。

TL; DR

Jupyter-PHP の PHP8 用修正パッチと macOS/Windows10/Raspbian で動く JupyterLab の Dockerfile を作ってみました。

作り方や経緯、「Jupyter?何それ。typoってない?」と言う方は TS; DR をご覧下さい。

PHP8 カーネル入り Dockerfile の URL

使い方

ダウンロードとビルド
$ # Dockerfile 一式のクローン(ダウンロードでも可)
$ git clone https://github.com/KEINOS/Jupyter-PHP8.git
...

$ # Docker イメージのビルド
$ cd Jupyter-PHP8
$ docker build -t jupyter:local .
...(いささか時間がかかります)...
コンテナ(Jupyterサーバ)の起動
$ # Docker イメージからコンテナ(Jupyter サーバ)の起動。
$ # ローカルのポート 8001 をコンテナの 8000 ポート(Jupyter サーバのポート)につなげて起動する
$ docker run --rm -p 8001:8000 -v $(pwd)/data:/workspace jupyter:local
...(途中でアクセストークンが表示されるのでコピーしておく)
[I 12:43:22.853 LabApp] Jupyter Notebook 6.1.5 is running at:
[I 12:43:22.854 LabApp] http://44a5fb18069a:8000/?token=375a1f68224543bde87ffd8d5ce97b89ee707e3591ddcf90
[I 12:43:22.854 LabApp]  or http://127.0.0.1:8000/?token=375a1f68224543bde87ffd8d5ce97b89ee707e3591ddcf90
[I 12:43:22.855 LabApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
...(アクセス・ログが表示されていく。サーバを終了するには Ctrl+C)
  • 上記で Jupyter サーバのコンテナが起動したら、ブラウザからアクセスします。
    • ポートは 8001 です。8001 で都合が悪い場合は、上記の docker run で 8001 を他のポート番号にしてください。
    • ローカル(同一マシン)からアクセスする場合:
      • http://localhost:8001/?token=<アクセストークン>
    • 同じネットワークの別のマシンからアクセスする場合
      • http://<ホストのIPアドレス>:8001/?token=<アクセストークン>

イメージ・インフォ

イメージの諸情報

  • Intel/AMD および ARM v7l の CPU で動作確認しています。
    • macOS Catalina (OSX 10.15.7, MacBook Pro Early 2015, Docker version 19.03.13)
    • Raspbian Stretch (Debian 9, Raspberry Pi3+, Docker version 19.03.13)
    • Windows 10 Pro 64bit (20H2 ビルド 19042.630, Intel Pentium Silver N5000, Docker v20.10.0-rc1)
  • Docker のベースイメージ: keinos/php8-jit:latest Alpine ベース
  • PHP のバージョン: PHP8.0.0(2020/11/28 時点の master)+ JIT 有効
  • デフォルトで JupyterLab が起動しますが、以下のサービスもインストールされています。
    • jupyter core
    • jupyter-notebook
    • qtconsole
    • ipython
    • ipykernel
    • jupyter client
    • jupyter lab
    • nbconvert
    • pywidgets
    • nbformat
    • traitlets

TS; DR

Jupyter や Jupyter Notebook をご存知でしょうか。私は知りませんでした。

いや、名前や存在自体は知っていたのです。ペチペチ PHPer の私は「Python かぁ... Python かぁ...」とか言いながら、完全に理解すらしていない機械学習の記事や本を読んでいると、Jupyter やらノートブックと言う単語にチョイチョイ出くわしていたのです。それが何であるかの説明もなく、当然のごとく話しが進むのです。

しかし、どうもテキスト・エディタや IDE のように見えます。なんてっちゃって IDLE みたいなものかな?、と。Win の頃は秀丸エディタ、サクラ・エディタ、Mac に移ってからは Coda2、Atom、XCode を経て、やっと VSCode に落ち着いた自分としては新たなエディタとして使うには汎用性に欠けると思い込んでおり、避けていました。

すると、先日(2020/11/18)とあるニュースが目に飛び込んできました。

『Microsoft、「Visual Studio Code」の新しい拡張機能「Jupyter」を発表』
「Python」言語拡張と切り離して、それ以外のプログラミング言語でも利用可能に

『Microsoft、「Visual Studio Code」の新しい拡張機能「Jupyter」を発表』@ インプレス Watch より)

「ん? VSCode で Jupyter?どう言うこと?」となりました。「VSCode で秀丸エディタが使える!」と言うような意味不明なニュースに見えたからです。

そこで、偏見や食わず嫌いなのかもしれない、と思い。改めて調べてみました。

Jupyter/Jupyter Notebook とは

プログラムのソースコードに、注釈や補助情報としてコメントを入れることがあると思います。

恐れずに言うなら、Jupyter は「その逆」です。つまり、Jupyter は Markdown のドキュメントにプログラムをコメントとして埋め込んでステップ実行できるものと言えそうです。

そのファイルが「ノートブック」(正しくは「Jupyter ノートブック」)と呼ばれ、JSON 形式の「Jupyter ノートブック・フォーマット」で書かれたものです。拡張子は .ipynbiPython NoteBook の略です。

例えば、以下のような「なんちゃって論文」を Markdown ドキュメントで書きたいとします。

# All about Hello, world

```python3
print("HelloWorld")
```

しかし、コードブロック内の print() をユーザー(読者)が実行して試せるようにしたいので、Jupyter ノートブック形式で作りたいとします。

最終的に以下のような JSON 形式になるのですが、ざっと構成だけみて下さい。「へぇ」と思う反面、手打ちで作成するには結構面倒だと思います。

sample.ipynb
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# All about Hello, world"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "print(\"HelloWorld\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}

上記をみると、Markdown 行のブロックと、コード行のブロックで分かれており、Jupyter のフォーマットでは、この各ブロックを「セル」(cell)と呼んでいます。そして最後に実行環境のメタデータが埋め込まれているのがわかると思います。シンプルですが、柔軟性があるフォーマットだと思います。

つまり、エディタを使ってドキュメントを書く時は以下を繰り返して、積み上げるイメージで作成して行きます。

  1. セルを挿入する
  2. 各々のセルに「markdown」や「code」といった属性を付ける
  3. セルに内容(テキスト)を記入

そして、これらのファイルのエディタやビューアとなるのが Jupyter NotebookJupyterLab と呼ばれる Python で書かれた Web アプリです。

Jupyter Notebook は従来からあるシンプルなエディタ&ビューアーですが、ファイルとしてのJupyter Notebook なのか、アプリとしての Jupyter Notebook なのかがわかりづらいことから、「クラシック」(Classic Jupyter Notebook)とも呼ばれます。

JupyterLabClassic Jupyter Notebook の後続の上位バージョンです。より多機能になっており、IDE に近いアプリになっています。

公式のオンラインのデモが触れるので試してみて下さい。

どちらも画面上部に「▶︎」の再生ボタンが表示されるので、押していって動きをみてください。ステップ実行的に実行されていくのがわかると思います。本記事は、これに PHP8 を動かすための記事ということになります。

Jupyter Notebook で書くと何が良いのか

何回目か分からない人工知能(AI)ブームの昨今、毎日のように機械学習の新しい論文やアイデアが発表されているそうです。そして、どの分野の論文もそうですが査読、つまり「内容の確認と検証」などを経て認知されます。

しかし、論文と同じ環境を用意したり、論文中に書かれたコードが本当に同じ結果を出すのか、写経のごとく別途コードを入力して検証するのも大変です。

Jupyter のノートブック形式であれば、「▶︎」の再生ボタンを押して行けば Markdown 中に埋め込まれた順にコードが実行されていきます。コードエディタや IDE で言うステップ実行と同じ感覚です。

つまり、論文と共に検証実証もセットで配布できる仕組みが研究者に好まれているのだと思います。

さすが Python パイセン。後輩の PHP が、止まることより動き続けることに重きを置くフラジャイル開発に始まり(嘘)、型々言うな、小さな気合と根性の積み重ねが大事なんだ、とスタミナ勝負のアジャガイル開発を続け(嘘)、問題を後任へ後任へとパスしながら前へ進むスクラム開発を経て(違)PHP が一世を風靡したのは今や昔(本当)。

体育会系の PHP と違い、教育を念頭に力を入れてきた理系の Python が、ここに来て真価が現れてきたのだと思います。

これは PHPer の私も見習わないと!、と思い Jupyter で PHP は使えないのか調べ始めました。

Jupyter のカーネルとは

木星の Jupiter でなく Jupyter なのは、元々は「Julia、Pythonと R 言語向けだったから」だそうで、それらを組み合わせて Ju-Pyt-Er つまり Jupyter なんだそうです。お洒落ですね。PHP が Programmers Hate PHP なのとは大違い(違)。

その後、「この仕組みが勉強やアイデアの共有には便利」と言うことで他の言語でも使えるようになりました。その際の仲介役、つまり Jupyter から受け取ったスクリプトを該当するプログラム言語で実行するプログラムを「カーネル」と呼ぶようになります。

つまり、PHP を Jupyter で動かすためには PHP カーネルが必要になります。

しかし、残念なことに 2020/11/19 現在、PHP カーネルは IPHPJupyter-PHP の2つしか存在していません。

しかも IPHP は開発は中止、Jupyter-PHP を見ろと言いつつも、肝心の Jupyter-PHP は事実上放置されています。

? 【カーネルとは】

OS では Linux や Darwin のカーネルがあったり、機械学習ではカーネル関数があったりと、「カーネル」と言う言葉をあちこちで聞きます。ケンタの白髭のおっさんのカーネル(Colonel)ではなく、Kernel です。

Kernel とは「何かの中核や本質をなすもので、何かに覆われており見つけにくいもの」を言います。語源は中期オランダ語の「cornel」(粗食)から中高ドイツ語の「kornel」(穀物)を経て、corn の語源でもある古英語の「cyrnel」(種や、木を見た時の根っこなど。corn+-nel と同義の cyrnel)から来ています。日本語で「種」「核」「(牡蠣などの)身」的なニュアンスです。

例えば、OS の場合は、シェルなどを通してしかユーザーがアクセスできない OS の中核をなすプログラム群を「カーネル」と言います。機械学習では、一見すると A と B に直線で分けづらい(線引きしづらい) X,Y の2次元データに、もう 1 データ加えて 3 次元にすると面で線引きできる(現れてくる)データを「カーネル」と言います。

ディープラーニングで動画の顔画像の入れ替え実施編&そこから学ぶ機械学習の基礎の基礎 @ Qiita より)

Jupyter-PHP を PHP8 で動かす

公式の Docker の PHP イメージ(php:8.0-rc-alpine)は RaspberryPi Zero で動かなかったり、JIT がデフォルトで有効になっていなかったり、リージョンが日本になっていなかったり、mbstring が有効になっていなかったり、と、すぐに使うには色々と面倒臭いので設定済みのイメージを使います。

これをベースに Dockerfile を構築することにしました。最終的な成果物はこちらです。

さて、問題の PHP カーネルですがゼロから作るよりは既存のカーネルを修正して動くなら、それを使うのがベストと言うもの。IPHPJupyter-PHP で違いを調べてみました。

IPHPIPython が使えれば composer だけで使えていたらしいのですが不具合や限界も多く、Jupyter-PHP は、さらに PHP エクステンションの zmq を必要としています。

しかし、理論上 Jupyter フォーマットが変わらない限り、受け取った PHP コードをパースして PHP ランタイムに渡せば良いだけです。そこで、なんとか Jupyter-PHP を使えないものか奮闘してみました。

Jupyter-PHP のソースコード を見てみると composer.json があるので、composer require でインストールできるのかと思いきや、エラーでできません。

composer.json を覗くと "require":{"php":">=7.0"} と PHP のバージョンを制限していました。そこで --ignore-platform-reqs オプションを付けてみたところ、今度はエラーは出ないものの、やはり動きません。Jupyter にカーネルが現れないのです。どうやら、肝心のカーネルがしかるべきパスに設置されないようです。

つまり、Jupyter-PHP のカーネルをスコープ(許された範囲)外に設置する必要があります。しかし、composer だけでは、どこのパスに Jupyter のカーネル設置先のディレクトリがあるか分からず、検索するにしても OS にも依存するため composer.json 単体では完結できないのです。

そこで、次に README にあるようにインストーラーを実行してみましたが、やはりエラーでインストールできませんでした。

インストーラーの中身を確認してみると、カーネルのインストール先ディレクトリを検索して見つけたのち、内部で composer コマンドを呼び出して composer install していました。

この時に --ignore-platform-reqs オプションが指定されていないため「PHP8 ではバージョンが合わない」とエラーが出ていたのです。

そのため、Jupyter-PHP を PHP8 で動かすためには、主に2カ所の修正が必要です。

  • Jupyter-PHP-Installer の composer.json にある PHP バージョン指定の削除
  • Jupyter-PHP-Installer の src にあるインストール・スクリプト内に --ignore-platform-reqs を加え、依存パッケージの PHP バージョンを無視させる

そして、これらの修正を行うパッチを作成しました。Jupyter-PHP-Installer のリポジトリをクローンかダウンロードして適用します。

また、JupyterLab に表示されるカーネルの選択パネル(アイコン?)の表記が「PHP」だったので、わかりやすいように「PHP8」に変更するパッチも作成しました。

Jupyter-PHP-Installer でインストール後、PHP のカーネル kernel.json を検索して、パッチをあてて下さい。

PHP に関する注意点としては、composer のバージョンと、Jupyter-PHP が依存する PECL パッケージの zmq の PHP エクステンション(PHP 拡張機能)です。

  1. composer のバージョンを v1系にすること。最新の v2 系だと、追随していない Composer パッケージが多いため、エラーがでまくります。
  2. zmq はソースから PECL パッケージをインストールすること。pecl install zmq で入るコンパイル済みパッケージは古すぎるため動きません。また、zmq が依存する zip の PHP エクステンションも一緒に入れておく必要があります。

あとは、JupyterLab や Classic Jupyter Notebook を動かすのに必要な Python3 と、インストールに使う pip を入れます。

  1. Python3 を Alpine Docker に入れる
  2. pip を Alpine に入れる
  3. Jupyter 一式をインストールする

ここまで来て、やっと出来上がったのですが、1つだけ後悔していることがあります。いつものクセで Docker のベースイメージを Alpine にしたことです。

Docker の Alpine Linux ベースのイメージは軽量であるため、複数のコンテナを起動して利用するのには向いています。つまり、1コンテナに1つの機能(サービス)を動かして使う場合です。しかし、軽量である反面、Alpine はクセが強いため、1つのコンテナにアレコレとインストールするのには向いていません。

Alpine にも apk と言うパッケージマネージャーがあるのですが、お世辞にも Ubuntu や Debian などの apt ほど素直にインストールできません。そのため、PHP やら Python やら node.js を入れてアレコレとアプリをインストールするなら、サイズは巨大になるものの Ubuntu や Debian ベースの Docker イメージの方が慣れていない人には楽だったのかもしれないと感じました。

参考文献

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

PHP: SSL サイトで file_get_contents できないときは openssl の疎通確認をするとよい

Docker のコンテナ内から https の URL に対して、 curl できたのに PHP で file_get_contents できないパターンがありました。

「?」と思ったので検証してみます。

Windows 10 Pro を使用しています。

まとめ

SSL 化されたコンテンツに対する file_get_contents の可否と、 443 番ポートに関する openssl の疎通の可否が一致した。
このことから、 file_get_contentsopenssl には少なからず関連があり、 file_get_contents がうまくいかない場合に openssl での疎通確認を行うことは効果的である。

準備

まずはプロジェクトフォルダを好きな場所に作成します。

mkdir -p /path/to/dir
cd /path/to/dir

サーバーで使う証明書を用意する

今回は mkcert で証明書を作成します。
mkcert をインストールします。

私は Windows 使いなので chocolatey でインストールします。
PowerShell を管理者権限で開いて実行します。

cinst -y mkcert

初回のみ、次のコマンドを実行します。

mkcert -install

セキュリティに関する警告が出てきますが、「はい」をクリックします。

$ mkcert -install
Created a new local CA ?
The local CA is now installed in the system trust store! ⚡️

絵文字が出てくるのがかわいいですね :wink:

次に localhost に対する証明書を発行します。

mkdir ssl
cd ssl
mkcert localhost php-apache.com
$ mkcert localhost php-apache.com

Created a new certificate valid for the following names ?
 - "localhost"
 - "php-apache.com"

The certificate is at "./localhost+1.pem" and the key at "./localhost+1-key.pem" ✅

It will expire on 28 February 2023 ?

ルート証明書も後で使うので、コピーしておきます。
ルート証明書の場所は次のコマンドで確認できます。

mkcert -CAROOT

確認したら、そこから rootCA.pem をコピーします。

使用するファイルを用意する

今回用意するファイルは以下の通りです。

/path/to/dir
│  docker-compose.yml
│  Dockerfile
│
├─html
│      hello.php
│      index.php
│
└─ssl(用意済み)
        localhost+1-key.pem
        localhost+1.pem
        rootCA.pem
./docker-compose.yml

今回はコンテナ内でコマンドを打ちますので、ポートを開ける必要はありません。

./docker-compose.yml
version: '3'

services:
  php-apache:
    container_name: php-apache.com
    build: .
    volumes:
      - ./html:/var/www/html
./Dockerfile

この後の検証で何度か編集しますので、今は空のファイルを用意しておきます。

Dockerfile
./html/index.php
./html/index.php
<?php
$url = 'https://php-apache.com/hello.php';
echo file_get_contents($url);
./html/hello.php
./html/hello.php
<?php
header('Content-type: text/plain; charset=UTF-8');
echo 'hello';
exit;

これで準備は終わりです :thumbsup:

概要

ここからは実際の検証に入る前に、検証の概要について説明します。

今回はコンテナの状態を 4 つ作り、それぞれに対して 3 つの方法を試します。

コンテナの状態

  • 最初の状態: mkcert で作った証明書を配置して ssl を有効にしただけの状態
  • 状態 2: 最初の状態で、新たに CA 証明書を配置し、環境変数 CURL_CA_BUNDLE に設定した状態
  • 状態 3: 最初の状態で、 CA 証明書を配置し、 OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定した状態
  • 状態 4: 最初の状態で、 CA 証明書を配置し、環境変数 CURL_CA_BUNDLE に設定し、 OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定した状態

方法

  • PHP の file_get_contents 関数で https://php-apache.com/ を読み込む
winpty docker exec -it php-apache.com php -r "echo file_get_contents('https://php-apache.com/'), PHP_EOL;"

成功とする基準: PHP の Error や Warning が出ない

  • コンテナの中から curl コマンドで https://php-apache.com/ にリクエストする
winpty docker exec -it php-apache.com curl https://php-apache.com/

成功とする基準: curl に関する エラーメッセージが出ない

  • コンテナの中から openssl コマンドで php-apache.com:443 への疎通確認を行う
winpty docker exec -it php-apache.com openssl s_client -quiet -connect php-apache.com:443

成功とする基準: verify error が出ない

以上の方法を行う前には、 docker-compose up -d でコンテナを立ち上げ、行った後には docker-compose down -v でコンテナを終了します。

検証

最初の状態: mkcert で作った証明書を配置して ssl を有効にしただけの状態

Dockerfile を以下のように編集します。

Dockerfile
FROM php:apache-buster

RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini

# mkcert で作った証明書を配置する
COPY ./ssl/"localhost+1.pem" /etc/ssl/certs/ssl-cert-snakeoil.pem
COPY ./ssl/"localhost+1-key.pem" /etc/ssl/private/ssl-cert-snakeoil.key

# Enable SSL
RUN a2enmod ssl && a2ensite default-ssl
結果

file_get_contents, curl, openssl ともに失敗しました。

No. 方法 結果
1 file_get_contents 失敗
2 curl 失敗
3 openssl 失敗
詳細
  • file_get_contents

SSL に関するエラーが出ていることに注目します。

$ winpty docker exec -it php-apache.com php -r "echo file_get_contents('https://php-apache.com/'), PHP_EOL;"
PHP Warning:  file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed in Command line code on line 1

Warning: file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed in Command line code on line 1
PHP Warning:  file_get_contents(): Failed to enable crypto in Command line code on line 1

Warning: file_get_contents(): Failed to enable crypto in Command line code on line 1
PHP Warning:  file_get_contents(https://php-apache.com/): failed to open stream: operation failed in Command line code on line 1

Warning: file_get_contents(https://php-apache.com/): failed to open stream: operation failed in Command line code on line 1
  • curl
$ winpty docker exec -it php-apache.com curl https://php-apache.com/
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
  • openssl
$ winpty docker exec -it php-apache.com openssl s_client -quiet -connect php-apache.com:443
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify error:num=21:unable to verify the first certificate
verify return:1
...

状態 2: 最初の状態で、新たに CA 証明書を配置し、環境変数 CURL_CA_BUNDLE に設定した状態

参考: [curl] HTTPS通信できない (unable to get local issuer certificate) - noknow

Dockerfile は以下の通りです。

Dockerfile
FROM php:apache-buster

RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini

# mkcert で作った証明書を配置する
COPY ./ssl/"localhost+1.pem" /etc/ssl/certs/ssl-cert-snakeoil.pem
COPY ./ssl/"localhost+1-key.pem" /etc/ssl/private/ssl-cert-snakeoil.key

# CA 証明書を配置する
COPY ./ssl/rootCA.pem /etc/ssl/certs/rootCA.pem
ENV CURL_CA_BUNDLE=/etc/ssl/certs/rootCA.pem

# Enable SSL
RUN a2enmod ssl && a2ensite default-ssl
結果

curl が成功したのに対し、
file_get_contents と openssl は失敗しました。

いわゆる curl できたのに file_get_contents できないパターン ですね。

No. 方法 結果
1 file_get_contents 失敗
2 curl 成功
3 openssl 失敗
詳細
  • file_get_contents
$ winpty docker exec -it php-apache.com php -r "echo file_get_contents('https://php-apache.com/'), PHP_EOL;"
PHP Warning:  file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed in Command line code on line 1

Warning: file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed in Command line code on line 1
PHP Warning:  file_get_contents(): Failed to enable crypto in Command line code on line 1

Warning: file_get_contents(): Failed to enable crypto in Command line code on line 1
PHP Warning:  file_get_contents(https://php-apache.com/): failed to open stream: operation failed in Command line code on line 1

Warning: file_get_contents(https://php-apache.com/): failed to open stream: operation failed in Command line code on line 1
  • curl

PHP でエラーが起こっていますが、 curl に関してエラーが出ていない ので成功とします。

$ winpty docker exec -it php-apache.com curl https://php-apache.com/
<br />
<b>Warning</b>:  file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed in <b>/var/www/html/index.php</b> on line <b>3</b><br />
<br />
<b>Warning</b>:  file_get_contents(): Failed to enable crypto in <b>/var/www/html/index.php</b> on line <b>3</b><br />
<br />
<b>Warning</b>:  file_get_contents(https://php-apache.com/hello.php): failed to open stream: operation failed in <b>/var/www/html/index.php</b> on line <b>3</b><br />
  • openssl
$ winpty docker exec -it php-apache.com openssl s_client -quiet -connect php-apache.com:443
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify error:num=21:unable to verify the first certificate
verify return:1
...

状態 3: 最初の状態で、 CA 証明書を配置し、 OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定した状態

参考: [OpenSSL] [エラー] Verification error: unable to get local issuer certificate - noknow

Dockerfile は以下の通りです。

Dockerfile
FROM php:apache-buster

RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini

# mkcert で作った証明書を配置する
COPY ./ssl/"localhost+1.pem" /etc/ssl/certs/ssl-cert-snakeoil.pem
COPY ./ssl/"localhost+1-key.pem" /etc/ssl/private/ssl-cert-snakeoil.key

# OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定する
COPY ./ssl/rootCA.pem /etc/ssl/certs/rootCA.pem
RUN ln -s /etc/ssl/certs/rootCA.pem $(openssl version -d | cut -d' ' -f2 | sed 's/"//g')/cert.pem

# Enable SSL
RUN a2enmod ssl && a2ensite default-ssl
結果

file_get_contents と openssl が成功したのに対し、
curl は失敗しました。

No. 方法 結果
1 file_get_contents 成功
2 curl 失敗
3 openssl 成功
詳細
  • file_get_contents
$ winpty docker exec -it php-apache.com php -r "echo file_get_contents('https://php-apache.com/'), PHP_EOL;"
hello
  • curl
$ winpty docker exec -it php-apache.com curl https://php-apache.com/
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
  • openssl
$ winpty docker exec -it php-apache.com openssl s_client -quiet -connect php-apache.com:443
depth=1 O = mkcert development CA, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI), CN = mkcert MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify return:1
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify return:1
...

状態 4: 最初の状態で、 CA 証明書を配置し、環境変数 CURL_CA_BUNDLE に設定し、 OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定した状態

Dockerfile は以下の通りです。

Dockerfile
FROM php:apache-buster

RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini

# mkcert で作った証明書を配置する
COPY ./ssl/"localhost+1.pem" /etc/ssl/certs/ssl-cert-snakeoil.pem
COPY ./ssl/"localhost+1-key.pem" /etc/ssl/private/ssl-cert-snakeoil.key

# CA 証明書を配置する
COPY ./ssl/rootCA.pem /etc/ssl/certs/rootCA.pem
ENV CURL_CA_BUNDLE=/etc/ssl/certs/rootCA.pem

# OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定する
RUN ln -s /etc/ssl/certs/rootCA.pem $(openssl version -d | cut -d' ' -f2 | sed 's/"//g')/cert.pem

# Enable SSL
RUN a2enmod ssl && a2ensite default-ssl
結果

file_get_contents, curl, openssl ともに成功しました。

No. 方法 結果
1 file_get_contents 成功
2 curl 成功
3 openssl 成功
詳細
  • file_get_contents
$ winpty docker exec -it php-apache.com php -r "echo file_get_contents('https://php-apache.com/'), PHP_EOL;"
hello
  • curl
$ winpty docker exec -it php-apache.com curl https://php-apache.com/
hello
  • openssl
$ winpty docker exec -it php-apache.com openssl s_client -quiet -connect php-apache.com:443
depth=1 O = mkcert development CA, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI), CN = mkcert MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify return:1
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify return:1
...

最終結果と考察

あらためてそれぞれの状態での結果を一つの表にまとめてみると、file_get_contents と openssl で結果が一致していることがわかります :bulb:

No. 方法 最初の状態 状態 2 状態 3 状態 4
1 file_get_contents 失敗 失敗 成功 成功
2 curl 失敗 成功 失敗 成功
3 openssl 失敗 失敗 成功 成功

OS や PHP, Apache の状態によっては結果が変わる可能性もあるため、一概に file_get_contents と openssl の疎通可否を必要十分条件ということはできませんが、少なくともある程度有益な情報が得られたとは思います :thinking:

もし、同じエラーに詰まった方がいらっしゃいましたら、参考にしていただけると幸いです :wink:

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

PHP 任意精度四則演算クラス

PHPには以前よりBCMathという任意精度数学関数がありますが、利用できるよう自由に設定することができないケースもあるかと思いますので、そういった場合の代用として作成しました。
BCMathほど多くのことはできませんが、とりあえず桁数制限のない小数を含む四則演算と剰余が使用できます。

スクリプト

decimalcalc.php
<?php
class DecimalCalc {
    // 内部保存値
    private $result;
    // 商の小数部の取得桁数デフォルト
    private $divDecimalLength = 20;
    // 商の小数部丸め時の切り捨てモードデフォルト
    private $divTruncate = 0;

    private $sl1;
    private $sl2;

    public function __construct($result = '0') {
        $this->result = $this->toNumericString($result);
        $this->sl1 = strlen(PHP_INT_MAX) - 4;
        if($this->sl1 < 1) $this->sl1 = 1;
        $this->sl2 = strlen((int)sqrt(PHP_INT_MAX)) - 4;
        if($this->sl2 < 1) $this->sl2 = 1;
    }

    // 内部保存値の設定
    public function init($num = '0') {
        $this->result = $this->toNumericString($num);
        return $this;
    }

    // 加算
    public function add($num = '0') {
        $a = $this->result;
        $b = $this->toNumericString($num);
        $p = max($this->decimalLength($a), $this->decimalLength($b));

        // bが0ならaをそのまま返す
        if($b === '0') return $a;

        // 減算引き渡し用
        $a_ = $a;
        $b_ = $b;

        $a = $this->decimalShift($a, $p);
        $b = $this->decimalShift($b, $p);

        $aSign = $bSign = '';
        if($a[0] === '-') {
            $a = substr($a, 1);
            $aSign = '-';
        }
        if($b[0] === '-') {
            $b = substr($b, 1);
            $bSign = '-';
        }

        // bのみマイナスの場合はbのマイナス符号を削除して減算へ
        if($aSign === '' && $bSign === '-') {
            return $this->sub(substr($b_, 1));
        }
        // aのみマイナスの場合はbにマイナス符号を付加して減算へ
        if($aSign === '-' && $bSign === '') {
            return $this->sub("-$b_");
        }

        $sLength = $this->sl1;
        $sNumber = 10 ** $sLength;
        $sFormat = "%0{$sLength}s";

        $ra = [];
        $l = strlen($a) / $sLength;
        for($i = 0; $i < $l; ++$i) {
            $ra[] = (int)substr($a, -$sLength);
            $a = substr($a, 0, -$sLength);
        }
        $rb = [];
        $l = strlen($b) / $sLength;
        for($i = 0; $i < $l; ++$i) {
            $rb[] = (int)substr($b, -$sLength);
            $b = substr($b, 0, -$sLength);
        }

        $l = max(count($ra), count($rb));
        // 加算演算
        for($i = 0; $i < $l; ++$i) {
            if(!isset($ra[$i])) $ra[$i] = 0;
            if(!isset($rb[$i])) $rb[$i] = 0;
            $ra[$i] += $rb[$i];
            if($ra[$i] >= $sNumber) {
                if(!isset($ra[$i + 1])) $ra[$i + 1] = 0;
                $ra[$i + 1] += (int)($ra[$i] / $sNumber);
                $ra[$i] %= $sNumber;
            }
        }

        for($i = 0; $i < count($ra) - 1; ++$i) $ra[$i] = sprintf($sFormat, $ra[$i]);

        $result = implode('', array_reverse($ra));
        $result = $this->decimalShift($result, -$p);
        if($aSign === '-' && $bSign === '-') $result = '-'. $result;

        return $this->result = $result;
    }

    // 減算
    public function sub($num = '0') {
        $a = $this->result;
        $b = $this->toNumericString($num);
        $p = max($this->decimalLength($a), $this->decimalLength($b));

        // bが0ならaをそのまま返す
        if($b === '0') return $a;

        // 加算引き渡し用
        $a_ = $a;
        $b_ = $b;

        $a = $this->decimalShift($a, $p);
        $b = $this->decimalShift($b, $p);

        $aSign = $bSign = '';
        if($a[0] === '-') {
            $a = substr($a, 1);
            $aSign = '-';
        }
        if($b[0] === '-') {
            $b = substr($b, 1);
            $bSign = '-';
        }

        // bのみマイナスの場合はbのマイナス符号を削除して加算へ
        if($aSign === '' && $bSign === '-') {
            return $this->add(substr($b_, 1));
        }
        // aのみマイナスの場合はbにマイナス符号を付加して加算へ
        if($aSign === '-' && $bSign === '') {
            return $this->add("-$b_");
        }

        // 戻り値用符号
        $resultSign = '';

        // 比較用文字列生成
        $mLength = max(strlen($a), strlen($b));
        $f = "%0{$mLength}s";
        $aCmp = sprintf($f, $a);
        $bCmp = sprintf($f, $b);

        // ab入れ替えの場合の戻り値用符号反転
        if($aSign === '' && $aCmp < $bCmp || $aSign === '-' && $aCmp > $bCmp) $resultSign = '-';

        // bのほうが大きければaとbを入れ替え
        if($aCmp < $bCmp) [$a, $b] = [$b, $a];

        $sLength = $this->sl1;
        $sNumber = 10 ** $sLength;
        $sFormat = "%0{$sLength}s";

        $ra = [];
        $l = strlen($a) / $sLength;
        for($i = 0; $i < $l; ++$i) {
            $ra[] = (int)substr($a, -$sLength);
            $a = substr($a, 0, -$sLength);
        }
        $rb = [];
        $l = strlen($b) / $sLength;
        for($i = 0; $i < $l; ++$i) {
            $rb[] = (int)substr($b, -$sLength);
            $b = substr($b, 0, -$sLength);
        }

        $l = max(count($ra), count($rb));
        // 減算演算
        for($i = 0; $i < $l; ++$i) {
            if(!isset($ra[$i])) $ra[$i] = 0;
            if(!isset($rb[$i])) $rb[$i] = 0;
            $ra[$i] -= $rb[$i];
            if($ra[$i] < 0) {
                $ra[$i] += $sNumber;
                if(isset($ra[$i + 1])) --$ra[$i + 1];
            }
        }

        for($i = 0; $i < count($ra) - 1; ++$i) $ra[$i] = sprintf($sFormat, $ra[$i]);

        $result = implode('', array_reverse($ra));
        $result = $this->decimalShift($result, -$p);
        $result = preg_replace('/^0+/', '', $result);
        $result = preg_replace('/^$/', '0', $result);
        $result = preg_replace('/^\./', '0.', $result);

        return $this->result = $resultSign. $result;
    }

    // 乗算
    public function mul($num = '0') {
        $a = $this->result;
        $b = $this->toNumericString($num);
        $p = $this->decimalLength($a) + $this->decimalLength($b);

        // bが0なら0を返す
        if($b === '0') return $this->result = '0';

        $a = str_replace('.', '', $a);
        $b = str_replace('.', '', $b);

        $aSign = $bSign = '';
        if($a[0] === '-') {
            $a = substr($a, 1);
            $aSign = '-';
        }
        if($b[0] === '-') {
            $b = substr($b, 1);
            $bSign = '-';
        }

        $sLength = $this->sl2;
        $sNumber = 10 ** $sLength;
        $sFormat = "%0{$sLength}s";

        $ra = [];
        $l = strlen($a) / $sLength;
        for($i = 0; $i < $l; ++$i) {
            $ra[] = (int)substr($a, -$sLength);
            $a = substr($a, 0, -$sLength);
        }
        $rb = [];
        $l = strlen($b) / $sLength;
        for($i = 0; $i < $l; ++$i) {
            $rb[] = (int)substr($b, -$sLength);
            $b = substr($b, 0, -$sLength);
        }

        $c = [];
        // 乗算演算
        for($i = 0; $i < count($ra); ++$i) {
            for($j = 0; $j < count($rb); ++$j) {
                $dp = $i + $j;
                if(!isset($c[$dp])) $c[$dp] = 0;
                $c[$dp] += $ra[$i] * $rb[$j];
                if($c[$dp] >= $sNumber) {
                    if(!isset($c[$dp + 1])) $c[$dp + 1] = 0;
                    $c[$dp + 1] += (int)($c[$dp] / $sNumber);
                    $c[$dp] %= $sNumber;
                }
            }
        }
        for($i = 0; $i < count($c) - 1; ++$i) $c[$i] = sprintf($sFormat, $c[$i]);

        $result = implode('', array_reverse($c));
        $result = $this->decimalShift($result, -$p);
        $result = preg_replace('/^0+$/', '0', $result);

        $resultSign = ($aSign !== $bSign) ? '-' : '';
        return $this->result = $resultSign.$result;
    }

    // 除算
    public function div($num = '0', $m = null, $truncate = null) {
        if(is_null($truncate)) $truncate = $this->divTruncate;
        if(is_null($m)) $m = $this->divDecimalLength;
        if($m < 0) $m = 0;
        $m = (int)$m;

        $a = $this->result;
        $b = $this->toNumericString($num);
        $p = max($this->decimalLength($a), $this->decimalLength($b));
        if($b === '0') {
            try {
                throw new Exception(__METHOD__. ': Division by zero');
            } catch (Exception $e) {
                return $e->getmessage();
            }
        }

        $a = $this->decimalShift($a, $p + $m + 2);
        $b = $this->decimalShift($b, $p);

        $aSign = $bSign = '';
        if($a[0] === '-') {
            $a = substr($a, 1);
            $aSign = '-';
        }
        if($b[0] === '-') {
            $b = substr($b, 1);
            $bSign = '-';
        }

        $b_ = $b;
        $b = preg_replace('/^0+/', '', $b);
        $ol = strlen($b_) - strlen($b);
        if($ol) $a .= str_repeat('0', $ol);

        $k = strlen($b);
        $a_ = substr($a, 0, strlen($b));

        $c = [0];
        $sp = 0;
        $f = '%0'. (strlen($b) + 1). 's';
        $spb = sprintf($f, $b);
        // 除算演算
        do {
            if(!isset($c[$sp])) $c[$sp] = 0;
            while(sprintf($f, $a_) >= $spb) {
                $a_ = $this->init($a_)->sub($b);
                if(++$c[$sp] > 9) break;
            }
            ++$sp;
            $a_ .= isset($a[$k]) ? $a[$k] : '0';
        } while(++$k < strlen($a));

        $c = $this->decimalShift(implode('', $c), -($m + 1 + $ol));

        $nd = explode('.', $c);
        $n = $nd[0];
        $d = (isset($nd[1]) ? $nd[1] : '0'). str_repeat('0', $m);
        $n = preg_replace('/^0+/', '', $n);
        $n = preg_replace('/^$/', '0', $n);

        $dd = $d[$m];
        $nr = str_split($n);
        $dr = str_split(substr($d, 0, $m));

        if($truncate == 0) {
            if($dd >= 5) {
                if($m > 0) ++$dr[count($dr) - 1];
                else ++$nr[count($nr) - 1];
            }
            for($i = count($dr) - 1; $i > 0; --$i) {
                if($dr[$i] > 9) {
                    ++$dr[$i - 1];
                    $dr[$i] %= 10;
                }
            }
            if($dr[0] > 9) {
                $dr[0] %= 10;
                ++$nr[count($nr) - 1];
            }
            for($i = count($nr) - 1; $i > 0; --$i) {
                if($nr[$i] > 9) {
                    ++$nr[$i - 1];
                    $nr[$i] %= 10;
                }
            }
        }

        $n = preg_replace('/^0+/', '', implode($nr));
        if($n === '') $n = '0';
        $d = preg_replace('/0+$/', '', implode($dr));

        $resultSign = $aSign !== $bSign ? '-' : '';
        if(preg_match('/^[0]+$/', "$n$d")) $resultSign = '';
        $result = $resultSign. $n. ($d !== '' ? ".$d" : '');

        return $this->result = $result;
    }

    // 剰余
    public function mod($num = '0') {
        $a = $this->result;
        $b = $this->toNumericString($num);
        if($b === '0') {
            try {
                throw new Exception(__METHOD__. ': Division by zero');
            } catch (Exception $e) {
                return $e->getmessage();
            }
        }
        $q = $this->div($b, 0, 1);
        $this->init($b);
        $p = $this->mul($q);
        $this->init($a);
        $d = $this->sub($p);
        return $this->result = $d;
    }

    private function toNumericString($s) {
        $s = (is_string($s) || is_numeric($s)) ? "$s" : '0';
        if(!is_numeric($s)) {
            $s = '0';
        }
        // 指数表記パース
        $s = preg_replace_callback('/([\d.]*)([eE])([-+]?\d+)/', function($m){
            if($m[3] == 0) {
                return $m[1];
            }
            elseif(!preg_match('/[1-9]/', $m[1])) {
                return '0';
            }
            elseif($m[3] > 0) {
                $d = explode('.', $m[1]);
                if(!isset($d[1])) $d[1] = str_repeat('0', $m[3]);
                else $d[1] .= str_repeat('0', $m[3]);
                $d[0] .= substr($d[1], 0, $m[3]);
                $d[1] = preg_replace('/0+$/', '', substr($d[1], $m[3]));
                return $d[0]. ($d[1] !== '' ? '.'. $d[1] : '');
            }
            $d = explode('.', $m[1]);
            if(!isset($d[1])) $d[1] = '0';
            $d[0] = str_repeat('0', abs($m[3])). $d[0];
            $d[1] = substr($d[0], $m[3]). $d[1];
            $d[0] = substr($d[0], 0, $m[3]);
            return $d[0]. '.'. $d[1];
        }, $s);
        // +符号除去
        $s = str_replace('+', '', $s);
        // 0調整
        $s = preg_replace('/^(-)?0+/', '${1}', $s);
        $s = preg_replace('/^(-)?\./', '${1}0.', $s);
        $s = preg_replace('/^[-0.]+$/', '0', $s);
        $s = preg_replace('/(\.\d*?)0*$/', '$1', $s);
        $s = preg_replace('/\.$/', '', $s);
        $s = preg_replace('/^$/', '0', $s);
        return $s;
    }

    private function decimalLength($s) {
        $tmp = explode('.', $s);
        return isset($tmp[1]) ? strlen($tmp[1]) : 0;
    }
    private function decimalShift($s, $p) {
        if($p === 0) return $s;
        $tmp = explode('.', $s);
        if(!isset($tmp[1])) $tmp[1] = '';
        if($p > 0) {
            $tmp[1] .= str_repeat('0', $p - strlen($tmp[1]));
            $tmp[0] = preg_replace('/^(-)?0+/', '$1', $tmp[0]);
            return implode('', $tmp);
        }
        $sign = '';
        if($tmp[0][0] === '-'){
            $sign = '-';
            $tmp[0] = substr($tmp[0], 1);
        }
        if(strlen($tmp[0]) < abs($p)) {
            $tmp[0] = str_repeat('0', abs($p) - strlen($tmp[0])). $tmp[0];
        }
        $tmp[1] = substr($tmp[0], $p). $tmp[1];
        $tmp[0] = substr($tmp[0], 0, $p);
        $tmp[0] = preg_replace('/^0+/', '', $tmp[0]);
        if($tmp[0] === '') $tmp[0] = '0';
        $tmp[1] = preg_replace('/0+$/', '', $tmp[1]);
        return $sign. ($tmp[1] !== '' ? implode('.', $tmp) : $tmp[0]);
    }
    // 商の小数部の取得桁数設定
    public function setDivDecimalLength($num = 20) {
        if($num < 0) $num = 0;
        $num = (int)$num;
        $this->divDecimalLength = $num;
        return $this;
    }
    // 商の小数部丸め時の切り捨てモード設定
    public function setDivTruncate($num = 0) {
        if($num !== 0) $num = 1;
        $this->divTruncate = $num;
        return $this;
    }
}

使用例

インスタンス生成

new DecimalCalc([$num]);

各演算の元となる内部値を初期値として設定できます。
数値でも指定できますが、値によっては渡す時点で精度が失われている場合もあるので、文字列での指定推奨です。
省略時のデフォルト値は'0'です。

example
$d = new DecimalCalc('123456789.012');
$d = new DecimalCalc();

メソッド

加算

add($num)

内部値に対して加算を行なった結果を返します。
内部値は加算後の値に更新されます。

example
$d = new DecimalCalc('123456789.0123');
echo $d->add('4567.8901234567');
// 123461356.9024234567

減算

sub($num)

内部値に対して減算を行なった結果を返します。
内部値は減算後の値に更新されます。

example
$d = new DecimalCalc('123456789.0123');
echo $d->sub('4567.8901234567');
// 123452221.1221765433

乗算

mul($num)

内部値に対して乗算を行なった結果を返します。
内部値は乗算後の値に更新されます。

example
$d = new DecimalCalc('123456789.0123');
echo $d->mul('4567.8901234567');
// 563937047202.96281105481741

除算

div($num [,$length [,$truncateMode]])

内部値に対して除算を行なった結果を返します。
内部値は除算後の値に更新されます。
$lengthは小数を第何位まで取得するかを指定します。
省略時のデフォルトは20です。
$truncateModeは小数部の捨てられる部分の丸めモードを指定します。
省略時のデフォルトは0(丸め)、0以外を指定で切り捨てになります。

example
$a = '123456789.0123';
$b = '4567.8901234567';
$d = new DecimalCalc($a);
echo $d->div($b);       // 27027.09252535073003630397
$d = new DecimalCalc($a);
echo $d->div($b, 7);    // 27027.0925254
$d = new DecimalCalc($a);
echo $d->div($b, 7, 1); // 27027.0925253

剰余

mod($num)

内部値に対して除算を行ない剰余を取得します。
内部値は剰余で更新されます。

example
$d = new DecimalCalc('123456789.0123');
echo $d->mod('4567.8901234567');
// 422.6456357691

内部値設定

init($num)

内部値を更新します。

example
$d = new DecimalCalc();
echo $d->init('123456789.0123')->add('4567.8901234567'); // 123461356.9024234567
echo $d->init('1111.1111')->add('4567.8901234567'); // 5679.0012234567

商の小数取得桁数設定

setDivDecimalLength($divDecimalLength)

除算時の商の小数部の取得桁数のデフォルト値を設定します。
divメソッドに第2引数を指定している場合はそちらが優先されます。

example
$d = new DecimalCalc();
$d->setDivDecimalLength(0);
echo $d->init('1234')->div('567'); // 2
echo $d->init('1234')->div('567', 5); // 2.17637

商の小数丸めモード設定

setDivTruncate($divTruncate)

除算時の商の小数部の丸めモードデフォルト値を設定します。
divメソッドに第3引数を指定している場合はそちらが優先されます。

example
$d = new DecimalCalc();
$d->setDivTruncate(1);
echo $d->init('1234')->div('567', 5);    // 2.17636
echo $d->init('1234')->div('567', 5, 0); // 2.17637

関数風に使うラッパーのサンプル

wrapper.php
<?php
// wrapper sample
require_once('./decimalcalc.php');

function add($a, $b) {
    return (new DecimalCalc($a))->add($b);
}

function sub($a, $b) {
    return (new DecimalCalc($a))->sub($b);
}

function mul($a, $b) {
    return (new DecimalCalc($a))->mul($b);
}

function div($a, $b, $int = false) {
    return (new DecimalCalc($a))
        ->div($b, $int ? 0 : null, $int ? 1 : null);
}

function mod($a, $b) {
    return (new DecimalCalc($a))->mod($b);
}

$a = '123456789.0123456';
$b = '456789.012345678';
echo add($a, $b), "\n"; // 123913578.024691278
echo sub($a, $b), "\n"; // 122999999.999999922
echo mul($a, $b), "\n"; // 56393704720318.0983387568023168
echo div($a, $b), "\n"; // 270.27092525360239422787
echo div($a, $b, true), "\n"; // 270
echo mod($a, $b), "\n"; // 123755.67901254

演算結果の信頼性について

ランダムに設定したAとBの値で当スクリプトとBCMathとで演算を行い結果を比較、という内容で数十万回ほど確認してみたところ結果に差異は出ませんでしたが、それでも絶対に計算ミスが出ないということを保証するものではありませんのでご了承ください。

検証スクリプト(参考)

実行にはBCMathが必要です。
実行中はそこそこ負荷がかかりますのでご注意ください。

test.php
<?php
require_once('./decimalcalc.php');

$d = new DecimalCalc();
$scale = 100;
bcscale($scale);

$v = false; // true:実行内容表示 / false:進捗のみ

for($i = 0; $i < 100000; ++$i) {
    if(!$v) echo "$i\r";
    else echo str_repeat('#', 64), " $i\n\n";

    $a = mkRand();
    $b = mkRand();
    if($v) echo "A: $a\nB: $b\n\n";

    $r1 = $d->init($a)->add($b);
    $r2 = preg_replace('/\.$/', '', preg_replace('/0+$/', '', bcadd($a, $b)));
    test($v, 'add', $a, $b, $r1, $r2);

    $r1 = $d->init($a)->sub($b);
    $r2 = preg_replace('/\.$/', '', preg_replace('/0+$/', '', bcsub($a, $b)));
    test($v, 'sub', $a, $b, $r1, $r2);

    $r1 = $d->init($a)->mul($b);
    $r2 = preg_replace('/\.$/', '', preg_replace('/0+$/', '', bcmul($a, $b)));
    test($v, 'mul', $a, $b, $r1, $r2);

    $r1 = $d->init($a)->div($b, $scale, 1);
    $r2 = preg_replace('/\.$/', '', preg_replace('/0+$/', '', bcdiv($a, $b)));
    test($v, 'div', $a, $b, $r1, $r2);

    $r1 = $d->init($a)->mod($b);
    $r2 = preg_replace('/\.$/', '', preg_replace('/0+$/', '', bcmod($a, $b, 50)));
    test($v, 'mod', $a, $b, $r1, $r2);
}
echo "\n";

function mkRand() {
    $n = [];
    $d = [];
    $r = mt_rand(0, 3);
    for($i = 0 ; $i < $r; ++$i)
        $n[] = mt_rand(0, 10) ? mt_rand() : (mt_rand(0, 1) ? '9999999999999999' : '0000000000000000');
    $r = mt_rand(0, 3);
    for($i = 0 ; $i < $r; ++$i)
        $d[] = mt_rand(0, 10) ? mt_rand() : (mt_rand(0, 1) ? '9999999999999999' : '0000000000000000');
    $n = implode($n);
    $d = implode($d);
    if($n === '') $n = '0';
    if($d === '') $d = '0';
    if(preg_match('/^0+$/', "$n$d")) $n = '1';
    if(mt_rand(0, 1)) $n = "-$n";
    return mt_rand(0, 1) ? "$n.$d" : "$n$d";
}

function test($v, $l, $a, $b, $r1, $r2) {
    if($v) {
        echo "> $l\n",
            $r1,"\n", $r2,"\n",
            $r1 === $r2 ? 'true' : '**** false ****', "\n\n";
    }
    else {
        if($r1 !== $r2) {
            echo "$l   \n A: $a\n B: $b\n R1: $r1\n R2: $r2\n\n";
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Slackで投稿内容やファイルを閲覧するページを自作したお話 ①導入編

背景

Slackは非常に便利なコミュケーションツールなのですが、無料枠で使っていると閲覧上限1万件&ストレージ制限5GBが大きな制約となります。
自分の所属する団体では150人とかで使うので、1万件の制限があると3ヶ月分ぐらいしか閲覧できません。
(課金しろという話ですが、スタンダードプランにしても10万/月以上かかるので、、)

解決策??

ワークスペースを複数で運用したり、ログを取得するGASスプリクトをコピペして運用したりしていました。

参考) コピペのみ。Slackメッセージログを自動で保存する方法

問題点

ワークスペースを複数で運用すると、メッセージの共有ができないなど、コミュケーションツールとしての良さが失われてしまいます。また、GASスプリクトはログを残しておくという点では良いですが、後で閲覧するには不適です。

解決策: ログの取得や閲覧ページを自作する!

既存の方法で解決しないなら自分で作ってしまえ!という発想です。
とりあえず、

  • ログの取得・保存
  • 見やすい閲覧ページ

を実装しようと計画しました。

仕様

使用言語

  • React6.14.6
  • PHP7.4.4
  • MySQL5.7

フロントにはReact、バックエンドはPHPで実装しました。

Slackからデータを取得

SlackAPIでAppを作成してデータを取得します。APIを用いて以下のデータを取得しています。

  • ユーザー
  • チャンネル
  • チャット
  • ファイル

これらのデータはPHPで取得した後、MySQLでDBに格納します。

詳しくは別記事で説明します。

取得したデータを閲覧

フロントは勉強も兼ねてReactで実装しています。

また、DBに格納されたデータをフロントで取得するエンドポイントはPHPで作成しています。

詳しくは別記事で説明します。

まとめ

PHP+SlackAPIでデータ取得、MySQLでデータ格納して、PHPでエンドポイント作成、閲覧ページはReact+axiosという感じです。

編集後記

このシステムを作るのに、SlackAPIのドキュメントを何周もしました。
わからないところは、Google翻訳に助けられながらもなんとか理解できました。

続きの記事

時間あるときに書いていくので、気長にお待ち下さい、、

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