たけるのプログラミング

作ったものとか、気ままにアップします。

【Laravel】中間テーブルを触ってみる【追加・削除・同期】

超参考にしたドキュメントサイト
Eloquent:リレーション 8.x Laravel


例えばplayerとteamの関係について

1人のplayerは過去現在含めて複数のteamに所属して(いました)います。
1つのteamは過去現在含めて複数のplayerが所属して(いました)います。

つまりplayerとteamは多対多の関係になります。
多対多のリレーションを表すときは、多対多となるテーブルの中間地点に中間テーブルをかまし

1 対  対 1となるようなリレーションになるように設計します。

では上記に書いたplayerとteamのリレーションをlaravelに落とし込みたいと思います。

マイグレーションファイル

create_players_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('players', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });
    }

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

create_team_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('teams', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('teams');
    }
};
中間テーブル create_player_team_table

中間テーブルの命名規則
Eloquent:リレーション 8.x Laravel

によると

2つの関連するモデル名をアルファベット順に結合

となっています。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('player_team', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('player_id');
            $table->unsignedBigInteger('team_id');
            $table->string('note');
            $table->timestamps();
        });
    }

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

モデルの定義

Player.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Team extends Model
{
    use HasFactory;

    public function players()
    {
        return $this->belongsToMany(Player::class);
    }
}

Team.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Team extends Model
{
    use HasFactory;

    public function players()
    {
        return $this->belongsToMany(Player::class);
    }
}

実際に使ってみる

 $player = Player::find(1)->teams;
 dd($player);

実行結果

中間テーブルで、user_idが1と対応付くteamsテーブルのレコード(Teamモデルのインスタンス)を取得することが出来ました。
hasOneやbelongsToだと対応ずくテーブルは外部キーを持っていることが一般的ですが、今回は中間テーブルとのリレーションを持っている話なので
playersテーブルはteamsテーブルの外部キーを持ちませんし、teamsテーブルもplayersテーブルの外部キーを持っていません。

中間テーブルの値にアクセスする

先程の実行結果のTeamモデルのインスタンスがどのような値を持っているのか確認すると

teamsテーブルのカラムに加えて中間テーブルの外部キーplayer_idの値がpivot_player_id、外部キーteam_idの値がpivot_team_idという名前で取得できていることがわかる。
この値にアクセスするには以下のようにする。firstはコレクションの最初のインスタンスを取得して、そのインスタンスに対して->pivot->team_id;として中間テーブルの値を取得している。

    $player = Player::find(1)->teams->first()
    ->pivot->team_id;

しかし上記の実行結果を見てみると、中間テーブルで設定したはずのnoteカラムがない。noteカラムの値を設定するには
モデル定義にて
Player.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Player extends Model
{
    use HasFactory;

    public function teams()
    {
        return $this->belongsToMany(Team::class)->withPivot('note');
    }
}

withPivot(カラム名)とする。そうすることで->pivot->noteとすると、中間テーブルのnoteカラムにアクセスできるようになる。
Pivotは中心という意味で「with中心(Pivot)のカラム」と捉えると感覚的に理解がしやすい。

中間テーブルに値を追加、削除する方法

attach()

$player = Player::find(2);
$player->teams()->attach(2,['note'=>'シャックと共に優勝!']);

player_teamテーブルにレコードを追加します。user_idカラムに2、team_idカラムに2、noteカラムに'シャックと共に優勝!'が設定されます。

detach()

$player = Player::find(2);
$player->teams()->detach(2);

player_teamテーブルから、user_idカラムが2、team_idカラムが2のレコードを削除する。
もしdetach()のように引数にteam_idを指定しないと、user_idカラムが2のレコードを全て削除する。

sync()

$player = Player::find(2);
$player->teams()->sync([1 => ['note' => 'hello'],2 => ['note'=>'hello2']]);

syncは中間テーブルの追加や削除といった認識より、文字通り同期をとるイメージ。
上記コードは中間テーブルのuser_idカラムが2のレコードにおいて、team_idが1と2のものを同期するという意味。
つまり既存の中間テーブルにuser_idとteam_idの組み合わせが2,3のものがあった場合に削除される。
また上記コードはuser_idが2以外のレコードには影響はない。

syncWithPivotValues()

$player = Player::find(2);
$player->teams()->syncWithPivotValues([1,2,3],['note'=>'hello']);

$player->teams()->sync([1 => ['note' => 'hello'],2 => ['note'=>'hello'],3=>['note'=>'hello']]);

と同じ意味

syncWithoutDetaching()

$player = Player::find(2);
$player->teams()->syncWithoutDetaching([4 =>['note'=>'hello']]);

