20200522のPHPに関する記事は15件です。

ボタンを押した時に処理を実行させたい時

今回は投稿内容を編集し、編集完了ボタンを押したらdbの投稿内容を書き換える(更新する)というもの

if文で条件を指定してからデータベースへアクセスし書き換えるという流れ

<?php
/ post_rewriteはinputタグのname属性/

if(isset($POST['post_rewrite'])){
$statement = $db->prepare('UPDATE posts SET message=? WHERE id=?');
$statement->execute(array($
SESSION['message'],$SESSION['postsid']));
?>

HTML文(ボタンの部分のみ)

これで修正完了するボタンを押すとphpが処理される

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

PHP LaravelをIISで稼働させる

PHP LaravelをIISで構築します。

OS:Windows10 Pro
Webサーバー:IIS10

IIS10でPHPを稼働させるには、「CGI」を有効にする必要があります。
「コントロールパネル」から「プログラムと機能」を選択します。
「Windowsの機能の有効化または無効化」を選択して表示されたウインドウから
「インターネットインフォメーションサービス」を選択して、
「World Wide Webサービス」-「アプリケーション開発機能」-「CGI」にチェックを入れて「OK」してください。

インストールが完了したら、IISマネージャーを立ち上げてください。
https://windows.php.net/download/
からNon Thread SafeのZIPファイルをダウンロードします。

ZIPを解凍したら、[php.ini-production]を[php.ini]にリネームしてコピーしてください。

php.iniファイルをエディタで開いて、以下の設定を追加・変更します。

fastcgi.impersonate = 1
fastcgi.logging = 0
cgi.fix_pathinfo = 1
cgi.force_redirect = 0

展開したフォルダ内にある、[php-cgi.exe]を実行してください。

エラーが出ていないことを確認してくださいね。

次に、ACLを設定します。
PHPフォルダにデフォルトのIIS起動ユーザである「IIS_IUSRS」にアクセス権を追加します。
アクセス権はデフォルトのまま、変更の必要はありません。

次に、PHPランタイムを登録します。
IISマネージャーを起動して、左側の接続ペインからコンピュータ名をクリックします。
真ん中のビューの下側に、機能ビューとコンテンツビューが表示されていますが、
そのうち、機能ビューを選択して、「ハンドラーマッピング」をダブルクリックします。
右側の操作ペインから「モジュールマップの追加」を選択して、以下の内容を登録してください。
・要求パス  *.php
・モジュール FastCgiModule
・実行可能ファイル PHPのインストールパス\php-cgi.exe
・名前     PHP

「要求の制御」ボタンをクリックして、マップタブの「要求のマップ先が次の場合のみハンドラーを呼び出す」に
チェックをいれて、「ファイルまたはフォルダ」を選択して「OK」します。

動作確認
にinfo.phpファイルを作成します。
エディタで開いて、以下を入力してください。
<?php phpinfo(); ?>

ブラウザを開いて、
http://localhost/info.phpにアクセスしてPHP情報が表示されていることを確認できれば完了です。

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

大阪府のコロナ追跡システムを考えてみる3

大阪府のコロナ追跡システムの開発費が80万円以内、14日間で納めるシステム開発続き。
初めての方は1からご覧ください。

詳細設計(内部設計)

DFD(データフロー図)とデータベース設計書を作成する。

DFD
20753BFF-5B9F-428B-BB2B-A30F5D51651D.jpeg
事業者情報ばっかり。。。

登録画面設計

Htmlを用いて事業者ごとに登録画面を作成する。

データベース設計

事業者管理テーブル

名前 カラム名 データ型 制約
事業者ID Store_id char Primary,Not NULL
事業者名 Store_name varchar Not NULL
登録日 Reg_date date Not NULL

来客管理テーブル

名前 カラム名 データ型 制約
来客ID Vis_id char Primary,Not NULL
事業者ID Store_id char Foreign,Not NULL
メールアドレス Address varchar Not NULL
来店日 Vis_date Timestamp Not NULL

QRコード作成システム設計

URL(https://covid-19tsuuchi.com/Store_ID) をQRコードとして発行してする。

通知システム

感染者の情報を受けたら、来客テーブルを基に接触可能性がある者に通知する。
通知のフローは以下のフローチャートに従う。

C1863FCC-6722-48BB-90D1-0DFDACF88792.jpeg

まとめ

運用でカバーという言葉は好きではないですが、そうなりそうですね。
次回はプログラミングに入ります。先に宣言しますが、テストはしません。

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

CircleCIについて

前提

本日学んだことを書いていきます。

本題

CI(継続的インテグレーション)

変更されたソースコードがGitHubなどのリポジトリに提出(プッシュ)されるたびに自動でテストを行うことで、変更後のソースコードをリポジトリのメインライン(masterブランチなど)に統合(gitで言うところのマージ)可能な状態にする仕組み

CD(継続的デリバリー)

メインラインのソースコードが変更されるたびに、自動で本番環境などにデプロイする仕組み

RefreshDatabase

TestCaseクラスを継承したクラスでRefreshDatabaseトレイトを使用すると、データベースをリセットする

なお、トレイトは継承と似たPHPの機能で、汎用性の高いメソッドなどをトレイトとしてまとめておき、他の複数のクラスで共通して使う、という使い方をする

テストのメソッド名

Laravelでは、テストにPHPUnitというテストフレームワークを使用する
PHPUnitでは、テストのメソッド名の先頭にtestを付ける必要がある
ここではindexメソッドのテストであるとわかるよう、testIndexという名前にすることにした

public function testIndex()

※メソッド名をtest始まりにしたくない場合は、以下の例のようにメソッドのドキュメントに@testと記述する

/**
 * @test
 */
public function initialBalanceShouldBe0()
{
    $this->assertSame(0, $this->ba->getBalance());
}

assertRedirect
引数として渡したURLにリダイレクトされたかどうかをテストする

ArticleControllerTest.php
$response->assertRedirect(route('login'));

route('login')は、ログイン画面のURLを返す

factory関数とcreateメソッド
factory関数を使用することで、テストに必要なモデルのインスタンスを、ファクトリというものを利用して生成できる
factory(User::class)->create()とすることで、ファクトリによって生成されたUserモデルがデータベースに保存される
また、createメソッドは保存したモデルのインスタンスを返すので、これが変数$userに代入される

actingAsメソッド

ArticleControllerTest.php
$response = $this->actingAs($user)
            ->get(route('articles.create'));

引数として渡したUserモデルにてログインした状態を作り出す
その上で、get(route('articles.create'))を行うことで、ログイン済みの状態で記事投稿画面へアクセスしたことになり、そのレスポンスは変数$responseに代入される

Arrange-Act-Assertについて

テストの書き方のパターンとして、AAA(Arrange-Act-Assert)というものがある
日本語で言うと、準備・実行・検証

ArticleControllerTest.php
public function testAuthCreate()
    {
        // テストに必要なUserモデルを「準備」
        $user = factory(User::class)->create(); 

        // ログインして記事投稿画面にアクセスすることを「実行」
        $response = $this->actingAs($user)
            ->get(route('articles.create'));

        // レスポンスを「検証」
        $response->assertStatus(200)
            ->assertViewIs('articles.create');
    }

引数と戻り値の型宣言

public function isLikedBy(?User $user): bool

isLikedByメソッドの引数$userの手前には、?Userと記述されている
Userと記述されていることで、引数$userの型はUserクラスのインスタンスであることが宣言されている
さらにUserの手前に?があることで、引数としてnullが渡されることを許容している
PHP7.1から、このnullableな型宣言が使用できる
: boolとあるのは、このメソッドの戻り値が論理型(trueかfalse)であることを宣言している

三項演算子の利用

return $user
    ? (bool)$this->likes->where('id', $user->id)->count()
    : false;

$userがnullでなければ、

(bool)$this->likes->where('id', $user->id)->count()

の結果をメソッドの呼び出し元に返す
$userがnullであれば、falseを返す

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

LaravelでUnitテストだけ実行する

結論、以下のようにしてtestスイートを指定してテストが実行できます。

php vender/bin/phpunit --testsuite Unit

説明

プロジェクトが大きくなってくるとテストも増えてきて、テストを実行するときに部分的に実行したくなると思います。

LaravelだとテストはデフォルトでUnitとFeatureの2つのテストの種類がありますが、このうち高速なUnitテストだけをより高頻度で実行したすることができたら便利ですよね。

Laravelには、デフォルトでphpunit.xmlにテストの設定が書かれていてUnitテストとFeatureテストはそれぞれ別々のtestスイートとして設定されています。

phpunit.xml
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>

これによりphpunitを実行するときに特定のtestスイートを指定するとその部分だけ実行できます。

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

Laravel でいつも最初から仕込むようなファイルたちはパッケージにしてしまおう

はじめに

以前、Laravel の Validation を正しく拡張する という記事を書きましたが、こういったバリデーションの拡張などは最初から仕込みで動いていて欲しいわけです。

なので composer でインストール後、コマンドを叩けばある程度の設定が最初から入ってしまう状態にしたい、という欲求が湧いてきます。

これに答えてくれるのがパッケージ化です。

FuelPHP を使ってた頃は作り方が簡単だったんですが、Laravel のパッケージは定型があまりないので自由度が高い分、ちとやり方がめんどくさいですが整理のためにも記録として残しておきます。

Laravel におけるパッケージとは簡単に言えば vendor ディレクトリ以下にインストールされるファイル群たちの構成要素を自分で一つ増やす感じの配置になりますね。

開発の準備

ディレクトリは任意です。
ただ動作確認的には自分自身の Laravel と組み合わせながら作るのが一番手っ取り早いとは思いますので、開発中は Laravel をインストール後に app たちと同一階層とかにパッケージ専用ディレクトリを作るのが自分としてはわかりやすかったです。

$ php artisan tinker
Psy Shell v0.9.12 (PHP 7.4.3 — cli) by Justin Hileman
>>> base_path();
=> "/FULL/PATH/Parent/PackageDir"

この base_path() と同じ所にいれるのがよかったです。このディレクトリで下記を実行。

$ mkdir packages
$ cd packages
$ composer init

いろいろ聞かれると思いますが、あとでどうせ編集するので適当でよいです。
この composer.json を最終的に下記のように調整しました。一部情報はダミーです。

packages/composer.json
{
    "name": "funaffect/packages",
    "description": "Funaffect Laravel Packages",
    "authors": [
        {
            "name": "moobay9",
            "email": "dummy@mail.dummy"
        }
    ],
    "require": {
        "php": "^7.0",
        "laravel/framework": "5.8.*"
    },
    "autoload": {
        "psr-4": {
            "Funaffect\\LaravelPackages\\": "app/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "Funaffect\\LaravelPackages\\Providers\\StartupServiceProvider"
            ]
        }
    }
}

ここでは

  • 依存
  • 名前空間とオートロード
  • Laravel 本体から呼ばれる ServiceProvider

を合わせて設定しています。

名前空間とオートロード

composer.json の autoload 部分ですが、今回は app/ にしていますが、これは任意です。
ただディレクトリは具体的にどこだよ、って感じになりますが、

base_path().'/packages/app/'

です。app と同じ階層に今回は config と resources を置きました。
ディレクトリ構造を Laravel がインストールされているものと合わせたかったからです。

名前空間は任意ですきなものをつかたら良いと思いますが、今後ついてまわるのでちゃんと考えて名前をつけましょう。
今回は psr-4 の下の Funaffect\\LaravelPackages の部分ですね。

サービスプロバイダの準備

今回は Funaffect\\LaravelPackages\\Providers\\StartupServiceProvider という名前にしたサービスプロバイダーでこのパッケージのインストールから利用まで全てをカバーさせます。
設置パスは

base_path().'/packages/app/Providers/StartupServiceProvider.php'

になりますので、ここにファイルを作りました。

サービスプロバイダでできること

最初にファイルを展開することと、アプリケーションが動作中にもサービスプロバイダーを通過して起動するようになります。
このサービスプロバイダで自分がやりたかったのは、下記の数ありました。

  • バリデーションの拡張ファイルの設置
  • バリデーションルールの追加
  • config のファイルを設置
  • 言語ファイルの設置
  • middleware の追加

これは後ほど細部を説明します。

動作させてみる

親のディレクトリ(Laravel の Root)にある composer.json で、一個下のパッケージ開発中の composer.json を認識させる必要があります。

composer.json
"repositories": [
    {
        "type": "path",
        "url": "/packages",
        "symlink": true
    }
],

このあと

composer require funaffect/packages 

でインストールできます。require は packages/composer.jsonname と一致させてください。
場合によってはそのあとで

php artisan vendor:publish --tag=packages

が必要です。

サービスプロバイダに設定していく

サービスプロバイダー内は基本的に register()boot() で設定していきますが、register はドキュメントなどにも記載がありますが、まだロードが終わってなかったりインスタンス化されてないクラスがあったりしますので大抵は boot 側に記載していくことになると思います。

configファイル を app/config へ設置する

packages/app/Providers/StartupServiceProvider.php
    public function boot() {
        $this->publishes([
            __DIR__.'/../../config/packages.php' => config_path('packages.php'),
        ], 'packages');
    }

publishes() で設置するべきディレクトリに設置するべきファイルをおくだけですね。今回はパッケージ内にある packages/config/packages.php をいつも使う config のディレクトリの中へコピーできるようにしました。

ミドルウェアを追加する

ミドルウェアは通常、Route から始まるクラスが出てくるあたりで設定しますので、一般的には routes/web.php の中でやっていることが多いと思います。
ですので、web というミドルウェアグループがありますんで、これに追加してあげる形にしました。

packages/app/Providers/StartupServiceProvider.php
use Funaffect\LaravelPackages\Http\Middleware\ConvertDot;
// (略)

    public function boot() {
        $this->app['router']->pushMiddlewareToGroup('web', ConvertDot::class);
    }

バリデーションを拡張する

バリデーションの拡張はクラスにまとめる場合と一個一個追加する場合があります。
例では大まかに ExtensionValidator でクラスごとバリデーションを拡張、 max_lengthmin_length を個別に追加しています。
それとは別にルールを一つ追加するために publishes で先ほどの config と同じようにファイルを設置しました。
(設置先はパスでわかると思います)

packages/app/Providers/StartupServiceProvider.php
use Funaffect\LaravelPackages\Http\Validators\ExtensionValidator;
    public function boot() {
        // Validation Extension
        $this->app['validator']->resolver(function($translator, $data, $rules, $messages, $attribute) {
            return new ExtensionValidator($translator, $data, $rules, $messages, $attribute);
        });

        $this->app['validator']->replacer('max_length', function ($message, $attribute, $rule, $parameters) {
            return str_replace(':max', $parameters[0], $message);
        });

        $this->app['validator']->replacer('min_length', function ($message, $attribute, $rule, $parameters) {
            return str_replace(':min', $parameters[0], $message);
        });

        $this->publishes([
            __DIR__.'/../Rules/CurrentPassword.php' => app_path('Http/Rules/CurrentPassword.php'),
        ], 'packages');
    }

初期設定を上書きする

config 配下の app.config を毎度毎度書き直していますが、これもめんどくせえとなったときには最初から変えておく手段が欲しいなと思った時に、 mergeConfigFrom() という真逆の動きをする関数がサービスプロバイダ内部にあります。なのでこれの src と dst を入れ替えた mergeConfigFor() という関数を作り、register で実行するようにしました。

packages/app/Providers/StartupServiceProvider.php
    public function register()
    {
        // Config
        $this->mergeConfigFor(__DIR__.'/../../config/app.php', 'app');
    }

    protected function mergeConfigFor($path, $key)
    {
        if (! ($this->app instanceof CachesConfiguration && $this->app->configurationIsCached())) {
            $this->app['config']->set($key, array_merge(
                $this->app['config']->get($key, []), require $path
            ));
        }
    }

ここまでで、大体やりたいことは初期構築時に組み込めるようになりました。

実際に運用する場合

こちらで公開してますので、README にも同じことを書いていますが、Laravel をインストール後、composer.json に以下を追加してください。

composer.json
"repositories": [
    {
        "type": "vcs",
        "url": "https://github.com/moobay9/laravelpackages.git",
        "symlink": true
    }
],
"require": {
    "funaffect/packages": "dev-master"
},

composer install 実行後、php artisan vendor:publish --tag=packages をお忘れなく。

なお、packagist には登録していません。

まとめ

駆け足で振り返ってきましたが、とりあえず備忘録の代わりなのでミスが見つかったら随時修正していきます。
ミスを発見した方はお手数ですがコメントでお知らせください。
git のほうは pull req していただいても構いません。

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

[PHP] ereg() から preg_match() への変更でチョットした留意点

PHP7ではereg()が廃止となりpreg_match()を利用することになりました。
古いコードの書き換えをされている方もいると思います。

ereg()をpreg_match()に変更し
マッチングパターンをPOSIXからPCREに書き換えると各所で紹介されていますね。

ちょっと見落としがちかなと思うのは戻り値の違いです。
自分は望む結果が得られずに?となりましたので記事に残しておきます。

ereg()もpreg_match()もint型の戻り値を返しますが

ereg()
 マッチした場合にはマッチした文字列の長さを返し、
 マッチしなかった場合 またはエラーとなった場合は FALSE を返します。
preg_match()
 マッチした場合に 1 を返します。 マッチしなかった場合は 0、エラーが発生した場合は FALSE を返します。

という違いがあります。
マッチング結果を使用して何か処理を行う場合、結果に違いが生じるかもしれません。

<?php
$string = 'ab123cdefg@abcd678efg@a456bcdeg';        // 評価文字列
$pattern = '([a-z]+@)';                             // @とその前のアルファベット(不定長)を検出
while($Ret = ereg($pattern, $string, $matches)){    // $Ret マッチした文字列の長さ
    $pos = strpos($string, $matches[0]);            // $pos マッチした文字列が最初に現れた場所
    $string = substr($string, $pos + $Ret);         // $pos + $Ret を切り捨て評価文字列を再作成
    $result[] = $string;
}
var_dump($result);
?>

-- 結果 ------------------------------------------
array (size=2)
  0 => string 'abcd678efg@a456bcdeg' (length=20)
  1 => string 'a456bcdeg' (length=9)

このようなコードのereg()をpreg_match()に置き換えるだけだと

<?php
$string = 'ab123cdefg@abcd678efg@a456bcdeg';
$pattern = '/([a-z]+@)/';                                // PCREパターンに書き換え
while($Ret = preg_match($pattern, $string, $matches)){   // preg_match に変更、$Retに真偽値 1 が入る
    $pos = strpos($string, $matches[0]);
    $string = substr($string, $pos + $Ret);          // $Retが真偽値の為 $pos + 1 しか切り捨てない
    $result[] = $string;
}
var_dump($result);
?>

-- 結果 ------------------------------------------------
array (size=8)
  0 => string 'defg@abcd678efg@a456bcdeg' (length=25)
  1 => string 'efg@abcd678efg@a456bcdeg' (length=24)
  2 => string 'fg@abcd678efg@a456bcdeg' (length=23)
  3 => string 'g@abcd678efg@a456bcdeg' (length=22)
  4 => string '@abcd678efg@a456bcdeg' (length=21)
  5 => string 'fg@a456bcdeg' (length=12)
  6 => string 'g@a456bcdeg' (length=11)
  7 => string '@a456bcdeg' (length=10)

という具合に結果が変わってしまいます。
マッチした文字列長を後で取得することにします。

<?php
$string = 'ab123cdefg@abcd678efg@a456bcdeg';
$pattern = '/([a-z]+@)/';
while(preg_match($pattern, $string, $matches)){   // 戻り値は格納しない
    $pos = strpos($string, $matches[0]);
    $pos += strlen($matches[0]);                  // strlenでマッチした文字列長を取得
    $string = substr($string, $pos);
    $result[] = $string;
}
var_dump($result);
?>

-- 結果 -------------------------------------------
array (size=2)
  0 => string 'abcd678efg@a456bcdeg' (length=20)
  1 => string 'a456bcdeg' (length=9)

これで変更前と同じ結果が得られました。

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

Laravelで複数のSQL文を実行するときはDB::unpreparedを使う

DBのdumpのような大量のSQLをLaravelから実行するときはDB::unprepared()という関数を使うと、実行することができます。

生のSQL文を実行するときはDB::statement()をよく使いますが、複数のSQL文には対応していません。

以下はファイルに入っている複数のSQL文を読み込んで一括で実行するサンプルコードです。

$sql = file_get_contents(database_path('dump.sql'));
DB::unprepared($sql);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPMailerによるメール送信

なぜ今更

あるEC2から異なるリージョンのEC2で起動しているメールサーバを使ってメールを送りたかった。sendmailをインストールしてmailコマンドでやってみたら以下のようなエラーが出た。

connect to mailhost.domain.com[3.136.146.20]:25: Connection timed out
to=<admin@domain.com>, relay=none, delay=30, delays=0.02/0.03/30/0, dsn=4.4.1, status=deferred (connect to mailhost.domain.com[3.136.146.20]:25: Connection timed out) B8B79CA81C: to=<user@domain.com>, relay=none, delay=30, delays=0.02/0.03/30/0, dsn=4.4.1, status=deferred (connect to mailhost.domain.com[3.136.146.20]:25: Connection timed out)
message-id=<20200521063716.D013DCA81E@ip-172-31-40-247.ap-northeast-1.compute.internal>
from=<>, size=6462, nrcpt=1 (queue active)

接続タイムアウトになっている。メールサーバはオハイオリージョンなので「us-east-2.compute.internal」のはずだが、送り元が東京リージョンだからなのか「ap-northeast-1.compute.internal」となっていて、接続先がおかしくなっている。
どう対処していいのかわからず、そもそもセキュリティ的に許されているかもわからなかった。

稼働監視の通知をしたかったのでLineやSlackに飛ばすことも考えたが面倒なのでやはりメールで行いたかった。
ということで言語ライブラリでメールてみた。

結論

以下のスクリプトでできた。
PHPMailerはこちらから入手。
配置構造

 ├ mailsend.php
 └ PHPMailer/
[mailsend.php]
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;

require 'PHPMailer/src/Exception.php';
require 'PHPMailer/src/PHPMailer.php';
require 'PHPMailer/src/SMTP.php';

$inquiry_no;
$mail = new PHPMailer(true);

$error = null;

try {
  // サーバ設定
  $mail->isSMTP();
  $mail->Host = 'mail.domain.com';        // メールサーバ
  $mail->SMTPAuth = true;
  $mail->Username = 'sender@domain.com';  // 送信アカウント
  $mail->Password = 'password';           // 送信アカウントのパスワード
  $mail->SMTPSecure = false;              // TLSなどの暗号化非対応のサーバならfalseを設定。使えるなら'tls'か'ssl'を設定。
  $mail->SMTPAutoTLS = false;             // SMTPSecureをfalseにする場合はfalseにする。それ以外なら未設定で。
  $mail->Port = 587;

    // 送り先情報
  $mail->setFrom('sender@domain.com');       // 送信元アドレス
  $mail->addAddress('reciever@domain.com');  // 送信先アドレス
  $mail->addReplyTo('sender@domain.com');    // 返信先アドレス

  // 本文
  $mail->isHTML(false);
  $mail->CharSet = 'UTF-8';

  $mail->Body = "ここに本文を記載";
  $mail->Subject = "ここに件名を記載";

  $mail->send();
} catch (Exception $e) {
    $error = $e;
    echo $mail->ErrorInfo;
}
if ($error!=null) {
    echo "send";
}

課題

Postfix+doevcotでメールサーバはEC2に立てられるけど、リレーとか理解してない。

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

AmazonLinux2にPHP7.4をインストールする

利用できる拡張パッケージを確認する

#amazon-linux-extras

 42  php7.4                   available    [ =stable ]

PHP7.4を有効にする

#amazon-linux-extras enable php7.4

確認

#amazon-linux-extras

42  php7.4=latest            enabled      [ =stable ]

PHP7.4インストール

 # yum clean metadata
 # yum install php

他、利用したいphpパッケージを都度インストールします。

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

スクレイピングいろいろ

ウェブスクレイピングの手法をいくつかご紹介致します。
GAS、VBA、PHP、Python、それぞれを使って、Yahoo!Japanの主要ニュースを取得します。
結論からいうと、GAS + Spreadsheetのほうが気軽に使えてイチオシです。

GAS + Spreadsheet

良い点

Googleアカウントさえあれば、一切の環境を用意する必要がなく、GASのトリガーで定期的に実行できます。

悪い点

大規模の実行にはパフォーマンスの懸念があります。
セキュリティ上、重要なデータの扱いはちょっと厳しいです。

コード

gas.js
function yahoo() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('yahoo');
  //書き込むシート名を指定
  var getUrl = 'https://www.yahoo.co.jp/';
  //ターゲットページを指定
  var content = UrlFetchApp.fetch(getUrl).getContentText('UTF-8');
  var content = content.replace(/\r?\n/g, '');
  var data = content.match(/<div class="_2jjSS8r_I9Zd6O9NFJtDN-" aria-label="主要 ニュース">(.*?)<\/div><div>/);
  //コンテンツのブロックを設定
  var data = data[0];
  var URLs = data.match(/<a class="yMWCYupQNdgppL-NV6sMi _3sAlKGsIBCxTUbNi86oSjt" href="(.*?)"/g);
  //URL群を一括で配列へ
  var Titles = data.match(/<span class="fQMqQTGJTbIMxjQwZA2zk _3tGRl6x9iIWRiFTkKl3kcR">(.*?)<\/span>/g);
  //記事名群を一括で配列へ
  for (var i = 0; i < 8; i++) {
    var URL = URLs[i];
    URL = URL.replace('<a class="yMWCYupQNdgppL-NV6sMi _3sAlKGsIBCxTUbNi86oSjt" href="', '');
    URL = URL.replace('"', '');
    sheet.getRange(i + 2, 1).setValue(URL); 

    var Title = Titles[i];
    Title = Title.replace('<span class="fQMqQTGJTbIMxjQwZA2zk _3tGRl6x9iIWRiFTkKl3kcR">', '');
    Title = Title.replace('<\/span>', '');
    sheet.getRange(i + 2, 2).setValue(Title);

  }
  Browser.msgBox('Done!');
}

