初めてのラズパイ~キーボード,マウスなくたってssh接続でどうにかする編~

先週、オープンソースカンファレンス名古屋にいってきました!

そこで、なんとラズパイをいただきました!!

ただ、、、後で知ったんですが学生限定だったらしく…ごめんなさい!

 

っということで、早速ラズパイ始めようと思い、
ラズパイ知識0の状態から、
ラズパイの導入→ssh接続するまでを書きたいと思います。

参考にしたサイト:
– https://www.d4af.com/post/2017/10/raspberry-pi-headless-setup/
https://blog.bnikka.com/raspberrypi/raspberrypi-install.html
http://masatolan.com/raspberry-pi/raspberry-pi-security/
https://qiita.com/mascii/items/0d1a280ac58ed8f6f999

 

準備

今回使うものリスト
– ラズパイ(多分、Raspberry Pi Type A)
– SDカード(8GB)
– 電源アダプター
– HDMI(画面とラズパイをつなぐ)
– 無線LAN(USBポートでさせるタイプ)
↑ ラズパイ3・ラズパイZeroWでは標準搭載されているらしい。
– Mac(SDカードに焼く& ssh接続用PC)

 

 

1.SDカードにOSとなるraspbianを焼く[Mac]

参考:https://qiita.com/shippokun/items/9070fc58f69d8c063e44

 

1-1. 以下の公式サイトより、Raspbian(OS)をダウンロードします。

私自身、ラズパイて最初からOS入ってるものだと勘違いしていました。
導入するOSの種類を選べるのも面白い!!!
ダウンロードに結構な時間がかかりました。
今回はすっごい時間がかかりましたが、
「Raspbian Buster with desktop and recommended software」
にしました。
https://www.raspberrypi.org/downloads/raspbian/

 

1-2. SDにRaspbian(OS)を焼く

書き込む前にフォーマットする必要があります。
今回は、ターミナルで行います。(ディスクユーティリティよりGUIでも行えます。)

MacにSDカードを差す前の状態

$ diskutil list
/dev/disk0 (internal):
...

/dev/disk1 (synthesized):
...

 

SDカード差し込み後(/dev/disk2が増えている=これがSDカード)

$ diskutil list
/dev/disk0 (internal, physical):
...

/dev/disk1 (synthesized):
...

/dev/disk2 (external, physical):
...

 

SDカードをアンマウントして、データを削除する。
「デバイス名」は、つけたい名前でokです!ただし大文字でないとエラーになったかと思います。
ここでdisk1とか選んじゃうとMacのデータを消しちゃったりするので注意してください。

$ diskutil unMountDisk /dev/disk2
Unmount of all volumes on disk2 was successful
$ diskutil eraseDisk MS-DOS [デバイス名] /dev/disk2

SDカードをアンマウントして、OSイメージをコピーする。

$ diskutil unMountDisk /dev/disk2
$ sudo dd if=/Users/[ユーザ名]/Downloads/[OSイメージ名] of=/dev/rdisk2 bs=1m

 

2. SDカードにssh接続用ファイルを作成[Mac]

デフォルトのままではssh接続が無効なので、有効化させます。

/boot以下にsshという名前の空ファイルを作成するだけ!

$ cd /Volumes/boot
$ touch ssh

 

3. SDカードにWi-Fi設定ファイルを作成[Mac]

デフォルトのままではWi-Fi接続情報がないため、ssh接続もできません。

そこでWi-Fi情報を設定します。

/boot以下にwpa_supplicant.confという名前のファイル作成する。

/boot/wpa_supplicant.conf

country=JP
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
    ssid="<SSID>"
    psk="<WPA2のPSK>"
    key_mgmt=WPA-PSK
}

 

psk以下は、wifiのパスワードそのままでもいけますが、
セキュリティ的に暗号化したほうがいいので、
http://jorisvr.nl/wpapsk.htmlを使って作成したものを設定したほうがいいそうです。

 

3. ラズパイにSDカードを差して起動[ラズパイ]

アンテナの確認だけなので、キーボード・マウスなくても大丈夫です。

raspi_start_view

 

Wi-Fiがうまく繫ればラズパイデスクトップ上のアンテナがこんな感じになります。

wifi_on

 

ちなみに繋がっていない場合は、こんな感じです。

wifi_off

 

4. ラズパイのMacアドレスとIPを見つける[Mac]

二つの方法があります。

1.ルータの設定画面
2.arp-scanコマンドより

 

1. ルータの設定画面の場合

Buffaloの場合は、http://192.168.11.1/login.htmlでログイン後、
ip_config
詳細設定>LAN>DHCPリースより、
自分がWi-Fiに接続しているMacアドレス以外のものがあればそれだと推測する方法があります。

この画面で手動割り当てにしておくと、
固定IP化されて毎回IP探す必要がなくなって楽です!

 

2. arp-scanコマンドよりの場合

