たかぎそ

プログラミングメモ

SwitchBotのシーリングライトプロが追加できない(インターネットに接続されない)時に解決した方法

解決した方法

これでSwitchBotアプリからシーリングライトプロを操作できるようになりました。

経緯

  • SwitchBotアプリにシーリングライトプロを追加する
  • SSIDを選択してパスワードを入力する
  • インターネットに繋がらない
  • 初期化しても解決しない

という問題が起きていてサポートに連絡したら「iPhoneAndroidのデザリングで追加してみて!」という指示が来たので

にしてWifiルーターの電源切ってAndroidのSwitchBotアプリからシーリングライトプロを追加したら普通に追加された。 (追加する時に使うSSIDiPhoneのインターネット共有のもの。パスワードも。)

当然家の中でiPhoneテザリングをずっと使うわけがないので、

で再度Android端末からシーリングライトプロを追加したら、無事自宅のWifiルーターでもシーリングライトプロをSwitchBotアプリで操作できるようになった。

その他

SwitchBotアプリのシーリングライトプロの詳細からWifi設定いじった辺りで接続がおかしくなったような気がする。 Wifi設定変更するなら一回アプリからデバイスを削除して、再度追加する時にネットワーク変更したほうが良いような気がする。

LaravelのテストをGitHub Actions (MySQL)で実行する

テストはローカルで実行できていたけど、せっかくなのでGitHub Actionsでも実行できるようにしてみた。

sqliteだとテーブル定義が想定と異なりテストの実行が失敗するのでMySQLを使うことにする。

Laravelのバージョンは10.17.1

$ php artisan --version
Laravel Framework 10.17.1

最終的な成果物

.github/workflows/unittest.yml

# ベースになった設定
# https://laravel-news.com/laravel-ci-with-github-action

name: Run tests

