20200726のPHPに関する記事は22件です。

Vue + Vue Router + Vuex + Laravel のチュートリアル(@MasahiroHarada 様 作成)を進める最中に詰まった部分の原因と対策

はじめに

こちらの記事は、@MasahiroHarada 様が作成された以下の記事をのチュートリアルを、実際に自分が行ってみて、詰まった部分や勘違いしていた部分を、他のチュートリアルに挑戦される方向けに残したものです。

Laravel 6 & PHP 7.4 対応】Vue + Vue Router + Vuex + Laravel チュートリアル(全16回)を書きました。

本当に素晴らしい記事で、Laravel、Vue、spa設計の知見が深まりました。
この場を借りて、改めて感謝申し上げます。

※記事の執筆にあたり、一応ご本人から確認はいただいておりますが、共著でもないため、間違いやご指摘、ご意見は全て私の方に直接いただきますよう、お願い申し上げます。

それでは、よろしくお願いします。

私の開発環境と前提

私が使用した開発環境です。

Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (3) SPA開発環境とVue Routerの記事を参考に、Dockerで作成しました。

Laravelは7.19.0、OSはMacです。

詳しいことは上記の記事にまとめてくださっていますが、前提条件としてご了承ください。
次章より、実際に詰まった部分と解決策を述べます。

npm run watch で何も表示されない

最初に遭遇したエラーは、(3) SPA開発環境とVue Routerのフロントエンドのビルドの項目で、npm run watchを入力しても何も表示されなかったことです。

これは、SPAの仕組み上当然のことで、バックエンドの(Laravelの)サーバーを立てていなかったことが原因でした。

ターミナル等でシェルをもう一つ用意し、バックエンドのサーバーをphp artisan serve --host 0.0.0.0 --port 8081で立ててから、もう一つの方でnpm run watchを行うと無事表示されました。

Illuminate\Foundation\Auth\RegistersUsers.php がない

(4) 認証APIの部分です。
これは、私と同じLaravelの7系、もしくは6系を使っている方は遭遇するかと思います。

対策の詳細はこちらの公式にも載っています。
https://readouble.com/laravel/7.x/ja/authentication.html

具体的には、以下のコードをcomposerファイルのあるディレクトリで実行すると現れてくれるはずです。

# Laravel6系
composer require laravel/ui:^1.0 --dev

# Laravel7系
composer require laravel/ui

参考記事
更新! Laravel6/7「make:Auth」が無くなった 〜Laravel6/7でのLogin機能の実装方法〜MyMemo

なぜか新規登録ができない

これが個人的には一番びっくりなエラーでした。コードはあっていて新規登録をしようとしても、

POST http://localhost:3000/api/register 405 (Method Not Allowed) message: "The POST method is not supported for this route. Supported methods: GET, HEAD."

というエラーが出ます。これの解決策は、なんとパスワードを8文字以上にすることでした。
Railsのdeviseというログイン機能を作ってくれるライブラリをよく使用していて、そちらは6文字だったので油断しておりました。

参考記事
ユーザー認証のパスワード制限の変更

コントローラーで処理がされない

チュートリアルを進めていき、理解している部分は基本的にコピペで行っていたのですが、それ通りに貼り付けているはずなのにコントローラーが反応してくれない場面がありました。

原因はDIのし忘れ(namespaceの書き忘れ)でした。

Laravelでは別ファイルから使いたい要素を持ってくるときに、use App\Http\Requests\StoreComment;と言った形で主にファイル冒頭に記載をするのですが、内容のコードだけを貼って、こちらをコピペすることを忘れていました。

結果、参照ができずにエラーが出たわけです。しっかりと記事を読んで、コピーのし忘れがないか確認しましょう。

Failed to load resource: the server responded with a status of 405 (Method Not Allowed) が出る

これは、ルーティングに誤りがありました。今回のチュートリアルでは、api.phpとweb.php二つのルーティングがあります。apiを叩く処理を間違った方に書いており、このエラーが発生しました。

「ルーティングを書いている場所はあっているか?」チェックをしてみると解決するかもしれません。

エラーメッセージがインポートできない

(8) エラーハンドリングの章で記載ミス?と思われるものがあります。(2020/7/26現在)コメント欄で@MasahiroHarada 様が回答されていますが、一応記事がそのままだったのでこちらにも記載いたしました。

router.js
import SystemError from './pages/errors/System.vue'
// ここのままだとエラーになるので、
// import SystemError from './pages/errors/500.vue'
// としてあげましょう

/* 中略 */

const routes = [
  /* 中略 */
  {
    path: '/500',
    component: SystemError
  }
]

写真投稿APIのテストが通らない

Dockerで作開発をしていると、 (9) 写真投稿APIのテストが通りません、原因は記事のコメント欄にもあったのですが、一応こちらにまとめます。

基本的には紹介されているこちらの記事通りにやっていただきたいのですが、私はDocker初学者だったので「再ビルドしてください」というのが、どうするのかわかりませんでした。

私の場合docker build -t vuesplash_vuesplash_web .で解決しました。(ピリオドも必要です)

謝辞

以上が、自分がチュートリアル実行時に遭遇したエラーです。初歩的なミスもあり、そのような振り返り学習ができたという点でも、こちらのチュートリアルは素晴らしかったです…本当にありがとうございました。

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

【PHP】2次元配列をキーを指定して並べ替える関数

array_multisortより便利?

array_multisortがあまり理解できていないのが悪いですが、とりあえずユーザ定義関数を作った。

キーを指定して、そのキーの値を基準に並べ替えができるように。

課題は某サイトで与えられたもの。

<?php
    // 自分の得意な言語で
    // Let's チャレンジ!!

    [$x, $y] = explode(" ", trim(fgets(STDIN)) );
    $k = trim(fgets(STDIN));

    $num_data = trim(fgets(STDIN));

    for($i = 0; $i < $num_data; $i++ ) {
        [$xi, $yi, $price] = explode(" ", trim(fgets(STDIN)));
        $datas[$i]['x'] = $xi;
        $datas[$i]['y'] = $yi;
        $datas[$i]['price'] = $price;
        $datas[$i]['distance'] = ( $x - $xi )**2 + ( $y - $yi )**2;
    }

    $sum = 0;
    $sorted = val_sort($datas, 'distance');
    for( $i = 0; $i < $k; $i++ ) {
        $sum += (int)$sorted[$i]['price'];
    }
    $ave = $sum / $k;

    echo $ave .PHP_EOL;

    function val_sort($array, $key) {
    // Loop through and get the values of our specified key
        foreach($array as $k=>$v) {
            $b[] = $v[$key];

        }
        asort($b); // キーと要素を維持しつつ昇順
        foreach($b as $k=>$v) {
            $c[] = $array[$k]; // 展開される順番にキーを取り出して元の配列のキーに指定して代入し直す
        }

        return $c; // 代入し直した配列を返す
    }
?>

勉強になったこと

配列の展開をするときにforeachを使えるのは知っていたけど、キーも一緒に取り出して、そのキーをうまく使うと便利になるものだなということ。

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

MacにLaravelインストールしたメモ

この度初めてMacを購入してLaravelの環境構築を行ったのでメモ。調べながらトライしてなんとか完了。が、2つハマったポイントがありました。

1. システム環境

  ・macOS Catalina10.15.6

   すべてMacのターミナルで作業。
  (ちなみにハマった理由の1つがOSのverとターミナルが関係。)

2. 手順

  Ⅰ.Composerインストール
  以下を実行。

curl -sS https://getcomposer.org/installer | php

つぎにcomposerをどの作業場所からでも呼べるように設定。

mv composer.phar /usr/local/bin/composer

インストール後,以下のコマンドで

composer
   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
Composer version 1.10.9 2020-07-16 12:57:00

上記表示されれば正常にインストール完了。

  Ⅱ.Composerを使ってLaravelインストーラーをインストール

  以下を実行。

composer global require "laravel/installer"

※ハマりポイント1
しかし、ここでエラー出てしまいました。どうもmacにはじめからインストールされているPHPではext-zipに対応してませんよ、ということらしい...(Problem1の2段目の部分)

Changed current directory to /Users/ryo3110/.composer
Using version ^3.2 for laravel/installer
./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Installation request for laravel/installer ^3.2 -> satisfiable by laravel/installer[v3.2.0].
    - laravel/installer v3.2.0 requires ext-zip * -> the requested PHP extension zip is missing from your system.


Installation failed, deleting ./composer.json.

これをPHPをインストールし直すことで対応。MacにはHomebrewというパッケージ管理システムがあるのでこちらを利用する。

以下のコマンドでHomebrewをインストール

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

インストールできたらbrewコマンドでphpをインストールする。普段を使用しているので7.4にした。ここは使い慣れているver.でいいはず。

$ brew install php@7.4

インストールできたら改めてLaravelインストーラーのインストールを試みる。

composer global require "laravel/installer"

無事インストーラーのインストール完了。

Ⅲ.Laravelコマンド使えるようにパスを通す

この時点ではターミナル開いてすぐにLaravelコマンドが使えないのでパスの設定を行う。以下を実行。

echo "export PATH=~/.composer/vendor/bin:$PATH" >> ~/.bash_profile

.bash_profileを読み込み。

source ~/.bash_profile

これでターミナルにlaravelと打てばlaravelコマンドが使える。

※ハマりポイント2
しかしターミナルをと閉じると使えなくなってしまう...どうもパスの設定が保存されていないのではと考えひたすら調べる。。。

https://www.infoq.com/jp/news/2019/07/macos-ditches-bash-for-zsh/

どうやらCatalinaから標準シェルがbashからzshに変わっていることで.bash_profileを読み込まないのが原因と判明。(Mac初めて買ったのに、変わってるとかそんなん知らん...)後ほど気づいたのですが、そもそも使用不可のコマンド打ったとき、zsh: command not foundと表示されるのでちゃんと確認しておくべきでした...

ともあれ、原因わかったので再度トライ。従来のbashにも設定できるようだが、Appleがzshに切り替えたということで今後しばらくその方針になることを考えて、zshを使う方針で対応。

ターミナルのホームディレクトリに.zshrcをつくる。(vimで作業)

vim .zshrc

下記を入力し保存する。

export PATH=$PATH:~/.composer/vendor/bin

これでなんとかターミナルを閉じてもすぐにLaravelコマンドが使えるようになった。

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

ララ「13歳と294日って、どうやって求めるルン?」 on PHP

はじめに

「スター☆トゥインクルプリキュア」第2話(2019年2月10日放送)で、こんなやりとりがありました。

ひかる「ララちゃん1ってすごいよねぇ。ロケット操縦して、修理までしちゃうんだもん」
ララ「いい大人ルン。できて当然ルン」
ひかる「えっ?大人って?いくつなの?」
ララ「地球の年齢だと…」
AI「13歳と294日です」
ひかる「わたしと同い年じゃん!」

さて、今ララの年齢は、地球基準で何歳と何日なのでしょうか?

それをプログラムで算出したいと思って調べたのですが、出てくるのは「何年何ヶ月と何日」というのを求めるものばかり…。
そこで、本記事では「任意の2つの日付間の経過年数と日数」をPHPで求める方法を考えたいと思います。

なおPHP独自のクラスや構文が出てきますが、アルゴリズムは他の言語でも活用できると思います。

前提条件

「任意の2つの日付」を、「基準日(1)」と「基準日(2)」と表すこととします。
なお基準日(1)は、基準日(2)より前であるものとします。

まず、基準日(1)ですが、同年7月7日の第22話でララの誕生日であることに触れられていること、キャラクター設定上も7月7日が誕生日とされていること、そしてひかると同じ「中学2年生」などなどから、第22話の放送の14年前に当たる「2005年7月7日」とします。

日付計算の際には、基準日(1)当日を含めないものとします。

PHPのバージョンは7.2.30を前提としていますが、7系であれば多分大丈夫だと思います。

考え方

基準日(2)によって、次の3パターンに分けます。
基準日(2)を、以下では$y_2年m_2月d_2日$とします。

そして先ほどの「2005年7月7日」は次のように設定します。

y_1 = 2005, m_1 = 7, d_1 = 7

基準日(1)と基準日(2)の月日が同じ場合

数式で表すと、$m_1 = m_2 かつ d_1 = d_2$の場合、となります。
この場合は単純に、$(y_2 - y_1)歳と0日$を返せば良いことになります。

例)基準日(2)が「2019年7月7日」の場合
年数:$2019 - 2005 = 14$
結果:14歳と0日

基準日(1)と基準日(2)の月日が違う場合(パターン1)

ここでは$y_2年m_1月d_1日$が基準日(2)より前の場合を取り上げます。
それ以外の場合は、パターン2を参照してください。

この場合は、年の計算は先ほどと同様に$(y_2 - y_1)$で計算します。
あとは日付を、$y_2年y_2月y_2日 - y_2年m_1月d_1日$(初日不算入)で計算します。

例)基準日(2)が「2019年7月26日」の場合:
年数:$2019 - 2005 = 14$
日数:$2019年7月26日 - 2019年7月7日 = 19$
結果:14歳と19日

例)基準日(2)が「2020年7月26日」の場合:
☆2020年は閏年
年数:$2020 - 2005 = 15$
日数:$2020年7月26日 - 2020年7月7日 = 19$
結果:15歳と19日

基準日(1)と基準日(2)の月日が違う場合(パターン2)

ここでは$y_2年m_1月d_1日$が基準日(2)より後の場合を取り上げます。
それ以外の場合は、パターン1を参照してください。

この場合、年の計算は$(y_2 - y_1 - 1)$になります。
日付ですが、$y_2年y_2月y_2日 - (y_2 - 1)年m_1月d_1日$(初日不算入)で計算します。

例)基準日(2)が「2019年4月27日」の場合
年数:$2019 - 2005 - 1 = 13$
日数:$2019年4月27日 - (2019-1)年7月7日 = 2019年4月27日 - 2018年7月7日 = 294$
結果:13歳と294日

例)基準日(2)が「2020年4月27日」の場合
☆2020年は閏年
年数:$2020 - 2005 - 1 = 14$
日数:$2020年4月27日 - (2020-1)年7月7日 = 2020年4月27日 - 2019年7月7日 = 295$
結果:14歳と295日