既にテーブルのplayer_idとteam_idカラムの組み合わせとして
(2,1) (2,2) (2,3)があったときに、(2,4)として追加できる。
attach()と異なる点ですが、attachの場合はattach(1)を2回実行すると(,1)(,1)のように重複できてしまいます。
sync系は同期なので重複の心配がありません。

【PHP】require_once関数でエラーになった件


↑tatum.phpはCelticsフォルダの中にあります。
上記ような構成のアプリケーションがあり、実行ファイルをindex.phpとする。

それぞれのコードの内容

index.php

<?php
require_once('Lakers/lebron.php');
hello();
?>

lebron.php

<?php
require_once('../Celtics/tatum.php');

tatum.php

<?php
function hello()
{
echo 'hello';    
}
?>

上記コードでindex.phpを実行するとエラーとなる。
エラーとなる原因はrequire_onceのパスの指定の仕方とPHPの仕様にある。

PHPは実行ファイル(index.php)のディレクトリ位置を基準として、requireを行うのでlebron.phpのrequire_onceのパスは

testing_php/../Celtics/tatum.php となり、当然そのディレクトリにはtatum.phpが存在しないのでエラーとなる。

解決方法

__DIR__ を使う。PHPでは__DIR__というマジカル定数が用意されています。
PHP: マジック定数 - Manual
PHPドキュメントによると

include の中で使用すると、 インクルードされるファイルの存在するディレクトリを返します。

つまり実行ファイルの影響を受けず、パスを指定することができる。
例えばlebron.phpの中で__dir__は

/var/www/html/Lakers

となり /から始まっているパスなので絶対パスになっていることが分かる。

なのでlebron.phpを以下のように変えればエラーが発生せずに実行を行うことができる。

<?php
require_once(__DIR__.'/../Celtics/tatum.php');

【JavaScript】Asyncとawaitを試す【殴り書き】

Async

関数の前にasyncを付けることによってpromiseを返すようになる。例えばreturn 'hello'とすると

      async function hello()
      {
          return 1;
      }

      console.log(hello());

実行結果

return <非 promise> がある場合、JavaScript は自動的にその値を持つ 解決された promise にラップします。

Async/await より引用

Await

Promiseの結果(fulfilled rejected)が確定するまで、待機する。
awaitはasyncが付いている関数の中で使うことができる。

      async function hello()
      {
          let x = await new Promise((resolve)=>{
              setTimeout(() => {
                  resolve('hello');
              }, 10000);
          });
          console.log(x);
      }
      hello();

実行結果にhelloと表示される。resolveでfulfilledの状態になったPromiseのPromiseResultの値が変数xに代入される。
awaitでresolveになるまで処理を止めた訳ですが、awaitを無くしたらどうなるのか。
結論は解決(resolve)するのを待たずに、promiseインスタンスが代入される。
10秒待たずにconsole.logが実行される。

async function hello()
      {
          let x = new Promise((resolve)=>{
              setTimeout(() => {
                  resolve('hello');
              }, 10000);
          });
          console.log(x);
      }
      hello();

実行結果

※10秒経つとresolveされるので、開発者ツールで上記に出力したインスタンスのPromiseStateを見るとfulfilledの状態になっています。

【JavaScript】非同期処理 Promise,thenについて色々試す【殴り書き】

前に以下のような記事を書いたことがありましたが、
【JavaScript】Promiseのthenメソッドの挙動について ちょっとした確認 - たけるのプログラミング

また非同期処理を実装する機会があったので、復習がてら色々なコードを試していけたらと思います。

そもそも非同期処理については、この記事を見るよりも良質な記事がたくさんあるので割愛しますが、

簡単に説明すると

JavaScriptはシングルスレッドでの実行になるので、例えばDBからデータを取ってくるような処理で多くの時間を要する場合、
その後に続く処理は、前の処理が終わるのを待たなくてはいけません。

そんな問題を解決するために利用されるのが非同期処理です。


fetchを使ってjsonデータを取得する

      const response = fetch("http://localhost/fetchtest")
      .then(function(response){
          console.log('データ取得しました!');
          return response.json();
      }).then(function(json){
          console.log(json);
      });

      console.log(response);
      console.log('(1)');
      console.log('(2)');
      console.log('(3)');

実行結果

まずどこからfetchをしているのかというとローカルに立ち上げたlaravelプロジェクトです。JavaScriptの話題とは関係ないですが、
laravelではモデルのインスタンスを要素に持つコレクションをreturnするだけでjson形式でフロントに返すことが出来ます。便利ですね。
またこのfetchは5秒かかるように、laravel側でsleep関数を使って処理を止めています。なのでJavaScriptを動かしているフロント側では
データを取得するのに5秒かかるように見える訳です。このように試している記事は自分が調べる限りまだ見てません。

fetchは非同期で行われる時間のかかる処理なのでまず

      console.log(response);
      console.log('(1)');
      console.log('(2)');
      console.log('(3)');

