複数チケットサービス検索用ライブラリを作って、はじめての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/shigaayano/.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にて追加することも可能

var properties = PropertiesService.getScriptProperties();

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

//スクリプトプロパティの取得
var [value] = properties.getProperty([key]);

 

 

 

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

var properties = PropertiesService.getScriptProperties();

/* メイン */
function mainSort() {  
  ...
  setOutputSheet(taskMaster, dataMaster, DayDate);
}

/* 5分以上かかる可能性のある処理 */
function setOutputSheet(taskMaster, dataMaster, DayDate){
  var startTime = new Date();
  //トリガーIDを保存するときに使用するkey
  var triggerKey = "trigger";  
  ...

  //開始時刻(startTime)と現時点の処理時点の時間を比較する 
  var diff = parseInt((new Date() - startTime) / (1000 * 60));
  if(diff >= 5){
  ...
    //トリガー(1分後)を登録する
    setTrigger(triggerKey, "mainSort");
    return;
  }
  
  //全て実行終えたらトリガー不要なためを削除する
  deleteTrigger(triggerKey);
  ... 
}


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

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

 

作ってみたやつ

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

 

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

縦にソートします。

 

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

 

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

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

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

  return arrPlayerMaster;
}

/**
 * 日付を配列で取得する
 * 
 * @return array arrDataMaster
 */
