今までのカレンダーシリーズでは、タイムゾーンのことをまったく考えていませんでしたが、もちろん考える必要あります。
Laravelのタイムゾーンは何?
Laravelのプロジェクトのタイムゾーンは、以下で設定されています。
...
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. The timezone
| is set to "UTC" by default as it is suitable for most use cases.
|
*/
'timezone' => env('APP_TIMEZONE', 'UTC'),
...
見ての通り、デフォルトはUTCです。
UTCとは、協定世界時のことで国際協定により定められた世界共通の標準時です。
これを上書きしたいなら、.envで以下のように設定できます。
... APP_TIMEZONE="Asia/Tokyo" ...
上の設定は日本時間ですが、その値はPHPでは以下でリストされていて、
https://www.php.net/manual/ja/timezones.php
アジアは、以下でリストされています。
https://www.php.net/manual/ja/timezones.asia.php
DateTimeZone
アプリで固定のタイムゾーン(つまり、APP_TIMEZONEを指定)でなく、タイムゾーンをダイナミックに変えたいなら、
さきのタイムゾーンの定数を、以下のDateTimeZoneのクラスのインスタンス作成時に指定します。
https://www.php.net/manual/ja/class.datetimezone.php
use DateTimeZone;
$tz = new DateTimeZone("Asia/Tokyo");
Carbonではこのタイムゾーンのインスタンスを使って、UTCの時間を指定のタイムゾーンの時間に換算します。
Carbon::now($tz);
直接、タイムゾーンの定数の指定も可能です。
Carbon::now("Asia/Tokyo");
OpeningHoursも同様にタイムゾーンの指定が可能です。
OpeningHours::create($dates, $tz);
Livewireのコンポーネントにタイムゾーンを
前回のコードをもとにタイムゾーンを取り入れました。以下で$this->tzの箇所を見てください。
namespace App\Livewire;
use DateTimeZone;
use Carbon\Carbon;
use Yasumi\Yasumi;
use Livewire\Component;
use Livewire\Attributes\Url;
use Spatie\OpeningHours\OpeningHours;
class Calendar extends Component
{
class Calendar extends Component
{
#[Url]
public int $year;
#[Url]
public int $month;
public $calendar = [];
const TZ = 'Asia/Tokyo';
private DateTimeZone $tz;//プロパティではないので、privateで宣言
public function boot()
{
$this->tz = new DateTimeZone(self::TZ);
}
public function mount()
{
$this->year ??= Carbon::now($this->tz)->year;
$this->month ??= Carbon::now($this->tz)->month;
}
public function previousMonth(): void
{
$this->month--;
if ($this->month < 1) {
$this->month = 12;
$this->year--;
}
}
public function nextMonth(): void
{
$this->month++;
if ($this->month > 12) {
$this->month = 1;
$this->year++;
}
}
public function render(): \Illuminate\View\View
{
$openingHours = $this->openingHours();
$this->generateCalendar($openingHours);
return view('livewire.calendar');
}
// アクションではないのでprivateとする
private function openingHours(): OpeningHours
{
$yasumi = Yasumi::create('Japan', $this->year, 'ja_JP');
$holidays = collect($yasumi->getHolidayDates())
->mapWithKeys(function ($date) { return [$date => []];})
->all();
$dates = [
'monday' => ['09:00-12:00', '13:00-18:00'],
'tuesday' => ['09:00-12:00', '13:00-18:00'],
'wednesday' => ['09:00-12:00'],
'thursday' => ['09:00-12:00', '13:00-18:00'],
'friday' => ['09:00-12:00', '13:00-20:00'],
'saturday' => ['09:00-12:00', '13:00-16:00'],
'sunday' => [],
'exceptions' => $holidays,
];
return OpeningHours::create($dates, $this->tz);
}
// アクションではないのでprivateとする
private function generateCalendar(OpeningHours $openingHours): void
{
$startOfMonth = Carbon::createFromDate($this->year, $this->month, 1, $this->tz);
$endOfMonth = $startOfMonth->copy()->endOfMonth();
$startDayOfWeek = $startOfMonth->dayOfWeek;
$totalDays = $endOfMonth->day;
$this->calendar = [];
$week = [];
for ($i = 0; $i < $startDayOfWeek; $i++) {
$week[] = null;
}
$today = Carbon::today();
for ($day = 1; $day <= $totalDays; $day++) {
$date = Carbon::createFromDate($this->year, $this->month, $day, $this->tz);
$dayInfo = [
'day' => $day,
'isHoliday' => ! $openingHours->isOpenOn($date->toDateString()),
];
$dayInfo['hours'] = [];
if ($date->greaterThanOrEqualTo($today)) {
$hours = $openingHours->forDate($date->toDateTime());
foreach ($hours as $hour) {
$dayInfo['hours'][]= $hour->format();
}
}
$week[] = $dayInfo;
if (count($week) == 7) {
$this->calendar[] = $week;
$week = [];
}
}
if (count($week) > 0) {
while (count($week) < 7) {
$week[] = null;
}
$this->calendar[] = $week;
}
}
}
上のコードでタイムゾーンの対応だけでなく、以下のような前回のコードのリファクターもあるので注意してください。
- プロパティの
$yearと$monthの宣言の上の行に、#[Url]のアトリビュートが使われています。これは、ブラウザのUrlのパラメータを反映させるためです(後に説明します)。 - タイムゾーンの初期化は、
mount()ではなくboot()で行っています。これは、mount()はLivewireのコンポーネントを含む画面がロードされるときに1回だけコールされますが、boot()は、コンポーネントのライフサイクルにおいて毎回コールされるからです(後に説明します)。 - 前回のコードでは、
openHours()やgenerateCalendar()の関数の定義は、publicでしたが上のコードではprivateとしています。Livewireのコンポーネントのpublicの関数(例えば、previousMonth())はアクションとしてクライアントからのアクセスが可能となるからです。
URLパラメータ
上のコードで、馴染みのないアトリビュートが登場しました。以下の#[Url]の部分です。
...
class Calendar extends Component
{
#[Url]
public int $year;
#[Url]
public int $month;
...
これは、php8から導入されたアトリビュートといわれるもので、Livewireではそのアトリビュートの次の行で宣言されている変数($yearや$month)のUrlにおけるパラメータを自動的に取り込むことを指示します。
例えば、
http://127.0.0.1:8001/calendar?month=1&year=2025
がブラウザのUrlとすると、コンポーネントの$yearと$monthのプロパティは、2025と1にそれぞれ初期化され、2025年の1月のカレンダーを表示してくれます。つまり、Urlにパラメータをつけることで欲しいの月のカレンダーを表示させることができるのです。便利ですね。
http://127.0.0.:8001/calendar
とパラメータがないなら、それらの変数は今日の年と月にmount()で初期化されます。
Livewireのライフサイクル
タイムゾーンの情報を保つ$tzの変数は、上のコードではmount()でなくboot()で初期化を行いましたが、どうしてそうなのでしょう。それには、コンポーネントで定義されている関数がコールされる順番の理解が必要です。
まず、ブラウザにおいて最初にカレンダーを表示したときは、以下の順番でコールされます。
boot() → mount() → render()
しかし、そのカレンダーにおいて、「次」のボタンを押して次の月へ移動するとき、つまりリアクティブのリクエストとなるときは、
boot() → nextMonth() → render()
の順番のコールとなり、mount()はコールされません。mount()で$tzを初期化してたら、render()でコールされるときにopeningHours()内で未初期化の変数のアクセスエラーとなります。