上記のコードの実行結果が先に表示されます。

console.log(response);

fetchメソッドはPromiseインスタンスを返します。
まず上記のコードでPromiseインスタンスの状態を見ることが出来ます。上記に載せた実行結果で
PromiseStateの値がpeddingとなっていました。このpendingは待機の状態のことで、言い換えるとlaravelからデータを取得している最中ということになります。
その後その処理が成功するとfulfilled
その後その処理が失敗するとrejectedとなります。


その後fetchメソッド成功するとPromiseStateがfulfilledとなります。
個人的な推測ですがfetchメソッドの内部にてresolveメソッドが実行されPromiseStateがfulfilledになっているのかなと思っています。

Promiseがfulfilledの状態になるとその後にthenメソッドを繋げて、非同期処理が成功した後に行いたい処理を定義することが出来ます。

thenメソッドの引数のコールバック関数の引数には、PromiseのPromiseResultの値が入っています。

またこのPromiseのPromiseResultはresolveメソッドの引数に指定した値が入ります。

console.log(new Promise(function(resolve){ resolve('JavaScript')}));

実行結果


なので1つ目のthenのコールバック関数の引数のresponseにはResponseインスタンスが入っていました。
つまり上記のことからfetch内部ではresolve(Responseインスタンス)のようになっていたと予想することが出来ます。

      const response = fetch("http://localhost/fetchtest")
      .then(function(response){
          console.log('データ取得しました!');
          return response.json();
      }).then(function(json){
          console.log(json);
      });

ResponseオブジェクトのjsonメソッドはPromiseインスタンスを返します。
ここで起こる疑問ですが、thenのコールバック関数にてPromiseインスタンスを返したら、thenメソッドはどのような値を返すのか。
結論からいうとthenはPromiseインスタンスを返しますが、コールバックのreturnによってそのPromiseインスタンスの状態は変わります。

ここから自分なりの解釈を含みます。
mdn web docsによると

Promise.prototype.then() - JavaScript | MDN

ハンドラ関数が
すでに履行されたプロミスを返した場合、 then によって返されるプロミスは、そのプロミスの値をその値として返します。

とありました履行=成功、値=PromiseResultのことかなと思います。
つまりreturn response.jsonで返されるPromiseインスタンスのPromiseResultの値が
thenメソッドで返されるPromiseインスタンスのPromiseResultの値に設定されるのではないかと思っています。

そうだとPromiseResultの値が次のthenのコールバック関数の引数に設定される事実と辻褄があいます。

thenのコールバック関数のreturnがpendding状態のPromiseインスタンスだった場合

new Promise(function(resolve){
        resolve(100);
    }).then(function(num){
        return new Promise(function(resolve){
            setTimeout(() => {
                resolve();
            }, 10000);
        });
    }).then(function(){
        console.log('これは実行される');
    });

実行結果で約10秒後に'これは実行される'と表示されました。
mdnドキュメントによると
Promise.prototype.then() - JavaScript | MDN

コールバック関数が

他の待機状態のプロミスオブジェクトを返した場合、 then によって返されたプロミスの解決/拒否は、ハンドラーによって返されたプロミスの解決/拒否結果に依存します。また、 then によって返されたプロミスの解決値は、ハンドラーによって返されたプロミスの解決値と同じになります。

特に以下の部分が大切ですね。そう考えるとjson()もこっちの例だったかもしれない。いやそうだ。

then によって返されたプロミスの解決/拒否は、ハンドラーによって返されたプロミスの解決/拒否結果に依存

そもそもreturnした後もPromiseのコールバック関数って実行されるんですね。

console.log(  new Promise(function(resolve){
        resolve(100);
    }).then(function(num){
        return new Promise(function(resolve){
            setTimeout(() => {
                console.log('returnした10秒後にresolveします');
                resolve(100);
            }, 10000);
        });
    }));

実行結果

なのでthenのコールバック関数がreturnした10秒後
thenで返したPromiseインスタンス上記の実行結果から
PromiseStateがfulfilledに変わって、PromiseResultが100に変わります。

【PHP PHPUnit】PHPUnitを使って簡単な単体テストを行う

参考にした記事
PHPUnitでユニットテスト① 導入編 | Points & Lines
1. アサーション — PHPUnit latest Manual



PHPUnitを触ってみた。

PHPUnit単体テスト(ユニットテスト)を行うためのフレームワーク

実際に導入から簡単なテストを行ってみた。

composerが導入されていることを前提としている。

1.PHPUnitのインストール

まずプロジェクトのディレクトリで以下のようなコマンドを実行する。

composer require --dev phpunit/phpunit ^latest

以下のコードを使って、バージョン情報を表示されたらインストール成功。

vendor/bin/phpunit --version  

2.テスト対象のクラスを書く


<?php