on:
  push:
    branches:
      - master
      - feature/**
  pull_request:
    branches:
      - master

jobs:
  tests:
    name: Run tests
    runs-on: ubuntu-latest
    steps:
      - name: Start MySQL
        run: |
          sudo systemctl start mysql.service
          mysql -uroot -proot -e "CREATE DATABASE IF NOT EXISTS testing"
          mysql -uroot -proot -e "SHOW DATABASES"

      - uses: actions/checkout@v3
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
          coverage: none

      - name: Run composer install
        run: composer install -n --prefer-dist

      - name: Run node.js
        run: |
          npm install
          npm run prod

      - name: Prepare Laravel Application
        run: |
          touch .env.testing
          echo "${{ vars.DOT_ENV_TESTING }}" >> .env.testing

      - name: Run tests
        run: php artisan test

Laravel Newsで紹介されていた設定にいくつかstepを追加して、php artisan testが通るようになった。

laravel-news.com

DBの名前はtestingにしているので適当なものに変更する。

テストで使う.env.testingの中身は、GitHub上でDOT_ENV_TESTINGという変数名で登録しておく。

Settings > Secrets and variables > Variable > Repository variables

これも適当な変数名にして大丈夫。

phpunit.xmlDB_CONNECTIONDB_DATABASEも設定する必要があるかも。

phpunit.xml

<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="mysql"/>
<env name="DB_DATABASE" value="testing"/>

.envのDBの設定はこんな感じ。

.env.testing

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=testing
DB_USERNAME=root
DB_PASSWORD=root

詰まった所

sqliteだとテーブル定義がおかしくなる。

本番環境ではMySQLを使っており、ローカルでの開発やテストの時はsqliteを使おうかと思っていたが、何かsqliteを使うと想定とは違うテーブル定義になってテストでエラーが発生していた。

 public function up()
 {
     Schema::table('users', function (Blueprint $table) {
         $table->string('twitter_id')
             ->after('id');
     });
 }

こういったafter()を使ったマイグレーションがうまく通ってないような気がする(もしかしたらドキュメントとかに書いてあるかも...?)

after()が使えないのは良いが、そもそもカラムが追加されてないようだったのでローカルの開発もテストも本番環境と同じMySQLを使うようにした。

Expected response status code [200] but received 500.

最初からプロジェクトにあるテストでExpected response status code [200] but received 500.というエラーが出ていた。

npm install && npm run prodを実行することでフロントのリソースを準備してあげることでエラーが解消された。

  • Tests\Feature\ExampleTest > the application returns a successful response
  Expected response status code [200] but received 500.
  Failed asserting that 200 is identical to 500.
  
  The following exception occurred during the last request:

.......

   Mix manifest not found at: /home/runner/work/aaa/aaa/public/mix-manifest.json

  at tests/Feature/ExampleTest.php:19
     15▕     public function test_the_application_returns_a_successful_response()
     16{
     17$response = $this->get('/');
     18▕ 
  ➜  19$response->assertStatus(200);
     20}
     21}
     22

GitHub ActionsでのMySQLやNode.jsの準備の仕方

ネットで記事を検索すると- servicesでimageを指定したりする方法を見かけたが、ubuntu-latestには最初からMySQLnode.jsがインストールされていてそれを使うのがお手軽そうだったのでそちらを採用した。

github.com

本当ならservicesで指定した方が良いのかもしれないが、その辺りは良く分からなかった。。。。

最後に

Proプランだと月3000分使えるので色々使っていきたいですね。

CIつながりでCircleCIの無料枠どのくらいだったっけなと思って調べたら、去年大幅に無料枠増やしたんですね。

circleci.com

2,500クレジット/週から30,000クレジット/月って凄いな。

30,000クレジットで6000分ということは5クレジットで1分?

ということは週500分から月6000分になったのか。。。

ProプランなのにGitHub Actions使ってなくてもったいないかなーと思って今回使ってみたけど、月3000分以上使う人はCircleCIの方が良いかもしれませんね。

Laravelでパスワードレス認証(マジックリンク)を実装する + Laravel Voltでログアウトする

Twitter, Gmail, Appleログインとかの方が良いかもしれないけど今回はマジックリンクで実装する。

下記のライブラリで実装する。

github.com

こっちでも良いかもしれない。

github.com

環境構築

Laravel Valetで環境構築をする。

zenn.dev

今までLaravel Sail使ってたけどmacOSではLaravel Valetの方が便利みたい。

実際使ってみたけどいちいちsail upとか打たなくてすむし、複数のサイトを気楽に管理できるから今のところ良さそう。

ファイル変更したときにブラウザリロードする必要があるけど、それも少し調べれば解決方法あるかも。

laravel new resend
valet open resend # 多分 http://resend.testというサイトが開く

resend.com

マジックリンクをメールで送信する必要があるので今回はResendを使用する。

以前アカウントを作って何も使ってなかったから使おうと思っただけで、他のサービスでも問題ないと思います。

resend.com

Userモデルの変更(primary keyをULIDにする)

最終的には下記のようなリンクが生成されるが、この時ユーザーIDが含まれて個人的に少し嫌なので、IDをULIDに変更する。

# idが連番
http://resend.test/magic-login/1?expires=1690636947&user_type=app-models-user&signature=07a7b9d4bbc20d7c889edfe627b9e7a7d24236c681f26172e6d13bac185a348e

# idがulid
http://resend.test/magic-login/01h6ce5tydqwtmvpe2j17ahhp1?expires=1690636947&user_type=app-models-user&signature=07a7b9d4bbc20d7c889edfe627b9e7a7d24236c681f26172e6d13bac185a348e

2014_10_12_000000_create_users_table.php

<?php

return new class extends Migration {
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            // $table->id();
            $table->ulid('id')->primary(); // <--- idをULIDにする
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('users');
    }
};

User.php

<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable, HasUlids; // <- HasUlids を追加

Webで検索するとboot()とかでidにulidセットしたり、$keyType = 'string';にしたり色々方法見かけたけど今はHasUlidsだけで大丈夫そう(多分)

userspasswordもいらないかも。

開発ではsqliteを使うと便利だと思う。

sqlite使う時は.envを少し修正する必要がある。

.env

DB_CONNECTION=sqlite # <--- 多分デフォルトはmysqlになっているのでsqliteに変更
DB_HOST=127.0.0.1
DB_PORT=3306
# DB_DATABASE=db.sqlite # DB_DATABASEはコメントアウトする
DB_USERNAME=root
DB_PASSWORD=

sqliteを使う時はDB_DATABASEコメントアウトしないと下記のようなエラーが出る。

理由はなんとなく分かるけどちゃんとは理解してない。

Database file at path [db.sqlite] does not exist. Ensure this is an absolute path to the database.

変更したらmigratedb:seedをやっておく。

php artisan migrate
php artisan db:seed

ログインURL生成

フォーム作って、コントローラーでリクエスト受けて...とかやってると面倒なので今回はcommandを使う。

php artisan make:command Tests/TestGenLoginLink

とりあえず一番最初のユーザーのログインリンクを生成する。

TestGenLoginLink.php

<?php

namespace App\Console\Commands\Tests;

use App\Models\User;
use Grosv\LaravelPasswordlessLogin\LoginUrl;
use Illuminate\Console\Command;

class TestGenLoginLink extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'test:gen:loginlink';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'ログインリンク生成';

    /**
     * Execute the console command.
     */
    public function handle()
    {
        $user = User::first();
        $generator = new LoginUrl($user);
        $url = $generator->generate();
        $this->info($url);
    }
}