例)基準日(2)が「2019年2月27日」の場合
年数:$2019 - 2005 - 1 = 13$
日数:$2019年2月27日 - (2019-1)年7月7日 = 2019年2月27日 - 2018年7月7日 = 235$
結果:13歳と235日

例)基準日(2)が「2020年2月27日」の場合
☆2020年は閏年
年数:$2020 - 2005 - 1 = 14$
日数:$2020年2月27日 - (2020-1)年7月7日 = 2020年2月27日 - 2019年7月7日 = 235$
結果:14歳と235日

実装していきます

長いので折りたたむルン
datediff.php
<?php
/**
 * dateDiffの戻り値クラス
 */
class DateDiffResult {
    /** 年数 */
    public $years;
    /** 日数 */
    public $days;

    function __construct(int $years, int $days) {
        $this->years = $years;
        $this->days = $days;
    }
}

/**
 * 指定年月日の0時0分0秒のDateTimeを返す
 * 
 * @param int $year 年
 * @param int $month 月
 * @param int $day 日
 * @return DateTime
 */
function getDateTime(int $year, int $month, int $day) : DateTime {
    $date = new DateTime();
    $date->setDate($year, $month, $day);
    $date->setTime(0, 0, 0);
    return $date;
}

/**
 * 基準日(1)から基準日(2)まで、何年と何日経過したかを求める(初日不算入)
 * 
 * @param DateTime $inputDate1 基準日(1)
 * @param DateTime $inputDate2 基準日(2)
 * @return DateDiffResult
 */
function dateDiff(DateTime $inputDate1, DateTime $inputDate2) : DateDiffResult {
    // 各変数をセットする
    $y1 = intval($inputDate1->format("Y"));
    $m1 = intval($inputDate1->format("n"));
    $d1 = intval($inputDate1->format("j"));
    $y2 = intval($inputDate2->format("Y"));
    $m2 = intval($inputDate2->format("n"));
    $d2 = intval($inputDate2->format("j"));
    $date1 = getDateTime($y1, $m1, $d1);
    $date2 = getDateTime($y2, $m2, $d2);
    // 基準日(1)と基準日(2)の月日が同じ場合
    if ($m1 == $m2 && $d1 == $d2) {
        return new DateDiffResult($y2 - $y1, 0);
    }
    // y_2年m_1月d_1日
    $date3 = getDateTime($y2, $m1, $d1);

    // y_2年y_2月y_2日 - y_2年m_1月d_1日
    $diff1 = $date3->diff($date2);
    if ($diff1->format("%R") == "+") {
        // y_2年m_1月d_1日が基準日(2)より前の場合
        return new DateDiffResult($y2 - $y1, intval($diff1->format("%a")));
    } else {
        // y_2年m_1月d_1日が基準日(2)より後の場合
        $date4 = getDateTime($y2 - 1, $m1, $d1);
        $diff2 = $date4->diff($date2);
        return new DateDiffResult($y2 - $y1 - 1, intval($diff2->format("%a")));
    }
}

テストします

長いので折りたたむルン
datedifftest.php
<?php
require_once("datediff.php");

use PHPUnit\Framework\TestCase;

class DateDiffTest extends TestCase {

    // 基準日(1)と基準日(2)の月日が同じ場合
    public function testDiff0()
    {
        $date1 = getDateTime(2005, 7, 7);
        $date2 = getDateTime(2019, 7, 7);
        $actual = dateDiff($date1, $date2);
        $this->assertEquals(14, $actual->years);
        $this->assertEquals(0, $actual->days);
    }

    // 基準日(1)と基準日(2)の月日が違う場合(パターン1)その1
    public function testDiff1_1() {
        /**
         * 基準日(2)が「2019年7月26日」の場合:
         * 年数:$2019 - 2005 = 14$
         * 日数:$2019年7月26日 - 2019年7月7日 = 19$
         * 結果:14歳と19日
         */
        $date1 = getDateTime(2005, 7, 7);
        $date2 = getDateTime(2019, 7, 26);
        $actual = dateDiff($date1, $date2);
        $this->assertEquals(14, $actual->years);
        $this->assertEquals(19, $actual->days);
    }

    // 基準日(1)と基準日(2)の月日が違う場合(パターン1)その2
    public function testDiff1_2() {
        /**
         * 基準日(2)が「2020年7月26日」の場合:
         * ☆2020年は閏年
         * 年数:$2020 - 2005 = 15$
         * 日数:$2020年7月26日 - 2020年7月7日 = 19$
         * 結果:15歳と19日
         */
        $date1 = getDateTime(2005, 7, 7);
        $date2 = getDateTime(2020, 7, 26);
        $actual = dateDiff($date1, $date2);
        $this->assertEquals(15, $actual->years);
        $this->assertEquals(19, $actual->days);
    }

    // 基準日(1)と基準日(2)の月日が違う場合(パターン2)その1
    public function testDiff2_1() {
        /**
         * 基準日(2)が「2019年4月27日」の場合
         * 年数:$2019 - 2005 - 1 = 13$
         * 日数:$2019年4月27日 - (2019-1)年7月7日 = 2019年4月27日 - 2018年7月7日 = 294$
         * 結果:13歳と294日
         */
        $date1 = getDateTime(2005, 7, 7);
        $date2 = getDateTime(2019, 4, 27);
        $actual = dateDiff($date1, $date2);
        $this->assertEquals(13, $actual->years);
        $this->assertEquals(294, $actual->days);
    }

    // 基準日(1)と基準日(2)の月日が違う場合(パターン2)その2
    public function testDiff2_2() {
        /**
         * 基準日(2)が「2020年4月27日」の場合
         * ☆2020年は閏年
         * 年数:$2020 - 2005 - 1 = 14$
         * 日数:$2020年4月27日 - (2020-1)年7月7日 = 2020年4月27日 - 2019年7月7日 = 295$
         * 結果:14歳と295日
         */
        $date1 = getDateTime(2005, 7, 7);
        $date2 = getDateTime(2020, 4, 27);
        $actual = dateDiff($date1, $date2);
        $this->assertEquals(14, $actual->years);
        $this->assertEquals(295, $actual->days);
    }

    // 基準日(1)と基準日(2)の月日が違う場合(パターン2)その3
    public function testDiff2_3() {
        /**
         * 基準日(2)が「2019年2月27日」の場合
         * 年数:$2019 - 2005 - 1 = 13$
         * 日数:$2019年2月27日 - (2019-1)年7月7日 = 2019年2月27日 - 2018年7月7日 = 235$
         * 結果:13歳と235日
         */
        $date1 = getDateTime(2005, 7, 7);
        $date2 = getDateTime(2019, 2, 27);
        $actual = dateDiff($date1, $date2);
        $this->assertEquals(13, $actual->years);
        $this->assertEquals(235, $actual->days);
    }

    // 基準日(1)と基準日(2)の月日が違う場合(パターン2)その4
    public function testDiff2_4() {
        /**
         * 基準日(2)が「2020年2月27日」の場合
         * ☆2020年は閏年
         * 年数:$2020 - 2005 - 1 = 14$
         * 日数:$2020年2月27日 - (2020-1)年7月7日 = 2020年2月27日 - 2019年7月7日 = 235$
         * 結果:14歳と235日
         */
        $date1 = getDateTime(2005, 7, 7);
        $date2 = getDateTime(2020, 2, 27);
        $actual = dateDiff($date1, $date2);
        $this->assertEquals(14, $actual->years);
        $this->assertEquals(235, $actual->days);
    }

    // 閏年チェック(1)
    public function testDiff_uruu_1() {
        $date1 = getDateTime(2020, 2, 28);
        $date2 = getDateTime(2020, 3, 1);
        $actual = dateDiff($date1, $date2);
        $this->assertEquals(0, $actual->years);
        $this->assertEquals(2, $actual->days);
    }

    // 閏年チェック(2)
    public function testDiff_uruu_2() {
        $date1 = getDateTime(2020, 2, 28);
        $date2 = getDateTime(2021, 2, 1);
        $actual = dateDiff($date1, $date2);
        $this->assertEquals(0, $actual->years);
        $this->assertEquals(339, $actual->days);
    }
}

作りました

ここまでのものを使って、hokkaidosm.net プリキュアデータ内にララちゃんっていくつなの?を追加しました。
Mastodonでは毎日午前0時に配信予定です。


  1. ひかるがララを呼ぶときに「ララ」と呼ぶのは、第3話の最後のほうからです。 

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

【スクレイピングまとめ】| Python Node.js PHP Ruby Go VBA | 6種類の言語でヤフートップをスクレイピング

Python

動画

IMAGE ALT TEXT HERE

リポジトリ

https://github.com/yuzuru-program/scraping-python-yahoo

ソース

index.py
import urllib.request as request
from bs4 import BeautifulSoup

req = request.Request(
    "https://www.yahoo.co.jp",
    None,
    {}
)

instance = request.urlopen(req)
soup = BeautifulSoup(instance, "html.parser")

li = soup.select('main article section ul')[0].select('li')

for m in li:
    print(m.text)
    print(m.select("a")[0].get("href"))
    print()

Node.js

動画

IMAGE ALT TEXT HERE

リポジトリ

https://github.com/yuzuru-program/scraping-node-yahoo

ソース

package.json
{
  "dependencies": {
    "cheerio": "^1.0.0-rc.3",
    "node-fetch": "^2.6.0"
  }
}
index.js
const fetch = require('node-fetch');
const cheerio = require('cheerio');

const main = async () => {
  // https://www.yahoo.co.jp/にリクエスト投げる
  const _ret = await fetch('https://www.yahoo.co.jp/', {
    method: 'get',
    headers: {
      'User-Agent':
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36',
    },
    referrer: '',
  }).catch((err) => {
    console.log(err);
  });

  if (_ret.status !== 200) {
    console.log(`error status:${_ret.status}`);
    return;
  }

  // jqueryチックに使えるように変換
  const $ = cheerio.load(await _ret.text());

  const _li = $('main article section ul').eq(0).find('li');

  // ヤフートップニュースを表示
  _li.map(function (i) {
    console.log(_li.eq(i).text());
    console.log(_li.eq(i).find('a').attr()['href']);
    console.log();
  });
};

main();

PHP

動画

IMAGE ALT TEXT HERE

リポジトリ

https://github.com/yuzuru-program/scraping-php-yahoo

ソース

index.php
<?php

require_once './phpQuery-onefile.php';