namespace app;

class Sample 
{
    public function plus100($num) 
    {
        $num += 100;
        return $num;
    }
}

オートロードを指定するためにnamespaceを記述している。そのためcomposer.jsonでは以下のように記述しておく。

{
    "require-dev": {
        "phpunit/phpunit": "^9.5"
    },
    "autoload": {
        "psr-4": {
            "app\\": "./"
        }
    }
}

3.テストコードを書く

<?php
require_once("vendor/autoload.php");

use PHPUnit\Framework\TestCase;
use app\Sample;

class SampleTest extends TestCase
{
    public function testplus100_1()
    {
        $sample = new Sample();
        
        $result = $sample->plus100(-1);
        
        $this->assertGreaterThan(100,$result);
    }

    public function testplus100_2()
    {
        $sample = new Sample();
        
        $result = $sample->plus100(1);
        
        $this->assertGreaterThan(100,$result);
    }

}

今回使ったアサーションメソッドはassertGreaterThan()

4.テスト実行&実行結果

vendor/bin/phpunit SampleTest.php
F.                                                                  2 / 2 (100%)

Time: 00:00.036, Memory: 4.00 MB

There was 1 failure:

1) SampleTest::testplus100_1
Failed asserting that 99 is greater than 100.

Fはアサーションに失敗した際に表示される。.はテストが成功した際に表示される。
3. コマンドラインのテストランナー — PHPUnit latest Manual

There was 1 failureと書いてあるように、1つ失敗があり、
testplus100_1でアサーションに失敗したよう。


今後はもう少し複雑なテストを書いてみたい。

【JavaScript】プロトタイプについて【継承 プロトタイプチェーン】

この記事はJavaScriptの経験が浅い人が書いています。
間違っている点等ございましたら、ご指摘いただけると幸いです。

参考にした記事
【JavaScript】 プロトタイプとは?prototypeプロパティはプロトタイプではない件について
JavaScriptのプロトタイプからオブジェクト指向を学ぶ - Qiita
Object のプロトタイプ - ウェブ開発を学ぶ | MDN


プロトタイプはメモリの節約継承機能を提供するものです。

例えば以下のようなコード

const str = new String('hello');

Stringオブジェクトをインスタンス化します。

strはStringオブジェクトなので、感覚的にStringオブジェクトで用意されているメソッドを使えることは分かります。

例えば以下のようなコード

console.log(str.charAt(2));

ここで開発者ツールを使ってstrの構造を見てみたいと思います。

するとcharAtメソッドが定義されていないことが分かります。ではなぜcharAtメソッドを使えることができるのか。





結論から書くと、prototypeの仕組みによって上記のようにメソッドを使うことができています。

例えば以下のコード

const a = new b;

aの[[Prototype]]にb.prototypeの参照を渡している

つまり上記に書いた

const str = new String('hello');

strの[[Prototype]]にString.prototypeの参照を渡しているということになります。

なので先ほど使ったメソッドのcharAtはString.prototypeオブジェクトのプロパティとして登録されているはずです。

証拠としてドキュメントには以下のように定義されています。

String.prototype.charAt

String.prototype.charAt() - JavaScript | MDN

【JavaScript】Promiseのthenメソッドの挙動について ちょっとした確認

この記事はJavaScript経験浅く、Promise勉強したての人が書いています。
間違い等ございましたら、指摘やアドバイス等いただけると嬉しいです。


thenメソッドはPromiseオブジェクトを返す。

なのでチェーンすることが可能です。

例えばサーバーからfetchメソッドを使ってjson形式のデータを取得してみる

fetch('https://jsonplaceholder.typicode.com/users')
.then(function(response){
    return response.json();
})
.then(function(json){
    console.log(json);
});

まずこのコードをconsole.logで出力すると以下のようになる。

fetch('https://jsonplaceholder.typicode.com/users')

Promiseオブジェクトを返している。そのため後にthenメソッドを続けることができる。
またPromiseResultの値がResponseオブジェクトになっている。

次にこのコードの戻り値をconsole.logで出力すると以下のようになる。

.then(function(response){
    return response.json();
})

thenメソッドの引数にコールバック関数を使用しており、そのコールバック関数の引数であるresponseに①のPromiseResultの値が格納されている。

上記でも書いたようにthenメソッドはPromiseを返す。注目するべき点はコールバック関数内でreturnした値(response.json)がPromiseResultの値となっていること。
つまり①→②のようにPromiseResultの値を 後に続くthenメソッドのコールバック関数の引数の値として利用することができる。

補足

例えばthenのコールバック関数の戻り値にPromiseを指定したらどうなるのか試してみた。

console.log(
    fetch('https://jsonplaceholder.typicode.com/users')
    .then(function(response){
    //return response.json();
    return Promise.resolve(100);
})
);