下記のコマンドで最初のユーザーのログインリンクを生成できる。

$ php artisan test:gen:loginlink
http://resend.test/magic-login/01h6h6fk862hrz0t389gymex76?expires=1690650694&user_type=app-models-user&signature=60c851c17e1d2b5060f96c2366a8694731ecf35c24dcff512b97688e69be172a

実際にログインしてみる

welcome.blade.phpを下記のように書き換えてブラウザでURLにアクセスする

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Laravel</title>

    <!-- Fonts -->
    <link rel="preconnect" href="https://fonts.bunny.net">
    <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet"/>
</head>
<body>
<div>
    @if(Auth::check())
        logged in: {{Auth::user()->id }}
    @else
        guest
    @endif
</div>
</body>
</html>

ログイン前

ログイン後

ログインリンクをメールで送る

Resend.comではResendファサードが用意されているようだけど、今回はMailファサードを使用する。

resend.com

メールの内容はMarkdownを使って記述もできるので今回はそちらを使用する。

php artisan make:mail LoginLink --markdown=emails.auth.login_link

laravel.com

LoginLink.phplogin_link.blade.phpが生成されるのでそれぞれ修正する。

Mailableの方に変数を定義しておくと、bladeファイルの方からアクセスできる。

LoginLink.php

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class LoginLink extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Create a new message instance.
     */
    public function __construct(
        public string $url // <--- ログインリンク用の変数追加
    ){}

...

login_link.blade.php

<x-mail::message>

<x-mail::button :url="$url">
Login
</x-mail::button>

Thanks,<br>
{{ config('app.name') }}
</x-mail::message>

とりあえずこれでメールを送る準備が出来た。

Controller用意してとかやってると面倒なので、ログインリンク送信もCommandを使用する。

今回はUser::first()のログインリンクを指定のメールアドレスに送信しているが、本来はUserに登録されたメールアドレスを使用する(重要)

ライブラリの作者も間違ったユーザーへ送信しないようにREADMEで注意をしている。

The biggest mistake I could see someone making with this package is creating a login link for one user and sending it to another. Please be careful and test your code. I don't want anyone getting mad at me for someone else's silliness.

(このパッケージで誰かが犯しそうな最大のミスは、あるユーザー用のログインリンクを作成し、それを別のユーザーに送信することです。気をつけてコードをテストしてください。他人の愚かな行為で私に怒られたくないのです。)

php artisan make:command Tests/TestSendLoginLink

resend.comは初期設定だと登録したメールアドレスにしか送信できないようなので、もし送信したいメアドを変更したい場合は設定が必要(未確認)

<?php

namespace App\Console\Commands\Tests;

use App\Mail\LoginLink;
use Grosv\LaravelPasswordlessLogin\LoginUrl;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;

class TestSendLoginLink extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'test:mail:loginlink';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'ログインリンク送信テスト';

    /**
     * Execute the console command.
     */
    public function handle()
    {
        $to = "<resend.comに登録したメアド>";

        // 念のため
        $confirm = $this->confirm("send email to?: $to");
        if (!$confirm) {
            return;
        }

        // こんなことは普通やらないけど今回は例としてfirstのユーザーを使用している
        $user = User::first();
        $generator = new LoginUrl($user);
        $url = $generator->generate();

        $result = Mail::to($to)->send(new LoginLink($url));
    }
}

