20191203のPHPに関する記事は25件です。

Laravelマイグレーション

マイグレーションでできること

テーブル定義を管理する仕組みのこと。
マイグレーションファイルを作成し、それを実行することでファイル内で定義した内容をもとにデータベーステーブルを作成する。

参照記事
https://www.hypertextcandy.com/how-laravel-migration-works

マイグレーションファイルの作成

php artisan make:migration create_{テーブル名}_table

database/migrationディレクトリに年_月_日_時間_create_{テーブル名}_table.phpというファイルが作成される。
updownという2つのメソッドを持つクラスが存在する。

upメソッドの中にどういうテーブルにするかの詳細を書いていく。
*カラム名・型など

公式ページ参照
https://readouble.com/laravel/5.5/ja/migrations.html

downメソッドはロールバック(マイグレーションを元に戻す機能)の時に実行される。

マイグレーション実行

php artisan migrate

実行することによってデータベースに3つのテーブルが作成される。
①作成したファイルのテーブル
→ファイル内で定義したテーブルが作成される
②migrationsテーブル
→マイグレーションを実行する度に追加されたファイル名が保存されるテーブル
③password_resetテーブル

マイグレーションの変更

php artisan make:migration {マイグレーションファイル名} --table={テーブル名}

{マイグレーションファイル名}の部分はそのままクラス名になるためわかりやすい名前にする
*カラム追加の場合
→add_cloumn_name_users_table

現在あるマイグレーションファイルを編集するのではなく新たに変更を加える内容を記載したファイルを作成し実行する。
例えばカラム名を変更する際はrenameColumnメソッドを使用する。
おそらくそのまま使用しても使えないため変更追加用パッケージをインストールする必要がある。

composer require doctrine/dbal

これをインストールすればカラム名を変更することが可能となる。

ロールバック

php artisan migrate:rollback

downメソッドを実行する
データベースのmigrationsテーブルのバッチ単位で処理される。
ロールバックを行うことによってバージョン管理のようにマイグレーション内容を元に戻すことができる。

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

書かないに越したことはない

The most readable code is no code at all.

最も読みやすいコードは、無いコードだ。(訳しにくい。。)

Every line of code you write is a line that has to be tested and maintained.

コードを書いたらテストしないといけない。それにメンテしないといけない。

By reusing libraries or eliminating features, you can save time and keep your codebase lean and mean.

ライブラリを使うか、機能を削るかして、時間を節約し、コードベースを無駄のないものにできる。

全て、The Art of Readable Code(リーダブルコード)より。

ふむふむ。確かに。さて、

理念を実現するには仕組みが必要だ。

ということで、PHPのfunctionを学習するツールを作ってみた。このツールにできることは以下の通り。

  • クイズを出す。
  • 全てのクイズ(3000個超)に正解するまで延々出す。
  • 全部正解するとクリア。間違えると、始めからやり直し。
  • 一日程度アクセスしないと、始めからやり直し。(←習慣化してほしいという願いを込めて)
  • 回答すると、正誤に関わらずPHPマニュアルへのリンクを表示する。

それだけ。

(仮URL)http://ec2-18-212-83-165.compute-1.amazonaws.com/

技術要素

  • Yii(PHPフレームワーク)
  • Codeception
  • MySQL
  • Ubuntu
  • Docker
  • Docker-compose
  • Github Actions
  • AWS Codedeploy
  • AWS Codepipeline
  • AWS EC2とかVPCとか

まだテストコードほとんど書いてないとか、アクセス解析してないとか、冗長化してないとか、HTTPS非対応とか、そもそもドメインとってないとか、言い出せば切りがない程作り込みはやってない。

経緯

リーダブルコードにはlibraryって書いてあるけど、functionも再利用できる部品だし、まあいいかなと。で、functionを利用するためには、どんなfunctionがあるかをまず知らないといけない。

ただ、大量のfunctionをいっぺんに覚えるなんて無理。毎日少しづつなら希望があるかも。でも、マニュアルを淡々と一ページずつ読み進めながら覚えるなんて単純作業できない。やっぱ無理、全然無理。

そういえば、子供の時、英単語とか覚える時どうしてったっけ。友達と問題の出しっこしてたっけ。単純作業は無理だけど、クイズ形式ならなんとかやる気が保てたっけ。

でも、大人になるとそんな奇特な友達はもはや居ないなあ。

ないものは作るしかない。

以上

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

symfony/mailerで試行錯誤したこと

Symfony Advent Calendar 2019 5日目の記事です。
昨日は@77webさんSymfonyで再利用可能なバンドルのコントローラをテストする方法 でした!

はじめに

Symfony 4.4.0は11/21にリリースだったのですね。

私は、その翌日の、11/22からSymfonyを初めました。

LTSだと長くメンテされるし、安定しているだろうと考えて飛びつきましたが、Symfony/Mailerは4.3系から導入されたので、まだまだ成熟には遠く、またノウハウも蓄積されていません。更に検索結果も前任の「swiftmailer」しか出てこないので、試行錯誤を致しました。

参考にしていただければ幸いです。

sendmailで送信する方法

TL;DR

まず、はじめに困ったのがsendmailでの送信方法。
結論を先に書くと、DSNで以下の指定をすると送信ができます。

.env
MAILER_DSN=sendmail+smtp://default
または
MAILER_DSN=sendmail://default

ただし、後述しますが、少し困ったちゃんなのです。

sendmailのDSN調査

Transport Setup」でSMTPやサードパーティの「Amazon SES」、「Gmail」、「Sendgrid」などの設定方法は書いてありますが、「sendmail」はないのです。

未対応なのかな?とソースを確認すると、「SendmailTransportFactory.php」と「SendmailTransport.php」が存在します。

image.png

SendmailTransportFactory.phpの中にsendmailのスキーマの記載がありました!

SendmailTransportFactory.php
final class SendmailTransportFactory extends AbstractTransportFactory
{
~~~省略~~~

    protected function getSupportedSchemes(): array
    {
        return ['sendmail', 'sendmail+smtp'];
    }
}

dockerの開発環境で送信ができない。。。

これでsendmail用のDSNがわかったので、「sendmailでメール送信ができる!」とワクワクして、「Creating & Sending Messages」のサンプルコードを実行しました。

Connection to "process /usr/sbin/sendmail -bs" has been closed unexpectedly.

image.png

どうやらsendmail -bsでないと送信ができないようです。
確認した環境は、docker上のAlpine+ssmtpなのでsendmail -tには対応していますが、sendmail -bsには未対応なのです。

postfixを入れたコンテナ作るのも面倒ですしね。

レンタルサーバで確認 & 困ったことが。。。

面倒ですが、サンプルコードをさくらのレンタルサーバにアップして確認をしてみました。

きちんと送信ができて、以下のように受信できました。
image.png

メールの構造は以下のような感じです。

Delivered-To: fuga@gmail.com
Received: by 2002:a5d:841a:0:0:0:0:0 with SMTP id i26csp4330355ion;
        Mon, 25 Nov 2019 22:05:37 -0800 (PST)

~~~中略~~~

Return-Path: <hoge@example.com>
Received: from www1970.sakura.ne.jp (localhost [127.0.0.1]) by www1970.sakura.ne.jp (8.15.2/8.15.2) with ESMTP id xAQ65aic007417 for <fuga@gmail.com>; Tue, 26 Nov 2019 15:05:36 +0900 (JST) (envelope-from hoge@example.com)
Received: from [127.0.0.1] (hoge@localhost) by www1970.sakura.ne.jp (8.15.2/8.15.2/Submit) with SMTP id xAQ65aQW007416 for <fuga@gmail.com>; Tue, 26 Nov 2019 15:05:36 +0900 (JST) (envelope-from hoge@example.com)
X-Authentication-Warning: www1970.sakura.ne.jp: hoge owned process doing -bs
From: hoge@example.com
To: fuga@gmail.com
Subject: Time for Symfony Mailer!
Message-ID: <0ab330134ce12286539d0ae3112374a4@example.com>
MIME-Version: 1.0
Date: Tue, 26 Nov 2019 15:05:36 +0900
Content-Type: multipart/alternative; boundary="_=_symfony_1574748336_057222644d28500198a452dc1b84b1b1_=_"

--_=_symfony_1574748336_057222644d28500198a452dc1b84b1b1_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

Sending emails is fun again!
--_=_symfony_1574748336_057222644d28500198a452dc1b84b1b1_=_
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable

<p>See Twig integration for better HTML integration!</p>
--_=_symfony_1574748336_057222644d28500198a452dc1b84b1b1_=_--

よくよく見ると、ヘッダーに警告が追加されています。

X-Authentication-Warning: www1970.sakura.ne.jp: hoge owned process doing -bs

こちらをググると、
メールヘッダに X-Authentication-Warning: が付かないようにする」が詳しく紹介されています。

MTAの設定を変更するというのは、できれば避けたいところですね。。。

sendmail -t で送信する(ペンディング)

さきほどDSNを調査している時に、
SendmailTransport.php」で、sendmail -bssendmail -tに変更する記載があったことを思い出しました。