VBA + Excel

良い点

リテラシーの低い利用者向けで、おなじみのエクセルで使って頂ける文はやはりありがたいです。

悪い点

IEオブジェクトに依存しますので、Windows端末は必須となります。
IEの独特の仕様で現代的なウェブサイトでうまく動作しないケースがしばしばあります。

ライブラリの追加

ExcelのVBA画面で
メニューバー > ツール > 参照設定
に下記2つのライブラリにチェックをつけます。
・Microsoft HTML Object Library
・Microsoft Internet Controls

コード

Sub GetData_Click()
Application.ScreenUpdating = False
Dim objIE As InternetExplorer
Dim htmlDoc As HTMLDocument
Dim NewsItem IHTMLElement
Dim NewsList, NewsTitle, NewsURL As IHTMLElementCollection
Dim PageURL As String
Dim LastRow As Long
PageURL = "https://www.yahoo.co.jp/"

'新しいIEオブジェクトを作成してセット
Set objIE = CreateObject("Internetexplorer.Application")
objIE.Visible = False

'IEでURLを開く
objIE.navigate PageURL
'読み込み待ち
Do While objIE.Busy = True Or objIE.readyState < READYSTATE_COMPLETE
  DoEvents
Loop
'objIEで読み込まれているHTMLドキュメントをセット
Set htmlDoc = objIE.document