下記のコマンドで実際にメールを送信できる。

$ php artisan test:mail:loginlink


 send email to?: xxxx@test.com (yes/no) [no]:
 > yes

$

下記のようなメールが送信されてきてボタンをタップするとログインが完了する。

Laravel Voltでログアウト(おまけ)

先日LaraconUSでLaravel Voltが発表されましたね。

www.youtube.com

見た感じVueというかReactというかそういったライブラリっぽい書き方をPHPで実現できるライブラリのようです。

Livewireも使ったこと無いけど良い機会なので試しに使ってみようと思います。

ついでにBlade Componentも使ったこと無いのでそれも使ってみる(素のBladeしか使ったことなかった)

Laravel Folioというルーティングライブラリも発表されたが今回は使わない。

Livewire 3.0(beta)とVolt 1.0(beta)をインストールする。

$ composer require livewire/livewire:^3.0@beta livewire/volt:^1.0@beta
$ php artisan volt:install

とりあえずrootとなるBlade Componentを作成する。

--viewフラグを付けるとapp/View/Components/にComponentクラスが生成されない(匿名コンポーネントが生成される。)

$  php artisan make:component app --view

コンポーネントresources/views/components/に追加される。

とりあえずwelcome.blade.php<body>内以外をコピーしてくる。

app.blade.php

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Laravel</title>

    <!-- Fonts -->
    <link rel="preconnect" href="https://fonts.bunny.net">
    <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet"/>
</head>
<body>
{{$slot}}
</body>
</html>

Blade Componentに別の要素を追加する時は$slotを使うので<body>内は$slotとしている。

そしてwelcome.blade.phpは下記のように書き換えることができる。

welcome.blade.php

<x-app>
<div>
    @if(Auth::check())
        logged in: {{Auth::user()->id }}
    @else
        guest
    @endif
</div>
</x-app>

Volt Componentを追加する。

make:voltでVolt Componentが生成できる。

生成されたコンポーネントresources/views/livewireディレクトリに生成される。

$ php artisan make:volt auth

下記のようなテンプレートが生成される。

auth.blade.php

<?php

use function Livewire\Volt\{state};

//

?>

<div>
    //
</div>

ここにログアウト機能を追加していく。

Laravel Voltでログアウトする

最終的にはこんな感じになった

auth.blade.php

<?php

use Illuminate\Support\Facades\Auth;
use function Livewire\Volt\{computed};

$user = computed(function () {
    return Auth::user();
});

$logout = fn() => Auth::logout();

?>

<div>
    @if($this->user)
        logged in: {{ $this->user->id }}<br/>
        <button wire:click="logout">Logout</button>
    @else
        guest
    @endif
</div>

welcome.blade.phpを下記のように変更する。

<x-app>
    <livewire:auth />
</x-app>

実行結果。

ログイン状態:

ログアウトボタン押下:

gif動画をアップロードしようと思ったけどなぜか失敗するので静止画。

まとめ

マジックリンク実装ついでに使ったことない(+あまり使ったこと無い)機能も色々使ってみました。

Laravel Volt便利ですね。

TipKit試してみた

Xcode15 beta5でTipKitが使えるようになったので試してみた(もうbeta5まで出たのか...)

import SwiftUI
import TipKit

struct FavoriteLandmarkTip: Tip {
    
    var title: Text {
        Text("Save as a Favorite")
    }
    
    var message: Text? {
        Text("Your favorite landmarks always appear at the top of the list.")
    }
    
    var asset: Image? {
        Image(systemName: "star")
    }
}

struct ContentView: View {
    
    var tip = FavoriteLandmarkTip()
    
    var body: some View {
        VStack {
            TipView(tip, arrowEdge: .bottom)
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .task {
            try? await Tips.configure {
                DisplayFrequency(.immediate)
                DatastoreLocation(.applicationDefault)
            }
        }
        .padding()
    }
}

TipViewを消すとトルツメされる。

AVPlayerで最初から再生する方法

再生中のアイテムを最初にシークして再生。

if player.isEnded { // extension
    player.currentItem?.seek(to: .zero, completionHandler: nil)
}
player.play()

再生終了しているか確認する方法。

 

takagisou.hatenablog.jp