カレンダーWebアプリを作る際に、日本の祝日をAPIで取得したくなりました。良い機会なのでPHPのクラスとオブジェクト指向の復習がてらWebAPIを自作してみます。
今回作成したプログラムとAPIは商用・個人問わず利用しても構いませんが、その際のいかなる責任は一切負いません。
目次
取得するデータはJSON
今回はGoogleカレンダーから祝日データを引っ張って生成してもよかったのですが、目的がPHPでオブジェクト指向プログラムの復習だったのでjsonファイルを自前で用意しました。
元ソースは内閣府(url:https://www8.cao.go.jp/chosei/shukujitsu/gaiyou.html)で公開している祝日を参考に2019年から2023年までの祝日と振替休日を手動で作成しました。
/* Jsonデータの一部 */
{
"19":{
"0101":"元旦",
...
},
"20":{
"0101":"元旦",
...
},
...
}
以上のような感じでデータを羅列しました。
PHPのClassを復習
Class内で今回使用する要素について復習します。
アクセス権
PHPではclass内で定義するプロパティとメソッドにアクセス権を付与することでクラス内外でそのプロパティやメソッドにアクセスすることができるかを設定することができます。
アクセス権は'public'・'private'・'protected'の3種類があります。
まず、MyClass1でpublic、private、protectedの3種類のアクセス権でプロパティを定義して、クラス外でそれぞれのプロパティを呼び出してみましょう。
<?php
class MyClass1
{
public $public = 'ika';
private $private = 'tako';
protected $protected = 'ebi';
}
$object1 = new MyClass1;
echo $object1->public; // ika
echo $object1->private; // Fatal Error
echo $object1->protected; //Fatal Error
public はクラス外からアクセスすることができますが、private と protected はクラス外からアクセスするとエラーになります。
次はMyClass1でメソッドを生成し、メソッド内で3種類のアクセス権を持つプロパティにそれぞれアクセスし、クラス外でメソッドにアクセスしてみます。
<?php
class MyClass1
{
public $public = 'ika';
private $private = 'tako';
protected $protected = 'ebi';
public function myMethod1 () {
echo $this->public; // expected 'ika'
echo $this->private; // expected 'tako'
echo $this->protected; // expected 'ebi'
}
}
$object2 = new MyClass1;
$object2->myMethod1();
//ika
//tako
//ebi
MyClass1で定義したプロパティをクラス内のメソッドで使用することで全てのプロパティにアクセスできていることがわかります。この時メソッド(myMethod1)もpublicで定義していることからクラス外でメソッドにアクセスできていることも復習しておきましょう。
最後は、MyClass1から継承したMyClass2でメソッドを生成し、メソッド内でMyClass1で定義したプロパティにアクセスしてみます。
<?php
class MyClass2 extends MyClass1
{
public function myMethod2 () {
echo $this->public; // expected 'ika'
echo $this->private; // expected 'Fatal Error'
echo $this->protected; // expected 'ebi'
}
}
$object3 = new MyClass2;
$object3->myMethod2();
//ika
//Fatal Error
//ebi
MyClass2では全てでアクセスできる public に加えて、protected で定義されたプロパティもメソッド内で使用することでアクセスできますが、private で定義されたプロパティはエラーとなることがわかります。
また、プロパティはアクセス権を付与せずに定義してはいけませんが、メソッドはアクセス権を付与せずに定義することができます。この時のメソッドのアクセス権は public として動作します。なので、publicメソッドはわざわざ定義する必要はありませんが、クラス外で用いることを大前提としたメソッドに関して明記的にアクセス権とともに定義する場合があります。
プロパティはPHP7.1.0以降では const と定義することでメソッドと同様 public を省略することができます。const と定義していない public、prrotected のプロパティは定義元でも継承先でも再定義することができますが、 const と定義することでそのプロパティは再定義することができなくなります。
コンストラクタ
コンストラクタメソッドを定義するとクラスを新しく生成したタイミングで必ずメソッドが動作します。例えば
<?php
class SayHello
{
public function __construct () {
echo 'Hello!';
}
}
$hello = new SayHello; // Hello!
class SayName
{
private $fullName;
public function __construct ($name) {
$this->fullName = $name;
}
public function say (){
echo 'Hello!'.$this->fullName;
}
}
$sayName = new SayName('Yuya Akiyama');
$sayName->say();
// Hello!Yuya Akiyama
上記のSayHelloクラスではクラスを生成しただけで、コンストラクタメソッドが発動されてブラウザに"Hello!"と表示されます。
また、SayNameクラスではクラス生成時に引数を渡すことでクラス内のprivateプロパティである$fullNameに引数が代入され、sayメソッドでfullNameプロパティを使用することができます。
APIの仕様を考える
今回はREST方式でAPIにアクセスし、Json形式で取得します。
追記ー2022年08月
この祝日APIの進化版である新しい祝日APIを作りました。
以下のAPIは少々力技であり使い勝手に欠けます。祝日APIを目的でしたら、こちらの記事をご覧ください。
REST方式
詳しい定義の説明は端折りますが、例えば
/* 2020年9月のデータ */
https://sample.jp/api/20/09/
/* 2021年の総データ */
https://sample.jp/api/21/
とのように取得したいデータをURLから絞り込むことができるような方式です。
今回は/apiディレクトリを作成し、下層のディレクトリ毎にindex.phpを配置し、URLによってJsonデータを絞り込みし、'"Y-m-d":"祝日名"'の形式で祝日を出力します。
ディレクトリの構造
ディレクトリ構造は以下に示します。ルートディレクトリ直下のrootディレクトリはドキュメントを記述したファイルを想定しています。
root:
├─app
│ │ holidays.php
├─json
│ │ publicHoliday.json
├─api
│ │ index.php
│ └─20
│ │ │ index.php
│ │ └─09
│ │ │ │ index.php
│ index.php
/apiディレクトリ下層は2020年09月のディレクトリ以外を省略します。
index.phpには作成したプログラムを読み込んでクラスを生成することでカレントディレクトリに応じたJson形式で祝日を書き出させます。index.phpのプログラムは以下のようになります。
<?php
require_once('../app/holidays.php');
$generate = new Holidays;
ここで2行目のholidays.phpは今回作成するプログラムファイル名であり、階層はカレントディレクトリの深さによって分岐します。上のプログラムは/apiディレクトリ直下のindex.phpに書き込むプログラムです。
日本の祝日を取得できるAPIを作成
APIのサンプルはここ(https://zaty.jp/publicHoliday/api/)から使用できます。
作成したholidays.phpはgithubにて公開しています(https://github.com/Zaty-Akiyama/publicHolidayAPI)。
ではholidays.phpを作成していきます。
Jsonファイルから総データを取得する。
/jsonディレクトリのpublicHoliday.jsonから総データを取得するメソッドを作成します。ここでpublicHoliday.jsonは定数としてメソッド外で$jsonSrcに代入します。よってプログラム全体の書き出しは以下のようになります。
<?php
class Holidays
{
const jsonSrc = __DIR__.'/../json/publicHoliday.json';
function getJsonData () {
$json = file_get_contents(self::jsonSrc);
$json = mb_convert_encoding($json,'UTF8','ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN');
$records = json_decode($json,true);
return $records;
}
}
これ以降、以下のメソッドはクラス内に作成します。
URLによりリクエストを取得する。
このメソッドでURLから取得すべき祝日の年月を配列に代入します。
function classifyUri ($removeUri,$explode = true){
$calleeUri = $_SERVER['REQUEST_URI'];
$resultUri = str_replace($removeUri,'',$calleeUri);
if($explode){
$resultUriArray = explode('/',$resultUri);
array_shift($resultUriArray);
array_pop($resultUriArray);
return $resultUriArray;
}
return $resultUri;
}
このメソッドは第1引数にから/apiディレクトリまでのパスを渡すことで/apiディレクトリ以下のURLを取得します。第2引数はtrueにすることで/apiディレクトリ以下のURLのリクエストを配列に代入します。初期条件はtrueです。
取得したJsonをURLによってソートする
以上のgetJsonDataメソッドとclassifyUriメソッドを使って目的のデータのみを取り出します。
protected function sortOutJson () {
$arrayVanilla = $this->getJsonData();
$classifiedUri = $this->classifyUri('/publicHoliday/api');
if(array_key_exists(0,$classifiedUri)){
$limitedArray = $arrayVanilla[(string)$classfiedUri[0]];
if(array_key_exists(1,$classifiedUri)){
$arrayKey = array_keys($limitedArray);
$match = "/".$classifiedUri[1]."[0-9]{2}/";
for($count=0;$count<count($arrayKey);$count++){
if(preg_match($match,$arrayKey[$count])){
$sortedOutArray[$classifiedUri[0]][$arrayKey[$count]] = $arrayVanilla[$classifiedUri[0]][$arrayKey[$count]];
}
}
}else{
$sortedOutArray[$classifiedUri[0]] = $arrayVanilla[$classifiedUri[0]];
}
}else{
$sortedOutArray = $arrayVanilla;
}
if(isset($sortedOutArray)){
return $sortedOutArray;
}else{
return false;
}
}
これによってメソッドを実行するとURLで指定した通りに取得したJsonからのデータを必要なデータだけ配列に再分配することができるようになりました。
URLによりソートした配列のキーをY-m-dの形式に変換する。
Y-m-dの形式とは例えば”2020年3月20日”なら”2020-03-20”といった形式で表す形式の時間のフォーマットのことです。sortOutJsonメソッドの時点では[20]->["0320"=>"春分の日"]との形で格納される配列になっているので、["2020-03-20"=>"春分の日"]のような形の配列に再代入していきます。
protected function createFinalArray () {
$finalArray = [];
$sortedOutArray = $this->sortOutJson();
if($sortedOutArray){
foreach ($sortedOutArray as $parentKey => $parentValue) {
foreach ($parentValue as $childKey => $childValue) {
$replaceKey = substr_replace($childKey,'-',2,0);
$finalKey = '20'.(string) $parentKey.'-'.$replaceKey;
$finalArray[$finalKey] = $childValue;
}
}
}
return $finalArray;
}
コンストラクターで配列をJsonにして書き出す。
必要なメソッドは揃ったので、コンストラクターでcreateFinalArrayメソッドを呼び出して、配列をJson形式に変換し、echoで書き出します。
public function __construct () {
echo json_encode($this->createFinalArray(),JSON_UNESCAPED_UNICODE);
}
これで”new Holidays”とクラスを呼び出すことでAPIを吐き出すことが可能になりました。
【おまけ】 /apiディレクトリの下層を一気に生成する
holidays.phpは上記のディレクトリ階層に示した通り/appディレクトリに保存しており、/appディレクトリと同階層に/apiディレクトリを作成しています。
ここで/appディレクトリにcreateDir.phpという/apiディレクトリ以下のファイルとディレクトリを一気に作成するプログラムを紹介します。
<?php
$dir = __DIR__.'/../api';
for($year=19;$year<=23;$year++){
if(!file_exists($dir.'/'.$year)){
mkdir($dir.'/'.$year,0704);
}
if(!file_exists($dir.'/'.$year.'/index.php')){
$data = "<?php\nrequire_once('../../app/holidays.php');\n".'$generate'." = new Holidays();\n";
file_put_contents($dir.'/'.$year.'/index.php',$data);
}
for($month=1;$month<=12;$month++){
$bMonth = "00".(string) $month;
$stMonth = subStr($bMonth,strlen($bMonth)-2,strlen($bMonth));
$dirIn = $dir.'/'.$year.'/'.$stMonth;
if(!file_exists($dirIn)){
mkdir($dirIn,0704);
}
if(!file_exists($dirIn.'/index.php')){
$data = "<?php\nrequire_once('../../../app/holidays.php');\n".'$generate'." = new Holidays();\n";
file_put_contents($dirIn.'/index.php',$data);
}
}
}
if(!file_exists($dir.'/index.php')){
$data = "<?php\nrequire_once('../app/holidays.php');\n".'$generate'." = new Holidays();\n";
file_put_contents($dir.'/index.php',$data);
}
echo 'ディレクトリの生成終了';
このファイルを/appディレクトリに置き、https://sample.com/publicHoliday/app/createDir.phpにアクセスすると、自分のサーバーの/apiディレクトリ内が自動生成されます。
サーバーにディレクトリとファイルを生成するコードを丸写しするのはセキュアではないのでコードを確認の上、使用してください。
まとめ
以上で日本の祝日を表示するAPIを作成することができました。
プログラムに誤植や不具合があった場合は、以下のコメントかTwitterのDMにご連絡ください。
ぶっちゃけ年月毎のディレクトリを作成するならそのディレクトリに対応した直接Jsonを生成するプログラムを1つ作成すればいいだけ。今回はクラスの利用価値としてプログラムの使い回しを目的としてるし、.htaccessを使用せずURLにjsonファイルを指定しなくても良いからまあよしとするか。。