'各項目のコンテンツ取得
Set NewsList = htmlDoc.getElementsByClassName("_2jjSS8r_I9Zd6O9NFJtDN-")

For Each NewsItem In NewsList
  '各タグの検索用件を指定
  Set NewsTitle = NewsItem.getElementsByTagName("a")
  Set NewsURL = NewsItem.getElementsByClassName("fQMqQTGJTbIMxjQwZA2zk _3tGRl6x9iIWRiFTkKl3kcR")
  '最終行を取得
  LastRow = Worksheets("result").Cells(Rows.Count, 1).End(xlUp).Row
  '各セルに該当する値を埋める
  Worksheets("result").Cells(LastRow + 1, 1).Value = Right(NewsURL(0).href, 4)
  Worksheets("result").Cells(LastRow + 1, 2).Value = NewsTitle(0).innerText
  '見つからない場合にはスキップ
  On Error Resume Next
  Worksheets("result").Cells(LastRow + 1, 5).Value = Mid(Campagin(0).innerText, 2)
  Worksheets("result").Cells(LastRow + 1, 6).Value = Mid(Campagin(1).innerText, 1)
Next NewsItem

MsgBox "Done!"
End Sub

PHP + csv

良い点

パフォーマンスがよいです。依存するライブラリも少ないです。