function getDataMaster() {
  var rowAll = inputSheet.getRange('C1:ND1').getValues();
  var arrDataMaster = [];
  
  for(var 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)
{
  var col = inputSheet.getRange('C2:NQ'+(playerCount+1)).getValues();

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

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

  var now_i = 0;
  var nowDate = parseInt(properties.getProperty(dateKey));
  if(nowDate){ 
    var now_i = nowDate;
    properties.deleteProperty(dateKey);
  }
  for(var i = now_i; i < yearSum; i++){
    var date = dataMaster[i];
    
    var now_j = 0;
    var nowPlayerCount = parseInt(properties.getProperty(playerCountKey));
    if(nowPlayerCount){ 
      var now_j = nowPlayerCount;
      properties.deleteProperty(playerCountKey);
    }
    for(var j = now_j; j < lastRow; j++) { 
        //開始時刻(startTime)と現時点の処理時点の時間を比較する
        var 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) {
  var triggerId = PropertiesService.getScriptProperties().getProperty(triggerKey);
  if(!triggerId) return;
  
  ScriptApp.getProjectTriggers().filter(function(trigger){
    return trigger.getUniqueId() == triggerId;
  })
  .forEach(function(trigger) {
      ScriptApp.deleteTrigger(trigger);
  });
  PropertiesService.getScriptProperties().deleteProperty(triggerKey);
}

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

 

 

Facadeパターン

 

どうも!四年ぶりくらいに風邪ひいて病院いきました。
病院の看護婦さんが母のように優しくてホッとしました。

 

\今回はFacadeパターンです!/

 

前回の最小知識の原則~PrincipleOfLeastKnowledge

でも出しましたが、Facadeパターンについてまとめます。

 

Facadeパターンとは…

サブシステムの一連のインターフェースに対する、
統合されたインターフェースを提供する。
ファサードは、サブシステムをより使いやすくする
高水準インターフェースを定義する。

 

クラス図

・Facade   …Subsystemをより使いやすく統合したインターフェース

・Client     …ファサードのおかげで作業が簡単になった幸福なクライアン

・Subsystem   …より複雑なサブシステムたち

 

具体的な例

コードはこちら!

 

いろんな電気機器(アンプ/DVDプレーヤー/ポップコーンマシーン)の一連オペレーションを統括したインターフェースをもつ、ホームシアター[Facade]を実装します。

 

・HomeTheaterFacade.php[Facade]
…Subsystemの中で、ホームシアターに必要な
一連のオペレーションを統一したインターフェース

・main.php[Client]
…ホームシアターを使うクライアント

・Subsystem/[Subsystem]
…アンプやDVDプレーヤ、ポップコーンマシーン、
スクリーン、ライトなどの電気機器クラス

 

映画を見るための電気機器の一連のオペレーションを
Facadeパターンで作って、実行した例です。

main.php

$homeTheater = new HomeTheaterFacade(
    $amp,
    $tuner,
    $dvd,
    $cd,
    $projector,
    $screen,
    $light,
    $pop
);
$homeTheater->watchMovie('アナ雪');
$ php src/Facade/main.php
▶︎ 映画を見る準備をします!
ボップコーンマシーンをONにします
ボップコーンを作ります
シアターライトを10にします
スクリーンを下げてセットします
プロジェクターをONにします
プロジェクターをワイドスクリーンモードにします
アンプをONにします
アンプにDVDプレーヤをセットします
アンプにサラウンドサウンドをセットします
アンプにボリュームを5にします
DVDプレーヤーをONにします
DVD「アナ雪」をスタートします

 

Facadeにいろいろ統括系は任せてるので…

Subsystem側は自分の役目に専念できる。
Client側に使いやすいインターフェースをFacadeが提供できるので、
Clientも使いやすい!

 

依存性の高いコードになってしまうこともある!

最小知識の原則~PrincipleOfLeastKnowledgeにも書いたように、
複数のオブジェクトのメソッドを呼び出すことが多くなるので、
ある程度の決められた範囲内のメソッドのみを呼び出さないと、
依存性の高いコードになってしまいます。

なので、以下に気をつけて実装するとマルっ!
1. そのオブジェクト自身のメソッド
2. メソッドの引数として渡されたオブジェクトのメソッド
3. メソッドが作成またはインスタンス化したオブジェクトのメソッド
4. そのオブジェクトのコンポーネント(インスタンス変数で参照されるオブジェクト)[HAS-A関係]のメソッド

 

最小知識の原則~PrincipleOfLeastKnowledge~

「最小知識の原則」についての説明は、
Facadeパターンの章で出てきました。

Facadeパターンは、
サブシステムの一連のインターフェースに対する
統合されたインターフェースを提供するものです。

したがって、Facadeパターンは
複数のオブジェクトのメソッドを呼び出すことが想定されます。
しかし、ある程度の決められた範囲内のメソッドのみを呼び出さないと、
依存性の高いコードになってしまいます。
それを防ぐための原則が
「最小知識の原則」となります。

 

最小知識の原則

「直接の友達とだけ話すこと」
オブジェクト間のやりとりを最小限にするべき。

→多数のクラスが互いに結合し、
システムのある部分に対する変更が他部分に
連鎖してしまうような設計を回避する。

※デメテルの法則 (Law of Demeter, LoD) ともいう。

 

 

以下に属するメソッドだけを呼び出すべき

1. そのオブジェクト自身のメソッド
2. メソッドの引数として渡されたオブジェクトのメソッド
3. メソッドが作成またはインスタンス化したオブジェクトのメソッド
4. そのオブジェクトのコンポーネント(インスタンス変数で参照されるオブジェクト)[HAS-A関係]のメソッド

 

<?php

class Car
{
    //4.このクラスのコンポーネント(HAS-A)の
    //メソッドである、$engine.start()はOK!
    public $engin;

    public function __construct(){}

    //2.メソッドの引数として渡されたオブジェクト$keyの
    //メソッドである$key.turns()はOK!
    public function start(Key $key): void
    {
             //3.メソッドが作成したオブジェクト$doorsの
        //メソッドである$door.lock()はOK
        $doors = new Doors();  
        
        $authorized = $key.turns();
        
        if($authorized){
            $engine.start();
             
               //1.オブジェクト自身のメソッド
                    //である、updateDashboardDisplay()はOK!
            this->updateDashboardDisplay(); 
       
            $door.lock();
        }

    }

    public function updateDashboardDisplay()
    {
        //
    }

}

デメリット

この原則を意識しすぎると、ラッパークラスの記述が多くなり、
複雑になってしまうこともある。

なので、適当に活用する!

Adapterパターン

 

今年から、花粉症になったみたいです・・・。
鼻がムズムズして頭も痛くなるし大変ですね、なめてました。

 

 

今回は、
Adapterパターン
について書きます!

 

Adapterパターン

クラスのインターフェースを、
クライントの期待する別のインターフェースに変換する。

アダプタは互換性がないインターフェースのために、
そのままでは連携ができないクラス(Adaptee)を
(Clientと)連携できるようにさせます。

ラップするインターフェースを変換します。

 

 

▶︎ Adapter
Targetインターフェースを実装する。

 

▶︎ Target <<interface>>
ClientはこのTargetインターフェースを使っている感覚。
実際には、Adapterを通して、Adapteeを使っているが、
Adapteeを使っている認識は特にない。

 

▶︎ Adaptee
全てのリクエストはAdapeeに委譲される。
Adapteeの任意のサブクラスにもAdapterは適用できる
(コンポジションのため)

 

▶︎ Client
①Targetインターフェースを使って、
Adapterのメソッドを呼び出し、
Adapterにリクエストをする。
②AdapterがAdapteeインターフェースを使って
Adapteeのメソッドに変換して呼び出す
③Clientはリクエスト結果を受け取るが、
Targetインターフェースに沿っただけなので
変換をしてくれたAdapterの存在は知らない。

 

PHPで書いたみたサンプルコード

 

オブジェクトアダプタ/クラスアダプタ

実は、Adapterパターンには、
– オブジェクトアダプタ
– クラスアダプタ
の2つがある。

 

オブジェクトアダプタ

コンポジションを使うので、
Adapteeとそのサブ(具象)クラスにも適用可能、多様性あり。

 

 

クラスアダプタ

(多重)継承を使うので、
1つの特定のアダプティに特化したアダプタが実装可能。

オーバライドができる。

 

 

 

いいところ

・Adapterパターンを使わずに、互換性対応のために、
Client側のインターフェースに対する全ての実装を変更する場合、
多くのコード変更やそれによる影響調査などコストが高い

(例)TurkeyインターフェースをDockインターフェースへの互換性対応のために、
Turkeyインターフェースを変更する場合

– Turket.php

interface Turkey
{
-    public function gobble(): void;
+    public function quack(): void;
     public function fly(): void;
}

 

– WildTurkey.php(Turkeyインターフェースを実装する具象クラス)
他にも具象クラスがあれば、
全ての具象クラスのgobbleメソッドの修正が必要となる。

class WildTurkey implements Turkey
{
-    public function gobble(): void
+    public function quack(): void
     {
         echo "ゴロゴロ\n";
     }
 }

 


↓ Adapterを使うと…

1つのクラス内にすべての変更をカプセル化したクラスを提供できる。
Client/Adaptee/TargetInterfaceのコード変更は不要。

 

 

双方向アダプタを作ることも可能

実際に作ってみた!しかし、テキトーすぎて見せたくない。。。

時間あったらちゃんとさせます。

 

– DockAndTurkeyAdapter.php

class DockAndTurkeyAdapter implements DockAndTurkey
{
    /* @var Turkey $turkey */
    public $turkey;

    /* @var Dock $dock */
    public $dock;

    public function __construct($turkeyOrdock)
    {
        if($turkeyOrdock instanceof Turkey)  $this->turkey = $turkeyOrdock;
        if($turkeyOrdock instanceof Dock)  $this->dock = $turkeyOrdock;
    }

    public function quack(): void
    {
        $this->turkey->gobble();
    }

    public function gobble(): void
    {
        $this->dock->quack();
    }

    public function fly(): void
    {
        if($this->turkey !== null){
            for($i = 0; $i < 5; $i++){
                $this->turkey->fly();
            }
        } else {
            $this->dock->fly();           
        }
    }
}

– DockAndTurkey.php
DockインターフェースとTurkeyインターフェースの拡張インターフェース

use HFD\Adapter\Dock\Dock;
use HFD\Adapter\Turkey\Turkey;

interface DockAndTurkey extends Dock,Turkey
{
}

 

interfaceのextendsって初めて使いました。
こうやって使うんですね\(^^)/

 

 

 

 

Commandパターン

 

どうも。
最近、蒙古タンメンにハマりすぎて困っています。
麻婆麺が辛すぎて病みつきになります。

 

 

Commandパターン
を見ていきます。

Commandパターンの定義

リクエスト(命令)をオブジェクトとしてカプセル化し、
その結果、他のオブジェクトを異なるリクエスト、キュー
またはログリクエストなどでパラメータ化でき、
アンドゥ可能な操作もサポートする。
= リクエストをオブジェクトでカプセル化する。
= クライアントはいろんなリクエストを持つことができる。(→使える)

 

クラス図

縦長。。。

 

・Command
…全てのコマンド(オブジェクト)のためのインターフェースを定義する。

 

・ConcreteCommand
…Receiverとaction()の結びつきを(execute()に)定義する。
Invokerがexecute()することで
ConcreteCommandへリクエストを行い、
ConcreteCommandがReceiverの
1つ以上のaction()を呼び出すことでそれを実行する。

 

・Invoker
…コマンド(オブジェクト)を保持し、
ある時点でのコマンドのexecute()を呼び出すことで
コマンド(オブジェクト)にリクエストを実行するよう依頼する。
様々なコマンド(オブジェクト)をパラメタ化して保持できるが、
具体的なコマンド(オブジェクト)の内容は知らない。
(Commandインターフェースを実装している何かだと知っている程度)

 

・Receiver
…制御されるもの(制御対象)

・Client

 

具体的に…

この章でのは
「複数の電化製品のon/offを制御できるリモコン」
でした。

 

PHPで書いたみたコード

 

・Command
…制御したい命令を定義するインターフェース

・LightOnCommand / LightOffCommand [ConcreteCommand]
…具体的に制御対象であるライトの
制御(点けたり消したりする)方法がここにある

・RemoteControl [Invoker]
…制御対象(Receiver)をsetCommandで設定したり、
実際に制御するコントローラー
複数の制御対象をセットできる。

・Light [Receiver]
…制御対象となるライト

・RemoteLoader [Client]

 

いいところ

Invoker(RemoteControl)自体が
制御するReceiver(Light)を知っている場合、
Receiverが増えるたびに、if文で追記する形になってしまう。

RemoteControl.php

  if($receiver == 'Light') {
   $receiver.on();

  } else if($receiver == 'Door') {

   $receiver.open();

- }
+ //制御対象「Sprinkler」の追加
+ } else if($receiver == 'Sprinkler') {
+
+  $receiver.waterOn();
+
+ }

 


↓ Commandパターンを使うと…

 

Invoker(RemoteControl)は
具体的なコマンドオブジェクトの内容を知らずして
(インターフェスは知ってる)、
コマンドオブジェクトに具体的な処理(制御)を依頼できるので、
Invoker(RemoteControl)とReceiver(Light)を分離することができる。
= 各々の役割分担ができる。

RemoteControl.php
※Receiver(LightやDoorなど)について全く出てこない。

use HFD\Command\Command\Command;
use HFD\Command\Command\NoCommand;

class RemoteControl
{
    /**
     * @var Command[]
     */
    public $onCommands;

    /**
     * @var Command[]
     */
    public $offCommands;


    public function __construct(int $commandCnt)
    {
        for ($i = 0; $i < $commandCnt; $i++) { $this->onCommands[] = new NoCommand();
            $this->offCommands[] = new NoCommand();
        }
    }

    public function setCommand(int $slotNum, Command $onCommand, Command $offCommand): void
    {
        $this->onCommands[$slotNum] = $onCommand;
        $this->offCommands[$slotNum] = $offCommand;
    }

    public function onButtonWasPushed(int $slotNum): void
    {
        $this->onCommands[$slotNum]->execute();
        $this->undoCommand = $this->onCommands[$slotNum];
    }

    public function offButtonWasPushed(int $slotNum): void
    {
        $this->offCommands[$slotNum]->execute();
        $this->undoCommand = $this->offCommands[$slotNum];
    }
}

 
制御対象が増える可能性がある時の
拡張性を考えた時に良さそうなパターンですね!
 

 

 

 

 

Factoryパターン〜AbstractFactory〜

Head FirstのFactoryパターンの
例え話が「ピザ屋」のお話だったので、
すんごいピザが食べたくなり、ピザホール一人で食べました。
やっぱりマルゲリータですよね。
んで、追いオリーブオイル。

 

Factoryパターン一族

– Simple Factory[正確にはFactoryパターンではない]
– Factory Method
– Abstract Factory

今回は最後のAbstract Factoryについてです!

 Abstract Factory

オブジェクトを作成をカプセル化し、依存度を下げる。(Factory Methodと同じ)
FactoryMethodと異なり、オブジェクトコンポジションを使います。

具象クラスを指定することなく、
一連の関連オブジェクトや依存オブジェクトを作成するための
インターフェースを提供する。

→ 使い手は、抽象インターフェースを使って、
実際に作成される具体的な製品を知ることなく、
一連の製品を作成できる。

いいところ:
使い手は、具体的な製品の詳細から完全に分離されます。
→ 役割分担できて、個々のクラスの責任が小さくなる

 

クラス図はこんな感じ

 

 

Head Firstでは、
– 工場:ピザの食材を作る食材工場
– 製品:ピザの食材
を例としていました。

こんな感じ(PHPStormのクラス図作れるの教えてもらったので、早速)

PizzaIngredientFactoryクラスで、
関連する一連の食材(Dough, Sauce, Cheese, …)オブジェクトを
まとめてグループ化して作れるのがいいところです。

 

AbstractFactoryのサンプルコード

 

▶︎ PizzaIngredientFactory  ←抽象的な食材工場

interface PizzaIngredientFactory
{
    //実装クラスにピザの食材(製品)の作成方法は任す。
    public function createDough(): DoughInterface;
    public function createSauce(): SauceInterface;
    public function createCheese(): CheeseInterface;
       ...
}

 

▶︎ NYPizzaIngredientFactory (PizzaIngredientFactoryの実装クラス)

↑具象な食材工場

class NYPizzaIngredientFactory implements PizzaIngredientFactory
{
    //具体的なピザの食材(オブジェクト)を作成する。
    public function createDough(): DoughInterface
    {
        return new ThinCrustDough();
    }

    public function createSauce(): SauceInterface
    {
        return new MarinaraSauce();
    }
...

 

▶︎ DoughInterface ←抽象的な食材

interface DoughInterface
{
    public function getName(): string;
}

 

▶︎ ThinCrustDough (DoughInterfaceの実装クラス)

↑具象な食材

class ThinCrustDough implements DoughInterface
{
    private $name;

    public function __construct()
    {
        $this->name = "うっすい生地";
    }

    public function getName(): string
    {
        return $this->name;
    }
}

 

 

ただし、食材が追加された時
Product(MeetInterface, Bacon)の追加だけでなく、
PizzaIngredientFactoryと
PizzaIngredientFactoryを実装する全てのクラスを変更する必要があります。
→影響範囲が大きめです。

例: 肉(食材)を追加する場合

– PizzaIngredientFactory

interface PizzaIngredientFactory
{
    //実装クラスにピザの食材(製品)の作成方法は任す。
    public function createDough(): DoughInterface;
    public function createSauce(): SauceInterface;
    public function createCheese(): CheeseInterface;
   ...
+   public function createMeet(): CheeseInterface;
}

– NYPizzaIngredientFactory

class NYPizzaIngredientFactory implements PizzaIngredientFactory
{
    ...
+   public function createMeet()
+   {
+       //MeetInterfaceの実装クラスをインスタンス化する
+       return new Bacon();
+   }
}

 

 

 

ある程度、固定された一連のオブジェクトがあって、
それらを作成する必要がある時に使えそうですね。