function my_curl($url)
{
  $cp = curl_init();

  /*オプション:リダイレクトされたらリダイレクト先のページを取得する*/
  curl_setopt($cp, CURLOPT_RETURNTRANSFER, 1);

  /*オプション:URLを指定する*/
  curl_setopt($cp, CURLOPT_URL, $url);

  /*オプション:タイムアウト時間を指定する*/
  curl_setopt($cp, CURLOPT_TIMEOUT, 30);

  /*オプション:ユーザーエージェントを指定する*/
  curl_setopt($cp, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36');

  $data = curl_exec($cp);

  curl_close($cp);

  return $data;
}

$url = 'https://www.yahoo.co.jp';
$doc = phpQuery::newDocument(my_curl($url));

$ul = $doc->find('main article section')->find("ul:eq(0)");

for ($i = 0; $i < count($ul->find("li")); ++$i) {
  $li = $ul->find("li:eq($i)");
  echo  $li[0]->text();
  echo "\n";
  echo $li[0]->find("a")->attr('href').PHP_EOL;
  echo "\n";
}
?>

phpQuery-onefile.php

https://github.com/yuzuru-program/scraping-php-yahoo/blob/master/phpQuery-onefile.php

Ruby

動画

IMAGE ALT TEXT HERE

リポジトリ

https://github.com/yuzuru-program/scraping-ruby-yahoo

ソース

index.rb
require "nokogiri"
require "open-uri"

doc = Nokogiri::HTML(open("https://www.yahoo.co.jp"))

test = doc.css("main article section ul")[0].css("li")

test.each do |li|
  puts li.content
  puts li.css("a")[0][:href]
  puts
end

Go

動画

IMAGE ALT TEXT HERE

リポジトリ

https://github.com/yuzuru-program/scraping-go-yahoo

ソース

index.go
package main

import (
  "fmt"
  "log"
  "net/http"

  "github.com/PuerkitoBio/goquery"
)

func main() {
  req, _ := http.NewRequest("GET", "http://yahoo.co.jp", nil)
  req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36")
  res, _ := new(http.Client).Do(req)

  if res.StatusCode != 200 {
    log.Fatalf("status code error: %d %s\n", res.StatusCode, res.Status)
  }

  doc, err := goquery.NewDocumentFromReader(res.Body)
  if err != nil {
    log.Println(err)
  }

  li := doc.Find("main article section ul").Eq(0).Find("li")
  li.Each(func(index int, s *goquery.Selection) {
    fmt.Println(s.Text())

    tmp, err := s.Find("a").Attr("href")
    if err != true {
      log.Fatal(err)
    }
    fmt.Println(tmp + "\n")
  })
}

VBA

動画

IMAGE ALT TEXT HERE

ソース

'Microsoft HTML Object Library
'Microsoft Internet Controls

' IEのプロセスを削除する関数
Function IeProcessKill()
    CreateObject("WScript.Shell").Exec ("taskkill.exe /F /IM iexplore.exe")
    Application.Wait Now + TimeValue("0:00:2")
End Function


'ヤフートップスクレイピング
Sub main()
    Dim ie As InternetExplorer

    ' IEプロセスを削除'
    Call IeProcessKill

    'IE起動
    Set ie = New InternetExplorer

    'サイトを非表示
    ie.Visible = False

    Debug.Print "読み込み中..."
    Debug.Print

    'ヤフー
    ie.Navigate "https://www.yahoo.co.jp/"

    Do While ie.Busy = True Or ie.readyState < READYSTATE_COMPLETE
    Loop

    For Each tmp In ie.document.querySelector("main article section ul").getElementsByTagName("li")
        Debug.Print Trim(tmp.textContent)
        Debug.Print tmp.getElementsByTagName("a")(0).href
        Debug.Print
    Next tmp

    ' ブラウザ閉じる
    ie.Quit
    Set ie = Nothing
End Sub
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[それweb] アップロードされたファイルのウイルススキャンをリモートのClamAVにやらせる [でやるの?]

皆さんこんにちは。

皆さん、ウイルスチェックしてますか?
いや、自分のローカルとかじゃなくて、システム上で、アップロードされたファイルに対して、ですぜ?

そんな必要あるの?って思うかもしれませんが、某R社系列の案件であれば、普通に「やってね♡」って言ってきますし、ファイルのやり取りを頻繁に行うサービスであれば、ウイルスチェックをしないといつ脅威が紛れ込むとも限りませんし、そこから情報流出なんて事態になれば、一発で会社が潰れてもおかしくないわけで、備えておく分にはやはり有用なのではないかと思ったりするわけです。

というわけで、PHPでリモートのClamAVを使ったウイルスチェックをやっていきましょう。

3行で締める

  • ウイルスチェックサーバをWebサーバから切り離して運用したい
  • リモートのウイルスチェックサーバにTCPで通信して、ファイルのスキャンしてもらうためのPHPライブラリを作ったのだ
  • これでアップロードファイルを気軽にウイルスチェックできますね!

まあ、そんな話です

ウイルスチェックの導入

ClamAV

オープンソースのアンチウイルスソフトです。https://www.clamav.net/
Linuxで使えます。

Webサーバでウイルスチェック

以前の案件ではWebサーバの中にPHPだけでなく、ウイルスチェックソフトのClamAVも入れていました。ただ、ClamAV自体は起動が遅いので、clamdというdaemonを使っていたのですが、これがとても重い。メモリをドカ食いするので、コンテナで下手にメモリ上限を入れて置こうものなら、すぐに落ちるくらい重いです。
これをWebサーバの台数だけ用意するのですから、余計なリソースがかかってしまい、経済的ではなかったのです。

text10.png

該当案件の方には本当に申し訳ない。

ウイルスチェックの切り離し

ウイルスチェックの機構を各Webサーバに突っ込むからいけないのであって、こいつを分離してしまえば諸々解決です。

text10-7.png

こうしておけば、WebサーバはWebサーバの仕事のみをしていればよいわけですし、ウイルスチェックのサーバも一つあれば足りるので経済的です。
こっちの形式で実装することを考えましょう。

TCPでClamAVにファイルを送ってウイルスチェックするライブラリ

なんかいいのがなかったので作りました。

https://github.com/niisan-tokyo/web-clamav-php

説明は別記事で書きます。
あるものが使えればいいんです。

書くの忘れてたけど、多分php > 7 で動くと思います。

実装

簡単にアップロードしたファイルをウイルスチェックして、その結果を返すクソアプリを作ります。

環境

みんな大好きdocker-composeで作りますよ。

docker-compose.yml
version: "3"

services:
    workspace:
        build: workspace
        command: sleep infinity
        volumes:
            - ../:/var/www/
        environment:
            - LANGUAGE=en_US.UTF-8
            - LC_ALL=en_US.UTF-8
        ports:
            - 8888:8888

    clamav:
        image: dinkel/clamavd
        ports:
            - 3310

clamavはdockerhubに落ちてるTCPアクセスできるclamavサーバです。これで状況的には以下の先に挙げた図の状態を再現できます。
text10-7.png
workspaceは以下のDockerfileで作られるコンテナで、普通にcomposer入れただけのやつです。

FROM php:7.4

RUN apt-get update && apt-get install -y unzip

COPY --from=composer /usr/bin/composer /usr/bin/composer

あとはdocker-compose up -d でコンテナを立ち上げます。
(私の場合はVS Code Remote Container使ってます。)

アプリを作る

まず、workspace内でライブラリをダウンロードします。

composer init
composer require niisan-tokyo/web-clamav-php

とりあえず、これで必要なライブラリは入ったはずです。

次にフォームを作ります。

index.html
<!DOCTYPE html>
<head>
    <meta charset="utf-8" />
</head>
<body>
    <h1>ファイルを送信</h1>
    <form action="file.php" enctype="multipart/form-data" method="POST">
        <input type="file" name="upfile" /><br>
        <button type="submit" name="go">送信する</button>
    </form>
</body>

最後にアップロードされたファイルをウイルススキャンするページを作ります。

file.php
<?php
require 'vendor/autoload.php';
use Niisan\ClamAV\Manager;

$file = $_FILES['upfile'];

$scanner = new Manager(['url' => 'clamav']);// clamav のあるサーバのurlを指定。IPでも多分大丈夫
$result = $scanner->scan($file['tmp_name']);// 対象ファイルのパスを指定してスキャン

?>
<!DOCTYPE html>
<head>
    <meta charset="utf-8" />
</head>
<body>
<h1>問題<?php if ($result) {?> なし <?php } else {?> あり <?php } ?></h1>
    <a href="index.html">戻る</a>
</body>

とりあえず、こんな感じです。
ClamAVの動くサーバさえ用意できれば、実装は楽勝ですね。

動作確認

では、PHPのビルトインサーバで動作確認しましょう。

php -S 0.0.0.0:8888

まずはフォームにアクセスします。
http://localhost:8888/

webclamindex.png

次に、フォームにファイルを突っ込んで送信します。

webclamfile.png

よさそう。

まとめ

というわけで、リモートのClamAVを使ってファイルのウイルスチェックする機構を実装しました。
今回詳しく説明しなかったライブラリの作成のほうは割と苦労したのですが、サクッと使いたい人にとってはどうでもいいと思ったので、今回は入れませんでした。
ライブラリ除いたら、実装時間は5分くらいしかかからないので、もしも「あ、アップロードされたファイルのウイルスチェックもよろしくね♡」って言われたときは、この記事を参考にしていただけると幸いです。

今回はこんなところです。

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

組み合わせの列挙

phpで組み合わせのコードを書きたいです。

1から9までの重複のない4つの数字を使用し,それらの並びの組み合わせを出力したいです。
例えば

1 2 3 4

という並びの組み合わせは4!=24通りあります。

1234
1243
1324
.
.
.
中略
.
.
.
4321

丸投げする気はなく、このコードを書くためのヒントを頂けたら嬉しいです。

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

Laravelでプロジェクト作成~MVC作成 手順、コマンドまとめ

環境

言語:php html
フレームワーク:Laravel
サーバ環境:xampp
DB:MySQL(xamppで管理)
OS:Windows 10

※xampp、laravel、composer 環境構築は完了済み。

Laravelプロジェクト作成

laravel new プロジェクト名
サーバ起動
php artisan serve

http://localhost:8000/
にて、Laravelページが表示されることを確認。






ここから先MVC作成

コントローラ作成

php artisan make:controller HogeController
コントローラー追記

app/Http/Controllers/HogeController.php

public function contact()
{
    return view("contact"); //viewファイル"contact.blade.php”呼び出し
}

ルーティング作成

routes/web.php

Route::get("contact", "HogeController@contact");
// http://localhost:8000/contact が呼ばれたとき、HogeController内contactメソッド呼び出し

ビュー作成

resources/views/contact.blade.php(手動で新規作成する)

<!DOCTYPE HTML>
<html>
<head>
    <title>contact</title>
</head>
<body>
    <h1>Hello World!!</h1>
</body>
</html>

マイグレーション

※ユーザテーブルはデフォルトでマイグレーションファイルが作成されている。

マイグレーションファイルの作成
php artisan make:migration create_books_table

名前は複数形で作成するのが慣例

マイグレーションファイル書き込み

/database/migrationsフォルダにmigrationファイルが作成される。
この中に、変数を追記する。

Schema::create('books', function (Blueprint $table) {
     $table->id();
     $table->string("title"); //追記
     $table->text("body"); //追記
     //etc...
     $table->timestamps();
});
マイグレーション実行
php artisan migrate
マイグレーションが成功しているかの確認
php artisan migrate:status

モデル作成

php artisan make:model Book

名前は単数形で書くのが慣例(migrateは複数形)
app/ 直下に Book.phpが作成される。

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

Laravelで認可(Policy)

認可

簡単に言うと
認証はアクセスしてきているユーザーが誰なのかを認識するもの
認可はアクセスしてきたユーザーに操作を許可するかどうかを判定するもの
です

Policyを使った認可

簡単に言うと
EloquentのModelに紐づく真偽値を返す関数(Policy)を作っておき、それを簡単に呼び出せるようにしておくようなものです

前提条件

eclipseでLaravel開発環境を構築する。デバッグでブレークポイントをつけて止める。(WindowsもVagrantもdockerも)
本記事は上記が完了している前提で書かれています
プロジェクトの作成もapacheの設定も上記で行っています

Laravelで認可(Gate)
本記事は上記が完了している前提で書かれています
今回使うカラムも上記記事で作成済みです

Policyの作成

今回は下記記事で作成したTable2というModelを使います
Laravelでデータベースを操作する(Eloquent編)

コマンドラインで
cd sample
php artisan make:policy Table2Policy --model=Models\Table2
xdebugの設定をしているとeclipseが実行していいですかというプロンプトを出すのでOKを押します
eclipseプロジェクトを右クリック→リフレッシュ
/sample/app/Policies/Table2Policy.phpが現れます

/sample/app/Policies/Table2Policy.php修正

Table2Policy.php
‥‥
    public function create(User $user)
    {
        return $user->rank >= 2;
    }

    public function update(User $user, Table2 $table2)
    {
        return $user->rank >= $table2->int_col;
    }
‥‥

createメソッドとupdateメソッドを修正しました
Policyのメソッドの第一引数は必ず、ユーザーインスタンスになります

下記7メソッドが自動的にできあがっていると思います
viewAny
view
create
update
delete
restore
forceDelete
これらはコントローラーをつくるときにphp artisan make:controller XXXController --resourceのように--resourceオプションを付けて作成したリソースコントローラーの各メソッドに対応しています

コントローラメソッド ポリシーメソッド
index viewAny
show view
create create
store create
edit update
update update
destroy delete

コントローラーのindexメソッドではPolicyのviewAnyメソッドを使ってください
コントローラーのshowメソッドではPolicyのviewメソッドを使ってください
ということです

Policyの登録

/sample/app/Providers/AuthServiceProvider.php修正

AuthServiceProvider.php
‥‥
    protected $policies = [
        'App\Models\Table2' => 'App\Policies\Table2Policy',
    ];
‥‥

$policies配列に要素を追記しました
keyがModel、valueがPolicyになります
これでLaravelアプリケーションの中でPolicyを簡単に呼び出せるようになります

Controllerにメソッド追加

(1) use文追記
use Illuminate\Support\Facades\Auth;
use App\Models\User;
use App\Models\Table2;

(2) /sample/app/Http/Controllers/SampleController.phpにgate1メソッド、gate2メソッドを追記

    public function policy1()
    {
        return view('sample.policy1');
    }

    public function policy2(Request $request)
    {
        $int_col = $request->input('int_col');

        $user = Auth::user();

        $table2 = new Table2();
        $table2->int_col = $int_col;

        if (is_null($user) || !$user->can('create', Table2::class)) {
            return redirect('sample/policy1')->withInput();
        }
        if (is_null($user) || !$user->can('update', $table2)) {
            return redirect('sample/policy1')->withInput();
        }

        $data = ['table2' => $table2];

        return view('sample.policy2', $data);

    }

Policyは$user->canで呼べます
第一引数に与えた文字列はPolicyに定義したメソッド名です
第二引数はPolicyに定義したメソッドの第二引数になります
第二引数を定義していないPolicyの場合第二引数にはクラス名を渡します
Policyの第一引数は$userになります
$user->canはPolicyが返す真偽値がそのまま戻り値になります

(2) /sample/routes/web.phpに下記を追記
Route::get('sample/policy1', 'SampleController@policy1');
Route::get('sample/policy2', 'SampleController@policy2');

viewの作成

(1) /sample/resources/views/sample/policy1.blade.php

policy1.blade.php
<html>
    <head>
        <title>sample</title>
    </head>
    <body>

        <form action="{{ url('sample/policy2') }}" method="get">
            <div>int_col<input type="text" name="int_col" value="{{ old('int_col') }}"></div>
            <input type="submit" >
        </form>

    </body>
</html>

(2) /sample/resources/views/sample/policy2.blade.php

policy2.blade.php
<html>
    <head>
        <title>sample</title>
    </head>
    <body>

        <div>
        @can('create', App\Models\Table2::class)
            can create
        @elsecan('update', $table2)
            can update
        @else
            else
        @endcan
        </div>

        <div>
        @cannot('create', App\Models\Table2::class)
            cannot create
        @elsecannot('update', $table2)
            cannot update
        @else
            else
        @endcannot
        </div>

    </body>
</html>

blade内でPolicyを使うにはcanやcannotで呼び出せます
@can@cannotを使うことで、更新ボタンを特定のユーザーにだけ表示させたり、または、逆に非表示にしてりできます

動作確認

usersテーブルのrankカラムの値を適当な値にupdateします

update users set rank = 4;

ログインします
http://localhost/laravelSample/
右上のLOGINからログインします

http://localhost/laravelSample/sample/policy1
rankを4にupdateしてので
5以上の値を入力して送信ボタンをクリックするとpolicy1にリダイレクトされ先に進めないと思います
4以下の値を入力して送信ボタンをクリックするとpolicy2に遷移できます
今回はサンプルのため入力値のエラーチェックを一切行っていないので、gate1の入力欄には数値を入れてください。数値でない場合、制御されていない型エラーになります
実際にシステムを開発するときはちゃんとエラーチェックを入れましょう
エラーチェックの実装の仕方は下記に書いてあります
Laravelで入力値エラーチェック(validate)を実装する

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

エンジニア歴10ヶ月目で「PHP7技術者認定初級試験」を受けてみた

はじめに

2019年の9月にエンジニアに転職して、約10ヶ月が経過しました。
PHPも業務で初めて触ったのでエンジニア歴とPHP歴はほぼイコールです。

ネットで色々調べていたら、「PHP7技術者認定初級試験」という検定があることを知りました。
実際に試験を受けて合格したので、
どのような試験で、どのくらいの期間何を試験対策として行ったのかを簡単に共有できればと思います。

  • この試験を受けるか迷ってる方
  • PHPを触り初めて1年くらいの人で自分の理解度の把握をしたいと思われている方
  • 基本構文の習得程度で、FWでPHPを書いている方 などの参考になれば幸いです。

PHP7技術者認定初級試験とは

一般財団法人PHP技術者認定機構が実施している、
PHP技術の習得度合いを測るための試験です。

体系だてたPHP学習の総チェックにいかがですか?という感じで、
検定試験の部類で資格試験ではないです。

PHP5とPHP7の試験があり、
PHP5は上級試験/準上級試験の2つ
PHP7は初級試験のみ
という構成です。

私はPHP7初級試験を受けました。

試験内容はPHPの仕様や基本構文、設定周りなど基礎的な部分となります。
正誤選択形式で、試験会場のPCで受験します。
60分で40問。
7割正解で合格。
試験料は13,200円(税込)
結果は試験終了後にすぐ確認できます。

出題範囲はオライリーの『初めてのPHP』(PHP7版)の内容に沿って出題されます。
実際の出題内容については公言できないので、詳細な出題範囲は公式でチェックしてみてください。
https://www.phpexam.jp/summary/novice7/
スクリーンショット 2020-07-26 16.06.48.png

業務でPHPを使用していれば、問題集を何周かすれば合格できると思います。

試験対策

出題された問題については言及できませんので、
実際に試験当日までどのくらいの期間で何をやったのかをまとめます。
私の試験の結果は850点/1000点でした。

対策期間

  • 2週間(月~金で各日15分~30分程度、土日で2時間程度)

やったこと

おそらく業務でPHPに触っている方は試験問題集をやり込めば問題なく合格レベルまで達せると思います。

その他にPHPの基礎的な書籍で問題集での説明が不十分な箇所や自分の理解が浅いところを補完するとより完璧に近づきます。

公式の出題範囲は初めてのPHPですが、基本的な内容は同じなので同一の書籍がなくても問題ないと思います。
実際に私は独習PHPという書籍を持っていたのでそちらを使用しましたが、全く問題ありませんでした。

この試験でPHPに初めて触れるという方は初めてのPHP試験問題集の順番で進めるのがいいと思います。

対策のポイント

初級試験の内容は基礎的なもので、決して難しい内容ではありません。
基礎構文や条件分岐、繰り返し、配列の操作などは
業務で1年程度PHPでコードを書いていれば改めて勉強する必要はないと思います。

しかし、文章での正誤選択問題となるので細かい部分の理解が必須となります。
実際に問題集や教材で出てくるコードは実際に動かして一つ一つ確認して行くのが大切です。
特に組み込みメソッドの返り値などはしっかり把握する必要があります。

苦労したポイント

私はPHPの基本的な構文を最低限覚え、その後業務でFWでの開発に移ってしまいました。
そのため、PHPの仕様(動作環境)やPHPコマンド、設定周り、など根本的な部分の理解がなく、試験対策を通じて知ることが多かったです(恥ずかしい...)。

実際に問題集の1周目は基本的な条件分岐、ループ、配列操作、webフォームでのやり取り、DB操作、CookieとSessionなど以外は全然解けませんでした。

公式の出題範囲でいうと下記の部分の知識が完全に抜け落ちていました。

  • PHPの特徴
  • ファイルの操作
  • 情報の保存:データベース(PDOのオプションやプリペアドステートメント等の部分)
  • ファイルの操作
  • メールの送信
  • コマンドラインPHP(サーバ起動やドキュメントルートの設定,php.iniや構成ディレクティブあたり)

この辺りは私のようにすぐにFW中心の開発に移ってしまった方は抜けている部分が多いと思います。

オプション指定などについても細かく聞かれるので、問題集に出ている部分は確実に習得する必要があります。

本番の試験は問題集の出題より少しだけ難しく感じました。
より迷わせる問題が多く、問題集の内容を暗記だけではなくしっかり理解しているかを突いてくる問題が多かったです。

まとめ

実際にこの試験を受けてみて、PHPのことを全然知らなかった/理解していなかったと痛感しました。
「言語そのものの理解」を深めることの重要性も知ることができました。
試験対策をして、PHPについての理解が深まったことでFWを使っていてモヤモヤしていた部分がすっきりと理解できる、使えるようになった部分もたくさんありました。

特に有名な試験でもないし、合格したからと言って自身の価値向上に繋がるような試験ではないですが、
言語について再度しっかり学びたい方、これから習得を目指す方にはとてもいい試験だと思いました。

試験を通してPHPにより興味を持ったので、継続して言語について理解を深めていこうと思っています。

最後まで読んで頂きありがとうございました。

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

【PHP】Youtube Data APIによる動画情報取得

Youtube Data APIを試してみました。
PHPの環境があれば、数分で実行まで行けるんじゃないかと思います。
環境:Windows
言語:PHP

コマンドプロンプトで実行できるものです。
これを、ファンクション化したりと、使いまわしできたらいいんじゃないかなと。

参考サイト

公式

事前準備

まず、必要なライブラリをインストール 対象のphpファイルを置くディレクトリとかで。
composer require google/apiclient:^2.0

APIキーの取得

GoogleのAPIとサービス

こちらのAPIキーの取得の手順が参考になります。
APIキーを生成したら、メモっておいてください。

証明書の配置

以下のURLの「証明書の配置」が参考になります。
http://azwoo.hatenablog.com/entry/2017/12/15/004353
cacert.pemをダウンロードし、
「C:\php\extras\ssl\cacert.pem」のように、PHPインストールフォルダ内に配置してください。

コード

youtubeApitest.php
<?php

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

const MAX_SNIPPETS_COUNT = 5;
const DEFAULT_ORDER_TYPE = 'date';
const DEFAULT_TYPE = 'video';
const CHHANNEL_ID = 'UCL6JY2DXJNDOIqCP1CRADng';//某ちゃんねる
const API_KEY = "AIzaSyA-YBJhfKrI-aaaaaaaaaaaaaaaaaaaa";

  // Googleクライアントのインスタンス
  $http = new GuzzleHttp\Client([
    'verify' => 'C:\php\extras\ssl\cacert.pem'
    ]);
  $client = new Google_Client;
  $client->setHttpClient($http);
  $client->setApplicationName("youtube-api-data-test");
  $client->setDeveloperKey(API_KEY);

  // Youtubeのインスタンス
  $youtube = new Google_Service_YouTube($client);

  //ここに好きなYouTubeのチャンネルIDを入れる
  $params['channelId'] = CHHANNEL_ID;
  $params['type'] = DEFAULT_TYPE;
  $params['maxResults'] = MAX_SNIPPETS_COUNT;
  $params['order'] = DEFAULT_ORDER_TYPE;
  try {

      $searchResponse = $youtube->search->listSearch('snippet', $params);

  } catch (Google_Service_Exception $e) {
      echo htmlspecialchars($e->getMessage());
      exit;
  } catch (Google_Exception $e) {
      echo htmlspecialchars($e->getMessage());
      exit;
  }

  foreach ($searchResponse['items'] as $searchResult) {
    switch ($searchResult['id']['kind']) {
      case 'youtube#video':
        $videos = sprintf('<li>%s (%s)</li>',
            $searchResult['snippet']['title'], $searchResult['id']['videoId']);
            echo "タイトル:".$searchResult['snippet']['title']." URL:https://www.youtube.com/watch?v=".$searchResult['id']['videoId'];
        break;

    }
  }

実行コマンド

php  youtubeApitest.php

補足

この定数に、上記の事前準備で取得したAPIキーをセットします。
const API_KEY = "AIzaSyA-YBJhfKrI-aaaaaaaaaaaaaaaaaaaa";

Googleクライアントの初期化をします。上記の「証明書の配置」で配置したパスを「verify」で指定します。
  $http = new GuzzleHttp\Client([
    'verify' => 'C:\php\extras\ssl\cacert.pem'
    ]);
  $client = new Google_Client;
  $client->setHttpClient($http);
  $client->setApplicationName("youtube-api-data-test");
  $client->setDeveloperKey(API_KEY);
これをやらないと、以下の感じがでます。コンポーザーでインストールした「google/apiclient」のバージョンも影響あるらしく、エラー解消しない場合は、再インストールを・・。(自分はそれで解消)
PHP Fatal error:  Uncaught exception 'GuzzleHttp\Exception\RequestException' with message 'cURL error 60: SSL certificate problem: unable to get local issuer certificate (see http://curl.haxx.se/libcurl/c/libcurl-errors.html)' in C:\develop\ga-php\vendor\guzzlehttp\guzzle\src\Handler\CurlFactory.php:187
条件指定をします。パラメータについては、リファレンス Videosの「プロパティ」を見てみてください。
  $params['channelId'] = CHHANNEL_ID;
  $params['type'] = DEFAULT_TYPE;
  $params['maxResults'] = MAX_SNIPPETS_COUNT;
  $params['order'] = DEFAULT_ORDER_TYPE;
取得結果です。echoの所で、動画タイトルとURLを出力しています。取得内容については、先ほどのリファレンス Videosの「プロパティ」を見てみてください。
 foreach ($searchResponse['items'] as $searchResult) {
    switch ($searchResult['id']['kind']) {
      case 'youtube#video':
        $videos = sprintf('<li>%s (%s)</li>',
            $searchResult['snippet']['title'], $searchResult['id']['videoId']);
            echo "タイトル:".$searchResult['snippet']['title']." URL:https://www.youtube.com/watch?v=".$searchResult['id']['videoId'];
        break;

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

【PHP】問題集への解答

ID 10

200万までの素数の合計値を求める

<?php
$start = microtime(true);
play();
$end = microtime(true);
print "\n".'処理時間 = ' . ($end - $start) . '秒'."\n" ;

function play()
{
  printf("%8d",2);
  $sum=2;

    for($i=3;$i<=2000000;$i+=2)
    {
        $k=0;
        for($j=3;$j<=sqrt($i);$j+=2)
        {
            if($i%$j==0)
            {
                $k=1;
                break;
            }
        }

        if($k==0) {
        // printf("%8d",$i);
        $sum+=$i;
      }
    }

    print "\n\n\n$sum\n\n\n" ;

}

ID 16

2^{15}=32768の3+5+2+7+6+8=26
では2^{1000}はなにか

とりあえず2の1000乗を見てみると

print 2**1000;1.0715086071863E+301

指数表記される。
E+Nというのは10のN乗という意味
1.0715086071863E+301

1.0715086071863☓10^{301}

という意味。

<?php
$start = microtime(true);
play();
$end = microtime(true);
print "\n".'処理時間 = ' . ($end - $start) . '秒'."\n" ;

function play(){
  $num=2**500;
  var_dump($num);
  if(strpos((string)$num,'E') !== false){
  //'abcd'のなかに'bc'が含まれている場合
  preg_match('/(.*)E/',(string)$num,$match);
  $num=$match[1];
  $num=str_replace('.', '', $num);

}
var_dump($num);
  preg_match_all('/(\d)/',(string)$num,$match);
  print_r((array_sum($match[0])));

}

53だと思ったが違うみたい

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

日本語諸方言コーパスをDB化して遊ぶ (3) PHP Laravel で操作する

データベースに慣れるために、「日本語諸方言コーパス (COJADS) 」の元データをデータベース化してみました。第3回は前回作成した SQLite3 データベースを PHP Laravel で操作するところまで。完全に自分用の作業メモで、説明もいろいろ足りていないと思いますが、ご容赦ください。

Laravel の設定

Laravel は PHP のフレームワークのひとつです。そんな凝ったものは作らないので素の PHP でもいいんですが、Laravel の案件も多いし、せっかくなので Laravel を使ってみます(使ったことないけど、Ruby on Rails みたいなもんだろうという軽い気持ち)。

PHP と Laravel の導入

Laravel は PHP のフレームワークなので、まず PHP 環境を作って、依存関係管理の Composer を利用して Laravel を導入していきます。PHP 環境は XAMPP にしました。Windows 環境なのでインストーラーでさくっとインストールします(今回は PHP 7.4.8、Laravel 7.21.0 を使用しています)。

SQLite の有効化

SQLite をデータベースとして使えるように PHP の設定を変更します。私の環境では XAMPP でインストールした時点で有効化されていましたが、念のため php.exe と同じフォルダの設定ファイル php.ini を確認して、以下の行が ; でコメントアウトされていないことを確認しておきます。

php.ini
extension=pdo_sqlite

プロジェクトの作成

Composer で適当な場所に Laravel プロジェクトを作成します。今回は XAMPP フォルダに _projects フォルダを作成し、その中で cojads プロジェクトを管理することにします。

cmd
C:\xampp\_projects>composer create-project laravel/laravel --prefer-dist cojads

cojads プロジェクトの設定から、データベースとして SQLite を使用するように変更しておきます。DB_CONNECTIONsqlite にして、それ以降のデータベースの設定項目は不要なので、削除するかコメントアウトします。

.env内
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

↓↓↓次のように変更する↓↓↓

DB_CONNECTION=sqlite
#DB_HOST=127.0.0.1
#DB_PORT=3306
#DB_DATABASE=homestead
#DB_USERNAME=homestead
#DB_PASSWORD=secret

/config/database.php のデフォルト設定も、mysql になっているので sqlite に変更しておきます。

config/database.php内
'default' => env('DB_CONNECTION', 'mysql'),

↓↓↓次のように変更する↓↓↓

'default' => env('DB_CONNECTION', 'sqlite'),

データベースの設定

既存データベースのマイグレーション

まずはマイグレーションを行ないます。前回作成した cojads.sqlite3database.sqlite に改名して 1プロジェクトの database フォルダに配置します。マイグレーションは php artisan migrate コマンドで行なえます。

cmd
C:\xampp\_projects\cojads>php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (0.32 seconds)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (0.13 seconds)

モデルの作成

次に Model を設定します。初期段階では Model 用のフォルダもないので、ひとまず app フォルダに Models フォルダを新規作成し、以下のようにテーブルごとにモデルを作成していきます。

cmd
php artisan make:model Models/prefecture
php artisan make:model Models/location
php artisan make:model Models/speaker
php artisan make:model Models/discourse
php artisan make:model Models/utterance

そうすると、Models フォルダ内にそれぞれのモデルのクラス設定ファイルが作成されています。作成した段階ではクラス宣言のみで何の役にも立たないので、たとえば speaker テーブルなら以下のように属性とゲッターメソッドを書き込んでいきます。$guarded には主キー speakerId を設定します。

app/Models/speaker.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;

class Speaker extends Model{
    protected $table = 'speaker';
    protected $guarded = array('speakerId');
    public $timestamps = false;

    public function getData(){
        $data = DB::table($this->table)->get();
        return $data;
    }
}

ルーティングとサンプルページの作成

今回はデータベースの中身を閲覧できるシンプルなページを作ります。今回は「話者 (speaker) 」を閲覧できるページを作りましょう。まず routes/web.php にルーティングを追加します。以下では cojads/speaker に GET アクセスすると CojadsController コントローラーの speaker アクションを実行するようにしています。

web.php
Route::get('cojads/speaker', 'CojadsController@speaker');

次にルーティングに沿って CojadsController コントローラーを新規作成して……

cmd
php artisan make:controller CojadsController

さっき指定した Speaker モデルに関する speaker アクションだけ作ります。ここではデータを取得して cojads.speaker ビュー、つまり views/cojads フォルダの中の speaker という名前のファイルに値を送るようにしています。

app/Http/Controllers/CojadsController.php
<?php
namespace App\Http\Controllers;
use App\Models\Speaker;

class CojadsController extends Controller{
  public function speaker(){
    $md = new Speaker();
    $data = $md->getData();
    return view('cojads.speaker', ['data' => $data]);
  }
}

最後に今のアクションの指定に沿って /resources/views フォルダに cojads フォルダを作り、その中にビューを作成していきます。ここでは blade 記法を使うので speaker.blade.php を作成して、以下のようにテーブルを作って speaker テーブルの内容を順に描画していきます。

/resouces/views/cojads/speaker.blade.php
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>Show Speakers</title>
  </head>
  <body>
    <table>
      <thead>
        <tr>
          <th>話者ID</th>
          <th>性別</th>
          <th>生年</th>
        </tr>
      </thead>
      <tbody>
        @foreach($data as $d)
        <tr>
          <td>{{$d->speakerId}}</td>
          <td>{{$d->speakerSex}}</td>
          <td>{{$d->speakerBirthyear}}</td>
        </tr>
        @endforeach
      </tbody>
    </table>
  </body>
</html>

ここまでの作業が済んだら、正しく表示されるか確認します。php artisan serve を実行してローカルサーバを立ち上げて……

C:\xampp\_projects\cojads>php artisan serve
Laravel development server started: http://127.0.0.1:8000

先ほどルーティングした通り http://127.0.0.1:8000/cojads/speaker にアクセスします。

table.png

きちんと表示されていますね( CSS は適当にあてています)。

次回予告

次回は検索機能など、もうちょっとリッチな機能を実装します。


  1. これは Laravel がデフォルトで database フォルダの database.sqlite を参照する設定になっているからです。ファイル名を変えたくない場合は、config/database.php の内で database.sqlite を参照している部分を任意のファイル名に変更するか、ルートの .env 内でデータベースの場所をフルパスで指定する必要があります。たとえば DB_DATABASE=C:\xampp\_projects\cojads\database\cojads.sqlite3 など。 

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

【PHP】素数の計算

素数計算を高速化する。
こちらの記事を自分なりに備忘録としてまとめました。

1万以下の素数のコード

最初に書いたコード

$start = microtime(true);
play();
$end = microtime(true);
print "\n".'処理時間 = ' . ($end - $start) . '秒'."\n" ;

function play()
{  
    for ($i=max($prime)+1;$i<100000; $i++) {
        if (checkPrime($i)) {
          print $i."\n";
        }
    }
}

function checkPrime($num)
{
    $count=0;
    for ($i=2; $i <$num ; $i++) {
        if ($num%$i==0) {
            return false;
        }
    }
    return true;
}

2つの関数で生成する方法しか思いつかなかった

処理時間

処理時間 = 19.969306945801秒

約20秒かかった。

最初から考えて作り直す

素数は約数が1と自分自身のみ

その数が1と自分自身の2回だけ割り切れたならそれは素数

    for ($i=1; $i <10000 ; $i++) {
      // print $i."\n";
      $yakusuu=0;
        for ($j=1; $j <=$i ; $j++) {
            if ($i%$j==0) {
                $yakusuu++;
            }
        }
        if ($yakusuu==2) {
            print $i."\n";
            // break;
        }
    }

処理時間

処理時間 = 1.4896070957184秒

高速化

素数の約数は1と自分自身のみ

ということは奇数のみをループにかければいい
もちろん2は除く。

    printf('%5d',2);
    for ($i=3; $i <10000 ; $i+=2) {
      // print $i."\n";
      $yakusuu=0;
        for ($j=3; $j <=$i ; $j+=2) {
            if ($i%$j==0) {
                $yakusuu++;
            }
        }
        if ($yakusuu==1) {
            printf('%5d',$i);
            // break;
        }
    }
            print "\n";


##処理時間

処理時間 = 0.39670991897583秒
```

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

LaravelでAPIを作成

背景

Unityで使用するAPIをPHPで作成した時のまとめ

開発環境

  • PHP v7.3.20
  • Laravel v7.20.0
  • Apache 2.4.43

手順

  • コントローラ作成
php artisan make:controller API/APIController  

コントローラにindexメソッドを作成する

  • route設定
api.php  
Route::match(['get', 'post'], '/path', 'API\APIController@index')  
  • ローカルサーバを立ち上げる
php artisan serve  
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HerokuにデプロイしたLaravelアプリをURL正規化する

はじめに

HerokuにデプロイしたLaravelのWebサービスをURL正規化しようとしましたが、
.htaccessのリダイレクトが有効にならずにハマったのでMiddlewareでリダイレクトしました。

やりたいこと

  • 「/index.php」を「/」にリダイレクトしたい
  • Herokuドメイン「***.herokuapp.com」を独自ドメイン「***.com」にリダイレクトしたい

この記事で説明しないこと

  • Herokuの独自ドメイン設定
  • 独自ドメインSSL化

環境

Laravel

$ php artisan --version
Laravel Framework 7.19.0

作業内容

URL正規化用のMiddleware登録

GithubからMiddlewareをダウンロード

GithubにLaravelでURL正規化する為のMiddlewareを公開されている方がいたので使用させて頂きました。
- 参考:Laravel Middleware で URL を正規化する例です。

ダウンロードしたファイルを以下フォルダへコピーします。

.
├── app
│   └── Http
│       └── Middleware
│           └── Normalize.php

Normalize.phpを変更

handle()

先頭に独自ドメインへのリダイレクトを追加します。
※ドメインの***の部分は読み替えてください。

/app/Http/Middleware/Normalize.php
/**
 * Handle an incoming request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Closure  $next
 * @return mixed
 */
public function handle($request, Closure $next)
{
    // ここから
    if ($_SERVER['HTTP_HOST'] == '***.herokuapp.com') {
        return redirect()->to('https://***.com' . $request->getRequestUri(), 301, [], $secure = true);
    }
    // ここまで

    $url = $_SERVER['REQUEST_URI']; // PHP 標準のリクエストを使用
    $normalized = $this->normalize($url);
    if ($url != $normalized) {
        header('Location: ' . $normalized, true, 301); // PHP 標準のリダイレクトを使用
        exit();
    }

    ....

normalize()

今回はパスの末尾をスラッシュで置き換える必要が無かったためスラッシュ追加部分をコメントアウトしました。
また、ローカルではlocalhost:8080ポートで動かしているためhostとportを追加しました。

/app/Http/Middleware/Normalize.php
/**
 * @param $url
 * @return string
 */
private function normalize($url)
{
    // 'host'、'port'を追加
    $elements = ['scheme', 'host', 'port', 'path', 'query', 'fragment'];

    $isHtmlEncoded = $url !== ($decoded = htmlspecialchars_decode($url));
    $url = $decoded;
    $parsed = parse_url($url);
    foreach ($elements as $element) {
        $parsed[$element] = $parsed[$element] ?? '';
    }
    parse_str($parsed['query'], $params);
    preg_match('/(.*)\/(.*)/', $parsed['path'], $matches);
    $dirname = $matches[1] ?? '';
    $dirname .= '/';
    $basename = $matches[2] ?? '';

    /* 今回不要のためコメントアウト
    // パス末尾に / を追加
    if ($basename !== '' && mb_strpos($basename, '.') === false) {
        $dirname .= $basename . '/';
        $basename = '';
    }
    */

    // index.* を削除
    $basename = preg_replace('/^index\.(.*)/', '', $basename);
    // パスの // -> / 置き換え
    $dirname = preg_replace('/\/\/+/', '/', $dirname);
    // クエリ並び替え
    ksort($params);

    $parsed['scheme'] .= empty($parsed['scheme']) ? '' : '://';
    // 'port'を追加
    $parsed['port'] = empty($parsed['port']) ? '' : ':'.$parsed['port'];
    ....

Middlewareを登録

Normalizeをグローバルミドルウェアとして登録します。

/app/Http/Kernel.php
protected $middleware = [
    ....

    \App\Http\Middleware\Normalize::class, // <= 追記
    ];

確認

あとはHerokuへデプロイしリダイレクトが確認できれば完了です。

参考にしたサイト

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

HerokuにデプロイしたLaravelアプリのURLを正規化する

はじめに

HerokuにデプロイしたLaravelのURLを正規化しようとしましたが、
.htaccessのリダイレクトが有効にならずにハマったのでMiddlewareでリダイレクトしました。

やりたいこと

  • 「/index.php」を「/」にリダイレクトしたい
  • Herokuドメイン「***.herokuapp.com」を独自ドメイン「***.com」にリダイレクトしたい

この記事で説明しないこと

  • Herokuの独自ドメイン設定
  • 独自ドメインSSL化

環境

Laravel

$ php artisan --version
Laravel Framework 7.19.0

作業内容

URL正規化用のMiddleware登録

GithubからMiddlewareをダウンロード

GithubにLaravelでURL正規化する為のMiddlewareを公開されている方がいたので使用させて頂きました。
- 参考:Laravel Middleware で URL を正規化する例です。

ダウンロードしたファイルを以下フォルダへコピーします。

.
├── app
│   └── Http
│       └── Middleware
│           └── Normalize.php

Normalize.phpを変更

handle()

先頭に独自ドメインへのリダイレクトを追加します。
※ドメインの***の部分は読み替えてください。

/app/Http/Middleware/Normalize.php
/**
 * Handle an incoming request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Closure  $next
 * @return mixed
 */
public function handle($request, Closure $next)
{
    // ここから
    if ($_SERVER['HTTP_HOST'] == '***.herokuapp.com') {
        return redirect()->to('https://***.com' . $request->getRequestUri(), 301, [], $secure = true);
    }
    // ここまで

    $url = $_SERVER['REQUEST_URI']; // PHP 標準のリクエストを使用
    $normalized = $this->normalize($url);
    if ($url != $normalized) {
        header('Location: ' . $normalized, true, 301); // PHP 標準のリダイレクトを使用
        exit();
    }

    ....

normalize()

今回はパスの末尾をスラッシュで置き換える必要が無かったためスラッシュ追加部分をコメントアウトしました。
また、ローカルではlocalhost:8080ポートで動かしているためhostとportを追加しました。

/app/Http/Middleware/Normalize.php
/**
 * @param $url
 * @return string
 */
private function normalize($url)
{
    // 'host'、'port'を追加
    $elements = ['scheme', 'host', 'port', 'path', 'query', 'fragment'];

    $isHtmlEncoded = $url !== ($decoded = htmlspecialchars_decode($url));
    $url = $decoded;
    $parsed = parse_url($url);
    foreach ($elements as $element) {
        $parsed[$element] = $parsed[$element] ?? '';
    }
    parse_str($parsed['query'], $params);
    preg_match('/(.*)\/(.*)/', $parsed['path'], $matches);
    $dirname = $matches[1] ?? '';
    $dirname .= '/';
    $basename = $matches[2] ?? '';

    /* 今回不要のためコメントアウト
    // パス末尾に / を追加
    if ($basename !== '' && mb_strpos($basename, '.') === false) {
        $dirname .= $basename . '/';
        $basename = '';
    }
    */

    // index.* を削除
    $basename = preg_replace('/^index\.(.*)/', '', $basename);
    // パスの // -> / 置き換え
    $dirname = preg_replace('/\/\/+/', '/', $dirname);
    // クエリ並び替え
    ksort($params);

    $parsed['scheme'] .= empty($parsed['scheme']) ? '' : '://';
    // 'port'を追加
    $parsed['port'] = empty($parsed['port']) ? '' : ':'.$parsed['port'];
    ....

Middlewareを登録

Normalizeをグローバルミドルウェアとして登録します。

/app/Http/Kernel.php
protected $middleware = [
    ....

    \App\Http\Middleware\Normalize::class, // <= 追記
    ];

確認

あとはHerokuへデプロイしリダイレクトが確認できれば完了です。

参考にしたサイト

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

障害・エラー発生時に原因を特定するための勘所まとめ(WEBエンジニア向け)

こんな人に読んで欲しい

  • サーバーサイドエンジニアの人
  • 障害・エラー発生時にあわわわわってなってしまう人
  • 担当しているサービスをあんまり理解してないから調べ方わからん、と言う人
  • どこから調べればよいかわからない人

何について話す?

WEBエンジニアとして、サービスを提供する開発をしていると

  • リリース時
  • 運用中
  • 何もしていないのに突然

などエラーや障害が発生することがあるでしょう。
自分が所属するチームでは一つのコンテンツを長く管理しつつ、
他チーム離散など政治的事情で新しいコンテンツを受け入れる機会が多いです。
新しいコンテンツを受け入れた際に、突然来ます、エラーが、障害が・・・

そんなエラーが発生した際にコンテンツへの理解がなくても原因を特定できた、
という機会がままあります。
(テーブル構成とかアーキテクトとか全く知らないけど、エラーを見つけて対応もできた、とか)

そのような経験を通して、コンテンツへの理解があるからこそ調べられる調査と、
理解がなくても調べられる調査があると感じました。

よくわからないコンテンツでもある程度の調査ができる勘所を紹介していきます。

調査能力を身に付けるための技術を身に付ける

コンテンツへの理解がなくても調べられる、と言いましたが技術的な知識は必要です。
AWSを扱っているのであれば「CloudWatchLogsってなんぞ?」
Linuxサーバで構築しているのであれば「apacheってなんぞ?log出力されているところってどこぞ?」
などと言われてしまうと調査ハードルは格段に上がると思います。

以下の資格を取れる程度の知識を身に付けておくことをおすすめします。

  • AWSソリューションアーキテクト(最低限クラウドプラクティショナー程度)
  • LinuC(最低限Level1程度)

これらの知識がなくてもこれから提案する調査の勘所は参考になると思うので、
引き続き見てもらえますと幸いです。

障害発生時フェーズ

1.緊急レベルを判断する

まずこのエラーは即時対応する必要があるのか、今週中に対応すればいいのか、
それとも放置してもいいものなのかを判断する必要があります。
会社などで決められているルールがあればそれを遵守すればいいですが、自分であれば以下のような基準で考えています。

即時対応レベル

  • サービス提供が不可能な状態が発生している(操作できない・不正データが登録される等)
  • サービスは提供できているが利用者のユーザビリティを著しく落としている(画面の表示崩れ等)
  • 簡易的な攻撃で不正動作ができてしまう状態(パラメータ操作やテキストエリアでSQLインジェクションができる等)

近日対応レベル

  • 社内管理画面、運用をする際に弊害があるが、代替手段が取れる状態
  • ユーザへのサービスに影響がないエラーログ等が出ている(Noticeエラーなど)

放置レベル

  • ユーザーが使う頻度が極端に少ないコンテンツでサービス提供はできている状態でのエラー
  • バッチなどで自動リカバリする仕組みが存在しているもの

2.上司・関係者に報告する

エラーレベルを判断したら関係者に報告しましょう。
上記で緊急対応レベルとわかったら即座に報告した方がいいです。
このような報告を怠ってエラー解消に尽力しがちですが、あとで何故すぐに報告しなかったんだ!!となってしまいます。障害発生を関係者が検知できていないと、関係者が行うべき対策が遅れてしまうことになります。他の人にお願いするでもいいので報告しましょう。

また、自分起因のエラーが発生した時に

この事象を隠蔽することができないか

頭をよぎることがあると思います。
自分も何度も頭をよぎりますが、これは隠蔽しようとしてばれた時が最も怖いです。
自身に対する信頼が失墜します。誠実さを示して関係者に正直に報告しましょう。

ログ調査フェーズ

ここからが調査についてです。

1.障害が発生しているサーバーを特定する(知っている場合は省略可)

心当たりがあるサーバに接続して以下の部分を調べます。
apache設定を見て、対象コンテンツのディレクトリ設定があれば対象サーバになります。
apache設定がそもそもどこにあるかも特定する必要がありますが、以下で動いている可能性が高いです。

OSとインストール方法 ディレクトリ
CentOS,RHEL,Fedoraなど(パッケージ) /etc/httpd/conf/
SUSE,Debian,MacOSなど(パッケージ) /etc/apache2/
ソースインストール /usr/local/apache2/conf/

以下のコマンドを入力すればある程度のapacheの場所がわかると思います。

$ ps aux | grep httpd

▼参考Qiita
Apacheの設定ファイル httpd.confの場所

2.ログ出力の場所を特定する

対象ソースの中からログをどこに吐き出しているかソースをgrepしましょう。
ログ設定は以下のようなパターンが多いと思います。

  • アプリケーション独自に定義している
  • プログラミング言語のフレームワークの設定で定義している
  • apacheでログ設定されている
  • 各ミドルウェア・プログラミング言語のデフォルトのログ出力場所(/var/log/php等)

もしもソースにそのような設定が無かった場合、apacheを見たり会社独自のナレッジサイトがあるならそこから調査するのもいいと思います。

3.ログファイルから対象エラーログを特定する

対象のディレクトリ設定までわかれば、実際にログファイルの中で原因となるエラーを特定します。
もし対象ディレクトリ内に複数ファイルがあってどのファイルかわからない場合、障害が起きているサービスを動かしながら以下のコマンドを実行するといいです。

find [ログディレクトリ] -mmin -10

このコマンドを実行することで、対象サービスを動かした際にログが出力されたファイルが何なのか、ある程度特定することができます。

対象のファイルがわかったらファイル内をgrepしてみましょう。
grepの仕方は様々ですが以下のように調べていくといいと思います。

cat [対象ログファイル] | grep -i error
cat [対象ログファイル] | grep -i exception

とか、エラー系の文言でgrepすると引っかかると思います。もしソースのログ出力で特定のメッセージを出力することがわかっているなら、そのメッセージでgrepしてみるといいと思います。

もしくは以下のようにtailfコマンドをしつつ、エラーが発生しているサービスを動かして吐き出されたログを見ていく方法もおすすめです。

tailf [対象ログファイル]

エラーログが出力されていない場合

アクセスログを見てそもそもサーバまで到達しているか見る

コンテンツによってはエラーログが出力されていないものもあると思います。
本来であればエラーログは出るはずなのに出ていないと言うものもあるかもしれません。
その切り分けをしたいのであれば、アクセスログを見てサーバまで到達しているかを確認すると良いと思います。
もし到達していない場合はネットワーク側の問題かもしれません(今回WEBエンジニア向けなのでネットワーク問題は割愛します)

エラーログを仕込む

そもそもソースでエラーログが出力されていない場合はエラーログを出力する処理を組み込んだ上でリリースして計測した方がいいかもしれません。かなりクリティカルな場合はインフラ側でアクセスを止めてメンテナンス画面にするなど、サービスを止める選択も関係者と相談する必要があります。

WEBコンテンツであればデベロッパーツールでエラーが出ていないか確認する

ログが出ていなければそもそもサーバ側が原因ではなく、
フロント側のJSなどでエラーが発生している可能性は大いにあります。

GoogleChromeなどでOption+Command+iでデベロッパーツールを開き、「Console」から何かエラーが発生していないか確認するといいと思います。(以下のような感じ)

Qiita_と__2__英語のアウトプットに使えるツール_無料編__-_YouTube.png

プログラム調査フェーズ

ここからはログである程度のエラーを特定したら、そのエラーを元にプログラムにどのような問題があるか調査していきます。

1.調査はサーバ上で行わない

まず、多くの人は対象ソースの調査をログ調査に引き続きサーバで行いがちな印象があります。
もし調査している人がlinuxコマンドをある程度体得しており、vimmerであれば止めないのですが個人的に対象ソースをSCPツール等でローカルに落としてきて、エディタや開発統合環境を使用して調査した方が良いです。

ローカルで調査をするメリット

  • 開発統合環境やエディタなどで関数ジャンプができる(プラグインを入れておきましょう)
  • 全体grepを行った後に、対象ソースに飛びやすい(個人的におすすめのエディタはsublime)

2.各プログラム言語のデバッグ方法を理解する

テスト環境やローカル環境でも同事象が発生しているが、原因がわからない場合、
愚直ですが一つ一つの処理を追っていって原因を特定する必要があると思います。
そこで必要になってくる技術がプログラミングのデバッグです。

言語にあったdebug方法を理解しておくことで、原因特定までの速度が格段に上がると思います。

Printデバッグを極力使わないように意識する

Printデバッグというのは

echo $test
exit

などのプリントして変数の内容を確認しながらexitで処理を止めたりしてデバッグしていく方法です。
この方法は簡単かつ脳死しながらデバッグをすることができるのですが、各プログラミング言語にはよりわかりやすくデバッグする仕組みが存在するので、そちらを積極的に使用していくべきです。
例えば以下のような例になります。

言語 デバッグ方法
PHP debug_backtrace関数を使用したPrintデバッグをする、xdebugを使用する
NodeJS debugコマンドを使用する
Ruby byebugコマンドを使用する

調査結果報告フェーズ

調査をある程度することができたら、再度関係各署に報告を行います。
報告する内容は以下のような項目です。

1.報告する内容

  • 発生した事象
  • 対象のサービス
  • 発生期間
  • 発生期間中のサービスへのアクセス数、障害影響件数
  • 発生した原因
  • 対応内容(暫定対応と恒久対応の提示)
  • 再発防止策

など提示して報告すれば良いかと思います。

2.障害影響件数の抽出方法

この障害によってどの程度の影響があったか?は報告時に提示すべき項目かと思います。
その際に件数洗い出しで見る部分はアクセスログとエラーログです。

① エラーログ件数ー実際その障害によりエラーが発生した件数はいくつか
② アクセスログ件数ー障害発生中にどの程度のアクセスが来ていたか

基本的には①の件数を出せば良いのですが、②は画面崩れなどフロント側で発生していたエラーに対する件数を図る際に抽出すると良いと思います。

kibanaなどのログをGUI化するサービスを使用していない場合は、愚直にcutコマンドやxargsを駆使して対象エラー件数を抽出しましょう。

さいごに

いかがでしたでしょうか。
会社によって環境は様々なので、障害調査に関する正解をここで提示できないのですが、
自分の経験から他のところでも上記のような観点で調査をすれば、原因特定が早いのではないかと思いまとめさせていただきました。
ご参考いただければ幸いと思いつつ、もっと良い観点での調査方法があればコメントいただけますと幸いです。

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

サービスで障害が起きた時の調査方法まとめ(WEBエンジニア向け)

こんな人に読んで欲しい

  • サーバーサイドエンジニアの人
  • 障害・エラー発生時にあわわわわってなってしまう人
  • 担当しているサービスをあんまり理解してないから調べ方わからん、と言う人
  • どこから調べればよいかわからない人

何について話す?

WEBエンジニアとして、サービスを提供する開発をしていると

  • リリース時
  • 運用中
  • 何もしていないのに突然

などエラーや障害が発生することがあるでしょう。
自分が所属するチームでは一つのコンテンツを長く管理しつつ、
他チーム離散など政治的事情で新しいコンテンツを受け入れる機会が多いです。
新しいコンテンツを受け入れた際に、突然来ます、エラーが、障害が・・・

そんなエラーが発生した際にコンテンツへの理解がなくても原因を特定できた、
という機会がままあります。
(テーブル構成とかアーキテクトとか全く知らないけど、エラーを見つけて対応もできた、とか)

そのような経験を通して、コンテンツへの理解があるからこそ調べられる調査と、
理解がなくても調べられる調査があると感じました。

よくわからないコンテンツでもある程度の調査ができる勘所を紹介していきます。

調査能力を身に付けるための技術を身に付ける

コンテンツへの理解がなくても調べられる、と言いましたが技術的な知識は必要です。
AWSを扱っているのであれば「CloudWatchLogsってなんぞ?」
Linuxサーバで構築しているのであれば「apacheってなんぞ?log出力されているところってどこぞ?」
などと言われてしまうと調査ハードルは格段に上がると思います。

以下の資格を取れる程度の知識を身に付けておくことをおすすめします。

  • AWSソリューションアーキテクト(最低限クラウドプラクティショナー程度)
  • LinuC(最低限Level1程度)

これらの知識がなくてもこれから提案する調査の勘所は参考になると思うので、
引き続き見てもらえますと幸いです。

障害発生時フェーズ

1.緊急レベルを判断する

まずこのエラーは即時対応する必要があるのか、今週中に対応すればいいのか、
それとも放置してもいいものなのかを判断する必要があります。
会社などで決められているルールがあればそれを遵守すればいいですが、自分であれば以下のような基準で考えています。

即時対応レベル

  • サービス提供が不可能な状態が発生している(操作できない・不正データが登録される等)
  • サービスは提供できているが利用者のユーザビリティを著しく落としている(画面の表示崩れ等)
  • 簡易的な攻撃で不正動作ができてしまう状態(パラメータ操作やテキストエリアでSQLインジェクションができる等)

近日対応レベル

  • 社内管理画面、運用をする際に弊害があるが、代替手段が取れる状態
  • ユーザへのサービスに影響がないエラーログ等が出ている(Noticeエラーなど)

放置レベル

  • ユーザーが使う頻度が極端に少ないコンテンツでサービス提供はできている状態でのエラー
  • バッチなどで自動リカバリする仕組みが存在しているもの

2.上司・関係者に報告する

エラーレベルを判断したら関係者に報告しましょう。
上記で緊急対応レベルとわかったら即座に報告した方がいいです。
このような報告を怠ってエラー解消に尽力しがちですが、あとで何故すぐに報告しなかったんだ!!となってしまいます。障害発生を関係者が検知できていないと、関係者が行うべき対策が遅れてしまうことになります。他の人にお願いするでもいいので報告しましょう。

また、自分起因のエラーが発生した時に

この事象を隠蔽することができないか

頭をよぎることがあると思います。
自分も何度も頭をよぎりますが、これは隠蔽しようとしてばれた時が最も怖いです。
自身に対する信頼が失墜します。誠実さを示して関係者に正直に報告しましょう。

ログ調査フェーズ

ここからが調査についてです。

1.障害が発生しているサーバーを特定する(知っている場合は省略可)

心当たりがあるサーバに接続して以下の部分を調べます。
apache設定を見て、対象コンテンツのディレクトリ設定があれば対象サーバになります。
apache設定がそもそもどこにあるかも特定する必要がありますが、以下で動いている可能性が高いです。

OSとインストール方法 ディレクトリ
CentOS,RHEL,Fedoraなど(パッケージ) /etc/httpd/conf/
SUSE,Debian,MacOSなど(パッケージ) /etc/apache2/
ソースインストール /usr/local/apache2/conf/

以下のコマンドを入力すればある程度のapacheの場所がわかると思います。

$ ps aux | grep httpd

▼参考Qiita
Apacheの設定ファイル httpd.confの場所

2.ログ出力の場所を特定する

対象ソースの中からログをどこに吐き出しているかソースをgrepしましょう。
ログ設定は以下のようなパターンが多いと思います。

  • アプリケーション独自に定義している
  • プログラミング言語のフレームワークの設定で定義している
  • apacheでログ設定されている
  • 各ミドルウェア・プログラミング言語のデフォルトのログ出力場所(/var/log/php等)

もしもソースにそのような設定が無かった場合、apacheを見たり会社独自のナレッジサイトがあるならそこから調査するのもいいと思います。

3.ログファイルから対象エラーログを特定する

対象のディレクトリ設定までわかれば、実際にログファイルの中で原因となるエラーを特定します。
もし対象ディレクトリ内に複数ファイルがあってどのファイルかわからない場合、障害が起きているサービスを動かしながら以下のコマンドを実行するといいです。

find [ログディレクトリ] -mmin -10

このコマンドを実行することで、対象サービスを動かした際にログが出力されたファイルが何なのか、ある程度特定することができます。

対象のファイルがわかったらファイル内をgrepしてみましょう。
grepの仕方は様々ですが以下のように調べていくといいと思います。

cat [対象ログファイル] | grep -i error
cat [対象ログファイル] | grep -i exception

とか、エラー系の文言でgrepすると引っかかると思います。もしソースのログ出力で特定のメッセージを出力することがわかっているなら、そのメッセージでgrepしてみるといいと思います。

もしくは以下のようにtailfコマンドをしつつ、エラーが発生しているサービスを動かして吐き出されたログを見ていく方法もおすすめです。

tailf [対象ログファイル]

エラーログが出力されていない場合

アクセスログを見てそもそもサーバまで到達しているか確認する

コンテンツによってはエラーログが出力されていないものもあると思います。
本来であればエラーログは出るはずなのに出ていないと言うものもあるかもしれません。
その切り分けをしたいのであれば、アクセスログを見てサーバまで到達しているかを確認すると良いと思います。
もし到達していない場合はネットワーク側の問題かもしれません(今回WEBエンジニア向けなのでネットワーク問題は割愛します)

エラーログを仕込む

そもそもソースでエラーログが出力されていない場合はエラーログを出力する処理を組み込んだ上でリリースして計測した方がいいかもしれません。かなりクリティカルな場合はインフラ側でアクセスを止めてメンテナンス画面にするなど、サービスを止める選択も関係者と相談する必要があります。

WEBコンテンツであればデベロッパーツールでエラーが出ていないか確認する

ログが出ていなければそもそもサーバ側が原因ではなく、
フロント側のJSなどでエラーが発生している可能性は大いにあります。

GoogleChromeなどでOption+Command+iでデベロッパーツールを開き、「Console」から何かエラーが発生していないか確認するといいと思います。(以下のような感じ)

Qiita_と__2__英語のアウトプットに使えるツール_無料編__-_YouTube.png

プログラム調査フェーズ

ここからはログである程度のエラーを特定したら、そのエラーを元にプログラムにどのような問題があるか調査していきます。

1.調査はサーバ上で行わない

まず、多くの人は対象ソースの調査をログ調査に引き続きサーバで行いがちな印象があります。
もし調査している人がlinuxコマンドをある程度体得しており、vimmerであれば止めないのですが個人的に対象ソースをSCPツール等でローカルに落としてきて、エディタや開発統合環境を使用して調査した方が良いです。

ローカルで調査をするメリット

  • 開発統合環境やエディタなどで関数ジャンプができる(プラグインを入れておきましょう)
  • 全体grepを行った後に、対象ソースに飛びやすい(個人的におすすめのエディタはsublime)

2.各プログラム言語のデバッグ方法を理解する

テスト環境やローカル環境でも同事象が発生しているが、原因がわからない場合、
愚直ですが一つ一つの処理を追っていって原因を特定する必要があると思います。
そこで必要になってくる技術がプログラミングのデバッグです。

言語にあったdebug方法を理解しておくことで、原因特定までの速度が格段に上がると思います。

Printデバッグを極力使わないように意識する

Printデバッグというのは

echo $test
exit

などのプリントして変数の内容を確認しながらexitで処理を止めたりしてデバッグしていく方法です。
この方法は簡単かつ脳死しながらデバッグをすることができるのですが、各プログラミング言語にはよりわかりやすくデバッグする仕組みが存在するので、そちらを積極的に使用していくべきです。
例えば以下のような例になります。

言語 デバッグ方法
PHP debug_backtrace関数を使用したPrintデバッグをする、xdebugを使用する
NodeJS debugコマンドを使用する
Ruby byebugコマンドを使用する

調査結果報告フェーズ

調査をある程度することができたら、再度関係各署に報告を行います。
報告する内容は以下のような項目です。

1.報告する内容

  • 発生した事象
  • 対象のサービス
  • 発生期間
  • 発生期間中のサービスへのアクセス数、障害影響件数
  • 発生した原因
  • 対応内容(暫定対応と恒久対応の提示)
  • 再発防止策

など提示して報告すれば良いかと思います。

2.障害影響件数の抽出方法

この障害によってどの程度の影響があったか?は報告時に提示すべき項目かと思います。
その際に件数洗い出しで見る部分はアクセスログとエラーログです。

① エラーログ件数ー実際その障害によりエラーが発生した件数はいくつか
② アクセスログ件数ー障害発生中にどの程度のアクセスが来ていたか

基本的には①の件数を出せば良いのですが、②は画面崩れなどフロント側で発生していたエラーに対する件数を図る際に抽出すると良いと思います。

kibanaなどのログをGUI化するサービスを使用していない場合は、愚直にcutコマンドやxargsを駆使して対象エラー件数を抽出しましょう。

さいごに

いかがでしたでしょうか。
会社によって環境は様々なので、障害調査に関する正解をここで提示できないのですが、
自分の経験から他のところでも上記のような観点で調査をすれば、原因特定が早いのではないかと思いまとめさせていただきました。
ご参考いただければ幸いと思いつつ、もっと良い観点での調査方法があればコメントいただけますと幸いです。

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

サービスで障害が起きた時の調査方法まとめ(バックエンドエンジニア向け)

こんな人に読んで欲しい

  • サーバーサイドエンジニアの人
  • 障害・エラー発生時にあわわわわってなってしまう人
  • 担当しているサービスをあんまり理解してないから調べ方わからん、と言う人
  • どこから調べればよいかわからない人

何について話す?

WEBエンジニアとして、サービスを提供する開発をしていると

  • リリース時
  • 運用中
  • 何もしていないのに突然

などエラーや障害が発生することがあるでしょう。
自分が所属するチームでは一つのコンテンツを長く管理しつつ、
他チーム離散など政治的事情で新しいコンテンツを受け入れる機会が多いです。
新しいコンテンツを受け入れた際に、突然来ます、エラーが、障害が・・・

そんなエラーが発生した際にコンテンツへの理解がなくても原因を特定できた、
という機会がままあります。
(テーブル構成とかアーキテクトとか全く知らないけど、エラーを見つけて対応もできた、とか)

そのような経験を通して、コンテンツへの理解があるからこそ調べられる調査と、
理解がなくても調べられる調査があると感じました。

よくわからないコンテンツでもある程度の調査ができる勘所を紹介していきます。

調査能力を身に付けるための技術を身に付ける

コンテンツへの理解がなくても調べられる、と言いましたが技術的な知識は必要です。
AWSを扱っているのであれば「CloudWatchLogsってなんぞ?」
Linuxサーバで構築しているのであれば「apacheってなんぞ?log出力されているところってどこぞ?」
などと言われてしまうと調査ハードルは格段に上がると思います。

以下の資格を取れる程度の知識を身に付けておくことをおすすめします。

  • AWSソリューションアーキテクト(最低限クラウドプラクティショナー程度)
  • LinuC(最低限Level1程度)

これらの知識がなくてもこれから提案する調査の勘所は参考になると思うので、
引き続き見てもらえますと幸いです。

障害発生時フェーズ

1.緊急レベルを判断する

まずこのエラーは即時対応する必要があるのか、今週中に対応すればいいのか、
それとも放置してもいいものなのかを判断する必要があります。
会社などで決められているルールがあればそれを遵守すればいいですが、自分であれば以下のような基準で考えています。

即時対応レベル

  • サービス提供が不可能な状態が発生している(操作できない・不正データが登録される等)
  • サービスは提供できているが利用者のユーザビリティを著しく落としている(画面の表示崩れ等)
  • 簡易的な攻撃で不正動作ができてしまう状態(パラメータ操作やテキストエリアでSQLインジェクションができる等)

近日対応レベル

  • 社内管理画面、運用をする際に弊害があるが、代替手段が取れる状態
  • ユーザへのサービスに影響がないエラーログ等が出ている(Noticeエラーなど)

放置レベル

  • ユーザーが使う頻度が極端に少ないコンテンツでサービス提供はできている状態でのエラー
  • バッチなどで自動リカバリする仕組みが存在しているもの

2.上司・関係者に報告する

エラーレベルを判断したら関係者に報告しましょう。
上記で緊急対応レベルとわかったら即座に報告した方がいいです。
このような報告を怠ってエラー解消に尽力しがちですが、あとで何故すぐに報告しなかったんだ!!となってしまいます。障害発生を関係者が検知できていないと、関係者が行うべき対策が遅れてしまうことになります。他の人にお願いするでもいいので報告しましょう。

また、自分起因のエラーが発生した時に

この事象を隠蔽することができないか

頭をよぎることがあると思います。
自分も何度も頭をよぎりますが、これは隠蔽しようとしてばれた時が最も怖いです。
自身に対する信頼が失墜します。誠実さを示して関係者に正直に報告しましょう。

ログ調査フェーズ

ここからが調査についてです。

1.障害が発生しているサーバーを特定する(知っている場合は省略可)

心当たりがあるサーバに接続して以下の部分を調べます。
apache設定を見て、対象コンテンツのディレクトリ設定があれば対象サーバになります。
apache設定がそもそもどこにあるかも特定する必要がありますが、以下で動いている可能性が高いです。

OSとインストール方法 ディレクトリ
CentOS,RHEL,Fedoraなど(パッケージ) /etc/httpd/conf/
SUSE,Debian,MacOSなど(パッケージ) /etc/apache2/
ソースインストール /usr/local/apache2/conf/

以下のコマンドを入力すればある程度のapacheの場所がわかると思います。

$ ps aux | grep httpd

▼参考Qiita
Apacheの設定ファイル httpd.confの場所

2.ログ出力の場所を特定する

対象ソースの中からログをどこに吐き出しているかソースをgrepしましょう。
ログ設定は以下のようなパターンが多いと思います。

  • アプリケーション独自に定義している
  • プログラミング言語のフレームワークの設定で定義している
  • apacheでログ設定されている
  • 各ミドルウェア・プログラミング言語のデフォルトのログ出力場所(/var/log/php等)

もしもソースにそのような設定が無かった場合、apacheを見たり会社独自のナレッジサイトがあるならそこから調査するのもいいと思います。

3.ログファイルから対象エラーログを特定する

対象のディレクトリ設定までわかれば、実際にログファイルの中で原因となるエラーを特定します。
もし対象ディレクトリ内に複数ファイルがあってどのファイルかわからない場合、障害が起きているサービスを動かしながら以下のコマンドを実行するといいです。

find [ログディレクトリ] -mmin -10

このコマンドを実行することで、対象サービスを動かした際にログが出力されたファイルが何なのか、ある程度特定することができます。

対象のファイルがわかったらファイル内をgrepしてみましょう。
grepの仕方は様々ですが以下のように調べていくといいと思います。

cat [対象ログファイル] | grep -i error
cat [対象ログファイル] | grep -i exception

とか、エラー系の文言でgrepすると引っかかると思います。もしソースのログ出力で特定のメッセージを出力することがわかっているなら、そのメッセージでgrepしてみるといいと思います。

もしくは以下のようにtailfコマンドをしつつ、エラーが発生しているサービスを動かして吐き出されたログを見ていく方法もおすすめです。

tailf [対象ログファイル]

エラーログが出力されていない場合

アクセスログを見てそもそもサーバまで到達しているか確認する

コンテンツによってはエラーログが出力されていないものもあると思います。
本来であればエラーログは出るはずなのに出ていないと言うものもあるかもしれません。
その切り分けをしたいのであれば、アクセスログを見てサーバまで到達しているかを確認すると良いと思います。
もし到達していない場合はネットワーク側の問題かもしれません(今回WEBエンジニア向けなのでネットワーク問題は割愛します)

エラーログを仕込む

そもそもソースでエラーログが出力されていない場合はエラーログを出力する処理を組み込んだ上でリリースして計測した方がいいかもしれません。かなりクリティカルな場合はインフラ側でアクセスを止めてメンテナンス画面にするなど、サービスを止める選択も関係者と相談する必要があります。

WEBコンテンツであればデベロッパーツールでエラーが出ていないか確認する

ログが出ていなければそもそもサーバ側が原因ではなく、
フロント側のJSなどでエラーが発生している可能性は大いにあります。

GoogleChromeなどでOption+Command+iでデベロッパーツールを開き、「Console」から何かエラーが発生していないか確認するといいと思います。(以下のような感じ)

Qiita_と__2__英語のアウトプットに使えるツール_無料編__-_YouTube.png

プログラム調査フェーズ

ここからはログである程度のエラーを特定したら、そのエラーを元にプログラムにどのような問題があるか調査していきます。

1.調査はサーバ上で行わない

まず、多くの人は対象ソースの調査をログ調査に引き続きサーバで行いがちな印象があります。
もし調査している人がlinuxコマンドをある程度体得しており、vimmerであれば止めないのですが個人的に対象ソースをSCPツール等でローカルに落としてきて、エディタや開発統合環境を使用して調査した方が良いです。

ローカルで調査をするメリット

  • 開発統合環境やエディタなどで関数ジャンプができる(プラグインを入れておきましょう)
  • 全体grepを行った後に、対象ソースに飛びやすい(個人的におすすめのエディタはsublime)

2.各プログラム言語のデバッグ方法を理解する

テスト環境やローカル環境でも同事象が発生しているが、原因がわからない場合、
愚直ですが一つ一つの処理を追っていって原因を特定する必要があると思います。
そこで必要になってくる技術がプログラミングのデバッグです。

言語にあったdebug方法を理解しておくことで、原因特定までの速度が格段に上がると思います。

Printデバッグを極力使わないように意識する

Printデバッグというのは

echo $test
exit

などのプリントして変数の内容を確認しながらexitで処理を止めたりしてデバッグしていく方法です。
この方法は簡単かつ脳死しながらデバッグをすることができるのですが、各プログラミング言語にはよりわかりやすくデバッグする仕組みが存在するので、そちらを積極的に使用していくべきです。
例えば以下のような例になります。

言語 デバッグ方法
PHP debug_backtrace関数を使用したPrintデバッグをする、xdebugを使用する
NodeJS debugコマンドを使用する
Ruby byebugコマンドを使用する

調査結果報告フェーズ

調査をある程度することができたら、再度関係各署に報告を行います。
報告する内容は以下のような項目です。

1.報告する内容

  • 発生した事象
  • 対象のサービス
  • 発生期間
  • 発生期間中のサービスへのアクセス数、障害影響件数
  • 発生した原因
  • 対応内容(暫定対応と恒久対応の提示)
  • 再発防止策

など提示して報告すれば良いかと思います。

2.障害影響件数の抽出方法

この障害によってどの程度の影響があったか?は報告時に提示すべき項目かと思います。
その際に件数洗い出しで見る部分はアクセスログとエラーログです。

① エラーログ件数ー実際その障害によりエラーが発生した件数はいくつか
② アクセスログ件数ー障害発生中にどの程度のアクセスが来ていたか

基本的には①の件数を出せば良いのですが、②は画面崩れなどフロント側で発生していたエラーに対する件数を図る際に抽出すると良いと思います。

kibanaなどのログをGUI化するサービスを使用していない場合は、愚直にcutコマンドやxargsを駆使して対象エラー件数を抽出しましょう。

さいごに

いかがでしたでしょうか。
会社によって環境は様々なので、障害調査に関する正解をここで提示できないのですが、
自分の経験から他のところでも上記のような観点で調査をすれば、原因特定が早いのではないかと思いまとめさせていただきました。
ご参考いただければ幸いと思いつつ、もっと良い観点での調査方法があればコメントいただけますと幸いです。

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

EC-CUBE4でメールアドレス以外でログインする

今回は、EC-CUBEのフロント側 (ユーザがECサイトで買い物をする部分)のログインロジックを変更します。

想定

EC-CUBE4(Symfony3.4)を使用します。
ある程度EC-CUBE4やSymfonyに触れたことがある方向けとして、SymfonyとEC CUBE4の基礎は省略します。

EC CUBE標準のフロントログインロジックは、登録されたメールアドレスを用いて行います。
ここでは、カラムlogid_id (ログインID)を追加して、ログインIDでログインできるようにすることが目的です。

DBの変更

Entityクラスの拡張

まずは、DBテーブルdtb_customerにlogin_idというカラムを追加しましょう。

SymfonyのDBロジックのDoctrineでサポートされている、Entityクラスに対するTraitクラスを使用してカラム追加を行います。
Eccube\Entity\Customerに対するCustomerTraitクラスを新規で作成しましょう。

app/Customize/Entity/CustomerTrait.php
<?php

namespace Customize\Entity;

use Doctrine\ORM\Mapping as ORM;
use Eccube\Annotation as Eccube;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @Eccube\EntityExtension("Eccube\Entity\Customer")
 */
trait CustomerTrait
{
    /**
     * @ORM\Column(name="login_id", type="string", length=255, nullable=true, unique=true)
     */
    private $login_id;

    /**
     * Set login_id.
     *
     * @param string $login_id
     *
     * @return Customer
     */
    public function setLoginId($login_id)
    {
        $this->login_id = $login_id;

        return $this;
    }

    /**
     * Get login_id.
     *
     * @return string
     */
    public function getLoginId()
    {
        return $this->login_id;
    }
}
Entityクラスの記述をDBに反映する

作成後に、EC CUBEディレクトリの最も浅いところで下記のコマンドをそれぞれ実行しましょう。
Windows環境の方は、頭にphpを付け足して実行してください。

$ bin/console eccube:generate:proxies
$ bin/console doctrine:schema:update --dump-sql --force

1つ目のコマンドは、Entityのproxyファイルを生成するコマンドです。
app/proxy/entity内に、対応するEntityのプロキシファイルが作成されれば成功です。
今回は、app\proxy\entity\src\Eccube\Entity\Customer.phpが作成されます。
ファイルの中身は、Eccube\Entity\Customerの記述の中で、Customize\Entitiy\CustomerTraitが呼び出されるものとなります。

2つ目のコマンドでは、CustomerTraitにアノテーションで記載したSQLを実行する形となります。
下記の出力結果が表示されれば成功で、この場合dtb_customerにカラム追加が実行されます。
nullable=trueとすればDEFAULT NULLとしてカラム追加できます。
nullable=falseとすればNOT NULLとしてカラム追加できますが、login_id追加時に値はまだNULLとなっているので、この場合では実行時にエラーとなります。

下記が表示されれば成功です。

 The following SQL statements will be executed:

     ALTER TABLE dtb_customer ADD login_id VARCHAR(255) DEFAULT NULL;
     CREATE UNIQUE INDEX UNIQ_8298BBE35CB2E05D ON dtb_customer (login_id);

 Updating database schema...

     2 queries were executed


 [OK] Database schema updated successfully!

ログインロジックの書き換え

security.yamlの変更

カラム追加ができたので次に移りましょう。

app\config\eccube\packages\security.yamlを書き換えて、ログインロジックに用いるクラスを変更します。

providersでは、ECサイトの管理画面側とフロント画面側のログインロジックで使用しているクラスが記述されています。
ここでは、フロント側で後ほど使用するクラスを呼び出すよう追記しましょう。

app/config/eccube/packages/security.yaml L1~L17に追加
    providers:
        # https://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded
        # In this example, users are stored via Doctrine in the database
        # To see the users at src/App/DataFixtures/ORM/LoadFixtures.php
        # To load users from somewhere else: https://symfony.com/doc/current/security/custom_provider.html
        member_provider:
            id: Eccube\Security\Core\User\MemberProvider
        customer_provider:
            # id: Eccube\Security\Core\User\CustomerProvider # 本来書いてあったロジック
            id: Customize\Security\Core\User\CustomerProviderCustomized # 新たに呼び出すクラスを記述

次に、ログイン時にlogin_idを使用することを明示化します。
動作上はlogin_emailでも支障ないのですが、書き換えましょう。

app/config/eccube/packages/security.yaml L49~L59に追加
            form_login:
                check_path: mypage_login
                login_path: mypage_login
                csrf_token_generator: security.csrf.token_manager
                default_target_path: homepage
                # username_parameter: 'login_email' login_emailは使わずに、login_idを使用する
                username_parameter: 'login_id'
                password_parameter: 'login_pass'
                use_forward: false
                success_handler: eccube.security.success_handler
                failure_handler: eccube.security.failure_handler
ログインロジッククラスの拡張

次に、フロント側で使用するログインロジッククラスを作成します。

app\Customize\Security\Core\User\CustomerProviderCustomized.php
<?php

namespace Customize\Security\Core\User;

use Eccube\Entity\Customer;
use Eccube\Entity\Master\CustomerStatus;
use Eccube\Repository\CustomerRepository;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class CustomerProviderCustomized implements UserProviderInterface
{
    /**
     * @var CustomerRepository
     */
    protected $customerRepository;

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

    /**
     * Loads the user for the given username.
     *
     * This method must throw UsernameNotFoundException if the user is not
     * found.
     *
     * @param string $username The username
     *
     * @return UserInterface
     *
     * @throws UsernameNotFoundException if the user is not found
     */
    public function loadUserByUsername($username)
    {
        $Customer = $this->customerRepository->findOneBy([
            'login_id' => $username,
            'Status' => CustomerStatus::REGULAR,
        ]);

        if (null === $Customer) {
            throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
        }

        return $Customer;
    }

    /**
     * Refreshes the user.
     *
     * It is up to the implementation to decide if the user data should be
     * totally reloaded (e.g. from the database), or if the UserInterface
     * object can just be merged into some internal array of users / identity
     * map.
     *
     * @return UserInterface
     *
     * @throws UnsupportedUserException if the user is not supported
     */
    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof Customer) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }

        return $this->loadUserByUsername($user->getLoginId());
    }

    /**
     * Whether this provider supports the given user class.
     *
     * @param string $class
     *
     * @return bool
     */
    public function supportsClass($class)
    {
        return Customer::class === $class;
    }
}

ここでは、Eccube\Security\Core\User\CustomerProviderを踏襲しつつ、メソッド2つ(loadUserByUsernamerefreshUser)
を変更しています。
CustomerProviderを踏襲・オーバーライドして記述を少なくすることも可能ですが、今回は前述のようにUserProviderInterfaceをimplementsで拡張する方式となっております。

メソッドloadUserByUsernameは、主にログイン時に使用するメソッドになります。
引数で受け取る$usernameに対し、dtb_customer.login_idを検索するように書き換えました。

メソッドrefreshUserは、画面ロード時等に最新のユーザ情報をDBから取得します。
EC CUBEのデフォルトですと$this->loadUserByUsername($user->getUsername())でメールアドレスを引数とするので、こちらもログインIDを使用するように書き換えました。

ログインテンプレートの書き換え

FormTypeの拡張

EC CUBEのMYページのログイン画面のFormTypeを書き換えます。

security.yamlを変更したのと同様に、login_emailを削除してlogin_idを追加します。

app/Customize/Form/Extension/Front/CustomerLoginTypeExtension.php
<?php

/*
 * This file is part of EC-CUBE
 *
 * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
 *
 * http://www.ec-cube.co.jp/
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Customize\Form\Extension\Front;

use Eccube\Common\EccubeConfig;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Form\AbstractTypeExtension;
use Eccube\Form\Type\Front\CustomerLoginType;

class CustomerLoginTypeExtension extends AbstractTypeExtension
{
    /**
     * @var EccubeConfig
     */
    protected $eccubeConfig;

    /**
     * @var AuthenticationUtils
     */
    protected $authenticationUtils;

    public function __construct(AuthenticationUtils $authenticationUtils, EccubeConfig $eccubeConfig)
    {
        $this->authenticationUtils = $authenticationUtils;
        $this->eccubeConfig = $eccubeConfig;
    }

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->remove('login_email')
            ->add('login_id', TextType::class, [
                'attr' => [
                    'max_length' => $this->eccubeConfig['eccube_stext_len'],
                ],
                'constraints' => [
                    new Assert\NotBlank(),
                ],
                'data' => $this->authenticationUtils->getLastUsername(),
            ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getExtendedType()
    {
        return CustomerLoginType::class;
    }
}

最大文字数と必須入力を指定し、'data' => $this->authenticationUtils->getLastUsername()でログインエラー時に入力されたログインIDを返すようにしています。

Twigテンプレートの拡張

次にMYページのログイン画面のTwigテンプレートを拡張しましょう。
src/Eccube/Resource/template/default/Mypage/login.twigapp/template/default/Mypage/以下にコピーすることで、app以下のものが反映されるようになります。

app/template/default/Mypage/login.twig
{% block main %}
    <div class="ec-role">
        <div class="ec-pageHeader">
            <h1>{{ 'common.login'|trans }}</h1>
        </div>
        <div class="ec-off2Grid">
            <div class="ec-off2Grid__cell">
                <form name="login_mypage" id="login_mypage" method="post" action="{{ url('mypage_login') }}">
                    {% if app.session.flashBag.has('eccube.login.target.path') %}
                        {% for targetPath in app.session.flashBag.get('eccube.login.target.path') %}
                            <input type="hidden" name="_target_path" value="{{ targetPath }}" />
                        {% endfor %}
                    {% endif %}
                    <div class="ec-login">
                        <div class="ec-login__icon">
                            <div class="ec-icon"><img src="{{ asset('assets/icon/user.svg') }}" alt=""></div>
                        </div>
                        <div class="ec-login__input">
                            <div class="ec-input">
                                {{ form_widget(form.login_id, {'attr': {'style' : 'ime-mode: disabled;', 'placeholder' : 'ログインID', 'autofocus': true}}) }}
                                {# login_emailをコメントアウトする #}
                                {# {{ form_widget(form.login_email, {'attr': {'style' : 'ime-mode: disabled;', 'placeholder' : 'common.mail_address', 'autofocus': true}}) }} #}
                                {{ form_widget(form.login_pass,  {'attr': {'placeholder' : 'common.password' }}) }}
                            </div>

最後に

ログインIDの発行については、要件等に応じて別途下記のような実装が必要になるかと思います。

  • EC会員登録時に、会員自らがユニークなログインIDの入力を可能とする。
  • EC会員登録時に、ユニークなログインIDを生成する
  • etc..

何か間違った記述がある場合はコメント等にてご指摘等お願いします。

参考文献

[Symfony公式 ver3.4]How to Build a Traditional Login Form
https://symfony.com/doc/3.4/security/form_login_setup.html

SymfonyでGuardとEntityを使った認証 (@asaokamei様) (Quita)
https://qiita.com/asaokamei/items/5122e398e4cc17c84429

[EC CUBE公式]Entityのカスタマイズ
https://doc4.ec-cube.net/customize_entity

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

aタグで作ったボタンでPOST送信する

はじめに

PHPのフォームで値を送信する際、aタグで作ったボタンでも送信する方法です。
自分が実装する際に困った箇所もまとめます。

なぜaタグで送信する必要があったか

・inputタグ(type="submit")ではデザイン通りに実装できなかったため。
・formの外のボタンはaタグで実装されており、サイト・システム全体でボタンのスタイルや挙動を統一したかったため。

ソースコード

名前メールアドレスを送信するフォームを例に。
index.phpでフォームの値を送信してフォーム上に値を表示させてみます。

form.php
<?php
if ( isset( $_POST[ "name" ] ) ) {
  $name = htmlspecialchars( $_POST[ "name" ], ENT_QUOTES, "utf-8" );
  echo $name;
}
if ( isset( $_POST[ "email" ] ) ) {
  $email = htmlspecialchars( $_POST[ "email" ], ENT_QUOTES, "utf-8" );
  echo $email;
}
?>
<!-- input[type="submit"]で送信-->
<form method="POST" name="register" action="index.php">
  <input type="text" name="name" value="">
  <input type="text" name="mail" value="">
  <input type="submit" value="送信する">
</form>

<!-- aタグで送信する-->
<form method="POST" name="register" action="index.php">
  <input type="text" name="name" value="">
  <input type="text" name="mail" value="">
  <a href="#" onclick="document.register.submit();">登録する</a>
</form>

説明

・aタグにonclickでformのnameを指定してsubmitイベントを実行させる。

注意点

・同ファイルで同時に表示されているformのnameが同じの場合、formのが正しく動いてくれない。(値が送信できない)

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