悪い点

PHPを実行する環境が必要です。

必要なライブラリ

phpQuery-onefile.php
上記ファイルをダウンロードし、本体のphpファイルと同じ階層に置く

コード

scraping.php
<?php
  require_once("phpQuery-onefile.php");
  $path = "xxx/xxx.csv"; //出力するcsvを指定
  $header = "URL".", ". "Title"."\n";
  $file = fopen($path, "w");
  $target = "https://www.yahoo.co.jp/";
  $html = file_get_contents($target);
  $list = phpQuery::newDocument($html)->find("div._2jjSS8r_I9Zd6O9NFJtDN-");
  for($i = 0; $i < 8; $i++){
   $url[$i] = phpQuery::newDocument($list)->find("li:eq($i)")->find("a")->attr("href");
   $title[$i] = str_replace(",", "", phpQuery::newDocument($list)->find("span.fQMqQTGJTbIMxjQwZA2zk _3tGRl6x9iIWRiFTkKl3kcR:eq($i)")->text());
   $data .= ".$url[$i].", ".$title[$i]."."\n";
  }
    $content = $header . $data;
  fputs($file, $content);
  fclose($file);
?>

Python + csv

良い点

パフォーマンスがよいです。すでにPythonを使っていれば、連携しやすいです。

悪い点