$ sudo arp-scan -l
Password:
Interface: en0, datalink type: EN10MB (Ethernet)
Starting arp-scan 1.9.5 with 256 hosts (https://github.com/royhills/arp-scan)
192.168.11.1	xx:xx:xx:xx:xx:xx	BUFFALO.INC
192.168.11.2	yy:yy:yy:yy:yy:yy	Logitec Corporation

537 packets received by filter, 0 packets dropped by kernel
Ending arp-scan 1.9.5: 256 hosts scanned in 1.858 seconds (137.78 hosts/sec). 3 responded

今回、ラズパイのWi-FiドングルにLogitecを使っていたので、

192.168.11.2だというが想定できます。

 

5. Macからssh接続する[Mac]

4で推定したIPにMacからssh接続します。

ssh pi@192.168.11.2
pi@192.168.11.2's password:

デフォルトのユーザー名: pi
デフォルトのパスワード: raspberry

あとで変更します!

 

6. ssh接続情報を変更する[Mac]

デフォルトのままだとなんか怖いので変更しましょう。

●rootのパスワードを変更する

$ sudo passwd root
新しいパスワード:
新しいパスワードを再入力してください:
passwd: パスワードは正しく更新されました

 

● ポート番号を変更する
5行目くらいにあるポートの設定を変更する

pi@raspberrypi:~ $ sudo vi /etc/ssh/sshd_config
...
# possible, but leave them commented.  Uncommented options override the
# default value.

Port 12345  #多分、元は Port 22

 

● 繋がるか確認する。

$ ssh -p 12345 pi@192.168.11.2
pi@192.168.11.2's password:

感想

Wi-Fiの設定とか初めていじりました!
IP固定化とか、なんかインフラエンジニアっぽいくってかっこいい^^!!

手持ちのレゴでケース適当に作ったけど、適当すぎる。。。
lego_case

 

 

Composerでrequireしたときのエラー解決法

 

どうも!名古屋生活満喫してます。
大須が好きで毎週行ってしまってます。
ただ、友達がいないので一人で大体いってますw
名古屋で遊んでくれる友達欲しい。。。

今回の記事は、
PHPerなら、とても助かっているであろうComposerについてです。
いろんなパッケージ使おうとrequireしてたら、なんか導入できない!
英語でわかわからんってなって諦めてました。
そろそろ次に進めなければと思い、私が問題解決を行った方法をまとめます。

 

composerコマンドだけで、できる方法はありそうなので見つけたら教えてください!
私も見つけたら書きます\(^^)/

 

Composerとは…

公式サイト:https://getcomposer.org/

PHPで何かを作るときに、
既存のライブラリやパッケージを導入/更新するときに使います。
そのとき、依存管理もしてくれるツールです。
JavaScriptでいうnpm的なやつだと思っています。

Composerの公式サイトに飛んだときに気づいたのですが、
ロゴのおじさん、いろんなカラーパターンがある!!!
リロードするたびに変化するのでぜひ遊んでみてください!

composer_uncle

(多分5パターン)

 

 

本題に入ります。

例えばこんな例

hautelook/alice-bundleをdev環境に導入する時….
下記のエラーで導入できなかった例です。

$ composer require hautelook/alice-bundle --dev
Using version ^2.5 for hautelook/alice-bundle
./composer.json has been updated
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 hautelook/alice-bundle ^2.5 -> satisfiable by hautelook/alice-bundle[2.5.0].
    - hautelook/alice-bundle 2.5.0 requires doctrine/orm ^2.5.11 -> satisfiable by doctrine/orm[2.5.x-dev, 2.6.x-dev, 2.7.x-dev, v2.5.11, v2.5.12, v2.5.13, v2.5.14, v2.6.0, v2.6.1, v2.6.2, v2.6.3] but these conflict with your requirements or minimum-stability.


Installation failed, reverting ./composer.json to its original content.

 

日本語でざっくり訳すと(わたしの解釈です)

hautelook/alice-bundleのv2.5.0(バージョン指定してないので最新版をインストールしようとします)は、
doctrine/ormのv2.5.0以上が必要です。

 

 

ということは、doctrine/ormが今v2.5.0以下なんです。
いざ、確認!!!

$ composer show doctrine/orm    //導入中のpackage(doctrine/orm)詳細を表示
name     : doctrine/orm
descrip. : Object-Relational-Mapper for PHP
keywords : database, orm
versions : * v2.4.7
...

 

エラー警告通りで、doctrine/ormのバージョンは、2.5.0以下の2.4.7でした。
ということで、現状のパッケージ群のままでhautelook/alice-bundleを導入する想定で問題解決していきたいと思います。

そんなとき使うのが、「packagist 」です!
packagistでは、composerで導入できるパッケージ群が管理されていて検索ができてバージョンごとの詳細も確認できます。

そこで導入したいパッケージ(hautelook/alice-bundle)を検索して、左側のバージョン選択欄からバージョンを下げながら見ていって、
導入ができない原因のパッケージ(doctrine/orm)のバージョン(現在v2.4.7)が通る導入したいパッケージ(hautelook/alice-bundle)のバージョンを見つけます。

\日本語下手でごめんなさい…/

packagist検索結果

 

この方法により
hautelook/alice-bundlev1.4.1であれば、
doctrine/orm: ^2.4doctrine/ormv2.4以上なので
導入できることがわかりました!

 

したがって、hautelook/alice-bundleを最新版ではなくv1.4.1をバージョン指定で導入してみます。

$ composer require hautelook/alice-bundle:1.4.1 --dev
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 3 installs, 0 updates, 0 removals
  - Installing fzaninotto/faker (v1.8.0): Downloading (70%)
  ....

いけましたー!!!

$ composer show hautelook/alice-bundle
name     : hautelook/alice-bundle
descrip. : Symfony2 Bundle to manage fixtures with Alice and Faker.
keywords : Fixture, alice, faker, orm, symfony
versions : * v1.4.1
...

 

便利なComposerコマンド一覧

参考:https://qiita.com/KEINOS/items/86a16b06af6e936a1841

インストール済みのパッケージ情報の一覧を取得

$  composer show -i
You are using the deprecated option "installed". Only installed packages are shown by default now. The --all option can be used to show all packages.
...
phpunit/phpunit                    7.5.12   The PHP Unit Testing framework.

 

パッケージ名指定もできます。

$ composer show [vendor]/[package]
$ composer show phpunit/phpunit
name     : phpunit/phpunit
descrip. : The PHP Unit Testing framework.
keywords : phpunit, testing, xunit
versions : * 7.5.12
type     : library
license  : BSD 3-Clause "New" or "Revised" License (BSD-3-Clause) (OSI approved) https://spdx.org/licenses/BSD-3-Clause.html#licenseText
source   : [git] https://github.com/sebastianbergmann/phpunit.git 9ba59817745b0fe0c1a5a3032dfd4a6d2994ad1c
dist     : [zip] https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ba59817745b0fe0c1a5a3032dfd4a6d2994ad1c 9ba59817745b0fe0c1a5a3032dfd4a6d2994ad1c
path     : /path/vendor/phpunit/phpunit
names    : phpunit/phpunit
...

 

まとめ

英語のエラーって怖いって思いがちですが、
問題解決方法だったりヒントを表示してくれるだけでほんと助かるなーって気持ちに改めてなりました。

phpenvでいろんなPHPバージョンを操る

 

どうも、PHP大好きなんでPHPの記事ばっかで、すみません。

PHPってバージョンごとに書き方違うし、
レガシーな業務ではPHP5系だけど、個人ではPHP7系使いたいってことがよくあります。
そんなとき、composerで管理しちゃえばいいんですが、phpenv使ってバージョン切り替えできるとpsyshでも試せるので良いです!
そこで、phpenvでのバージョン切り替え方法をいっつも忘れてしまうので備忘録として書きます。

phpenvとphp-buildの導入

以下の記事を参考に導入しました。
phpenvとは別に、php-buildが必要なのを知らず私は導入に手こずりました^^;
https://qiita.com/ispern/items/97e3e6d910eb98b5de75

PHPのバージョンを切り替える

今回はグローバルのPHPバージョンを切り替える方法を記載します。
ローカルの場合はglobalの部分をlocalに置換すれば大体行けると思います。(多分)

▼現在のバージョンを確認

$ php -v
PHP 7.1.30 (cli) (built: Jul  4 2019 21:55:42) ( NTS )
...

 

▼現在適用されているphp.ini(設定ファイル)の確認
現在、phpenvで7.1.30が適用されいることがわかります。

$ php --ini
Configuration File (php.ini) Path: /Users/use_name/.phpenv/versions/7.1.30/etc
Loaded Configuration File:         /Users/use_name/.phpenv/versions/7.1.30/etc/php.ini
Scan for additional .ini files in: 
...

 

▼phpenvでインストールされているバージョン一覧と適用中バージョンの確認
*がついているのが、現在適用中のバージョンになります。
7.1.30の他に7.2.20を導入しています。

$ phpenv versions
  system
* 7.1.30 (set by /Users/user_name/.phpenv/version)
  7.2.20

 

▼phpenvでインストール可能なバージョンリスト一覧を表示

$ phpenv install -l
Available versions:
  5.2.17
...
  7.3.6
  7.3.7
...

 

▼バージョンを指定して、インストールをする。
今回は新たに、v5.6.1を入れます。
!!!時間かかります!!!

$ phpenv install 5.6.40
[Info]: Loaded extension plugin
[Info]: Loaded apc Plugin.
[Info]: Loaded composer Plugin.
...

 

インストールし終わったら、、、
▼インストール済みの一覧にあるか確認します。
5.6.1が新しく追加されています。

$ phpenv versions
  system
  5.6.1
* 7.1.30 (set by /Users/user_name/.phpenv/version)
  7.2.20

 

▼hashの更新をします。
!!!rehash忘れがちなので、忘れずに!!!

$ phpenv rehash

 

▼グローバルのPHPバージョンを切り替えます。

$ phpenv global 5.6.1

 

▼現在のバージョンを確認します。
見事に5.6.1に切り替えれています\(^^)/

$ phpenv versions
  system
* 5.6.1 (set by /Users/user_name/.phpenv/version)
  7.1.30
  7.2.20
$ php -v
PHP 5.6.1 (cli) (built: Jul 14 2019 18:29:23)
...

参考:https://www.aiship.jp/knowhow/archives/25452

まとめ

phpenvってpyenvって打ち間違えますよね!!!?
わたしだけですか?
(多分、みんなそうなはず)

 

これほしい!

DateTime::modifyに気をつける

 

どうも!実は、
この一ヶ月有給消化と言う名の
ニートしてまして、
明日から社会人に戻ります。。。
遅刻しないかとかすっごい心配です^^
新しい環境にdkwk(ドキドキワクワク)してます!

ちょっと遅刻に絡めて、
DateTime::modify
を使用する上で、気をつけるべきところを
備忘録として書いておきます。

 

※知ってる人は知ってるであろうことですし、
まあ、そりゃそうでしょって思う方もいると思います。

DateTime::modifyは使うたびに、
タイムスタンプを変更しています。
なので、何度も何度も使用すると予期せぬことに…

参考:https://www.php.net/manual/ja/datetime.modify.php

 

test.php

<?php

$d = new DateTime();
echo "{$d->format('Y-m-d')} \n";
// 本日、2019-06-30が起点になる

echo "{$d->modify('+1 days')->format('Y-m-d')} \n";
// 予想: 2019-06-30の1日後の「2019-07-01」

echo "{$d->modify('+1 month')->format('Y-m-d')} \n";
//予想: 2019-06-30の1ヶ月後の「2019-07-30」
$ php test.php
2019-06-30
2019-07-01
2019-08-01  //実は、2019-07-01の1ヶ月後になる!

$d->modify(‘+1 month’)の前に、
$d->modify(‘+1 days’)を実行しているために
このような結果になっています。

 

 

予想通りにしたいときは、
newし直したり、cloneしなおすといい感じになります。

<?php

$d = new DateTime();
echo "{$d->format('Y-m-d')} \n";
// 本日、2019-06-30が起点になる

echo "{$d->modify('+1 days')->format('Y-m-d')} \n";
// 予想: 2019-06-30の1日後の「2019-07-01」

/* ここから変更 */
$d_1 = new DateTime(); //newし直す
echo "{$d_1->modify('+1 month')->format('Y-m-d')} \n";
//予想: 2019-06-30の1ヶ月後の「2019-07-30」
$ php test.php
2019-06-30
2019-07-01
2019-07-30  //2019-06-30の1ヶ月後になる!(予想通り)

 

 

date_modify — DateTime::modify() のエイリアス

っていうのは、知らなかったです・・・。
時間を操るって癖があって難しいですねー
っていうことで、明日から遅刻しないように頑張ります!!!

 

Herokuのアドオン「Heroku Postgres」を使ってみる



どうも!
大好きな宮崎を旅立ちまして、名古屋市民になりました。
4日間荷物が来なかったので、
床に毛布一枚でねるという生活で腰が痛かったです。
荷物がきた時は嬉しくて、
4時間以内にすべてダンボールを開封し部屋完成させました。

Heroku好きなんですが、DB使えるの知らなかったです。
!! アドオンでPostgreSQLを使えるのです !!(無料枠あり)

使い方

1.Heroku Postgresの導入・データベースの作成

– コマンドより

$ cd [APP_NAME]
$ heroku addons:create heroku-postgresql:hobby-dev
Creating heroku-postgresql:hobby-dev on ⬢ [APP_NAME]... ⣾
Creating heroku-postgresql:hobby-dev on ⬢ [APP_NAME]... free
Database has been created and is available
 ! This database is empty. If upgrading, you can transfer
 ! data from another database with pg:copy
Created [DATABASE_NAME] as HEROKU_POSTGRESQL_GRAY_URL
Use heroku addons:docs heroku-postgresql to view documentation

データベースが作成されます。
[DATABASE_NAME]の部分に具体的なデータベース名が入ります。

$ heroku pg:info
(node:6031) [DEP0066] ...
=== DATABASE_URL
Plan:                  Hobby-dev
Status:                Available
Connections:           0/20
PG Version:            11.3
Created:               2019-06-19 01:54 UTC
Data Size:             8.1 MB
Tables:                3
Rows:                  1/10000 (In compliance)
Fork/Follow:           Unsupported
Rollback:              Unsupported
Continuous Protection: Off
Add-on:                [DATABASE_NAME]

 

– Herokuダッシュボードより
Herokuダッシュボード > Heroku Postgresを導入したいAPPを選択後、
Resorce  > Add-ons に遷移する。
「Heroku Postgres」を検索し、プランを選んでをProvisionする
今回はフリープランの、「Hobby Dev – Free」を選びました。

データベースが作成されます。
[DATABASE_NAME]の部分に具体的なデータベース名が入ります。

 

2.データベース操作

– コマンドより
※ローカルPCより、HerokuPostgresに接続して直接SQL操作をしたい場合、
ローカルPCにpsqlコマンドが導入されていないとできませんので以下を参考に導入してください。
参考:https://qiita.com/ucan-lab/items/fb74af3d78e71407db7b

$ heroku pg:psql [DATABASE_NAME]  //データベースに接続
(node:6110) [DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated
--> Connecting to [DATABASE_NAME]
psql (11.3)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.



-- テーブル作成
[APP_NAME]::DATABASE_URL=> CREATE TABLE member_tb (id serial PRIMARY KEY,user_id TEXT NOT NULL,display_name TEXT NOT NULL);
CREATE TABLE

[APP_NAME]::DATABASE_URL=> SELECT * FROM member_tb;
 id | user_id | display_name
----+---------+--------------
(0 rows)



-- レコード挿入
[APP_NAME]::DATABASE_URL=> INSERT INTO member_tb(user_id, display_name) VALUES ('abc123', 'taro');
INSERT 0 1

[APP_NAME]::DATABASE_URL=> SELECT * FROM member_tb;
 id | user_id | display_name
----+---------+--------------
  1 | abc123  | taro
(1 row)

 

– Herokuダッシュボードより
Datestoresにてデータベースを選択後、
Dataclips > Create a new dataclip

ここでは、読み込み(SELECT)のみ実行可能。
書き込み(CREATE TABLEやINSERT)はできませんでした・・・。

 

 

3.アプリとデータベースの接続例

データベースが作成されると自動で、
Setting > Config Varsに「DATABASE_URL」が追加されます。
これをgetenvで使用します!
※データベースが複数存在する場合、「HEROKU_POSTGRESQL_GRAY_URL」「HEROKU_POSTGRESQL_MAROON_URL」のように追加されます。

PHP Data Objects (PDO) 拡張モジュールを使いました。

<?php

$url = parse_url(getenv('DATABASE_URL'));
$dsn = sprintf('pgsql:host=%s;dbname=%s', $url['host'], substr($url['path'], 1));
$pdo = new PDO($dsn, $url['user'], $url['pass']);


$stmt = $pdo->prepare("SELECT * FROM member_tb WHERE id = ?");
$stmt->execute([$id]);
$result = $stmt->fetch();

 

参考:

http://neos21.hatenablog.com/entry/2018/12/06/080000
https://qiita.com/ucan-lab/items/fb74af3d78e71407db7b

まとめ

無料枠あるのすごいありがたいです^^
これでいっぱいアプリ作れますね。
わたしはHerokuPostgresを使ってLINEBOT作成中です。
近々、BOTの記事投稿できるように頑張りまーす!



複数チケットサービス検索用ライブラリを作って、はじめてのpackagist登録

どうもー!
5月なのに暑いですね、そして梅雨こないですねー
宮崎にも来る気配なしです。

 

今日も音楽フェスに行くのですが、
皆さん、チケットを取る際にどのチケット販売サービスを使っていますか?
どういう基準で選んでますか?
ポイントだったり会員ランクなどてしょうか。

 

入手困難なバンドなんかだと、
私の経験でよくあるのがA,Bというサービスではsold outにもかかわらず、
Cでは残りわずかで残っていたりするのです。
ありがたく即購入するんですが、
この事実に辿りつくまで、
サービスAで検索→sold outを確認
→サービスBで検索→sold outを確認
→サービスCで検索→残りわずかを確認
→嬉し恥ずかし焦る気持ちを抑える
→無事ゲット!!!(ゴール)

ゴールに行くまで3サイトも順番に回るのです。
順番や早さによっては、
先に早く見ておけば取れたかもしれないチケットを
失ってしまうのです。
そんな自分の経験から、複数サービス
(現在チケットぴあ、eplus、ローチケ)を
一括検索できるライブラリを作りました!
スクレイピングを使ってるので、
リニューアルとかされると困りますw

Github→kin29/ticket-hunter

 

Packagistにも登録済みですので、

$ composer require kin29/ticket-hunter

からも取得可能でございます。

 

今回、Packagistへの登録を初めてしました!

こちらを参考にさせてもらいました。

https://qiita.com/niikunihiro/items/fbd696e506e734782d8f

6. GithubのサービスフックにPackagistを登録する

は、自動になったのか勝手に登録された気がしました。

 

 

使い方は、簡単です!

  • 使いたい販売サービス名(TicketPia/Eplus/LawsonTicketのどれか、複数指定可)
  • キーワード(「雨のパレード」など)

を渡すだけです。

<?php

try {
  $ticketVendors = new Kin29\TicketHunter\TicketHunter(['TicketPia', 'Eplus', 'LawsonTicket'], '雨のパレード');
  $ticketVendors->echoJson($ticketVendors->getList());
} catch (\Exception $e) {
    die($e->getMessage());
}

出力結果はこんな感じです。

{
  "TicketPia": [
    {
      "title": "GRAPEVINE/雨のパレード〔愛知〕",
      "date_time": "2019/8/2(金)",
      "pref_id": "23",
      "pref_name": "愛知県",
      "stage": "名古屋クラブクアトロ",
      "sale_method": "先行抽選",
      "sale_status": "近日抽選受付2019/6/8(土) 10:00 ~ 2019/6/10(月) 18:00",
      "link": "http://ticket.pia.jp/pia/ticketInformation.do?eventCd=1922500&rlsCd=&lotRlsCd=27947"
    },
    ...
  ],
  "Eplus": [
    {...省略...},
  ],
  "LawsonTicket": [
    {...省略...},
  ]
}

販売前の場合、販売開始日を取得できるといいな
と今気づきました、issueに入れておこう!
日付のフォーマットも入れてないな、、、入れよう。
アウトプットしてると気づくことて多いですよね、大切。

 

このライブラリを作っていた矢先、先を越されたかのように、
Freaxがリリースれました、、便利ですね。

内容が被っていますが、ライブラリはなさそうなので作り続けました^^;

 

今後は、このライブラリを使ってAPIを作りたいなとおもっています。
BearSundayでまた作りたいなあと思っております。
さらに、検索フォームをSymfonyで作ろうかと思っています。

 

怠けないように宣言しておきます!

では、森道市場2019行ってきます!

 

 

php-cs-fixerとphp_codesnifferの違いって?

 

サボり気味でした。。。
五月病ですね、気引き締めます。
ネタもちょっと尽きてきた感ありますが、頑張ります!

 

コード整形系のツール

  • php-cs-fixer
  • php_codesniffer

って大変お世話になるんですが、
名前が似すぎて同じものだと思っていました。
なのでちょっと違いを調べてみました。

 

おまけで、

  • phpmd/phpmd
  • phpstan/phpstan-shim

についても、書きました!

 

php-cs-fixer

[friendsofphp/php-cs-fixer]

.php_cs/.php_cs.dist(ルール設定ファイル)に沿って、
PHPコードの修正を提示してくれたり、修正を実行してくれる。

 

(例)配列の書き方はshortに統一することを宣言

.php_cs.dist

...
'array_syntax' => ['syntax' => 'short'],
...

実行結果

// コード修正の提示
$ vendor/bin/php-cs-fixer fix -v --dry-run
Loaded config default from "/Users/XXXX/ticket-hunter-api/.php_cs.dist".
Using cache file ".php_cs.cache".
SSSSSSSFSSSSS
Legend: ?-unknown, I-invalid file syntax, file ignored, S-Skipped, .-no changes, F-fixed, E-error
1) src/test.php (array_syntax)

以下のように、書き直すことで S-Skipped となり解消されます。

-        $arrRet = array();
+        $arrRet = [];

 

 

ただし、実行結果の出力より対象ファイル名は分かりますが、
何行目かなどの詳細はわかりません。
そこで、修正の実行もしてもらう事もできます。「–dry-run」を外すだけです。

$ vendor/bin/php-cs-fixer fix -v --dry-run

 

 

さらに、.php_cs.distでルールを設定する際、
各ルールの具体例みたいなーって時は、以下コマンドで確認できます。

$ vendor/bin/php-cs-fixer describe [name]
$ vendor/bin/php-cs-fixer describe array_syntax
Description of array_syntax rule.
PHP arrays should be declared using the configured syntax.

Fixer is configurable using following option:
* syntax ('long', 'short'): whether to use the `long` or `short` array syntax; defaults to 'long'

Fixing examples:
 * Example #1. Fixing with the default configuration.
   ---------- begin diff ----------
   --- Original
   +++ New
   @@ -1,2 +1,2 @@
    <?php
   -[1,2];
   +array(1,2);

   ----------- end diff -----------

 * Example #2. Fixing with configuration: ['syntax' => 'short'].
   ---------- begin diff ----------
   --- Original
   +++ New
   @@ -1,2 +1,2 @@
    <?php
   -array(1,2);
   +[1,2];

   ----------- end diff -----------

 

 

 

php_codesniffer

[squizlabs/php_codesniffer]

phpcs(PHP Code Sniffer) …判定
phpcbf(PHP Code Beautifier and Fixer) …整形

既定のコーディング標準として用意されているもの(vendor/bin/phpcs -i)をベース
に判定/整形を行う。

phpcs.xml(設定ファイル)にexclude(除外)するものや追加ルールを記載し、カスタマイズすることが可能です。

 

//現在、デフォで適用されているコーディング規約の確認
$ vendor/bin/phpcs -i
The installed coding standards are PEAR, Zend, PSR2, MySource, Squiz, PSR1 and PSR12

 

例えば、既定のコーディング標準のままだとPEARはfunctionコメントが必須なので、
phpcs.xmlでexcludeする定義を書いたりしてカスタマイズすることができます。

phpcs.xml の例

<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="PHP_CodeSniffer" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="./vendor/squizlabs/php_codesniffer/phpcs.xsd">
 <rule ref="Generic.CodeAnalysis.UnusedFunctionParameter"/>
 <rule ref="PEAR.Commenting.FunctionComment">
 <exclude name="PEAR.Commenting.FunctionComment.MissingReturn"/>
 <exclude name="PEAR.Commenting.FunctionComment.MissingParamComment"/>
 <exclude name="PEAR.Commenting.FunctionComment.SpacingBeforeTags"/>
 <exclude name="PEAR.Commenting.FunctionComment.MissingParamTag"/>
 <exclude name="PEAR.Commenting.FunctionComment.Missing"/>
 <exclude name="PEAR.Commenting.FunctionComment.ParameterCommentsNotAligned"/>
 </rule>
</ruleset>

 

実行方法

$ vendor/bin/phpcs [適用コーディング(--standard=PSR2とか)] [対象ファイル名]
$ vendor/bin/phpcbf [適用コーディング(--standard=PSR2とか)] [対象ファイル名]

 

phpcs.xmlに沿って、src/以下を判定してもらう。

$ vendor/bin/phpcs --standard=phpcs.xml src

FILE: /Users/XXXX/YYY/src/test.php
----------------------------------------------------------------------
FOUND 0 ERRORS AND 2 WARNINGS AFFECTING 1 LINE
----------------------------------------------------------------------
 9 | WARNING | The method parameter $arrString is never used
 9 | WARNING | The method parameter $arrRet is never used
----------------------------------------------------------------------

Time: 57ms; Memory: 6MB

使われていないメソッドの引数があるよーって教えてくれました。

 

phpcbfでフォーマットしてもらう例は以下です。

$ vendor/bin/phpcbf --standard=PEAR src

PHPCBF RESULT SUMMARY
-----------------------------------------------------------------------------
FILE                                                         FIXED  REMAINING
-----------------------------------------------------------------------------
/Users/XXXX/YYY/src/test1.php                                 7      10
/Users/XXXX/YYY/src/test2.php                                 2      5
/Users/XXXX/YYY/src/test3.php                                 2      8
-----------------------------------------------------------------------------
A TOTAL OF 11 ERRORS WERE FIXED IN 3 FILES
-----------------------------------------------------------------------------

 

 

ついでに、、、

phpmd

[phpmd/phpmd]

phpmd.xml(ルール設定ファイル)に沿って、
「ここに使われてない変数あるよ」や、
「コンストラクタ関数が__constractじゃなくてclassnameで宣言されてるやつはダメよ」とか、「ここキャメルケースになってないよ」とかの注意をしてくれる。
若干、phpcsなどとルール設定がかぶる可能性はありそうです。

 

phpmd.xml の例

...
 <!--naming-->
 <rule ref="rulesets/naming.xml/ConstantNamingConventions"/>
 
 <!--unusedcode-->
 <rule ref="rulesets/unusedcode.xml/UnusedFormalParameter"/>
 <rule ref="rulesets/unusedcode.xml/UnusedLocalVariable"/>
 <rule ref="rulesets/unusedcode.xml/UnusedPrivateField"/>
 <rule ref="rulesets/unusedcode.xml/UnusedPrivateMethod"/>
 
 <!--controversial-->
 <rule ref="rulesets/controversial.xml/CamelCaseClassName"/>
 <rule ref="rulesets/controversial.xml/CamelCasePropertyName"/>
 <rule ref="rulesets/controversial.xml/CamelCaseMethodName"/>
...

 

実行方法

$ phpmd [file_path] [結果出力フォーマット(text/xml)] [ルール設定ファイル]
$ vendor/bin/phpmd src/TicketHunter.php text phpmd.xml

./Users/XXX/YYY/src/test.php:9 Avoid unused parameters such as '$arrString'
./Users/XXX/YYY/src/test.php:9 Avoid unused parameters such as '$arrRet'

 

 

phpstan

phpstan/phpstan-shim

静的解析ツールで、
– 構文エラーはないか?
– 関数に渡すパラメータの数が適切か?
– 未定義のものにアクセスしようとしていないか?
– 関数に渡す値が関数のパラメータの型宣言と一致するか?
– PHPDocの内容と関数の戻り値は同じか?
などをチェックしてくれます。

除外したいルールがあればphpstan.neonで設定のカスタマイズできるようです。

$ vendor/bin/phpstan analyse -l [レベル] [ファイルパス] -c phpstan.neon

レベルは0~7,maxがあり、レベルが高くなるにつれて厳しくチェックしてくれるとのことです。
またオプションで「–no-progress」を付与すると、進捗を表すバーを非表示にしてくれます。
その他詳細は $ vendor/bin/phpstan analyse -h で確認ができます。

 

$ vendor/bin/phpstan analyse -l max src tests -c phpstan.neon --no-progress
 ------ ----------------------------------
  Line   src/test1.php
 ------ ----------------------------------
  11     Undefined variable: $arrString
 ------ ----------------------------------


 [ERROR] Found 1 errors

参考:https://qiita.com/qiita_masaharu/items/24bf34579119628eefe2

 

 

 

まとめ

各ツールでルール設定が異なっている場合、
(変数名をphpcsではキャメルケース、phpmdではスネークケースを許容している等)
phpcsでは判定が通るけど、phpmdでは判定が通らない
ということが起きかねないなと感じました。
なので、ガチガチにルールを決めちゃうのも
手間がかかるし結構大変だなと思いました。

私はこちらをよく使わせてもらってます。
[Koriym.PhpSkeleton]

 

 



macのデフォルトPythonバージョンを2系→3系にあげる

備忘録でかきまっす。

多分デフォルトが2.7とかです

$ python --version
Python 2.7.10

 

まず、pyenvを導入(pyenv経由でpython3系いれるので)

$ brew install python

$ brew install pyenv

$ vi ~/.bash_profile
# 以下追記
if which pyenv > /dev/null; then eval "$(pyenv init -)"; fi

$ source  ~/.bash_profile

$ pyenv -v
pyenv 1.2.11

 

いざ、pyenv経由でpython3.7.3を入れようとするとエラーになるので、
ググったら、xcode-select –installするといいって言われるんですが、
そもそもxcode-select –installでエラーになるので、

$ pyenv install 3.7.3 


$ xcode-select --install
python-build: use readline from homebrew

BUILD FAILED (OS X 10.14.4 using python-build 20180424)

Inspect or clean up the working tree at /var/folders/hc/gl83_1zs025_kphfqj1_96840000gn/T/python-build.20190505153537.45480
Results logged to /var/folders/hc/gl83_1zs025_kphfqj1_96840000gn/T/python-build.20190505153537.45480.log

 

macOSバージョン確認して、

$ sw_vers
ProductName:  Mac OS X
ProductVersion: 10.14.4
BuildVersion: 18E226

OSバージョンに合わせて、sdkをインストールする。

$ sudo installer -pkg /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg -target /

 

すると、pyenv経由でpython3.7.3を入れれるようになります。(たぶん)

$ pyenv install 3.7.3

 

ただ、pythonコマンドは
まだ2系のままです。

$ python -V
Python 2.7.10

グローバルを3.7.3系にします。

$ pyenv global 3.7.3

$ pyenv versions
  system
* 3.7.3 (set by /Users/user_name/.pyenv/version)

$ python --version
Python 3.7.3

TemplateMethodパターン

どうも、今月ブログ更新をサボり気味で後悔してます。
「ブログ続けてるのすごい」って言われて嬉しかったので、
更新ちゃんと頑張ります。
遡ってみると2017年1月からブログを始めて早2年経ってました。
累計113記事でした。

\目標、1000記事/

今回は、TemplateMethodについてです。

 

TemplateMethodとは?

メソッドにおけるアルゴリズムの骨組みを定義し、
いくつかの手順をサブクラスに先送りします。

→ アルゴリズムとなる一連の手順(メソッド)を
定義するメソッド(テンプレート)があり、
その一連の手順の中のメソッドの1つ以上を
抽象メソッドとして定義し、
サブクラスがそれを実装する

 

クラス図

テンプレートメソッドのクラス図

 

コード

abstract class AbstractClass
{
    public function templateMethod(): void
    {
        $this->primitiveOperation1();
        $this->primitiveOperation2();
    }

    //それぞれ異なる手順→サブクラスにまかせる
    abstract public function primitiveOperation1();

    public function primitiveOperation2(): void
    {
        //共通の手順
    }
}
class ConcreteClass extends AbstractClass
{
    public function primitiveOperation1(): void
    {
        // TODO: Implement primitiveOperation1() method.
    }
}

 

いざ、実装!

Head Firstデザインパターンの具体例では、
コーヒーと紅茶の作り方がでてきました。

レシピ

▶︎コーヒーの作り方
1.お湯を沸かす
2.フィルターをセットして淹れる
3.コップにコーヒーを注ぐ
4.ミルクや砂糖を加える

▶︎紅茶の作り方
1.お湯を沸かす
2.ティパックをセットしてお湯をいれる
3.レモンをいれる
4.コップに注ぐ

 

ざっくりみると、コーヒーと紅茶の作り方は似ているので、
1~4の一連の手順をテンプレートメソッドとしてスーパークラスに定義し、
「1.お湯を沸かす」の部分は共通化できるので、スーパークラスで実装します。
コーヒーと紅茶で異なる部分はサブクラスで実装します。

実際のコードはこちら↓
[kin29/HeadFirstDezaPata]

テンプレートメソッド適用前と後のbefore-after

 

クラス図

カフェインでのテンプレートメソッド

 

コード

abstract class CaffeineBeverage
{
    public function prepareRecipe(): void
    {
        $this->boilWater();
        $this->brew();
        $this->pourInCup();
        $this->addCondiments();
    }

    abstract public function brew(): void;

    abstract public function addCondiments(): void;

    public function boilWater(): void
    {
        echo "お湯を沸かします\n";
    }

    public function pourInCup(): void
    {
        echo "カップに注ぎます\n";
    }
}

 

class Coffee extends CaffeineBeverage
{
    public function __toString(): string
    {
        return "--- コーヒーのレシピ ---\n";
    }

    public function brew(): void
    {
        echo "フィルターでコーヒをドリップします\n";
    }

    public function addCondiments(): void
    {
        echo "砂糖とミルクを追加します\n";
    }
}

class Tea extends CaffeineBeverage
{
    public function __toString(): string
    {
        return "--- 紅茶のレシピ ---\n";
    }

    public function brew(): void
    {
        echo "紅茶を浸します\n";
    }

    public function addCondiments(): void
    {
        echo "レモンを追加します\n";
    }
}
$ php src/TemplateMethod/main.php
--- コーヒーのレシピ ---
お湯を沸かします
フィルターでコーヒをドリップします
カップに注ぎます
砂糖とミルクを追加します
--- 紅茶のレシピ ---
お湯を沸かします
紅茶を浸します
カップに注ぎます
レモンを追加します

メリット

・共通メソッドの変更が必要となった場合、
スーパクラスの変更のみでいい。

・スーパクラスで骨組みを定義しているので、
新しいカフェインクラスの追加が必要になった場合(例えばココアとか?)、
実装がしやすい

 

 

令和もがんばろーっっと

GASの「起動時間の最大値を超えました」の壁を超えてみる!!!

 

新玉ねぎっておいしいですよね。甘くって。
ただ、生で食べる時は、まあまあ薄くきらないと苦くって悲しくなりました。

今回は、みんな大好きGASの
「最大起動時間の壁を超える!」
の巻です。

 

GASの起動時間の最大値は?

リファレンスより、

「Consumer(多分無課金の人)のScript runtime」は 6minのようですね。

 

つまり、

一回の処理が6分を超えると、
「起動時間の最大値を超えました」
と表示され処理が途中で終わってしまいます。
(そもそも、6分以上の処理させんなよって感じなんですが、、、)

ググったら、回避方法を書いてくれてる方がおり、とても助かりました。
おかげで、わたしのバカ長い処理も何回かに分けて、
最後まで実行完了することができるようになりました。

参考:http://direction-note.com/gas-beyond-execution-time/

やり方

処理が5分を超える場合は、
トリガーを設定して、1分後に処理を続きからするようにする。

 

①for文のカウントなどを保持し、正しく続きから処理を再開するために、
GASのスクリプトプロパティでデータを保持させます。

スクリプトのプロパティとは?

ファイル > プロジェクトのプロパティ > スクリプトのプロパティ
にあり、キーバリューでデータを保持できます。

スクリプトのプロパティ

↑ キー:nowRow、バリュー:10のデータを保持させている。
この画面の「行を追加」ボタンよりデータを追加/削除することが可能。

 

GASにて追加することも可能

let properties = PropertiesService.getScriptProperties();

//スクリプトプロパティの登録
properties.setProperty(key, value);

//スクリプトプロパティのバリューを取得
properties.getProperty(key); //return value

 

 

②最大処理時間を超える前の5分で処理を中断し、
1分後にトリガーを設定する。

let properties = PropertiesService.getScriptProperties();
let targetArray = ['foo', 'bar', 'baz'....];  //この配列が膨大量の時に有効

/* メイン */
function mainSort() {  
  ...
  setOutputSheet();
}

/* 5分以上かかる可能性のある処理 */
function setOutputSheet(){
  let startTime = new Date();
  //スクリプトプロパティにトリガーIDを保存するときに使用するkey名
  let triggerKey = "trigger";  
  ...
  for(let i = 0; i < targetArray.length; i++){
    //開始時刻(startTime)と現時点の処理時点の時間を比較する 
    let diff = parseInt((new Date() - startTime) / (1000 * 60));
    if(diff >= 5){
    ...
      //トリガー(1分後)を登録する
      setTrigger(triggerKey, "mainSort");
      return;
    }

    //todo 5分以上かかる可能性のある処理
  }   
  
  //全て実行終えたらトリガー不要なためを削除する
  deleteTrigger(triggerKey);
}


/**
 *指定したkeyに保存されているトリガーIDを使って、トリガーを削除する
 *
 * @param string triggerKey
 * @return void
 */
function deleteTrigger(triggerKey) {
  let triggerId = properties.getProperty(triggerKey);
  if(!triggerId) return;
  
  ScriptApp.getProjectTriggers().filter(function(trigger){
    return trigger.getUniqueId() == triggerId;
  })
  .forEach(function(trigger) {
      ScriptApp.deleteTrigger(trigger);
  });
  properties.deleteProperty(triggerKey);
}

/**
 * トリガーを発行する
 *
 * @param string triggerKey
 * @param string funcName
 * @return void
 */
function setTrigger(triggerKey, funcName){
  //既に同名で保存しているトリガーがあったら削除
  deleteTrigger(triggerKey);
  
  //1分後にトリガーを登録する
  let date = new Date();
  date.setMinutes(date.getMinutes() + 1);
  let triggerId = ScriptApp.newTrigger(funcName).timeBased().at(date).create().getUniqueId();
  Logger.log('setTrigger function_name "%s".', funcName);
  
  //あとでトリガーを削除するために「スクリプトのプロパティ」にトリガーIDを保存しておく
  properties.setProperty(triggerKey, triggerId);
}

 

作ってみたやつ

①②を組み合わせると、最大起動時間の壁を超えれるということです!
作ってみた全体はこんな感じです。
※このコード自体の処理は5分超えなかったのでsleep()とかして試してましたっw

 

こんな感じの横並びを、、、

縦にソートします。

 

プレーヤーが日々追加されることを想定して、
一年の日数 × 最終的なプレーヤー人数分で縦に並ぶように作ってます。

 

コード全体(テキトーなので参考程度で! )

let properties = PropertiesService.getScriptProperties();

//★メイン
function mainSort() {
  
  //プレーヤーマスター配列を作成
  let playerMaster = getPlayerMaster();
  let playerCount = Object.keys(playerMaster).length
  
  //日付の配列を取得
  let dataMaster = getDataMaster();
  let dataCount = Object.keys(dataMaster).length
  
  //日別結果データの取得
  let DayDate = getDayDate(playerCount, dataCount);
  
  //出力用シート(outputSheet)に日別結果の縦版の値を出力する。
  setOutputSheet(playerMaster, dataMaster, DayDate);
}

/**
 * 「No」「プレーヤー」を配列で取得する
 * 
 * @return arrShopMaster = {
 *   1: 'リュウ',
 *   2: 'ケン'
 * };
 */
function getPlayerMaster() {
  let ABcolumnAll = inputSheet.getRange('A2:B101').getValues();
  let LastRow = ABcolumnAll.filter(String).length;
  let arrPlayerMaster = [];
  
  //「-」を除く
  for(let i = 0; i < LastRow; i++){
    if(!ABcolumnAll[i][1].match(/\-/)){
      arrPlayerMaster[ABcolumnAll[i][0]] = ABcolumnAll[i][1]; 
    }  
  }

  return arrPlayerMaster;
}

/**
 * 日付を配列で取得する
 * 
 * @return array arrDataMaster
 */
function getDataMaster() {
  let rowAll = inputSheet.getRange('C1:ND1').getValues();
  let arrDataMaster = [];
  
  for(let i = 0; i < yearSum; i++){
    arrDataMaster[i] = rowAll[0][i];  
  }

  return arrDataMaster;
}

/**
 * 日付をキーにその日の結果を格納する。
 *
 * @param intger playerCount
 * @param intger dataCount
 * @return array arrRet
 */
function getDayDate(playerCount, dataCount)
{
  let col = inputSheet.getRange('C2:NQ'+(playerCount+1)).getValues();

  let arrRet = [];
  for(let i = 0; i < playerCount; i++){
    arrRet[i] = [];
    for(let j = 0; j < dataCount; j++){
      arrRet[i][j] = col[i][j];
    }
  }
  return arrRet;
}

/**
 * 5分以上かかる可能性のある処理
 *
 * @return void
 */
function setOutputSheet(playerMaster, dataMaster, DayDate){
  //処理の開始時刻を取得
  let startTime = new Date();
  //sleep(300000); //意図的に(300秒/60秒=)5分スリープさせる
  
  //途中経過保存用の変数
  let rowKey = "nowRow";   //何行目まで処理したかを保存するときに使用するkey
  let dateKey = "nowDate";  //yearSumが何行目まで処理したかを保存するkey
  let playerCountKey = "nowPlayerCount"; //lastRowが何行目まで処理したかを保存するkey
  let triggerKey = "trigger";  //トリガーIDを保存するときに使用するkey
  
  let nowRow = parseInt(properties.getProperty(rowKey));
  if(!nowRow) nowRow = 1;  //初回処理の時
  
  let lastRow = Object.keys(DayDate).length;

  let now_i = 0;
  let nowDate = parseInt(properties.getProperty(dateKey));
  if(nowDate){ 
    let now_i = nowDate;
    properties.deleteProperty(dateKey);
  }
  for(let i = now_i; i < yearSum; i++){
    let date = dataMaster[i];
    
    let now_j = 0;
    let nowPlayerCount = parseInt(properties.getProperty(playerCountKey));
    if(nowPlayerCount){ 
      let now_j = nowPlayerCount;
      properties.deleteProperty(playerCountKey);
    }
    for(let j = now_j; j < lastRow; j++) { 
        //開始時刻(startTime)と現時点の処理時点の時間を比較する
        let diff = parseInt((new Date() - startTime) / (1000 * 60));     
        if(diff >= 5){
        //何行まで処理したかなどを「スクリプトのプリロパティ」に保存する
        properties.setProperty(rowKey, nowRow);
        properties.setProperty(dateKey, i);
        properties.setProperty(playerCountKey, j);
        //トリガー(1分後)を登録する
        setTrigger(triggerKey, "mainSort");
        return;
      }

      outputSheet.getRange(nowRow, 1).setValue(date);
      outputSheet.getRange(nowRow, 2).setValue(playerMaster[j+1]);
      outputSheet.getRange(nowRow, 3).setValue(DayDate[j][i]);
      nowRow++;
    }
  }
  
  //全て実行終えたらトリガーや何行目まで実行したかなどのデータは
  //不要なためを削除する
  deleteTrigger(triggerKey);
  properties.deleteProperty(rowKey);
  properties.deleteProperty(dateKey);
  properties.deleteProperty(playerCountKey);
}


/**
 * 指定したkeyに保存されているトリガーIDを使って、トリガーを削除する
 *
 * @param string triggerKey
 * @return void
 */
function deleteTrigger(triggerKey) {
  let triggerId = properties.getProperty(triggerKey);
  if(!triggerId) return;
  
  ScriptApp.getProjectTriggers().filter(function(trigger){
    return trigger.getUniqueId() == triggerId;
  })
  .forEach(function(trigger) {
      ScriptApp.deleteTrigger(trigger);
  });
  properties.deleteProperty(triggerKey);
}

/**
 * トリガーを発行。トリガーを発行した箇所から
 *
 * @param string triggerKey
 * @param string funcName
 * @return void
 */
function setTrigger(triggerKey, funcName){
  //既に同じ名前で保存しているトリガーがあったら削除
  deleteTrigger(triggerKey);
  
  let date = new Date();
  date.setMinutes(date.getMinutes() + 1); //1分後に再実行
  
  let triggerId = ScriptApp.newTrigger(funcName).timeBased().at(date).create().getUniqueId();
  Logger.log('setTrigger function_name "%s".', funcName);
  
  //あとでトリガーを削除するために「スクリプトのプロパティ」にトリガーIDを保存しておくproperties.setProperty(triggerKey, triggerId);
}