SendmailTransport.php
class SendmailTransport extends AbstractTransport
{
    private $command = '/usr/sbin/sendmail -bs';
    private $stream;
    private $transport;
    /**
     * Constructor.
     *
     * If using -t mode you are strongly advised to include -oi or -i in the flags.
     * For example: /usr/sbin/sendmail -oi -t
     * -f<sender> flag will be appended automatically if one is not present.
     *
     * The recommended mode is "-bs" since it is interactive and failure notifications are hence possible.
     */
    public function __construct(string $command = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
    {
        parent::__construct($dispatcher, $logger);
        if (null !== $command) {
            if (false === strpos($command, ' -bs') && false === strpos($command, ' -t')) {
                throw new \InvalidArgumentException(sprintf('Unsupported sendmail command flags "%s"; must be one of "-bs" or "-t" but can include additional flags.', $command));
            }
            $this->command = $command;
        }
        $this->stream = new ProcessStream();
        if (false !== strpos($this->command, ' -bs')) {
            $this->stream->setCommand($this->command);
            $this->transport = new SmtpTransport($this->stream, $dispatcher, $logger);
        }
    }

コンストラクタの第1引数の$commandに、'/usr/sbin/sendmail -oi -t'を指定できれば実現できそうです。

しかし、呼び出し元を見ると無情にもNULL指定でした><

SendmailTransportFactory.php
final class SendmailTransportFactory extends AbstractTransportFactory
{
    public function create(Dsn $dsn): TransportInterface
    {
        if ('sendmail+smtp' === $dsn->getScheme() || 'sendmail' === $dsn->getScheme()) {
   ここ!! =>   return new SendmailTransport(null, $this->dispatcher, $this->logger);
        }
~~省略~~
    }
}

まだ、完全に使い方を理解していないDIでなんとかできないかな?と調べてみたり、SlackのSymfony Devsのsupportで質問してみましたが、無理そうです。

提案されたのは、「自分でクラスを拡張したらよいよ!」ということでした。

image.png
https://symfony-devs.slack.com/archives/C3EQ7S3MJ/p1574840970312800?thread_ts=1574811879.302500&cid=C3EQ7S3MJ

最終手段で拡張しましょう。。。

後日、見つけましたが、senmail -tについては、Issueも上がっていました。Feature指定でした;;
=> Mailer component: change sendmail command

SMTPで配信する

SMTP配信なら使えるということで、MailDevに接続を試みます。

本家がDocker Hubで公開していますし、日本語メールに対応されたバージョンもあります。

docker composeで構成したサービス名がmaildevなので、それを指定します。

.env
MAILER_DSN=smtp://maildev

Creating & Sending Messages」のサンプルコードを実行して完了!と思ったら。。。

image.png

25ポートで接続をするはずのに、465ポートのSMTPSで接続しようとしています。。。
まだまだ、symfony/mailerには知らない秘密があるようです。

ポート番号を指定してみました。

.env
MAILER_DSN=smtp://maildev:25

今度は、25番ポートに接続をするのですが、SSL通信をしようとして失敗しているようです。

image.png

ErrorExceptionをよく見ると、EsmtpTransport.php(112)startTLSを実行しています。

SMTPSは、SSL通信をするために465ポートを用意する必要があります。
しかし、465ポートを用意できない場合があるため、startTLSは、通信をしているポートを利用して、平文から暗号化通信に切り替えるための仕組みのようです。
STARTTLS by ウィキペディア

該当のソースを確認をすると、startTLSを実行する条件がわかります。

EsmtpTransport.php(112)
    protected function doHeloCommand(): void
    {
           ~~中略~~
        /** @var SocketStream $stream */
        $stream = $this->getStream();
        if (!$stream->isTLS() && \defined('OPENSSL_VERSION_NUMBER') && \array_key_exists('STARTTLS', $capabilities)) {
            $this->executeCommand("STARTTLS\r\n", [220]);

           ~~中略~~
        }

まず、startTLSになるための1つ目の条件である!$stream->isTLS()というのは、465ポートを指定していない場合になります。

以下、EsmtpTransport.php(48)のコンストラクタの処理の抜粋になります。

EsmtpTransport.php(48)
    public function __construct(string $host = 'localhost', int $port = 0, bool $tls = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
    {

         ~~~中略~~~

        if (null === $tls) {
            if (465 === $port) {
                $tls = true;
            } else {
                $tls = \defined('OPENSSL_VERSION_NUMBER') && 0 === $port && 'localhost' !== $host;
            }
        }

        if (!$tls) {
            $stream->disableTls();
        }
        if (0 === $port) {
            $port = $tls ? 465 : 25;
        }

$tlsはデフォルトNULLなので、ポート番号が465の場合に$tlsがTrueになります。また、ポート番号が465でない場合は、52行目のelseに入りますが、ポート番号が0でlocalhost以外の場合は、$tlsがTrueになります。

話はかわりますが、はじめに、DSNでMAILER_DSN=smtp://maildevの場合は、465に接続されたのは、この判定のためですね。

さて、2回目のDSNでは25番ポートを指定しましたので$tlsはfalseになり、結果として、!$stream->isTLS()となります。

startTLSになる2つ目の条件は、OPENSSL_VERSION_NUMBERのバージョンです。こちら最新の環境なら問題になることはないと思います。

startTLSになる最後の判定は、\array_key_exists('STARTTLS', $capabilities)になります。

$capabilitiesは、EsmtpTransport.php(130)にありますが、通信対象のサーバ、今回はMailDevから送られてくるもののようです。

EsmtpTransport.php(130)
    private function getCapabilities(string $ehloResponse): array
    {
        $capabilities = [];
        $lines = explode("\r\n", trim($ehloResponse));
        array_shift($lines);
        foreach ($lines as $line) {
            if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) {
                $value = strtoupper(ltrim($matches[2], ' ='));
                $capabilities[strtoupper($matches[1])] = $value ? explode(' ', $value) : [];
            }
        }
        return $capabilities;
    }

MailDevのSMTPサーバは、NodeMailerを使っているので、startTLSには対応しているようなのですが、MailDevはISSUE(Support for TLS (via ngrok or somehow?) )もあがっているのですが、対応をしてないようです。

対応していないなら、対応していないで、startTLSが使えるなんてMailDevが言わなきゃいいのです。

NodeMailerのオプションを見ていくと、options.hideSTARTTLSというコマンドがありました。

image.png

MailDevの方にもオプションがありました!

image.png

docker-compose.ymlで、MailDevの起動オプションに、--hide-extensions STARTTLSを追加します。

  maildev:
    image: kanemu/maildev-with-iconv
    ports:
      - 8025:80
    command: bin/maildev -w 80 -s 25 --hide-extensions STARTTLS

これで「Creating & Sending Messages」のサンプルコードを実行したら、無事に送信ができました!

image.png

image.png

最後に

HTMLメールを送信したいのですが、テキストを配信するだけでいろいろと躓いてしまいました。

枯れている技術のswiftmailerに浮気しようか悩み中。

でも、sendgridとか拡張性もあるので、将来性を買って、もう少し使ってみます。

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

【Laravel】基本的なルーティングのまとめ

はじめに

現在Laravelについて学習しています。
その学習の履歴として今回はルーティングについてまとめました。
ざっくりとまとめているので、大枠を掴むためや復習のために見ていただけると幸いです。

ルーティングについて

今回はHTTPメソッドgetについてまとめます。
全部で9種類です。
ルーティングは下記の形式です。

Route::get('URL', '処理');

処理がクロージャとアクションの場合に分けて説明します
クロージャとはfunction(引数)のことで無名関数という意味です。
アクションとはMVCのコントローラに含まれるものです。
クロージャの場合は6種類で、アクションの場合は3種類です。

クロージャーの場合

処理がクロージャの場合について解説します

1. URLからテンプレートをレンダリング

URLからテンプレートにレンダリングする場合について解説します。

routes/web.php
Route::get('/', function () {
    return view('welcome');
});

/にアクセスすると
view関数によりresources/views/welcome.blade.phpをレンダリングする

2. URLからHTMLを直書き

レンダリングするだけでなくHTMLのソースコードを直書きすることもできます。

routes/web.php
Route::get('hello', function () {
    return '<html><body><h1>Hello</h1></body></html>';
});

/helloにアクセスすると
<html><body><h1>Hello</h1></body></html>を表示します

3. 返すHTMLを変数として扱う(ヒアドキュメント)

上記の直書きの場合、HTMLが長くなるとソースが見にくくなります。
変数に代入して、見やすくしましょう

routes/web.php
$html = <<<EOF
<html><body><h1>Hello</h1></body></html>
EOF;

Route::get(hello, function ()  use ($html){
    return $html;
});

/helloにアクセスすると、
$htmlを表示します。

ヒアドキュメント
<<<EOF
~~
EOF;

~~の部分を変数に代入できるようにするために、EOFで挟みます。

4. URLにパラメータを挿入する

URLにルートパラメータを記載する方法を説明します。

routes/web.php
Route::get('hello/{msg}', function ($msg) {
$html = <<<EOF
<html>
<body>
<h1>Hello</h1>
<p>{$msg}</p>
</body>
</html>
EOF;
    return $html;
});

ルートパラメータに$msgを指定します。

5. ルートパラメータを2種類にする場合

ルートパラメータを2種類にする場合について説明します。

routes/web.php
Route::get('hello/{msg}/{pass}', function ($msg,$pass) {
});

URLの部分にパラメータを記述し、処理の部分に引数にルートパラメータを設定しましょう。

6. パラメタの入力を任意にする場合

上記の記述方法だとルートパラメータが必須とになります。
そのため、任意のパラメータにする方法を説明します。

routes/web.php
Route::get('hello/{msg?}', function ($msg='no_message') {
});

注意する部分は二つです。
- URL部分のルートパラメータに?をつける。
- 処理の引数の部分に初期値を設定する。

コントローラの場合

まず、コントローラを作成

ターミナル
php artisan make::controller HelloController

次に、コントローラにアクションを追加する

app/Http/Controllers/HelloController.php
public function index(){
    Return <<<EOF
        <html>
        <body>
            <h1>Hello</h1>
            if (count($msg)>0){
                <p>{$msg}</p>
            }
        </body>
        </html>
        EOF;
}

1. ルーティングでアクションを指定する

処理をアクションに指定しましょう

routes/web.php
Route::get(hello, HelloController@index);

/helloにアクセスするとHelloControllerindexアクションに飛びます

2. ルートパラメータの利用

ルートパラメータを設定します。
設定のやり方は上で説明したものと同じです。
入力を任意にするために?をつけています。

routes/web.php
Route::get(hello/{msg?}, HelloController@index);
app/Http/Controllers/HelloController.php
public function index($id= noname, $pass=unknown){}

3. シングルアクションコントローラ

シングルアクションコントローラ(1つのアクションしかないコントローラ)の場合について説明します。

routes/web.php
Route::get{'hello', 'HelloController'}

上記の場合はアクション名の指定はいりません。

app/Http/Controllers/HelloController.php
public function __invoke() {}

コントローラが少しだけ特殊です。
__invoke()が必要です。

以上で基本的なルーティングの説明は終わります。

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

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

2019年phpカンファレンス③~改善失敗して学ぶ、
レガシープロダクトに立ち向かうチーム作り。~

2019年phpカンファレンスでの『改善失敗して学ぶ、
レガシープロダクトに立ち向かうチーム作り。』の講演について、思ったことをまとめてみました。

講義資料

https://speakerdeck.com/oogfranz/gai-shan-shi-bai-sitexue-bu-regasipurodakutonili-tixiang-kautimuzuo-ri

話を聞いてみて

組織づくりには土台の考え方を固めることが重要なのだと感じました。

失敗を恐れない、挑戦をする。

これは良く言う話ですが、それ以上に「改善する」というフェーズや考えを持てるかがすごく重要なのだと感じました。

真似してみたい施策

技術負債箱

あ!これ治したい!!
けど、、、今じゃない、、、、、、

っていう負債は結構多く見かけます。
今じゃないけど、「あれ、3時間ぐらい猶予出来たんじゃない?」って時に触って、ちょこっと直して、対応したいというものを準備しておくのは意外と重要かもしれません。

あと負債を洗い出しておけば、負債が再度発生することを抑制できるのではないか。
これって負債だよねという意識をみんなで持っておくと、次に同様の処理を書こうとしたときに、「こういう風にしたほうがいい」という意見が出てきて、負債を作らないということが出来そうだなと思いました。

スクラム開発での振り返り

何事においても振り返りはすごく重要だと思います。
ただスクラム開発をすると、すぐに次の要件が来て、次々と対応しないといけない。となりかねません。

そういう時こそ細かく振り返る癖付けは重要だと。

一回癖や習慣にしてしまえば、初めの時は大変でも、徐々にそのあたりも自然とできるようになっていくものだと(自分に言い聞かす)。

対立していた部署との対話

これはどこでも発生しうるし、結局は売り言葉に買い言葉なんだと思います。

「開発チームのもやもやしか」解決しないようなものだと相手は不快に思います。
みんなハッピーとなるような改善案を常に出して、それの伝え方や進め方も相手をリスペクトした進め方にしていけば、おのずと物事は進むんじゃないかなぁと思います。

意識の統一

バイブルを作ろう。

今回の講義だと「リーダブルコード」が挙げられていたが、一例に過ぎないのかなぁと思います。
ほかの本でもいいと思いますし、人でもいいかと思います。

ただみんなが一定の基準で議論ができる土壌を作ってあげると、それを元に会話が広がっていくため、かなり有意義に、かなり有効的に話が進んでいくのだと感じました。

重要なこと

改善を恐れない土壌づくり

これが本当に大切なのだと改めて感じました。

今回あげた「真似してみたい施策」ですが、これはあくまで、実際にやったこと。
それをやることが目的ではなく、土壌づくりを目的にしないと、最終的にプロジェクトを進められる組織は作れないのではないかと思います。

それぞれがそれぞれをリスペクトし、また本気で改善していきたいと思わないとなかなか進まないのではないか。
ただ改善はちょっとするだけで、大幅にコスト削減となり、それを次の開発リソースへつなげられたりします。

改善と失敗はなかなかきついものですが、それでもそれを続けること、それに対してどんどん挑戦していく環境こそが本当に大切なことです。

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

PHPUnitで特定のクラス群だけのテストカバレッジを算出する

この記事はうるる Advent Calendar 2019 3日目の記事です。

はじめに

今回はPHPUnitにおけるカバレッジを、特定のクラスにだけ絞って算出する方法を紹介いたします。

テストカバレッジは、一般的に高ければいいというものでもなければ、低くてもいいというものではありません。
そのサービスにおいて最適な品質保証として、どの程度テストを網羅していればいいかどうかは、サービスにより異なります。

もちろんカバレッジを100%にしてあるに越したことはないですが
開発コストなどを考えると、どうでもいい機能はテストを書きたくないし、その代わりに重要な機能に対してはちゃんとテストコードを書きたい
なんてこともあると思います。

この重要な機能に対してのみのカバレッジを計測したいという思いから
特定のクラス群のカバレッジを計測しましたので、その紹介をしたいと思います。

システム構成

今回使用するアプリケーションは
Laravel5.5を使用しています。
PHPは7.0、PHPUnitは6.5.14です。

通常のCoverage出力

PHPUnitによる一般的なカバレッジ出力は以下です。

$phpunit --coverage-html "./coverage" tests/

この結果から
./coverage が生成され、./coverage/index.html を開くとカバレッジが表示されます。
RyCp0.png
(これは適当に拾って来たサンプルです)

カバレッジレポートの出力

今度はカバレッジレポートも出力してみます。

$phpunit --coverage-html "./coverage" --coverage-clover "./clover.xml" tests/

こうすることで、clover.xmlが生成されます。
スクリーンショット 2019-12-03 14.35.27.png
今回はCircleCI上で実行していたので、パスにcircleciと書いてありますがあまり気にしないでください。

こちらのカバレッジレポートには、どのクラスがどれくらいの大きさで、どれくらいカバレッジしているかという数値が細かく入っています。

そこから一部抜粋し、計算してみます。

<file name="/home/circleci/app/app/Http/Controllers/Admin/UserController.php">
  <class name="App\Http\Controllers\Admin\UserController" namespace="App\Http\Controllers\Admin">
    <metrics complexity="7" methods="5" coveredmethods="4" conditionals="0" coveredconditionals="0" statements="25" coveredstatements="22" elements="30" coveredelements="26"/>
  </class>
  <line num="18" type="method" name="__construct" visibility="public" complexity="1" crap="1" count="1"/>
  <line num="20" type="stmt" count="1"/>
  ...略
  <line num="63" type="stmt" count="0"/>
  <metrics loc="65" ncloc="62" classes="1" methods="5" coveredmethods="4" conditionals="0" coveredconditionals="0" statements="27" coveredstatements="23" elements="32" coveredelements="27"/>
</file>

file > metricsのパラメーターを参照

metrics count covered coverage
methods 5 4 4/5 = 80%
statements(lines) 27 23 23/27 = 85.16%

つまり、このUserControllerのカバレッジはlineベースで85%ほどあるというわけです。

最初にHTMLで出力したものは、このカバレッジレポート値をいい感じにUIに変換したようなイメージで考えると良いでしょう。

特定クラス群のCoverage計算

さてここからが本題です。
PHPUnitでカバレッジを出力すると、必ず全体のLine数やメソッド数をベースにして計算してしまうため
本来押さえておくべきクラスやメソッドのみのカバレッジ(割合)を算出することができません。

そのため、phpを用いて上記のカバレッジレポート(clover.xml)から、本当に欲しい部分のみのカバレッジを算出できる機能を実装します。

クラスの指定

phpに定数配列で直接指定してしまいます。
ここでは、clover.xmlに出力されるnamespaceをそのまま指定してあげます。
(指定したクラスのみを取り出して計算するため、検索トリガーにnamespaceを使います)

const TARGET_CLASSES = [
    'App\Http\Controllers\UserController',
    'App\Http\Controllers\PurcahseController',
    'App\Http\Controllers\LoginController',
];

xmlの読み込み

$xml = simplexml_load_file('/clover.xml'); // 適切なパスを指定

集計

$coverages = [];
$totalCoverage = [
    'title' => '【Total】',
    'methods' => 0,
    'coveredmethods' => 0,
    'statements' => 0,
    'coveredstatements' => 0
];

foreach ($xml->project->package as $package) {
    foreach ($package->file as $file) {
        $class = (string)$file->class['name'];
        if (in_array($class, self::TARGET_CLASSES)) {
            // 設定したクラスに対して、カバレッジを取得する
            // クラスごとにメソッド数ライン数を配列に入れ込んでいく
            $coverages[$class] = [
                'title' => '【' . $class . '】',
                'methods' => (integer)$file->metrics['methods'],
                'coveredmethods' => (integer)$file->metrics['coveredmethods'],
                'statements' => (integer)$file->metrics['statements'],
                'coveredstatements' => (integer)$file->metrics['coveredstatements']
            ];
            // 各クラスのカバレッジ計測
            $coverages[$class]['MethodCoverage'] = round(($coverages[$class]['coveredmethods']/$coverages[$class]['methods'])*100, 2);
            $coverages[$class]['StatementsCoverage'] = round(($coverages[$class]['coveredstatements']/$coverages[$class]['statements'])*100, 2);

            // 全体のカバレッジ集計
            $totalCoverage['methods'] += $coverages[$class]['methods'];
            $totalCoverage['coveredmethods'] += $coverages[$class]['coveredmethods'];
            $totalCoverage['statements'] += $coverages[$class]['statements'];
            $totalCoverage['coveredstatements'] += $coverages[$class]['coveredstatements'];
        }
    }
}

// 各クラスの集計が終わったところでトータルのカバレッジを計算
$totalCoverage['MethodCoverage'] = round(($totalCoverage['coveredmethods']/$totalCoverage['methods'])*100, 2);
$totalCoverage['StatementsCoverage'] = round(($totalCoverage['coveredstatements']/$totalCoverage['statements'])*100, 2);
var_dump($totalCoverage);

array(7) {
  'title' => string(11) "【Total】"
  'methods' => int(95)
  'coveredmethods' => int(91)
  'statements' => int(1190)
  'coveredstatements' => int(1173)
  'MethodCoverage' => double(95.79)
  'StatementsCoverage' => double(98.57)
}

これで、本当に計測したいクラスだけを集めたカバレッジを計測することができました。

おまけ1:CircleCIからカバレッジレポートを取得

私が担当しているサービスでは、前提のテストカバレッジを毎晩夜中にCiecleCIで計測しています。
全体のカバレッジは、CircleCIで出力したhtmlを開くことで参照ができますが
上記のロジックを用いて、特定のクラス群のみのカバレッジを計測する方法をご紹介します。

方法はいたってシンプルで、最初に読み込むxmlの参照先を変えるだけです。

$url = 'https://circleci.com/api/v1.1/project/github/:username/:project/latest/artifacts';
$token = $this->circleCiToken;
$branch = 'develop';
$filter = 'completed';
$artifacts = $this->execCurlCommand($url . '?' . http_build_query(['circle-token' => $token, 'branch' => $branch, 'filter' => $filter]));
$clover = Arr::first($artifacts, function ($file) {
    return $file['path'] === 'build/logs/clover.xml';
});
$xml = simplexml_load_file($clover['url'].'?circle-token='.$this->circleCiToken);

こちらはCircleCIのAPIを参考に実装しています。
https://circleci.com/docs/api/#artifacts-of-the-latest-build

:vcs-type = github
:username/ = CircleCIのユーザーネーム
:project = repogitory名
を入れます。

APITokenは、ドキュメントを参考に生成します。
https://circleci.com/docs/api/#add-an-api-token

artifacts-of-the-latest-buildを実行するとArtifactの中身が全て返って来ます。

var_dump($artifacts);

array(902) {
  [0] =>
  array(4) {
    'path' =>
    string(17) "phpunit/junit.xml"
    'pretty_path' =>
    string(17) "phpunit/junit.xml"
    'node_index' =>
    int(0)
    'url' =>
    string(65) "https://1111-11111111-gh.circle-artifacts.com/0/phpunit/junit.xml"
  }
  [1] =>
  array(4) {
    'path' =>
    string(24) "schemaspy/anomalies.html"
    'pretty_path' =>
    string(24) "schemaspy/anomalies.html"
    'node_index' =>
    int(0)
    'url' =>
    string(72) "https://1111-11111111-gh.circle-artifacts.com/0/schemaspy/anomalies.html"
  }
...
array(4) {
  'path' =>
  string(21) "build/logs/clover.xml"
  'pretty_path' =>
  string(21) "build/logs/clover.xml"
  'node_index' =>
  int(0)
  'url' =>
  string(69) "https://1111-11111111-gh.circle-artifacts.com/0/build/logs/clover.xml"
}
以下省略

ここから、clover.xmlへアクセスするためのurlを取得することで、xmlを取得することが可能です。

おまけ2:Slackへカバレッジを通知

最後に計測したカバレッジをSlackへ通知します。
今回はLaravelを使ってい実装しているため、Slack通知自体はLaravelの基本機能を使用します。
https://readouble.com/laravel/5.5/ja/notifications.html

実際に通知を送るロジックは以下です。

// 各クラスのカバレッジを1つずつSlackへ通知する
collect($coverages)->each(function ($coverage) {
    $slackMessage = (new SlackMessage)
        ->from($this->name)
        ->to($this->channel);

    $this->setAtachmentType($slackMessage, $coverage['MethodCoverage']);
    $slackMessage->attachment(function ($attachment) {
        $attachment
            ->fallback($this->content['title'])
            ->title($this->content['title'])
            ->fields($this->content['message']);
    });
});

ここでのポイントは、setAtachmentTypeメソッドで、slackで表示されるメッセージに色をつけている部分です。

private function setAtachmentType(&$slack, $coverageRate)
{
    switch (true) {
        case ($coverageRate >= 80):
            return self::SUCCESS;
        case ($coverageRate >= 50):
            return self::WARNING;
        default:
            return self::ERROR;
    }

    switch ($this->type) {
        case self::ERROR:
            $slack->error();
            break;
        case self::WARNING:
            $slack->warning();
            break;
        default:
            $slack->success();
            break;
    }
}

カバレッジの数値を判定し、メッセージへ色をつけています。
0~49%なら赤
50~79%ならオレンジ
80~100%なら緑
がつくようにしています。
全部緑ならテンションが上がりますし、気づかぬうちにオレンジや赤になった時でも視覚的に気付きやすくしています。

実際のメッセージはこちらです。
スクリーンショット 2019-12-03 17.55.25.png
スクリーンショット 2019-12-03 17.57.00.png

※色分けをわかりやすくするために、数値は変えています。

まとめ

以上でCircleCIからカバレッジを読み取り、特定のクラス群のカバレッジを計算しSlackへ通知する一連の流れを説明しました。
実際のロジックは部分的に切り取って紹介していますので、実際に実装する際にはアプリケーションロジックに合わせて適宜修正してください。

さらに通知の自動化をさせるために、定期的にバッチなどでこの機能を実行させれば
勝手に計算してSlackで通知してくれるところまでできそうです。

また、カバレッジの推移も取りたい場合はどこかのDBなどに突っ込んだり
それが面倒であればGASを用いてスプレッドシートに突っ込んでもいいかもしれませんね。

ひとまずこれで重要な機能のカバレッジの見える化ができました!

あとがき

Advent Calendar 3日目でした。
明日4日目は Yuuki Noda さんによる記事を乞うご期待!
https://adventar.org/calendars/4548

参考

https://blog.leko.jp/post/how-to-parse-of-coverage-report-with-phpunit/

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

Laravelで弐寺のクリアランプマネージャーをつくる

Laravel #2 Advent Calendar 20195日目のの記事です。

概要・動機

beatmania IIDXというゲームが好きです(唐突)

既存のクリアランプマネージャもあるにはあるが、機能が多くて僕には使いこなせません。
(低難度とか普段やらんしフォルダ分けもいらん)

身の丈にあったツールを使いたいので、クリアランプマネージャー兼地力推定サービスを自分用に車輪の再開発します。

前提として、自分はMVCが何ぞやということはゆるふわ理解している程度で、フルスタックのwebフレームワークは初心者です。

できたアプリはこんなかんじ↓
スクリーンショット 2019-12-03 15.30.42.png
(開発期間:2日)

クリアできそうな楽曲も教えてくれます!!(目玉機能)

github

https://github.com/dhaiibfiukkiu/estimator

MVCそれぞれの概要

モデル

正直ココが全てを決める気がする。

アプリの動作としては、
1.indexページに全楽曲の表とクリアした難易度のラジオボタン,submitボタン
2.ログイン後、submitでそのuserとクリアした楽曲を紐付けたclearsデータを挿入。
3.clearsデータに基づいてラジオボタンをchecked。また、地力の推定も行う。

といったことを想定しているので、userテーブル、musicsテーブル、clearsテーブルがいるかなと思った。

本来userとmusicsは多対多の関係であるので、clearsがうまく中間テーブルとして作用してくれる。
つまり、clearsの複合主キーがu_idm_idであるから、新たに主キーidを作る必要は無いと感じた。

このせいで後に苦労する羽目になるのだが(後述)
空白の ERD.png

musicsテーブルのeからfcまではそれぞれのクリア難易度(地力値)です。

値はこちらからパクっ拝借してきました。

単純にページをコピペすると、それぞれの楽曲は改行(\n)で、それぞれの要素はタブ文字(\t)で区切られていたので、こちらをlaravelのシーディングにうまく合う形に加工します。

musicTableSeeder.php
<?php                                                                                                                                   

    use Illuminate\Database\Seeder;
    use App\Music;

    class musicTableSeeder extends Seeder
    {
       /**
        * Run the database seeds.
        *
        * @return void
        */
       public function run()
       {

           $param=[
               'name' => '#MAGiCVLGiRL_TRVP_B3VTZ',
               'e' => '-2.914433',
               'n' => '-2.038145',
               'h' => '-1.965019',
               'exh' => '3.882106',
               'fc' => '8.346122',
           ];
           $musics = new Music;
           $musics->fill($param)->save();
     //(以下略)

目標はこの形です。そのために以下のPythonスクリプトを書きました。

makelist.py
input='''(ここに楽曲のリスト)
'''
input=input.split('\n')

for line in input:
    tmp=line.split('\t')
    if tmp[1]=='Infinity':
        continue
    name=tmp[0].replace("'",r"\'")
    e=tmp[1]
    n=tmp[2]
    h=tmp[3]
    exh=tmp[4]
    fc=tmp[5]
    print('''
        $param=[
            'name' => '{}',
            'e' => '{}',
            'n' => '{}',
            'h' => '{}',
            'exh' => '{}',
            'fc' => '{}',
        ];
        $musics = new Music;
        $musics->fill($param)->save();
    '''.format(name,e,n,h,exh,fc))

あとはこのスクリプトの出力をmusicのseederスクリプトにぶち込んでphp artisan db:seedしてやるだけです。(勿論テーブルのマイグレーションをした後にです)

あと、appディレクトリ以下のMusic,Clearモデルには以下のように記述しておきます。

Music.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Music extends Model
{
    protected $table = 'musics';

    protected $guarded = [
        'm_id',
        'name',
        'e',
        'n',
        'h',
        'exh',
        'fc'
    ];
}
Clear.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Clear extends Model
{
    protected $table = 'clears';
    protected $primaryKey = 'id';

    //仕方ないのでidをサロゲート(代理)キーに

    protected function rules(){
        return [
            'id' => 'integer',
            'm_id' => 'integer',
            'u_id' => 'integer',
            'info' => 'integer'
        ];
    }

    protected $fillable=[
        'id',
        'm_id',
        'u_id',
        'info'
    ];
}

ここに各要素のバリデーションのルールやらレコードの更新設定とかをするわけですね。

トラブル

Clear.phpの中に//仕方ないのでidをサロゲートキーにというコメントがあります。
実は、はじめにClearモデルを作成した際はidカラムは存在せず、m_idとu_idの複合主キーだけでした。

しかし、いざレコードをsaveメソッドで保存しようとすると、エラー。

どうやら複合主キーを使うとsaveメソッドが使えなくなるらしい。なんやそれ。

まあ、メソッドをオーバーライドすればできないこともないらしい。メンドクセー

参考

https://qiita.com/wrbss/items/7245103a5fef88cbdde9
https://github.com/laravel/framework/issues/5517

仕方がないので代理キーとしてidを追加しました。

ビュー

laravelでの開発と銘打っているし見た目にはそこまで拘りません。

resources/viewsフォルダにlayoutsフォルダ、estimatorフォルダを作成し、
layoutsにはおおもとのレイアウト、estimatorではそれを継承した具体的なビューを管理します。

/layouts/estimator.blade.php
<html>
    <head>
        <title>@yield('title')</title>
        <style>
body {font-size:16pt; color:#708090; margin: 5px;}

h1.title{
 position: relative;
  padding: 0.2em 0.5em;
  background: -webkit-linear-gradient(to right, rgb(255, 124, 111), #ffc994);
  background: linear-gradient(to right, rgb(255, 124, 111), #ffc994);
  color: white;
  font-weight: lighter;
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.56);
}
#logout{font-size:20pt; text-align:right; color:#f6f6f6;
    margin:-20px 0px -30px 0px; letter-spacing:-4pt;}
ul{font-size:12pt;}
td{font-size:16pt;}
hr{margin:25px 100px; border-top: 1px dashed #ddd;}
.menutitle{font-size:14pt; font-weight:bold; margin: 0px;}
.content{margin:10px;}
.footer{text-align:right; font-size:10pt; margin:10px;
    border-bottom:solid 1px #ccc; color:#ccc;}
.blink {
  animation: blinkAnimeA 0.1s infinite alternate;
}
@keyframes blinkAnimeA{
   0% { background: #4dffff }
  95% { background: #ffff1a }
 100% { background: #ffff1a }
}
.highlighted{
color: #0000ff;
text-decoration: underline;
}
.blinkchr {
  animation: blinkAnimeB 0.6s infinite alternate;
}
@keyframes blinkAnimeB{
   0% { color: #ff0000 }
  97% { color: rgba(255, 255, 255, 0.99) }
 100% { color: rgba(255, 255, 255, 0.99) }
}
.fixed_btn
{
  font-size: 1.5em;
  width: 30%;
  height: 10%;
  position: fixed;
  bottom: 10px;
  right: 10px;
  padding: 6px 40px;

  display: inline-block;
  padding: 0.5em 1em;
  text-decoration: none;
  background: #668ad8;/*ボタン色*/
  color: #FFF;
  border-bottom: solid 4px #627295;
  border-radius: 3px;
}
.fixed_btn:active{
  /*ボタンを押したとき*/
  -webkit-transform: translateY(4px);
  transform: translateY(4px);/*下に動く*/
  border-bottom: none;/*線を消す*/
}
        </style>
    </head>
    <body>
        <h1 class='title'>@yield('title')</h1>
        <h2>@yield('loginfo')</h2>
        <hr size="1">
        <div class="content">
            @yield('content')
        </div>
        <div class="footer">
            @yield('footer')
        </div>
    </body>
</html>

ここにはめ込むビューが以下

estimator/index.blade.php
@extends('layouts.estimator')
@section('title','地力Estimator')
@section('loginfo')
    @php//@parent
    @endphp
@if(Auth::check())
<p>DJ NAME:{{$user->name}}</p>
<p id='logout'><a href='/logout'>ログアウト</a></p>
@else
    <p>ログインしていません(<a href='/login'>ログイン</a>|<a href='/register'>登録</a>)</p>
@endif
@endsection

@section('content')
<form action='/' method='post'>
<center>
<button type='submit' class='fixed_btn'>record</button>
<!--推定値-->
<h1 class='suiteichi'>
@if($jiriki==-100&&!Auth::check())
推定値を取得するためにはログインしてください
@elseif($jiriki==-100&&Auth::check())
推定値を取得するためには、チェックボックスにチェックを入れたあとに、右下のボタンを押下してください
@else
推定地力:{{$jiriki}}
@endif
</h1>
<table border = '2'>
<tr><th>曲名</th><th>NO PLAY</th><th>EASY</th><th>NORMAL</th><th>HARD</th><th>EX-HARD</th><th>FULLCOMBO</th></tr>
{{csrf_field()}}
<!--<input type='submit' value='Estimate'>-->
@foreach($musics as $item)

@if(!isset($clears))
@php
$check=-1
@endphp
@else
@php
$check=$clears->where('m_id',$item->m_id)->first()->info
@endphp
@endif

    <tr>
        <!--<td>{{$item->name}}</td>-->

        <td
        @if($check==1)
        bgcolor='#98fb98'
        @elseif($check==2)
        bgcolor='#87cefa'
        @elseif($check==3)
        bgcolor='#ff6347'
        @elseif($check==4)
        bgcolor='#ffff00'
        @elseif($check==5)
        class='blink'
        @endif
        >{{$item->name}}</td>

        <td bgcolor='#a9a9a9'>
        <label><input type="radio" name="{{$item->m_id}}" value=0
        checked
        >NULL</input></label>
        </td>

        <td bgcolor='#98fb98'>
        <label
        @if($item->e <= $jiriki)
        class=highlighted
        @endif
        ><input type="radio" name="{{$item->m_id}}" value=1
        @if($check==1)
        checked
        @endif
        >{{round($item->e,7)}}</input></label>
        </td>

        <td bgcolor='#87cefa'>
        <label
        @if($item->n <= $jiriki)
        class=highlighted
        @endif
        ><input type="radio" name="{{$item->m_id}}" value=2
        @if($check==2)
        checked
        @endif
        >{{round($item->n,7)}}</input></label>
        </td></label>

        <td bgcolor='#ff6347'>
        <label
        @if($item->h <= $jiriki)
        class=highlighted
        @endif
        ><input type="radio" name="{{$item->m_id}}" value=3
        @if($check==3)
        checked
        @endif
        >{{round($item->h,7)}}</input></label>
        </td>

        <td bgcolor='#ffff00'>
        <label
        @if($item->exh <= $jiriki)
        class=highlighted
        @endif
        ><input type="radio" name="{{$item->m_id}}" value=4
        @if($check==4)
        checked
        @endif
        >{{round($item->exh,7)}}</input></label>
        </td>

        <td class='blink'>
        <label
        @if($item->fc <= $jiriki)
        class=highlighted
        @endif
        ><input type="radio" name="{{$item->m_id}}" value=5
        @if($check==5)
        checked
        @endif
        >{{round($item->fc,7)}}</input></label>
        </td>

    </tr>
@endforeach
</table>
</center>
</form>
@endsection

@section('footer')
copyright 2019 Okada Hibiki
@endsection

今回は、主要なページが一つだけだったこともあり、ロジックをビュー側に少し任せすぎてしまったかなというのが反省点。

コントローラ

estimatorController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Music;
use App\Clear;
use Illuminate\Support\Facades\Auth;

class estimatorController extends Controller
{
    public function index(Request $request){
        $user = Auth::user();
        $musics = Music::all();

        if(Auth::check()){
            $clears = Clear::where('u_id',$user->id)->get();///getをかかないとダメ!!!
            if(Clear::where('m_id',1)->where('u_id',$user->id)->count()==0){
                foreach($musics as $music){
                    $clear = new Clear;
                    $clear->m_id = $music->m_id;
                    $clear->u_id = $user->id;
                    $clear->info = 0;
                    $clear->save();
                }
            }
        }
        else{
            $clears=null;
        }

        if($request->method()=='POST'){
            if(Auth::check()){
                foreach($musics as $music){
                    $clear = Clear::where('m_id',$music->m_id)->where('u_id',$user->id)->first();
                    $m_id = $music->m_id;
                    $clear->info = (int)$request->$m_id;
                    //$clear->timestamps = false;
                    $clear->save();
                }
            }
            else{
                //
            }
        }

        if(Auth::check()){
            $clears = Clear::where('u_id',$user->id)->get();
            $jiriki = array();
            foreach($clears as $clear){
                switch($clear->info){
                case 0:
                    break;
                case 1:
                    array_push($jiriki,$musics->where('m_id',$clear->m_id)->first()->e);
                    break;
                case 2:
                    array_push($jiriki,$musics->where('m_id',$clear->m_id)->first()->n);
                    break;
                case 3:
                    array_push($jiriki,$musics->where('m_id',$clear->m_id)->first()->h);
                    break;
                case 4:
                    array_push($jiriki,$musics->where('m_id',$clear->m_id)->first()->exh);
                    break;
                case 5:
                    array_push($jiriki,$musics->where('m_id',$clear->m_id)->first()->fc);
                    break;
                }
            }
        }
//ここに推定値の処理
        if(!isset($jiriki)){$jiriki=-100;}
        elseif(count($jiriki)==0){$jiriki=-100;}
        else{
            rsort($jiriki);
            $num=ceil(count($jiriki)*0.3);
            $sum=0;
            for($i=0;$i<$num;$i++){
                $sum+=$jiriki[$i];
            }
            $sum=$sum/$num;
            $jiriki=$sum;
        }

        $param = ['musics' => $musics,'user' => $user,'clears' => $clears,'jiriki' => $jiriki];
        return view('estimator.index',$param);
    }
    public function logout(){
        Auth::logout();
        return redirect()->action('estimatorController@index');
    }
}

注意点としては、モデルの利用の際にuseしなきゃいけないことくらいか。

処理の概要としては、ログインがあるかをチェックして、しているのであればデータベースをチェック。そのユーザのクリア情報があれば取得し、なければNO PLAYとしたレコードをclearsテーブルに挿入。

POSTメソッドでのアクセスであれば、それを元にクリア情報を更新。

さらに、最新のクリア情報から自力を計算します。

この地力の計算が困ったもので、それらしい確率分布を適用しても、地力低めなEASYクリアとかが増えると地力が下がる、という現象がどうしても起こってしまう。

ので、クリアした楽曲の地力上位3割の平均値を推定地力としています。こっちのほうがシンプルでいいし、さっきみたいな不具合も幾らか少なくなるので良い。

Auth

Laravelには標準で認証機能があります。
これがものすごく便利で、php artisan ui vue --authするだけでログイン機能が実装できます。
Laravelのバージョン5ではphp artisan make:authで(読んだ本はこっちだった)
6系ではそれが使えない点に注意してください。

今回のアプリケーションでは、トップページの閲覧にログインは必須ではないが、データの保存や推定値の表示にはログインが必須であるような実装にしました。

そして、標準のAuthを利用すると、/register,/login,/homeといったpathが用意されます。

初期状態だと、/registerで登録、/loginでログイン後、/homeにリダイレクトされる仕様だったのですが、これを/にリダイレクトするよう変更します。

この処理はAuth内のコントローラに記述されています。
/app/Http/Controllers/Auth/以下のRegisterController.php,LoginController.php内の
protected $redirectTo = '/home';
という記述を
protected $redirectTo = '/';
に変更すればOK。

ルーティング

routes/web.php
<?php

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

Route::get('/', 'estimatorController@index');
Route::post('/','estimatorController@index');
Route::get('/logout','estimatorController@logout');

Auth::routes();

Route::get('/home', 'HomeController@index')->name('home');

ルーティングとはいえどメインは/で動作するestimatorControllerだけなのでシンプルですね。

おわりに

フルスタックのフレームワークを使ったのは初めてですが、簡単に自分にとって実用的なアプリが作れました。

正直今回のアプリならSPAと相性が良さそうなので、気が向いたらそれもつくってみようかな(たぶんやらん)

参考文献

David Skler(2017)初めてのPHP
掌田津耶乃(2017)PHPフレームワーク Laravel入門

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

【CakePHP2】formタグのスコープ外からformの内容をcontrollerに送信

やりたいこと

formタグの外にあるボタンやリンクでもcontrollerに値を渡したい

やったこと

test.ctp
                            <?= $this->Form->create('HogeList', ['type' => 'GET']); ?>
                            <div class="searchInputArea" style="margin-bottom: 0px;margin-top: 0px;">
                                <table class="em9">
                                    <tr>
                                        <th>
                                            ID
                                        </th>
                                        <td>
                                            <?php
                                                echo $this->Form->input('id', [
                                                    'type'     => 'text',
                                                    'id'       => 'item_id',
                                                    'size'     => 30,
                                                    'label'    => false,
                                                    'div'      => false,
                                                    'empty'    => true,
                                                    'required' => false,
                                                ]);
                                            ?>
                                        </td>
                                    </tr>
                                    <tr>
                                        <th>
                                            名前
                                        </th>
                                        <td>
                                            <?php
                                                echo $this->Form->input('name', [
                                                    'id'       => 'name',
                                                    'type'     => 'text',
                                                    'size'     => 30,
                                                    'label'    => false,
                                                    'div'      => false,
                                                    'empty'    => true,
                                                    'required' => false,
                                                ]);
                                            ?>
                                        </td>
                                    </tr>
                                    <tr>
                                        <th>
                                            ステータス有効のみ
                                        </th>
                                        <td>
                                            <?php
                                                echo $this->Form->input('status', [
                                                    'type'  => 'checkbox',
                                                    'div'   => false,
                                                    'value' => TestAppModel::STATUS_ACTIVE,
                                                    'label' => '',
                                                ]);
                                            ?>
                                        </td>
                                    </tr>
                                </table>
                            </div>
                            <div class="submitArea hasSubAction">
                                <ul class="submitButtons">
                                    <li>
                                        <label class="submitBase primary" for="submit">
                                            <?php echo $this->Form->hidden('mode', ['value'=> '']); ?>
                                            <?php
                                                echo $this->Form->submit('検索', [
                                                    'type'  => 'submit',
                                                    'id'    => 'submit',
                                                    'div'   => false,
                                                    'label' => false
                                                ]);
                                            ?>検索
                                        </label>
                                    </li>
                                </ul>
                            </div>
                            <?php echo $this->Form->end(); ?>
                        </div>
test.ctp
        <!--formの外に追加-->
        <?php echo $this->Html->link('formの値を送信した使いたいリンク', 'javascript:clickOutOfFormLink();void(0)', ['class' => 'actionBtnBase hogeListOutput']); ?>

<!--jQuery部分-->
<script>
    // スコープ外から検索ボタンを押下
    function clickOutOfFormLink() {
        // 検索機能をform外の処理に切り替えるためhiddenを設定
        $('#HogeListMode').val('hogehoge');

        document.getElementById('submit').click();

        // 検索機能に戻すためhiddenをクリア
        $('#HogeListMode').val('');
    }
</script>

結果

TestController.php
    /**
     * 一覧
     *
     * @return void
     */
    public function index(): void {
        if (Hash::get($this->request->query, 'mode') === 'hoge') {
            // modeがhogeである場合はform外のリンクが押下されている
            echo 'hoge';

            // 入力値も使用可能?
            var_dump($this->request->query);
        }
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPer(ペチパー)な僕がPythonではまったお話

PHPプログラマでしたがPython初めました。

最近長年連れ添ってきたPHPを捨て(てはないですが)Pythonを
触り始めました。
そこで一つ長年PHPを書いていた僕が
Pythonで書き始めて はまった所なんかをまとめます。

まず初めに

ぱっとPythonのソースを見て思ったこと

  • 行末の「;」がないことへの不安感が半端ないです。
    ⇒ 慣れましょう。
  • if,for等にカッコが無いので閉じる部分がどこなの?という不安。
    ⇒ 慣れましょう。

Pythonでも末尾「;」とか書いても問題はないようですが基本書かないようです。
カッコについては、PHPでは

メインの処理
{
    処理の途中でまとまりの良いもの
    あわばよく後で関数化するかもしれないもの
}

のように、まとまったもの(まとまりが良いもの)にカッコをつけるという
使い方をしていたので少し不便に感じます。

本編(処理について)

いくつかPHPと違う(PHPの方が異端というお話はおいておいて。。。)ために
はまった点を記載します。

関数の引数

PHPは基本値渡し
pythonは基本参照渡し・・・
と書くと非常に語弊がありますね。
PHPはオブジェクト以外は指定しなければ値渡し
Pythonは基本は参照渡しだけどイミュータブルなオブジェクトについては書き換えできない。
なまじstringやintみたいな単純なものの挙動が同じで
『PHPと同じじゃん!』とか思ってしまったのが
そもそもの間違いでした。

それに加え
Pythonの何がイミュータブルなのかがわからず
なんとなく
dict、listはPHPの hash、arrayと同じ挙動だよね~とか思ってしまったのが大間違い。

PHPの場合

<?php
function echo_str_append_world($pram_str) {
    $pram_str .= " world!!";
    echo $pram_str . "\n";
}

$str_hello = 'hello';
echo $str_hello  . "\n";
echo_str_append_world($str_hello);
echo $str_hello . "\n";

function echo_array_append_world($pram_ary) {
    $pram_ary[] = " world!!";
    var_dump($pram_ary);
}

$ary_hello = ['hello'];
var_dump($ary_hello);
echo_array_append_world($ary_hello);
var_dump($ary_hello);
> hello
> hello world!!
> hello # 関数抜けると元の値に戻ってる。
> array(1) {
>   [0]=>
>   string(5) "hello"
> }
> array(2) {
>   [0]=>
>   string(5) "hello"
>   [1]=>
>   string(8) " world!!"
> }
> array(1) { # 関数抜けると元の値に戻ってる。
>   [0]=>
>   string(5) "hello"
> }

Pythonの場合

def echo_str_append_wold(param_str :str) -> None:
    param_str = param_str + " world!"
    print (param_str)

str_hello = "hello"
print(str_hello)
echo_str_append_wold(str_hello)
print(str_hello)


def echo_array_apped_world(param_ary : list) -> None:
    param_ary.append(" world!")
    print(param_ary)

ary_hello = ['hello']
print(ary_hello)
echo_array_apped_world(ary_hello)
print(ary_hello)
> hello
> hello world!
> hello  # (← 書き換わらない! なーんだPHPと同じか。)
> ['hello']
> ['hello', ' world!']
> ['hello', ' world!']  # (← (゚ω゚)ノノ ナンデ 書き換わってる!!)

イミュータブル: int, str, tupleなど
ミュータブル: list, dictなど
などってあたりが、まだ勉強不足なのですが
とりあえずよく利用するものは こんなとこではないかと。
Pythonを触る場合は基本参照渡し!
イミュータブルなオブジェクトだけ 値渡しと覚えておく。

イテレータの再利用

そもそもPHPでイテレータをあまり使っていなかったのですが。。。
PHPのイテレータだと先頭に巻き戻すというのもあり
再利用が可能だったのです。。。

Pythonの場合進んだものを戻すことはできません。
なので
『デバッグ用に処理に利用したイテレータを
再度ループして中身を確認しよう。。。』
という気持ちで↓のようなコードを書いた僕は
すっかりはまってしまったのでした。

hoge_list = [1,2,3,4,5]
for _number in hoge_list:
    print(f"number:{_number}")

for _number in hoge_list:
    print(f"Debug number:{_number}")


moge_ite = iter(hoge_list)
for _number in moge_ite: # 本処理のループ
    print(f"number:{_number}")

for _number in moge_ite: # デバッグ用のループ
    print(f"Debug number:{_number}")
number:1
number:2
number:3
number:4
number:5
Debug number:1
Debug number:2
Debug number:3
Debug number:4
Debug number:5 # ちゃんと処理されてる。確認できた~。
number:1
number:2
number:3
number:4
number:5
# (゚v゚*) あれ?空になってる?値セットできてなかった?

実際にはテーブルのデータ取得だったり、
他の関数からの戻り値を確認する際に
『実際に変数にどんな値がはいっているのかな?』
と思って上記のようなコードを書いて はまりました。

「出力する場所がまずい?」と思って本処理用のループの前で
出力するように変更するとデバッグ出力されるので
(この場合、本処理が動かないわけですが。。。)
さらに気づくのに時間がかかってしまいました。。

わかっていれば全くはまる要素ではないのですが
はまるとなかなか気づけない点でした。
PHPでイテレータを使ってこなかったのも原因ですかね。

内包表記

はまったというか、PHPには全くなかったので
はじめ何のことかさっぱりわからなかったのが
『内包表記!!』
すごく便利なのですが
(短くかけるだけでなく実行速度も速くなるという優れもの)
とにかく所見だと「?なんとなくリストを作ってる?よくわからん!」ってなります。
さらにlambda(無名関数) も一緒に扱うことがあり
より「なんだこれ?」となりましたが。これはぜひ覚えていきたいものです。

# PHP ではこんなイメージ
list_num = []
for _i in range(10):
    list_num.append(_i**2)
print(list_num)

# 内包表記!!
list_num = [_i ** 2 for _i in range(10)]
print(list_num)

# 内包表記と lambda(無名関数) !!
list_num = list(map(lambda _i:_i**2, range(10)))
print(list_num)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

その他

Pythonの line_profiler が非常に便利です。
pip install line_profiler
と導入も簡単
例えば上記の内包表記の確認コードをループ数を増やして
プロファイルにかけるならこんな感じ

import line_profiler

def profiling()->None:
    range_num = 1000
    list_num = []
    for _i in range(range_num):
        list_num.append(_i**2)
    print(list_num)

    # 内包表記!!
    list_num = [_i ** 2 for _i in range(range_num)]
    print(list_num)

    # 内包表記と lambda(無名関数) !!
    list_num = list(map(lambda _i:_i**2, range(range_num)))  #
    print(list_num)

pr = line_profiler.LineProfiler()
pr.add_function(profiling)
pr.enable()

profiling()

pr.disable()
pr.print_stats()
Timer unit: 1e-06 s

Total time: 0.009735 s
File: naihou.py
Function: profiling at line 3

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     3                                           def profiling()->None:
     4         1          2.0      2.0      0.0      range_num = 1000
     5         1          2.0      2.0      0.0      list_num = []
     6      1001       1961.0      2.0     20.1      for _i in range(range_num):
     7      1000       2533.0      2.5     26.0          list_num.append(_i**2)
     8         1        995.0    995.0     10.2      print(list_num)
     9
    10                                               # 内包表記!!
    11         1        978.0    978.0     10.0      list_num = [_i ** 2 for _i in range(range_num)]
    12         1       1036.0   1036.0     10.6      print(list_num)
    13
    14                                               # 内包表記と lambda(無名関数) !!
    15         1       1226.0   1226.0     12.6      list_num = list(map(lambda _i:_i**2, range(range_num)))  #
    16         1       1002.0   1002.0     10.3      print(list_num)

リストをfor文で回して作成 ⇒ 4494.0
内包表記 ⇒ 978.0
内包表記+lambda ⇒ 1226.0
内包表記すごい!って実感することうけあいです。

まとめ

PHPの違いというよりも自分の無知をさらしただけみたいになりましたが。。。
ずっと同じ場所で同じ言語で開発を続けていると
別文化に触れたときにいろいろと衝撃を受けますね。

とりあえず、PHPerな人が Pythonを読むという点では
まずは内包表記になれる とずいぶんと読みやすくなる気がします。

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

推測せずに計測してLaravelパフォーマンスチューニング

はじめに

Laravelで開発した検索APIを、blackfireというプロファイラーを用いてパフォーマンスチューニングをしたので、その知見を共有します。

https://blackfire.io/

blackfireには有料プランが存在しますが、本記事で扱う内容は全て無料プランの範囲内です。

この記事で扱うこと

  • 僕のプロジェクトを高速化した方法

この記事で扱わないこと

  • blackfireのインストール方法
  • blackfireでの計測方法

→ いずれもドキュメント先人の記事を参照してください。

  • あなたのプロジェクトを高速化する方法

→ 自力で見つけて下さい。本記事はそのヒントとなることを目指しています。

前提条件

  • PHP: 7.3
  • Apache: 2.4
  • MySQL: 5.7
  • Laravel: 5.6.1
  • memcached (本記事内で導入)
  • 検索API
  • ページングなし
  • 検索結果は最大で1,000件程度
  • 検索結果1行ごとに、Laravelのrouteヘルパー関数で生成した、それ自身のURLを含む

1,000件検索

範囲を選択_030.png

画面左上の表示から、1.4sかかっていることが分かります。
画面右のツリーを見て処理が重そうな箇所を探しましょう。

route()

改善前

範囲を選択_032.png

まず、MasterDatumViewModel::document()という関数が気になりました。これは検索結果のViewModelクラスで、検索結果1行ごとのURLを返すメソッドです。ソースコードを下記します。

    public function document(): string
    {
        return route('document', ['masterDatumId' => $this->masterDatum->id()->toInt()]);
    }

この関数は検索結果の数だけ呼び出されます。1,000件検索では1,000回呼び出され、トータルで542msかかっています。これは全体の実行時間の40%弱を占め、そのほとんどがroute()呼び出しによるものです。

1行ごとのURLが変わることは滅多にないので、検索結果のIDをキーに含めてキャッシュしてしまいましょう。

改善後

    public function document(): string
    {
        $key = $this->documentCacheKey();
        $cache = cache()->store();
        return $cache->get($key, function () use ($key, $cache) {
            $route = route('document', ['masterDatumId' => $this->masterDatum->id()->toInt()]);
            $cache->put($key, $route);
            return $route;
        });
    }

    public function documentCacheKey(): string
    {
        $id = $this->masterDatum->id()->toInt();
        return "master_datum:document:route:{$id}";
    }

改善後のソースコードです。キャッシュドライバはmemcachedを使用しました。

範囲を選択_034.png

1.57sと、却って遅くなってしまいました。画面右のツリーを見ると、キャッシュリポジトリを取得するcache()CacheManager::store()の呼び出しがかなりの割合を占めます。

ViewModelクラスですし、インスタンス毎に異なるキャッシュリポジトリを使うことも考えにくいので、ここは思い切ってstatic変数にしてしまいましょう。

再改善後

    public function document(): string
    {
        $key = $this->documentCacheKey();
        // 呼び出し回数が多いため初回呼び出し時にstaticとして保持
        static $cache;
        $cache = $cache ?? cache()->store();
        return $cache->get($key, function () use ($key, $cache) {
            $route = route('document', ['masterDatumId' => $this->masterDatum->id()->toInt()]);
            $cache->put($key, $route);
            return $route;
        });
    }

    public function documentCacheKey(): string
    {
        $id = $this->masterDatum->id()->toInt();
        return "master_datum:document:route:{$id}";
    }

キャッシュリポジトリをstatic変数とし、クラス内で初回のみ取得するよう変更しました。

範囲を選択_035.png

結果は1.1sとなり、0.3sの高速化ができました。

余談

デプロイ時にはキャッシュを忘れずに削除しましょう。

\Illuminate\Database\Eloquent\Builder\get()

改善前

範囲を選択_036.png

次に気になったのは、SearchMasterDataAdapter::findMasterData()という関数です。

これは、入力された検索条件をクエリビルダーに適用し、検索結果をドメインモデルに変換して返すメソッドです。ソースコードを下記します。

    public function findMasterData(SearchQuery $query): iterable
    {
        $builder = $this->eloquentMasterData->newQuery();
        $keyword = $query->keyword();
        $sort = $query->sort();

        if ($keyword !== null) {
            $builder->ofKeyword($keyword);
        }

        foreach ($query->businessCategories() as $businessCategory) {
            $builder->ofBusinessCategory($businessCategory);
        }

        if ($sort !== null) {
            $builder->sort($sort);
        }

        return $builder->get()->map(static function (MasterData $masterDatum) {
            return $masterDatum->toDomainModel();
        });
    }

この中で大きな比率を占めるのが$builder->get()です。

ツリーを見ると、Eloquentが検索結果をMasterDataモデルにhydrateする処理に時間がかかっているようです。
適切にデータがセットされたMasterDataモデルのtoDomainModel()さえ呼べれば用は足りるので、PDOで取得した検索結果をMasterDataモデルにセットしてみましょう。

改善後

    public function findMasterData(SearchQuery $query): iterable
    {
        // 省略

        $pdo = $builder->getConnection()->getPdo();
        $bindings = $builder->getBindings();

        $stmt = $pdo->prepare($builder->toSql());
        $results = $stmt->fetchAll(\PDO::FETCH_ASSOC);

        return \array_map(static function (array $row) {
            static $masterData;
            $masterData = $masterData ?? new MasterData();
            return $masterData->setRawAttributes($row)->toDomainModel();
        }, $stmt->fetchAll(\PDO::FETCH_ASSOC));
    }

まずはクエリビルダーをSQLとパラメータに変換して、PDOで取得します。それをMasterData::setRawAttributes()に渡すことでMasterDataモデルに値をセットし、ドメインモデルに変換します。

範囲を選択_037.png

先程の1.1sから、911msまで高速化できました。

1,000件検索のプログラム改善はここまでとします。

gzip

改善前

次はブラウザでAPI呼び出しを見てみましょう。

範囲を選択_038.png

1,000件ものデータとなるとその容量も小さくなく、433kbとなっています。その転送にはおよそ456msかかっています。
これをWebサーバーで圧縮して、転送速度を改善しましょう。

改善後

.htaccess
<IfModule mod_deflate.c>
    <IfModule mod_filter.c>
        FilterDeclare COMPRESS
        FilterProvider COMPRESS DEFLATE "%{Content_Type} =~ m#^application/json#"
        FilterChain COMPRESS
        FilterProtocol COMPRESS DEFLATE change=yes;byteranges=no
    </IfModule>
</IfModule>

コンテンツの圧縮の方法はここでは扱いませんが、Content-Typeapplication/jsonで始まるレスポンスを圧縮するよう設定しました。
(ちなみにm#^application/jsonの後に$を付けて完全一致にすると、Content-Type: application/json; charset=utf-8のようなケースで圧縮されません。)

範囲を選択_039.png

データ量が73.5kbまで低減しました。転送時間は大差ありませんが、これは開発環境がローカルであることが原因です。
本番環境では転送時間が大きく改善していることを確認できました。

1件検索

次に1件検索時のプロファイルをします。1,000件検索時とは違って、もっとアプリケーションのベース部分の問題が判るはずです。

サードパーティ製ライブラリ

改善前

範囲を選択_042.png

1件検索時の結果は478msとなりました。
この内、およそ50%程度をサードパーティ製ライブラリの処理が占めていました。ソースコードを読むと改善の余地が十分にありそうですが、vendor以下のコードを変更する訳にはいきません。

ライブラリの修正を待つのも一つの手ですが、今回は時間的猶予がありませんでした。そのため、自分で改善したクラスをサービスコンテナに登録する形で対応しました。

改善後

./app/Providers/AspectServiceProvider.php
use App\Aop\AspectManager;
use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use Ytake\LaravelAspect\AnnotationConfiguration;

final class AspectServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton('aspect.manager', static function (Application $app) {
            /** @var AnnotationConfiguration $annotationConfiguration */
            $annotationConfiguration = $app->make(AnnotationConfiguration::class);
            $annotationConfiguration->ignoredAnnotations();

            // ↓自分で作成したクラス
            return new AspectManager($app);
        });
    }

    public function boot()
    {
    }
}

詳細は省きます。重要なのは、サービスコンテナを利用することで既存の処理も置換可能ということです。
Laravelドキュメントのサービスコンテナのページを読んで下さい。
https://readouble.com/laravel/6.x/ja/container.html

範囲を選択_043.png

284msとなり、およそ200msもの改善ができました。

まとめ

  • 1,000件検索では1.4s→911ms。約35%の改善。
  • 1件検索では478ms→284ms。約40%の改善。

本記事で行なった施策が、そのままあなたのプロジェクトに適用できることは稀だと思います。
あなたのプロジェクトに最適な施策を見つけて下さい。

推測するな、計測せよ。
よきパフォーマンスチューニングを。

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

MACBOOK PROでMAMPを利用したDATABASE作成でつまづいた話

備忘録として書き残すので大した内容ではありませんがもし同じように悩む人の役に立てばこれ幸い也。

勉強書「スラスラわかるPHP 志田仁美 著」

経緯

PHPの勉強でホームページ制作。コンテンツ内に掲示板を作成することにした。当方MACBOOK PRO でMAMPを使って環境構築し順調に設計していたがMySQLで作成したデータベースが読み込めない。ぜんぜん読み込めない。why?

スクリーンショット 2019-12-03 14.50.28.png

原因

phpの記入ミスやMySQLのデータベースに正しくログインできていなかったのでは?とうろたえて数日経過。
なんてことはない。
自身のPCにMySQLをインストールしてデータベースを使用していただけだった。
MAMPにはローカル開発環境を立ち上げるために必要なソフトウェア(Apache、MySQL、PHP)がパッケージ化されているんだから余計なことしなくてよかったのだ。

解決法

備忘録なので簡潔に

  1. MAMPを起動してターミナルを起動する
  2. ターミナルからMAMPの作業ディレクトリへ移動
$ cd /Applications/MAMP/Library/bin/

このコマンドで作業ディレクトリへ

3.MAMPの作業ディレクトリからMySQLへログイン

通常のMySQLへのログインと同じ

$./mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 4
Server version: 5.7.26 MySQL Community Server (GPL)

Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

4.データベースを作成する

こちらも通常どうりに使用する。
今回は簡易的な掲示板作るための最低限のデータベースとテーブルを作成した。

データベース作成

mysql> create database (データベース名);
Query OK, 1 row affected (0.07 sec)

データベース選択

mysql> use (データベース名)
Database changed

テーブル作成

mysql> CREATE TABLE bbs(
    ->   id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    ->   name VARCHAR(255) NOT NULL,
    ->   title VARCHAR(255),
    ->   body TEXT NOT NULL,
    ->   date DATETIME NOT NULL,
    ->   pass CHAR(4) NOT NULL
    -> )DEFAULT CHARACTER SET=utf8;

これで完成。
PHPは問題なかったため掲示板は問題なく動作しました。
同じ間違いは二度と犯さないようにしよう。

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

PHP基礎 Part5

概要

PHPの独学で学んだことをアウトプットしていく

前回の内容

PHP基礎 Part4

for文

繰り返しで処理を行いたい時に用いる文法
基本的な形は、以下の通り

sample.php
<?php
  for ($i=1; $i <= 5; $i++){  // ()の中に変数を定義し、繰り返す条件と変数の更新を入れる
    echo $i;                  // 結果の出力
  }
// 結果:12345
?>

while文

for文と同じような繰り返しで処理を行いたい時に使用する文法

sample.php
<?php
  $i = 1           // 変数の定義
  while ($i <= 5){  // 繰り返す条件の設定 
    echo $i;       // 結果の出力
    $i++;          // 変数の更新
  }
// 結果:12345
?>

for文は終了タイミングや実行回数が決まっている時に使用し、
while文は終了タイミングが決まっていない時や設定した条件を満たす場合に使用するくらいの認識。

break文

繰り返し処理の中断を命令する
for文、while文とif文を組み合わせて使用する

sample.php
<?php
  for($i = 1; $i <= 10; $i++) // 変数の定義
    if ($i == 5){             // breakの条件設定
      break;                  // if文の条件を満たす場合、中断処理
    }
      echo $i;                // if文の条件を満たさない場合、結果の出力
  }
// 結果:1234
?>

continue文

繰り返し処理のスキップを命令する
break文同様、for文、while文とif文を組み合わせて使用する

sample.php
<?php
  for($i = 1; $i <= 10; $i++) // 変数の定義
    if ($i % 2 == 0){         // continueの条件設定
      continue;               // if文の条件を満たす場合、スキップ
    }
      echo $i;                // if文の条件を満たさない場合、結果の出力
  }
// 結果:13579
?>

foreach文

配列や連想配列を用いた繰り返し処理を命令する
書き方は以下の通り

1.配列を用いた繰り返し処理

sample.php
<?php
  $languages = array("日本語","英語","フランス語");
  foreach ($languages as $value){
    echo $languages;

// 結果:日本語英語フランス語
?>

2.連想配列を用いた繰り返し処理

sample.php
<?php
  $scores = array(
       "国語" => 68,
       "数学" => 83,
       "英語" => 92
  );

  foreach ($scores as $key => $value){
    echo $key.":".$value;

// 結果:国語:68数学:83英語:92
?>

次回

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

PHP の json_encode($hoge, JSON_FORCE_OBJECT) の JSON_FORCE_OBJECT ってなんだっけ

json_encode() とは

https://www.php.net/manual/ja/function.json-encode.php

json_encode ( mixed $value [, int $options = 0 [, int $depth = 512 ]] ) : string

$value をJSONにして返します。
で、その第二引数の JSON_FORCE_OBJECT ってなんだっけ?と思ったのでメモ。
(そのほかのオプションもまた出会ったら追記していくかも)

そもそも第二引数( $options )って何?

JSON_FORCE_OBJECT などのビットマスク。(どうやって出力するか、的な?)
そのほかの各定数の意味については JSON 定数のページ に記載あり。

で、 JSON_FORCE_OBJECT の意味

配列をオブジェクトとして返してくれる。

JSON_FORCE_OBJECT (integer)
非連想配列を使用した場合に、配列ではなくオブジェクトを出力します。 出力を受け取る側がオブジェクトを期待しており、配列が空っぽである場合などに特に便利です。 この定数は PHP 5.3.0 以降で使用可能です。

ついでに他のパラメータについて

$value

エンコードする値。エンコードしたい配列とかをここに。

$depth

最大の深さ。正の数でないといけない。
(階層の深さってことかな?)

関連・参考

https://qiita.com/shosho/items/34d0e9cc68c376a0a972

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

PHP Invalid argument supplied for foreach() .. errorの対処法

Invalid argument supplied for foreach() ..のエラーの簡単な対処法について

エラーの原因は
「Invalid argument supplied for foreach() :foreachに使えない変数を使っていることなので

foreach()に合わせて、変数を修正すれば大丈夫です。

ちなみにforeach()が使える引数の説明は

「foreach は、配列を反復処理するための便利な方法です。 foreach が使えるのは配列とオブジェクトだけであり、 別のデータ型や初期化前の変数に対して使うとエラーになります。 この構造には二種類の構文があります。」(foreachの使用方法)

と明記されているので、配列かオブジェクトのどちらかに合わせてあげればオッケーです。

foreach((array)$data as $value){
  ...
}

の様に変数の直前に "(array)"を付けて、変数を強制的に配列にしてあげれば大丈夫です。
これが一番シンプルな気がします。

以上

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

OneSignalで特定のユーザーにWEBプッシュ通知を送る方法(PHP編)

OneSignalは無料で使いはじめることができるプッシュ通知配信サービスです。(WEBプッシュは3万ユーザーまで。モバイルは無制限で)
スクリーンショット 2019-11-26 11.41.10.png
こちらを使うと驚くほどかんたんにプッシュ通知を送れました。今回はLaravelにてOneSignalを使ってWEBブラウザ向け(Chrome、Firefox)にプッシュ通知を配信する方法をまとめます。

OneSignalでアプリを作成

スクリーンショット 2019-11-26 11.43.06.png
Website Push を選択。
スクリーンショット 2019-11-26 11.43.51.png
SITE NAMEとSITE URLを入力。
スクリーンショット 2019-11-26 11.44.22.png
ローカルでテストする場合はSITE URLは「https://localhost 」など適宜変更してつくってください。
DEFAULT ICON URLはプッシュ通知の許可ダイアログででてくるアイコンです。
スクリーンショット 2019-12-03 10.11.41.png

Permission Prompt Setupは、最初にサイトを訪れた時にプッシュ通知の許可確認をとる方法を選べます。

スクリーンショット 2019-12-03 10.12.13.png
Welcome Notificationでは許可された場合に送るプッシュ通知を設定。お礼のメッセージなどを。
ほかにもいろいろ設定はありますが、基本的にはこれだけで「SAVE」でOK。

スクリーンショット 2019-12-03 10.15.28.png
次に、「DOWNLOAD ONESIGNAL SDK FILES」を押してファイルをダウンロードして、下記の2つのファイルをサイト直下にアップしてください。
スクリーンショット 2019-12-03 10.17.30.png
最後に、JavaScriptのコードを埋め込めば完了です。

OneSignalでプッシュ通知を送る

これでプッシュ通知を送れるようになりました。OneSignalの管理画面からMessagesを開いてNEW PUSHをクリック。
スクリーンショット 2019-12-03 10.18.50.png
Send to Subscribed Usersで、購読している人に送れます。
スクリーンショット 2019-12-03 10.20.37.png

個別にプッシュ通知を送りたい

全体に送るものだけじゃなく、特定のユーザーにだけ通知を送りたいケースはよくあります。

アプリ側から下記のようにUserIdをセットすることができます。(下記の例はLaravelのbladeに書いています)

<script>
        var OneSignal = window.OneSignal || [];
        OneSignal.push(function () {
            OneSignal.init({
                appId: "ONESIGNALのAPP_ID",
            });

            @if(isset($loginUser))
            //onesignalにuser_idをセット
            OneSignal.on('subscriptionChange', function (isSubscribed) {
                if (isSubscribed == true) {
                    OneSignal.setExternalUserId('{{ $loginUser->id }}');
                    OneSignal.getExternalUserId().then(function (id) {
                    });
                } else if (isSubscribed == false) {
                    OneSignal.removeExternalUserId();
                }
            });
            @endif
        });
</script>

これでOneSignalのユーザーとアプリ側のユーザーを一致させます。

次にプッシュ通知を送りたいところで下記のようにして個別に送ることができます。headingsはタイトル、contentsは内容をいれます。

SendPush.php
$fields = array(
            'app_id' => ONESIGNALのAPP_ID,
            'include_external_user_ids' => [$user_id],
            'url' => アプリのURL,
            'headings' => array('en' => $title),
            'contents' => array('en' => $body)
);

$fields = json_encode($fields);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://onesignal.com/api/v1/notifications");
curl_setopt($ch, CURLOPT_HTTPHEADER,
            array('Content-Type: application/json; charset=utf-8', 'Authorization: Basic '.ONESIGNALのAPP_ID));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $fields);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

$response = curl_exec($ch);
curl_close($ch);

いかがだったでしょうか。OneSignalがあればこれだけかんたんにプッシュ通知を送ることができるようになります。ブラウザだけでなく、スマホアプリにも対応していますのでWEBもアプリもプッシュ通知はOneSignalさえあれば捗ります。

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

PHPのCountableインタフェースで配列以外をcountする

この記事は「GameWith Advent Calendar 2019」の3日目の記事です。

概要

PHPのcount()は、配列の個数を数えるときによく使う関数です。ただ実は配列以外も数えることができます。
それがタイトルにも書いた、「Countableインタフェース」です。このインタフェースを実装したオブジェクトもcount()することができます。意外と情報が少なそうだったのでまとめてみました。

使い方

Countableを実装したクラスに対して、count()メソッドを定義します。

class MyCountable implements Countable
{
    private $num = 255;

    public function count()
    {
        return $this->num;
    }
}

Countableを実装したオブジェクトは次のようにcount()関数で数えることが可能で、結果は自分で定義したcount()の結果を返すことができます。

$counter = new MyCountable();
var_dump(count($counter));  // int(255)

count可能かのチェック

count()可能かのチェックは、PHP7.3以降の場合は「is_countable」で、配列に加えてCountableのオブジェクトもチェックすることができます。

$counter = new MyCountable();
if (is_countable($counter)) {
    var_dump(count($counter));  // int(255)
}

PHP7.3未満の場合は少し手間ですが、is_array()instanceofを併用してチェックするのが良いと思います。

$counter = new MyCountable();
if (is_array($counter) || $counter instanceof Countable) {
    var_dump(count($counter));  // int(255)
}

注意点

次のようにint以外の値を返すようにした場合、実行はできますがintにキャストされた結果が返ってきます。

class MyCountable implements Countable
{
    private $num = 'test';

    public function count()
    {
        return $this->num;
    }
}

$counter = new MyCountable();
var_dump(count($counter));  // int(0)

まとめ

CountableインタフェースはPHP5からありましたが、私も最近まで存在を知らずis_countable()関数の追加で初めて知り、今回紹介をしてみました。
もし「こんな風に使うとCountableは便利」ってのがあったら、ぜひ教えてください!

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

WP All Export pluginで出たエラーを解決する with php72

背景

最近、Wordpressの制作依頼が多い。
Wordpressは開発だけではなく、コンテンツもセットで納品する前提なのが辛いですが、大量のコンテンツ入稿に役立つcsv入力・出力ができると、大量のコンテンツの操作が楽になります。
昔は、Really Simple CSV Importer にお世話になってましたが、最近は、メンテされていないため、使えなそう。。。

そこで、探したらありました。

WP All Import
WordPress XML Import. WordPress CSV Import. Easy.

WP All Export
Export anything in WordPress to CSV, XML, or Excel.

問題

WP All Export の方は、有効にしたところ、私の環境では、下記のエラーが発生してました。

■ Error 1
PHPのZipArchiveクラスが見当たりません

ZipArchive class is missing on your server.

Please contact your web hosting provider and ask them to install and activate ZipArchive.

■ Error 2
XML系のパッケージがないです

Required PHP components are missing.

WP All Export requires XMLReader, and XMLWriter PHP modules to be installed.

These are standard features of PHP, and are necessary for WP All Export to write the files you are trying to export.

Please contact your web hosting provider and ask them to install and activate the DOMDocument, XMLReader, and XMLWriter PHP modules.

対応...の前に前提

私の環境

AWS - EC2(Amazon linux)
PHP - PHP 7.2.x
WebS - Apache 2.4.x

対応

■ Error 1

$ sudo yum remove libzip
$ sudo yum --enablerepo=remi install php72-php-zip
  1. Amazon core パッケージに入っているlibzipがコンフリクトするため、最初に削除
  2. このコマンドで php72-php-pecl-zip.x86_64 をインストール
  3. Apache リロード
  4. OK > 完了

■ Error 2

$ sudo yum -y install php72-php-xml
  1. OK > 完了

最後に

私の環境では上記で解決しました。

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

EUC-JP環境でphpのstr_replaceを使い全角スペースの置換でバグが発生する件

はじめに

表題の通り、本記事は以下の状況にて発生する現象のTipsとなります。
- 文字コードがEUC-JPである環境で
- phpのstr_replace関数を使用し
- 全角スペースを置換した場合
- 想定していない単語まで置き換わってしまう

発生した現象

DBに入っている値がなんだかおかしい、というところからスタートした。
「ァー」が「ゼ」に代わって保存されているらしい。
クリストファーがクリストフゼといった具合。
プログラムを追っていくと、どうやらDBに値を投入する前の処理がおかしくなっている模様。

$trimmed_str = str_replace(" ", "", $str);

結論として、「全角スペースを置換すると'ァー'という文字列が影響を受ける」という現象だった。

原因

環境の文字コードがEUC-JPであることが原因だった。
EUC-JPはマルチバイト文字であり、全角文字は複数の16進数の組み合わせによって表現される。
参考: https://qiita.com/mpyw/items/a8dba1b80fe68523b8eb

下記のURL先の表のようなマッピングがされている。
http://charset.7jp.net/euc.html
表に当てはめると、「ァ」は'0501'、「ー」は'01bc'となる。
そして全角スペースは'0101'...
要するに、'050101bc'から'0101'を除外していしまい、'05bc'、つまり「ぜ」に変換されてしまったのだ。

対策

この状況を脱却するために、以下の対処をすることで解決できる。

EUC-JPを脱却する

環境の文字コードをEUC-JPからUTF-8に変更することで解決できる。
UTF-8では先頭バイトと後続するバイトで領域が分かれているため、今回の現象のようにほかの文字の組み合わせと混同することがない。
原因の箇所で紹介させていただいた記事に詳細が書かれているので、詳しくはそちらを参照してほしい。

マルチバイト文字対応の関数へ修正する

とはいえ、軽々に文字コードを修正できないケースもある。
(古の環境を修正せざるを得ないつらい状態の方もいるだろう)
この場合、マルチバイト対応の関数へ移行することで解決する。

$trimmed_str = mb_ereg_replace(" ", "", $str);

mb_ereg_replaceはマルチバイト対応なので、文字の組み合わせをその文字と混同しない。
https://www.php.net/manual/ja/function.mb-ereg-replace.php

誤った説明の箇所があれば、コメントいただけると嬉しいです。

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

Moodle 3.7 マニュアル - Amazon AWS EC2 インスタンスへのインストールガイド

原文

Amazon EC2 インスタンスへのインストールガイド

(訳者注:こちらも参考にしてください Moodle インストール
ec2 インスタンスは、Amazon により時間貸しされている仮想 web サーバです。価格は、あなたが借りるインスタンスのパワーにより決まります。

私は、Amazon EC2 の背景知識を得るために次の本を読むことを勧めます。"Host Your Web Site in the Cloud: Amazon Web Services Made Easy by Jeff Barr Copyright ©2010 Amazon Web Services" この本は、Aamazon の web サービスの公式本であり、ec2 インスタンス上に web サーバを構築する方法を教えるものです。

以下の指示は、Moodle を EC2 インスタンスでセットアップするための、一つの方法となります。

内容

1 ec2 インスタンスを借りる
1.1 'elastic ip' を追加して firewall で必要なポートを開く
2 あなたの ec2 インスタンスへ putty あるいはその他の ssh クライアントでアクセスする
2.1 ec2 インスタンスへ putty ssh クライアントを使用してアクセスする
3 Amazon Linux AMI 上の web サーバをセットアップする
3.1 サーバのサービスを起動して再起動時に自動でスタートするようにする
3.2 httpd.conf を編集する
4 MySQL をセットアップする
5 Moodle をインストールする

1 ec2 インスタンスを借りる

Amazon Web Services でアカウントをセットアップして、AWS マネジメントコンソール で、ec2 インスタンスを借ります。

私は、Elastic Block Store を元にした Amazon Linux AMI を使用しました。Elasitc Block Store を元にしたインスタンスを使用することにより、あなたのディスクを後でバックアップすることができるようになります。 "Amazon Linux AMI" は CentOS に基づいており、最低限の基本が含まれているのみのバージョンで、もっとセキュアになります。あなたは yum コマンドを使用してあなたのサーバに追加で必要なものをインストールすることができます。

1.1 'elastic ip' を追加して firewall で必要なポートを開く

AWS マネジメントコンソールにもう一度行きます。

  • 私は、Amazon Linux AMI により立ち上げた ec2 インスタンスに elastic ip を作成してアタッチしました。
  • 私は、22 と 80 番ポートを開きました。私の目的のためには、https を通してアクセスする必要がないため、これで十分です。

あなたは、elastic IP アドレスにドメイン名をマップしたいかもしれません。あなたがこれをするには、DNS プロバイダが必要でしょう。これを書いている時点で、Amazon はこのサービスを提供していません。(訳者注:現在は R53 等により提供されています。)

2 あなたの ec2 インスタンスへ putty あるいはその他の ssh クライアントでアクセスする

AWS マネジメントコンソールを通して、あなたは ssh キーをセットアップして、実行している ec2 インスタンスにアタッチできます。
あなたの Linux AMI を使用している ec2 のためのユーザ名は、ec2-user であり、秘密鍵を正しくセットアップすればパスワードを求められることはありません。

2.1 ec2 インスタンスへ putty ssh クライアントを使用してアクセスする

この鍵を Windows の putty 上で動作させるためには、puttygen により秘密鍵をインポートし、安全な場所にできるならばパスフレーズ(ローカルのパスワード)付きで守られた秘密鍵を保存する必要があります。あなたはそして Connection/SSH/Auth の設定を開き、正しい 'Private Key for Authentication' をブラウズして、鍵ファイルを putty の ssh 接続と紐づけることができます。(訳者注:訳者は Windows を使っていませんが、Linux 端末であれば、'ssh -i <秘密鍵のパス> ec2名' でログイン可能です。秘密鍵は、$ chmod 444 しておきます。)

3 Amazon Linux AMI 上の web サーバをセットアップする

あなたの現在のパッケージが最新であることを確認してください。

sudo yum -y update

あなたのサーバに必要な全てのソフトウェアをインストールするには、yum を使用します。apache、mysql と Moodle が必要とする php の extension をインストールするためには次のコマンドを使用できます(訳者注:現在は、httpd24 や php5系でなくても、httpd と php で httpd-2.4 で php7系になっているかもしれません)。

sudo yum -y install aspell aspell-en httpd24 mysql mysql-server php56 php56-cli php56-gd php56-intl php56-mbstring php56-mysqlnd php56-opcache php56-pdo php56-soap php56-xml php56-xmlrpc php56-pspell

あなたは、このコマンドでインストールしたすべてのパッケージ(依存性を含めて)をリストアップできます。

sudo yum list installed

3.1 サーバのサービスを起動して再起動時に自動でスタートするようにする

新しいサービスが自動で立ち上がるように設定してください。(訳者注:現在のイメージでは、systemctl コマンドを使うかもしれませんので、systemctl コマンドに置き換えてください。)

sudo /sbin/chkconfig httpd on
sudo /sbin/chkconfig mysqld on

sudo /sbin/service httpd start
sudo /sbin/service mysqld start

3.2 httpd.conf を編集する

httpd.conf を編集する必要がある場合は、そのファイルは /etc/httpd/conf にあります。

しかしデフォルトでは、/var/www/html/ ディレクトリが root web ディレクトリであり Moodle をそこにインストールできます。

あなたがどうしても httpd.conf を編集するのならば次のコマンドを使用して apache をリスタートし、変更を有効にしてください。(訳者注:ここでも、新しいイメージでは systemctl コマンドになると思います。)

sudo /sbin/service httpd restart

4 MySQL をセットアップする

sudo mysqladmin -u root password 'new-password'

mysql には追加のセキュリティ関連の変更を加えてください。"mysql_secure_installation" コマンドを実行して引き続き回答していくことによりとても容易に行えます。

sudo mysql -u root -p

mysql> DROP DATABASE test;                          [removes the test database]
mysql> DELETE FROM mysql.user WHERE user = ;        [Removes anonymous access]
mysql> FLUSH PRIVILEGES;

5 Moodle をインストールする

Now you need to :

http://あなたのelasticipアドレス/あなたのmoodleサーバのroot/ (もしあなたの ドメイン名が elasticipアドレスを指し示す設定をしているのなら、もちろんそれを使用するべきです)に行き、Moodle をインストールします。

インストーラが作成する config.php をコピーして moodleserver の root に行き、以下のコマンドを実行します。

sudo vi config.php

そして、vi を挿入モードにして putty 上で、あなたは右マウスキーを使用して内容をクリップボードに貼りつけることにより、以前の内容を削除する事が出来ます。

そしてもう一度あなたの Moodle インストール環境の root http://あなたのelasticipアドレス/あなたのmoodleサーバのroot/ に行きあなたの Moodle db の自動インストールを完了します。

カテゴリ:インストール

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

Moodle 3.8 マニュアル - Amazon AWS EC2 インスタンスへのインストールガイド

原文

Amazon EC2 インスタンスへのインストールガイド

(訳者注:こちらも参考にしてください Moodle インストール
ec2 インスタンスは、Amazon により時間貸しされている仮想 web サーバです。価格は、あなたが借りるインスタンスのパワーにより決まります。

私は、Amazon EC2 の背景知識を得るために次の本を読むことを勧めます。"Host Your Web Site in the Cloud: Amazon Web Services Made Easy by Jeff Barr Copyright ©2010 Amazon Web Services" この本は、Aamazon の web サービスの公式本であり、ec2 インスタンス上に web サーバを構築する方法を教えるものです。

以下の指示は、Moodle を EC2 インスタンスでセットアップするための、一つの方法となります。

内容

1 ec2 インスタンスを借りる
1.1 'elastic ip' を追加して firewall で必要なポートを開く
2 あなたの ec2 インスタンスへ putty あるいはその他の ssh クライアントでアクセスする
2.1 ec2 インスタンスへ putty ssh クライアントを使用してアクセスする
3 Amazon Linux AMI 上の web サーバをセットアップする
3.1 サーバのサービスを起動して再起動時に自動でスタートするようにする
3.2 httpd.conf を編集する
4 MySQL をセットアップする
5 Moodle をインストールする

1 ec2 インスタンスを借りる

Amazon Web Services でアカウントをセットアップして、AWS マネジメントコンソール で、ec2 インスタンスを借ります。

私は、Elastic Block Store を元にした Amazon Linux AMI を使用しました。Elasitc Block Store を元にしたインスタンスを使用することにより、あなたのディスクを後でバックアップすることができるようになります。 "Amazon Linux AMI" は CentOS に基づいており、最低限の基本が含まれているのみのバージョンで、もっとセキュアになります。あなたは yum コマンドを使用してあなたのサーバに追加で必要なものをインストールすることができます。

1.1 'elastic ip' を追加して firewall で必要なポートを開く

AWS マネジメントコンソールにもう一度行きます。

  • 私は、Amazon Linux AMI により立ち上げた ec2 インスタンスに elastic ip を作成してアタッチしました。
  • 私は、22 と 80 番ポートを開きました。私の目的のためには、https を通してアクセスする必要がないため、これで十分です。

あなたは、elastic IP アドレスにドメイン名をマップしたいかもしれません。あなたがこれをするには、DNS プロバイダが必要でしょう。これを書いている時点で、Amazon はこのサービスを提供していません。(訳者注:現在は R53 等により提供されています。)

2 あなたの ec2 インスタンスへ putty あるいはその他の ssh クライアントでアクセスする

AWS マネジメントコンソールを通して、あなたは ssh キーをセットアップして、実行している ec2 インスタンスにアタッチできます。
あなたの Linux AMI を使用している ec2 のためのユーザ名は、ec2-user であり、秘密鍵を正しくセットアップすればパスワードを求められることはありません。

2.1 ec2 インスタンスへ putty ssh クライアントを使用してアクセスする

この鍵を Windows の putty 上で動作させるためには、puttygen により秘密鍵をインポートし、安全な場所にできるならばパスフレーズ(ローカルのパスワード)付きで守られた秘密鍵を保存する必要があります。あなたはそして Connection/SSH/Auth の設定を開き、正しい 'Private Key for Authentication' をブラウズして、鍵ファイルを putty の ssh 接続と紐づけることができます。(訳者注:訳者は Windows を使っていませんが、Linux 端末であれば、'ssh -i <秘密鍵のパス> ec2名' でログイン可能です。秘密鍵は、$ chmod 444 しておきます。)

3 Amazon Linux AMI 上の web サーバをセットアップする

あなたの現在のパッケージが最新であることを確認してください。

sudo yum -y update

あなたのサーバに必要な全てのソフトウェアをインストールするには、yum を使用します。apache、mysql と Moodle が必要とする php の extension をインストールするためには次のコマンドを使用できます(訳者注:現在は、httpd24 や php5系でなくても、httpd と php で httpd-2.4 で php7系になっているかもしれません)。

sudo yum -y install aspell aspell-en httpd24 mysql mysql-server php56 php56-cli php56-gd php56-intl php56-mbstring php56-mysqlnd php56-opcache php56-pdo php56-soap php56-xml php56-xmlrpc php56-pspell

あなたは、このコマンドでインストールしたすべてのパッケージ(依存性を含めて)をリストアップできます。

sudo yum list installed

3.1 サーバのサービスを起動して再起動時に自動でスタートするようにする

新しいサービスが自動で立ち上がるように設定してください。(訳者注:現在のイメージでは、systemctl コマンドを使うかもしれませんので、systemctl コマンドに置き換えてください。)

sudo /sbin/chkconfig httpd on
sudo /sbin/chkconfig mysqld on

sudo /sbin/service httpd start
sudo /sbin/service mysqld start

3.2 httpd.conf を編集する

httpd.conf を編集する必要がある場合は、そのファイルは /etc/httpd/conf にあります。

しかしデフォルトでは、/var/www/html/ ディレクトリが root web ディレクトリであり Moodle をそこにインストールできます。

あなたがどうしても httpd.conf を編集するのならば次のコマンドを使用して apache をリスタートし、変更を有効にしてください。(訳者注:ここでも、新しいイメージでは systemctl コマンドになると思います。)

sudo /sbin/service httpd restart

4 MySQL をセットアップする

sudo mysqladmin -u root password 'new-password'

mysql には追加のセキュリティ関連の変更を加えてください。"mysql_secure_installation" コマンドを実行して引き続き回答していくことによりとても容易に行えます。

sudo mysql -u root -p

mysql> DROP DATABASE test;                          [test データベースを削除します]
mysql> DELETE FROM mysql.user WHERE user = ;        [anonymous アクセスを削除します]
mysql> FLUSH PRIVILEGES;

5 Moodle をインストールする

Now you need to :

http://あなたのelasticipアドレス/あなたのmoodleサーバのroot/ (もしあなたの ドメイン名が elasticipアドレスを指し示す設定をしているのなら、もちろんそれを使用するべきです)に行き、Moodle をインストールします。

インストーラが作成する config.php をコピーして moodleserver の root に行き、以下のコマンドを実行します。

sudo vi config.php

そして、vi を挿入モードにして putty 上で、あなたは右マウスキーを使用して内容をクリップボードに貼りつけることにより、以前の内容を削除する事が出来ます。

そしてもう一度あなたの Moodle インストール環境の root http://あなたのelasticipアドレス/あなたのmoodleサーバのroot/ に行きあなたの Moodle db の自動インストールを完了します。

カテゴリ:インストール

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

Moodle 3.7 マニュアル - Amazon EC2 インスタンスへのインストールガイド

原文

Amazon EC2 インスタンスへのインストールガイド

(訳者注:こちらも参考にしてください Moodle インストール
ec2 インスタンスは、Amazon により時間貸しされている仮想 web サーバです。価格は、あなたが借りるインスタンスのパワーにより決まります。

私は、Amazon EC2 の背景知識を得るために次の本を読むことを勧めます。"Host Your Web Site in the Cloud: Amazon Web Services Made Easy by Jeff Barr Copyright ©2010 Amazon Web Services" この本は、Aamazon の web サービスの公式本であり、ec2 インスタンス上に web サーバを構築する方法を教えるものです。

以下の指示は、Moodle を EC2 インスタンスでセットアップするための、一つの方法となります。

内容

1 ec2 インスタンスを借りる
1.1 'elastic ip' を追加して firewall で必要なポートを開く
2 あなたの ec2 インスタンスへ putty あるいはその他の ssh クライアントでアクセスする
2.1 ec2 インスタンスへ putty ssh クライアントを使用してアクセスする
3 Amazon Linux AMI 上の web サーバをセットアップする
3.1 サーバのサービスを起動して再起動時に自動でスタートするようにする
3.2 httpd.conf を編集する
4 MySQL をセットアップする
5 Moodle をインストールする

1 ec2 インスタンスを借りる

Amazon Web Services でアカウントをセットアップして、AWS マネジメントコンソール で、ec2 インスタンスを借ります。

私は、Elastic Block Store を元にした Amazon Linux AMI を使用しました。Elasitc Block Store を元にしたインスタンスを使用することにより、あなたのディスクを後でバックアップすることができるようになります。 "Amazon Linux AMI" は CentOS に基づいており、最低限の基本が含まれているのみのバージョンで、もっとセキュアになります。あなたは yum コマンドを使用してあなたのサーバに追加で必要なものをインストールすることができます。

1.1 'elastic ip' を追加して firewall で必要なポートを開く

AWS マネジメントコンソールにもう一度行きます。

  • 私は、Amazon Linux AMI により立ち上げた ec2 インスタンスに elastic ip を作成してアタッチしました。
  • 私は、22 と 80 番ポートを開きました。私の目的のためには、https を通してアクセスする必要がないため、これで十分です。

あなたは、elastic IP アドレスにドメイン名をマップしたいかもしれません。あなたがこれをするには、DNS プロバイダが必要でしょう。これを書いている時点で、Amazon はこのサービスを提供していません。(訳者注:現在は R53 等により提供されています。)

2 あなたの ec2 インスタンスへ putty あるいはその他の ssh クライアントでアクセスする

AWS マネジメントコンソールを通して、あなたは ssh キーをセットアップして、実行している ec2 インスタンスにアタッチできます。
あなたの Linux AMI を使用している ec2 のためのユーザ名は、ec2-user であり、秘密鍵を正しくセットアップすればパスワードを求められることはありません。

2.1 ec2 インスタンスへ putty ssh クライアントを使用してアクセスする

この鍵を Windows の putty 上で動作させるためには、puttygen により秘密鍵をインポートし、安全な場所にできるならばパスフレーズ(ローカルのパスワード)付きで守られた秘密鍵を保存する必要があります。あなたはそして Connection/SSH/Auth の設定を開き、正しい 'Private Key for Authentication' をブラウズして、鍵ファイルを putty の ssh 接続と紐づけることができます。(訳者注:訳者は Windows を使っていませんが、Linux 端末であれば、'ssh -i <秘密鍵のパス> ec2名' でログイン可能です。秘密鍵は、$ chmod 444 しておきます。)

3 Amazon Linux AMI 上の web サーバをセットアップする

あなたの現在のパッケージが最新であることを確認してください。

sudo yum -y update

あなたのサーバに必要な全てのソフトウェアをインストールするには、yum を使用します。apache、mysql と Moodle が必要とする php の extension をインストールするためには次のコマンドを使用できます(訳者注:現在は、httpd24 や php5系でなくても、httpd と php で httpd-2.4 で php7系になっているかもしれません)。

sudo yum -y install aspell aspell-en httpd24 mysql mysql-server php56 php56-cli php56-gd php56-intl php56-mbstring php56-mysqlnd php56-opcache php56-pdo php56-soap php56-xml php56-xmlrpc php56-pspell

あなたは、このコマンドでインストールしたすべてのパッケージ(依存性を含めて)をリストアップできます。

sudo yum list installed

3.1 サーバのサービスを起動して再起動時に自動でスタートするようにする

新しいサービスが自動で立ち上がるように設定してください。(訳者注:現在のイメージでは、systemctl コマンドを使うかもしれませんので、systemctl コマンドに置き換えてください。)

sudo /sbin/chkconfig httpd on
sudo /sbin/chkconfig mysqld on

sudo /sbin/service httpd start
sudo /sbin/service mysqld start

3.2 httpd.conf を編集する

httpd.conf を編集する必要がある場合は、そのファイルは /etc/httpd/conf にあります。

しかしデフォルトでは、/var/www/html/ ディレクトリが root web ディレクトリであり Moodle をそこにインストールできます。

あなたがどうしても httpd.conf を編集するのならば次のコマンドを使用して apache をリスタートし、変更を有効にしてください。(訳者注:ここでも、新しいイメージでは systemctl コマンドになると思います。)

sudo /sbin/service httpd restart

4 MySQL をセットアップする

sudo mysqladmin -u root password 'new-password'

mysql には追加のセキュリティ関連の変更を加えてください。"mysql_secure_installation" コマンドを実行して引き続き回答していくことによりとても容易に行えます。

sudo mysql -u root -p

mysql> DROP DATABASE test;                          [removes the test database]
mysql> DELETE FROM mysql.user WHERE user = ;        [Removes anonymous access]
mysql> FLUSH PRIVILEGES;

5 Moodle をインストールする

Now you need to :

http://あなたのelasticipアドレス/あなたのmoodleサーバのroot/ (もしあなたの ドメイン名が elasticipアドレスを指し示す設定をしているのなら、もちろんそれを使用するべきです)に行き、Moodle をインストールします。

インストーラが作成する config.php をコピーして moodleserver の root に行き、以下のコマンドを実行します。

sudo vi config.php

そして、vi を挿入モードにして putty 上で、あなたは右マウスキーを使用して内容をクリップボードに貼りつけることにより、以前の内容を削除する事が出来ます。

そしてもう一度あなたの Moodle インストール環境の root http://あなたのelasticipアドレス/あなたのmoodleサーバのroot/ に行きあなたの Moodle db の自動インストールを完了します。

カテゴリ:インストール

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

PHP 7.4 から空ファイルの MIME タイプ判定が変わる

概要

mime_content_type() 関数はファイルの中身を見て MIME タイプを判定する関数であるが, PHP 7.4 で空ファイル判定に関して大分類カテゴリも変わる破壊的変更が入っている。

7.3
inode/x-empty
7.4
application/x-empty

テストをちゃんと書いてないと地味にバグの原因になりそうなので,ここに注意喚起として共有します。

検証用コード

<?php
var_dump(mime_content_type('data://application/octet-stream;base64,'));

https://3v4l.org/krp5U

データ URI スキーム,こういうときに便利。

独り言

最初 php.net の アップグレードガイド のユーザノートに投稿していたんですが,メンテナによって何故か消されるので諦めて Qiita に書くことにしました。スパムでも質問投稿でもページの話題から脱線してるわけでも無いのに一発削除はちょっと意味がわからない…

有用な投稿がよく分からない検閲に消されるのがかなり不快だったので,金輪際 php.net のユーザノートに投稿することは無いと思います。

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

qcachegrindでみるxdebugプロファイルのあるきかた

業務中、機能を拡張するにあたり、PHPで構築されたサーバサイドの処理状況ならびに負荷をみるためにqcachegrindでプロファイルを観測する機会がありました。

しゅくしょう.png

…ふーん、なるほどね…

…ふんふん…

  • 「全行程の処理割合が100%超えてるように見えるんだがどういうこと?」
  • ツリーどうなってるの?よくわからん」
  • 「結局一番やばいボトルネックどれよ」
  • そもそも見方なんもわからん…」

脳内で複数の私が私会議を始めてしまったので、いろいろ試行して自分なりに運用方法を考えてみることにしました。

⚠注意⚠

当記事全編の主旨は「試行を通して仕様を想像・理解して運用を考える」になっております。参考程度に留めてください:bow_tone1:

環境1

アプリ バージョン
PHP 5.6.15
Xdebug 2.3.2

導入と運用方法

qcachegrindの導入&運用に関しては、こちらの記事を参考にさせていただきました。
XDebugとqcachegrindを使ってPHPアプリのパフォーマンスボトルネックを見つける

試行

ひとまず以下のような、演算+メソッド+標準関数のみのソースで、メソッドを繰り返しcallするコードを用意しました。
これを実行したときのプロファイルを出力してqcachegrindで確認します。

test00.php
<?php
$a = range(1,100);
$b = array();
foreach ($a as $number) {
    $b[] = increaseNumber($number);
}
// 100回callされる
function increaseNumber($number){
    usleep(10000); // 0.1sのdelay
    return $number + 10;
}
var_dump($b);

スクリーンショット 2019-12-02 22.03.28.png

想定通り、ひとくちサイズのプロファイルになりました。

最初に表示したプロファイルは「WEBからのリクエストを或るフレームワークが受信〜画面描画するまで」の処理を表現したものだったので、ControllerのdispatcherやViewへのassignなど、フレームワークの基礎レイヤの処理まで含めて表現していたので、非常に情報量が多く混乱していました。

この試行ではmainプロセスがいくつかのFunction(処理)を呼び出している程度なので、比較的落ち着いて見ることができそうです。

まだわからないことが多いので、色々試行してみます。

最小試行

以下のような、何もしないソースを実行してみました。

kyomu.php
<?php

スクリーンショット 2019-12-02 22.34.23.png

mainプロセスのみのプロファイルになっているようです。ほんの少しですが処理時間もかかっています。

所要時間の単位

最初に実行したコードにて、繰り返し処理されるメソッドで都度0.01sのdelayを与えてみます。

test01.php
<?php
:
// 100回callされる
function increaseNumber($number)
{
    usleep(10000); // 0.01s
    return $number + 10;
}
:

スクリーンショット 2019-12-02 22.46.42.png

追加したusleep()が全処理系で大きな遅延要素になったようです。想定する遅延は10000μs*100=1000000μsで、usleep()の所要時間値が1061362なので、表現単位はμsの模様。

Mapを見てみる

ここで{main}Functionを選択したまま、上ブロックのCallee Mapと下ブロックのCaller Mapを見てみます。
スクリーンショット 2019-12-02 23.20.40.png
上はphp::usleepSelfの数値が、下は{main}Incl.の数値が出てますね。
なんだか遅延が大きすぎて見えにくい予感がしたので、遅延を1000μs毎に抑えたプロファイルで同図を見てみます。
スクリーンショット 2019-12-02 23.24.24.png
上図に違いが出ました。他のFunction名とSelfの値が出てきました。
どうやら周りの色はフチではなくそれぞれのFunctionカラーであり、その包含関係を表現しているようです。

ひとまず、PHPプロセスが呼び出された場合、原初の{main}FunctionのCallee Mapをみて、大きな占有率を持っている末端メソッドに狙いを定めると良さそうです。

なお、これに反して下ブロックのCaller Mapは、「どこからcallされたのか」という全く逆方向に捉えたMapを提示します。

上下のブロックが「callした」or「callされた」という相反する視点の情報を提示することは、覚えておいて損はなさそうです。

同一の関数を複数の処理内でcallした場合

usleep()は、左ブロックにおける1Function(処理)として扱われました。では、複数の箇所から関数を呼び出したとき、どう見えるのでしょうか?
メソッド外でもusleep()をcallしてみます。

test02.php
<?php

$a = range(1,100);
$b = array();
foreach ($a as $number) {
    $b[] = increaseNumber($number);
}
// 100回callされる
function increaseNumber($number)
{
    usleep(1000); // 0.001s
    return $number + 10;
}
usleep(100000); // 0.1s
var_dump($b);

スクリーンショット 2019-12-03 0.33.34.png

下ブロックCall Graphに違いが出ました。どうやら同一Functionの複数箇所からcallしている場合、一つのFunctionにCall Graphと情報が集約されるようです。

ここでFunctionphp::usleepを選択します。

スクリーンショット 2019-12-03 0.36.06.png

Call Graphのフォーカスがphp::usleepに移動しました。よく見ると、各パネルに表示されている数値も{main}を選択している状況から微妙に変わっています。どういうことでしょうか?

どうやら以下のような考え方をすれば良さそうです。

{main}選択中

  1. 自身のFunction{main}は総処理時間237784({main}Incl.
  2. そのうちincreaseNumber{main}がcallしたことによる処理時間が130275(increaseNumberIncl.
  3. php::usleepは、{main}increaseNumberからcallされており、それらによる総処理時間が225254(php:usleepのSelf)

php::usleep選択中

  1. 自身のFunctionphp:usleepは処理時間225254(php:usleepSelf
  2. そのうちincreaseNumberからphp:usleepをcallしたことによる処理時間が125023(increaseNumberIncl.からSelfを除いた数値)
  3. {main}は、直接と、increaseNumberを経由するのとでphp::usleepをcallしており、それらによる総処理時間が225254(php:usleepのSelf)

つまり、あくまで「選択したFunciton」の視点で表現されています。Call Graphを見るときは「選択しているFunctionが主役なんだな」と意識して読むと良さそうです。

画面の見方

色々試行してみたので、そろそろつまるところの「この画面でどういうことができる?わかる?」というところをほぐしていけそうです。

メニューバー

スクリーンショット 2019-12-03 1.07.15のコピー.png

No 読み取れること・使い方
1 Incl.Selfの表示値を、パーセンテージ or 実測値(μs)に切り替える。パーセンテージの場合、{main}FunctionのIncl.が概ね100%になる
2 選択中のFunctionから、呼び出し元Functionに移動できる(おそらくはCall Graph準拠)

左ブロック

スクリーンショット 2019-12-03 1.16.58.png

No 要素名 読み取れること・使い方
1 Incl. includeの略。その処理が呼び出している処理(さらにその処理が呼び出している処理、さらにさらにその処理が…)の処理時間すべてを含めた(include)、合計処理時間
2 self そのFunction内での純粋な処理(演算、代入等)に要した時間。 ここが一番でかいところが最大のボトルネックと考えて概ね差し支えないと思う。なお、別のFunctionをcallしている場合、そのFunctionの処理に要した時間は含まない
3 Called 呼び出し回数。${O(N+1)}$とか実施していたりすると明らかにこの数字に出るため、目安にしやすい

右の上下ブロック

左ブロックで選択したFunctionに応じて情報が表示される上下のブロックです。

わかるものだけボタンごとに説明します。
Callee MapCaller Mapは前項で説明したため割愛。

No ブロック位置 ボタン名 読み取れること
1 Callers どのFunction からcallされた か。Time per callカラムがあり、複数回callしている場合は、1callあたりの処理時間を提示している。当たり前だが末端処理だと顕著にレコードが出る
2 All Callers どのFunction からcallされた か。Callersとほぼ同じだが、こちらは処理時間の換算がCaller Mapと近く、直接呼ばれているもの以外も載っている
3 Callees どのFunction をcallした か。Callersと対になるもの。当たり前だが 冒頭処理だと顕著にレコードが出る
4 All Callees どのFunction をcallした か。Calleesとほぼ同じもので、All Callersと対になるもの。直接呼んでいるもの以外も載っている

なお、随所に出てくるDistanceカラムは、そのFunctionからcallしている別のFunctionがどれくらい離れているかを表現している模様。
そのFunction自体がcallしていれば1、そのFuncitonがcallしたFuncitonがcallしていれば2、...という感じ。

おわり

結局、総処理割合が100%を超えてしまっている直接の原因はわかりませんでした。ただ、総処理数か総処理時間が大きくなるとズレが大きくなるような気がします。
ズレることやズレ自体はあまり気にしなくて良さそうでした。

ひとまず今乗りこなせるツールとして運用が整理できて個人的にはよかったです。

運用さえわかればいくらでも活用できるはず!CIに含ませてやばい処理あったらアラート出して止めるとか!そのときにこのプロファイルをつければ説得力も改善方法もわかりやすいですよね!でももし日本語ドキュメントあるなら読みたい

引き続き、見える化データ化を促進して、考えることを止めないようにやっていきたいです。


  1. 最新のPHP(7.3)だと動作するxdebugがないようだったので、少々レガシーですがLaravel&homesteadを導入した直後の安定した仮想環境下で実施しています 

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

PHP基礎 Part4

概要

PHPの独学で学んだことをアウトプットしていく

前回の内容

PHP基礎 Part3

配列

変数は値を1つしか割り当てることが出来なかったが、配列の場合は複数の値を扱うことができる。
配列から値を出力する時は、配列のデータにそれぞれ割り当てられた「インデックス」を利用する
なお、インデックスは0から始まる

sample.php
<?php
  $fruits = array("りんご","みかん","ぶどう"); // 配列のデータを定義
  echo $fruits[0];                        // インデックスを指定し出力する

// 結果:りんご
?>

また、配列の値を追加したり、上書きする際は以下のようにする

sample.php
<?php
  $fruits = array("りんご","みかん","ぶどう"); // 配列のデータを定義
  $fruits[] = "いちご";                     // 「$配列名[] = 値」とすることで、配列の末尾に値を追加
  echo $fruits[3];                        // インデックスを指定し出力

  $fruits[2] = "パイナップル";               // 配列名にインデックスを指定することで、値を上書き
  echo $fruits[2];                        // インデックスを指定し出力

  // 結果:いちごパイナップル
?>

連想配列

配列と同じように複数のデータを扱う役割があるが、配列とは異なり
データの管理をキーという文字列を活用することである。

sample.php
<?php
  $customer = array(
        "name" => "紀伊田 太郎", // 「"キー名" => 値」という形を取る
        "age"  => 30,          // データとデータの間に「,」を入れる
        "address" => "東京都"
  );

  echo $customer["name"];      // 連想配列の出力は $配列名["キー"] となる

  // 結果:紀伊田 太郎
?>

また、以下のように連想配列に値を加えることも可能

sample.php
<?php
  $customer = array(
        "name" => "紀伊田 太郎", // 「"キー名" => 値」という形を取る
        "age"  => 30,          // データとデータの間に「,」を入れる
        "address" => "東京都"
  );

  $customer["job"] = "engineer";    // 連想配列の値の追加
    echo $customer["job"];          // インデックスを指定し、出力

  // 結果:engineer
?>

次回

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

プログミラミング学習始めて6週間、感動した関数

プログラミングを学んで2ヶ月が経過しました。
主にHTML, CSS, Java Script, PHPの学習をしました。
学んだ事は山のようになるのですが、その中でも今回は個人的に驚いた関数をランキング形式で記載します。
初心者のただの忘備録です。

ベスト3 PHPの日付取得

JSしか知らない時は、日付取得って結構大変なんだなと驚きました。
書いていたコードこちら。

$(function() {
  var now = new Date();
  var y = now.getFullYear();
  var m = now.getMonth() + 1;
  var d = now.getDate();
  var w = now.getDay();
  var wd = ["", "", "", "", "", "", ""];
  const today_time=(y+""+m +""+d+""+"(" + wd[w] + ")");
  console.log(today_time);
  $("#today").text(today_time)
});

いや〜プログラムで日付取得って結構大変じゃん!と毎回思いながらコピペ。
それがPHP学んだ途端に出てきた

<?= date(Y年m月d日)?>

これだけで取得と言うことに衝撃を受けました。
なんやこれ!?この1ヶ月私は何をしてたんや!?
と思いました。

ベスト2 Hash

これすごくないですか!?この4文字導入するだけですごい暗号作ってくれる!!何これ!?
感動しました。

ベスト1 新しいファイル作成

$file = fopen("data/data.txt","a");

PHPのコレです。
あったものに上書きとか追加ならまだわかるものの、ないのを作る、存在しないものをゼロから作ってるんですよ、この1文字です。
授業中思わず声が出た関数です。
みんな結構普通でしたが、すごくないですか!?私びびったんですけど。
ある意味怖いとも感じた関数です。

余談
アルゴリズムの授業で2進数、8進数、16進数を学美ました。私バカなのでイマイチ2進数とかのイメージがつかず、自分で簡単な電卓を作ったんです。理解を深めるために。
その時に驚いたのがparseInto, toString 。
これで10進数が一瞬で2進数に!!
驚きました!!

来週は制作物をあげられれば良いなと思っています。
(ただ今JSと戦っている最中です。機能1個付けるのにまだまだ時間がかかります)

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