Pythonの環境構築するにはちょっと手間です。すでにPythonを使っていれば、いろいろ連結して活用できるかもしれませんが、スクレイピングだけのためなら、PHPのほうが圧倒的に環境構築しやすいです。

必要なライブラリをインストール

pip3 install requests
pip3 install beautifulsoup4

コード

scraping.py
# coding: utf-8
import requests
from bs4 import BeautifulSoup
import csv

r = requests.get("https://yahoo.co.jp/")
data = BeautifulSoup(r.content, "html.parser")
list = data.find("div", "_2jjSS8r_I9Zd6O9NFJtDN-")
links = list.find_all("a", "")
titles = list.find_all("span", "fQMqQTGJTbIMxjQwZA2zk _3tGRl6x9iIWRiFTkKl3kcR")

with open('xxx/xxx.csv', 'w') as f:
# csvファイルの配置場所を決める
    writer = csv.writer(f)
    writer.writerow(['url', 'title'] )
    for i in range(7):
        writer.writerow([links[i].attrs['href'], titles[i].text])
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

うんざりするほど詰まった PHP → json → PHP の多次元配列データの受け渡し

CSVをアップロードして、データベースの情報を追記して表示し、DLできるようにする機能を作っていたのですが、追記する情報の中に、シングルクォート(')を含んでいる文字列(例 I'm Fine!)があるとDLできずにとてもとても詰まったので、ここにメモしておきます。

スクリーンショット 2020-05-21 20.31.26.png

環境

PC: MacBook Air
Browser: Chrome
Version: Laravel Framework 5.8.16

参考文献

[PHP]preg_replaceで複数の文字列置換
【PHP】複数文字列の置換(str_replace)
【PHP】JSONがnullになるケース
Javascript|JSONでの、レコードの表現方法(配列、連想配列)→{}と[]の違い
【JavaScript】JSONのparseとstringifyメソッドの使い方
PHP: json_decode を使って Unicode エスケープシーケンスを UTF-8 の文字列に変換する
PHPでjson_encodeとjson_decodeを使ってみる
htmlspecialchars
array_walk_recursiveでクロージャを使う
JSON でのエスケープ処理 (JSONの値に""", "\" を含める場合の処理)
PHP エスケープシーケンスのサンプル
JSONデータに改行コードを入れる方法
JSONについて調べてみた
How to deal with backslashes in json strings php
json_encode() escaping forward slashes
Posting JSON from hidden form field
how to use JSON.stringify and json_decode() properly
JSONのエスケープ
PHPとJavaScriptでHTMLエンティティを扱う時のおさらい
json_encode
json_encode can not escape single quotation marks?
How should I go about adding slashes to only single quotes and ignoring double quotes?
【PHP入門】正規表現で置換する方法
JSON.stringifyは、文字列化されるたびに二重引用符をエスケープします
How do I json_decode string with special chars (“ \ ” )
htmlspecialchars_decode


(:baby_tone2:以下、現象説明から解決まで:baby_tone2:)

現象説明

Laravelで作っている管理画面で、IDが載っているCSVを管理画面上でアップロードしたら、ajaxで、PHP側でそのIDをもとに紐づく情報をデータベースから取ってきて、CSVのデータにくっつけて、テーブルに表示し、CSVとしてDLもできるようにする機能を作ったのですが、
紐づく情報の中に、「'」(シングルクオート)が入っている場合に、JavaScriptのエラーが出てDLできない事象が起こりました。そして、Javascript側のエラーを解消すると、今度はPHP側でエラーが起こってしまう、という事態になりました。

まず、Javascript側でエラーになってしまっていた原因は、
そもそもの仕組みとして、DLボタンが押されたら、

scriptでformを生成してhiddenのvalue値にjson形式でデータを入れてsubmitさせる

という形だったのですが、
そのvalue値が、「'」(シングルクオート)があることにより、途中で途切れてしまう為、第一弾のエラーが起こってしまっておりました。

エラーが起きたコード
$(document).ready(function(){
    //CSVダウンロード
    function download_csv(){

    // csvデータを作成
    var csvData = '<?= json_encode($csv_data) ?>';

    // ダウンロードを実行
    $('<form target="_blank" action="【PHP側のURL】" method="POST">' +
        '<?= csrf_field() ?>' +
        '<input type="hidden" name="csv_data" value=\'' + JSON.stringify(csvData) + '\'>' +
        '</form>').appendTo(document.body).submit();
    }
    $("#dl-btn").on('click', function(e) {
        download_csv();
    });
});

そこで、json_encodeを見直し、「'」(シングルクオート)をエスケープしたりすることで、javascriptのエラーを起こさずに、PHP側に遷移できるようになったのですが、

しかしながら、今度はエラー第二弾。
PHP側で、POSTで送られてきた上記CSVのデータをjson_decode()した後に、foreachを回していたのですが、
そのforeachの部分で、エラーが起きてしまいました。

デバッグしてみると、json_decode()した後のデータが入っているはずの$csv_datがNULLとなっており、
NULLなのにforeachを回そうとしてエラーになっておりました。

エラーが起きたコード
    public function dlCsvConversion(Request $request)
    {
        assert(isset($_POST['csv_array']));
        $csv_dat = json_decode(json_decode($_POST['csv_array'], true),true);
        $csv = '';
        foreach ($csv_dat as $value){
            foreach ($value as $k => $val){
                if ($k === count($value)-1) {
                    $csv.=$val;
                } else {
                    $csv.=$val."\t";
                }
            }
            $csv.="\r\n";
        }
        header('Content-Encoding: UTF-16LE');
        header('Content-type: text/csv; charset=UTF-16LE');
        header('Content-Disposition: attachment; filename="convertedCsv-'.date('Ymd').'.csv"');
        header('Pragma: no-cache');
        header('Expires: 0');

        echo chr(255) . chr(254) . iconv("UTF-8", "UTF-16LE", $csv);
        exit;
    }

だけど、POST値はきちんと送られてきております。
なのに、json_decode()の結果がNULL。

検索してみると、json_decode()に失敗するとNULLとなってしまうことがわかりました。
そこで、色々と改善策を検索して試してみたのですが、
今回、json配列が[](鍵カッコ)で括られた、行列のみ多次元配列のデータだった為、key:valueのような形の{}で囲われたjson配列の記事はたくさんあったのですが、その違いのためかうまくいかない手法ばかりでした。

解決

ただ、試行錯誤しているうちに、

POST値がjson_encode()したはずなのに、なぜか「'」(シングルクオート)などがdecode済みの形で入ってきてしまっていたのを

javascript側を

json_encode(json_encode($csv_data, JSON_HEX_APOS))

上記のように最初に「'」(シングルクオート)だけ指定してjson_encodeし、さらに全体をjson_encodeし、JSON.stringify()は使わないようにすることで、

PHP側にencodeしたままの形で渡せるようになり、
さらに、今までは

""[[],[]]""

上記のように、javascript側で2回json_encode()していることで、「"」(ダブルクオーテーション)が入れ子になってしまっているのをjson_encode()を入れ子にして2回実施することで、解凍していたのですが、
それをpreg_replace()で最初と最後の「"」(ダブルクオーテーション)を取り除き、json_encode()を1回だけにしたところ、json_decode()がSyntax errorでNULLになってしまうのを無事解消することができました!!

修正後のコード(javascript側)
$(document).ready(function(){
    //CSVダウンロード
    function download_csv(){

    // csvデータを作成
    var csvData = '<?= json_encode(json_encode($csv_array, JSON_HEX_APOS)) ?>';

    // ダウンロードを実行
    $('<form target="_blank" action="【PHP側のURL】" method="POST">' +
        '<?= csrf_field() ?>' +
        '<input type="hidden" name="csv_array" value=\'' + csvData + '\'>' +
        '</form>').appendTo(document.body).submit();
    }
    $("#dl-btn").on('click', function(e) {
        download_csv();
    });
});
修正後のコード(PHP側)
    public function dlCsvConversion(Request $request)
    {
        assert(isset($_POST['csv_array']));
        $search = array('/^"/','/"$/');
        $replace = array('','');
        $csv_data = json_decode(preg_replace($search, $replace, $_POST['csv_data']),true);
        $csv = '';
        foreach ($csv_data as $value){
            foreach ($value as $k => $val){
                if ($k === count($value)-1) {
                    $csv.=$val;
                } else {
                    $csv.=$val."\t";
                }
            }
            $csv.="\r\n";
        }
        header('Content-Encoding: UTF-16LE');
        header('Content-type: text/csv; charset=UTF-16LE');
        header('Content-Disposition: attachment; filename="paygentCsvConverted-'.date('Ymd').'.csv"');
        header('Pragma: no-cache');
        header('Expires: 0');

        echo chr(255) . chr(254) . iconv("UTF-8", "UTF-16LE", $csv);
        exit;
    }

:alien:振り返り:alien:

そもそものコードもちょっとおかしかったのですが、どん詰まり状態から開放されてとても嬉しい気分です。
直したコード的にはあっけなかったのですが、エンコードのデバッグはとてもわかりづらくて辛かった。。
Unicode文字列とかまで試したりして、結局は使いませんでしたが、勉強にはなりました!
POST値がencode済だったのは多分JSON.stringify()していたせいですかね、、
直せたけど、曖昧なままな部分もある。。でもとりあえず、よしとしておきます!

以上!

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

Laravel 7 バッチ処理でusersテーブルのメールアドレスにメールを送る

目的

  • Authで実装した認証機能で使用するusersテーブルに登録されているメールアドレスに順々にメールを送信するバッチの作成方法をまとめる

実施環境

  • ハードウェア環境(下記の二つの環境で確認)
項目 情報
OS macOS Catalina(10.15.3)
ハードウェア MacBook Pro (16-inch ,2019)
プロセッサ 2.6 GHz 6コアIntel Core i7
メモリ 16 GB 2667 MHz DDR4
グラフィックス AMD Radeon Pro 5300M 4 GB Intel UHD Graphics 630 1536 MB
  • ソフトウェア環境
項目 情報 備考
PHP バージョン 7.4.3 Homwbrewを用いて導入
Laravel バージョン 7.0.8 commposerを用いて導入
MySQLバージョン 8.0.19 for osx10.13 on x86_64 Homwbrewを用いて導入

前提情報

概要

  1. バッチファイルの作成
  2. 確認

概要

  1. バッチファイルの作成

    1. アプリ名ディレクトリで下記コマンドを実行してバッチファイル(コマンドファイル)を作成する。ファイル名を「SendMailCommand.php」とする。

      $ php artisan make:command SendMailCommand
      
    2. アプリ名ディレクトリで下記コマンドを実行して先に作成したコマンドファイルを開く。

      $ vi app/Console/Commands/SendMailCommand.php
      
    3. 開いたコマンドファイルを下記の様に修正する。(開いたファイルで作成するコマンドでメールの送信処理を行う)

      アプリ名ディレクトリ/app/Console/Commands/SendMailCommand.php
      <?php
      
      namespace App\Console\Commands;
      
      use Illuminate\Console\Command;
      //下記を追加する
      //usersテーブル用のモデルファイルを紐づける
      use App\User;
      //メール送信用ファサードを紐づける
      use Illuminate\Support\Facades\Mail;
      //上記までを追加する
      
      class SendMailCommand extends Command
      {
          /**
           * The name and signature of the console command.
           *
           * @var string
           */
          //下記を修正する
          protected $signature = 'app:send_mail_users';
      
          /**
           * The console command description.
           *
           * @var string
           */
          //下記を修正する
          protected $description = 'usersテーブルのemail全てにメールを送信する';
      
          /**
           * Create a new command instance.
           *
           * @return void
           */
          public function __construct()
          {
              parent::__construct();
          }
      
          /**
           * Execute the console command.
           *
           * @return mixed
           */
          public function handle()
          {
              //下記を追加・修正する
              $users_infos = User::all();
      
              foreach ($users_infos as $users_info) {
                  echo $users_info['email']."\n";
      
                  Mail::raw("これはテストメールです", function($message) use ($users_info)
                  {
                      $message->to($users_info->email)->subject('test');
                  });
              }
              //上記までを追加・修正する
          }
      }
      
  2. 確認

    1. アプリ名ディレクトリで下記コマンドを実行してコマンドが追加されている事を確認する。

      $ php artisan list
      
    2. 先のコマンドの出力の中にapp:send_mail_usersがある事を確認する。

    3. アプリ名ディレクトリで下記コマンドを実行してバッチ処理が正常に動作するか確認する。(実際にメールが送信されるため注意する!!!)

      $ php artisan app:send_mail_users
      
    4. usersテーブルに登録されているメールアドレスの受信ボックスを確認し、下記の様なメールが届いていれば実装完了である。

      test_-_shun_okawa_gmail_com_-_Gmail.png

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

PHP デバイスサイズにより埋め込みサイトのwidthをコントロール

Get browser width using php
https://stackoverflow.com/questions/23239157/get-browser-width-using-php
サイト参照デバイスをJavaScriptにて取得しphp側に返す。

<?php
session_start();
if(isset($_POST['width'])){
   $_SESSION['screen_size'] = array();
   $_SESSION['screen_size']['width'] = intval($_POST['width']);
   $_SESSION['screen_size']['height'] = intval($_POST['height']);
}



if(!isset($_SESSION['screen_size'])){
?>
<html>
<head>
<script>
function getSize(){
document.getElementById('inp_width').value=screen.width;
document.getElementById('inp_height').value=screen.height;
document.getElementById('form_size').submit();
}
</script>
</head>
<body onload='getSize()'>
<form method='post' id='form_size'>
<input type='hidden' name='width' id='inp_width'/>
<input type='hidden' name='height' id='inp_height'/>
</form>
</body>
</html>


<?php
}else{
    var_dump($_SESSION['screen_size']);
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP】クラスとインスタンスについて

クラスとインスタンスの関係性

クラスがテンプレートの原本でインスタンスがテンプレートを利用して作成されたもの

クラスとインスタンスの関係性の例(ポケモンのゲームで例える)

▼クラス
ss_01.jpg

▼インスタンス
スクリーンショット 2020-05-22 3.22.27.png

こんな感じで
一回一回手動でコードを入力しなくても良いようにテンプレートを作成してそこへ数値を入れることができるようになっています。

コードで書くとこんな感じ

<?php
//こうげき,ぼうぎょ,すばやさ,とくしゅが空欄のポケモンのステータス原本
class Pokemon{
public $attack;
public $defense;
public $speed;
public $Special;
}

//こうげき,ぼうぎょ,すばやさ,とくしゅが空欄のポケモンが生成
$myu = new Pokemon();
//こうげき,ぼうぎょ,すばやさ,とくしゅに[16]の値を飛ばす
$myu -> attack = 16;
$myu -> defense = 16;
$myu -> speed = 16;
$myu -> special = 16;
//こうげき,ぼうぎょ,すばやさ,とくしゅに[16]を表示
echo  "こうげき" $myu -> attack.'<br>';
echo  "ぼうぎょ" $myu -> defense.'<br>';
echo  "すばやさ" $myu -> speed.'<br>';
echo  "とくしゅ" $myu -> special.'<br>';

?>

解説

■記述ルール
class+クラス名{クラスの中身(プロパティや関数など)}…クラスの生成
public+$プロパティ名…プロパティ
$変数名 = new クラス名()…クラス生成
インスタンス->プロパティ名…クラスのプロパティにアクセスする

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