20210228のPHPに関する記事は15件です。

[PHP]ディレクトリごとファイルをコピーする

PHPにはファイルのコピーをするためのcopy関数がありますが、ディレクトリとその中に含まれるファイルを丸ごとコピーすることはできません。
そこで、PHPでディレクトリごとファイルをコピーする方法をご紹介します。

/**
 * ディレクトリをコピーする
 *
 * @param  string $dir     コピー元ディレクトリ
 * @param  string $new_dir コピー先ディレクトリ
 * @return void
 */
function copy_dir($dir, $new_dir)
{
    $dir     = rtrim($dir, '/').'/';
    $new_dir = rtrim($new_dir, '/').'/';

    // コピー元ディレクトリが存在すればコピーを行う
    if (is_dir($dir)) {
        // コピー先ディレクトリが存在しなければ作成する
        if (!is_dir($new_dir)) {
            mkdir($new_dir, DIR_WRITE_MODE);
            chmod($new_dir, DIR_WRITE_MODE);
        }

        // ディレクトリを開く
        if ($handle = opendir($dir)) {
            // ディレクトリ内のファイルを取得する
            while (false !== ($file = readdir($handle))) {
                if ($file === '.' || $file === '..') {
                    continue;
                }
                // 下の階層にディレクトリが存在する場合は再帰処理を行う
                if (is_dir($dir.$file)) {
                    copy_dir($dir.$file, $new_dir.$file);
                } else {
                    copy($dir.$file, $new_dir.$file);
                }
            }
            closedir($handle);
        }
    }
}

こちらの方法で、対象となるディレクトリを丸ごとコピーできます。
さらに下の階層にディレクトリが存在する場合でも、コピーすることが可能です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel ファイル名が日本語の添付ファイルが送れなかった

はじめに

あるプロジェクトでファイル名が日本語のPDFをメールで送信する必要がありました。
公式や記事を見ながら実装したところ、何故か日本語部分だけが送信できないという事象が発生しました。

前提

Centos7系
PHP7系

LaravelのMailファサード・Mailableクラスを用いて実装しています。

HogeController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Mail\SampleMailable;
use Mail;

public function sendMail()
{
    Mail::to('hogehoge@hogehoge')->send(new SampleMailable());
}
SampleMailable.php
<?php

namespace App\Mail;

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

class SampleMailable extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Build the message.
     *
     * @return $this
     */

    public function build()
    {
        return $this->view('emails.test')
                    ->from('XXX@XXXX','Test')
                    ->subject('テストメールです。')
                    ->attach(storage_path('app/pdf/テストfile.pdf'));
    }
}

事象

送られてきたメールを確認すると、添付されているファイル名が以下のようになっていました。

結果
file.pdf

期待値
テストfile.pdf

調査

以下の順序で調査を行いました。

  1. config.phpのlocaleがjaになっているか
  2. 参考記事のようにファイル名を参照する際にbasename()を使用しているかどうか
  3. サーバーのlocaleが正しいか
  4. php.iniの設定が正しいかどうか

調査結果

php.configの値

config.php
    'locale' => 'ja',

basename()

今回、basenameは未使用。
laravelのソースも確認しましたが、使用されていませんでした。

サーバーのlocale

正しい値のようでした。

LANG=ja_JP.utf8
LC_CTYPE="ja_JP.utf8"
LC_NUMERIC="ja_JP.utf8"
LC_TIME="ja_JP.utf8"
LC_COLLATE="ja_JP.utf8"
LC_MONETARY="ja_JP.utf8"
LC_MESSAGES="ja_JP.utf8"
LC_PAPER="ja_JP.utf8"
LC_NAME="ja_JP.utf8"
LC_ADDRESS="ja_JP.utf8"
LC_TELEPHONE="ja_JP.utf8"
LC_MEASUREMENT="ja_JP.utf8"
LC_IDENTIFICATION="ja_JP.utf8"
LC_ALL=

php.iniの設定値

php.iniでlocale設定がされていなかったです。

原因

日本語を扱えるようにするにはphp.ini内で設定が必要ですが、デフォルト値のまま変更がされていませんでした。

対処法

効果がなかった対処法

こちらに記載されていたMailableのローカライズを参考にしましたが、期待通りに反映されませんでした。

HogeController.php
public function sendMail()
{
    Mail::to('hogehoge@hogehoge')->locale('ja')->send(new SampleMailable());
}

今回の対処法

大人の事情でサーバー上にあるphp.iniを編集することは叶いませんでしたので、以下の内容で対応しました。

AppServiceProvoder.php
class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        setlocale(LC_ALL, 'ja_JP.UTF-8');
    }
}

これでLaravel上の処理は全て日本語対応されるはずです。
ちなみに、LC_ALLは他のLC関連の環境変数よりも優先される環境変数です。

別の対処法

php.iniの設定を変更したほうが良いと思います。

php.ini
mbstring.language = Japanese
mbstring.internal_encoding = UTF-8
mbstring.encoding_translation = Off
mbstring.http_input = pass
mbstring.http_output = pass
mbstring.detect_order = UTF-8,SJIS,EUC-JP,JIS,ASCII

これでもうまくいくと思います。(未検証なので、ご存知の方がいらっしゃいましたら教えていただけると幸いです!)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(備忘録)PHPでデプロイとアプリ公開まで

参考記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

レンタルサーバーでPHPのSESSIONが作動しないとき

きっかけ

SESSIONがないとログイン機能を実装できないのに、コアサーバーではSESSIONがなぜか機能していなかった。例えば推移先のページにこのようなコードを打つと、「ログインし直して下さい。」が返されてしまう。

session_start();
if(!empty($_SESSION["userName"]))
echo $_SESSION["userName"]."がログイン中";
else {echo "ログインし直して下さい";exit;}

sessionが使えないときの対処法はすでにいくつか記事があるが、sudoコマンドが使えないレンタルサーバーでは少し異なる部分がある。
これを解決するのにかなりのサイトをめぐり試行錯誤したので結果をまとめておく。

環境

レンタルサーバー:コアサーバー
使用言語:php7.4
機能:SESSION

やりたいこと

  • SESSION変数に値が保存される。
  • SESSIONを自分の任意のディレクトリに保存できるようにする。※初期値の保存先は/tmpだが、これはセキュリティ上よろしくないし、なぜか私の場合は保存できなかったため。

やったこと

1.iniファイルのSESSION設定をいじる

(私はphp7.4を使いました)。コアサーバーでは各ユーザーに対して.iniファイルが用意されています。.htacessや.user.iniでも多分いけるとは思います。

cd ~/public_html/.fast-cgi-bin

session.save_path = /virtual/ユーザー名/session

ちなみに検索している最中に下記の設定を加える話があったが、あくまで任意の設定で、触らなくても動く。

session.hash_function = 1
 (sessionファイル名の名前はハッシュ値で出るらしいが、これのアルゴリズム)
session.hash_bits_per_character = 4
session.sid_bits_per_character = 6 
(sessionファイル名に使われる文字の種類が増える。4なら0-9とa-fまで、6なら0-9とa-zの大文字小文字が使われる)
session.sid_length = 64 
(sessionファイル名の長さ。長い方が特定されにくいらしい)
session.entropy_file = /dev/random
(sessionファイルの名前を乱数で生成する時、どこから取るかを指定。php7.1ですでに廃止された)

2セッションを保存するディレクトリを作成する。

私はホームディレクトリ(~/)にsessionディレクトリを作った

$ mkdir session
$ chmod u+rwx session
#実行権限を与えることでsessionにファイルを作れるようになる

sessionファイルがうまく作れるとlsでこんなファイルが生成される。今回は所有者グループの欄をapacheやngixに直す必要はなかった。(レンタルにはそもそも権限がない)

$ ls-l
-rw-------  1 username hpusers   0  2月 25 19:20 sess_3aQ6CWkJDFln6Fn0vDQVRSrU3gcAhkBIdxlWkTZemiNuqUH55ShVgqqMBabIYA,v

おまけ

全般的なデバッグについてはこちらの方が徹底的に分類してくださっているので、おすすめします。最終的にcookieの設定は関係なかったけど、sessionを理解する上では役立ちました。
https://note.kiriukun.com/entry/20191125-session-not-working-in-php#file

var_dumpにこんなものを入れると検証に役立つ。

  • var_dump(session_save_path());
    →セッションのセーブ先を出力する。iniファイルで設定したパスと同様になっているかはこれで確認する。ちなみにコアサーバーはかなりの時間を置くことでiniファイルの設定が反映される。一般的にはapacheを再起動するとiniファイルが反映されるようだが、間違ってもレンタルサーバーでしようとしてはいけない(というかできない)。

  • var_dump(shell_exec('ls '.session_save_path().' -la'));
    →セッションのセーブ先のファイル一覧を出力する。ここにSESSIONファイルらしきものが保存されていれば保存には成功している。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerでPHPUnitを少し使ってみる(Composer使用)

はじめに

Dockerをある程度使える前提で書いていきます。

『DockerでPHPUnitを少しさわってPHPのテスト少し試してみたい!』方向けです。

よろしくお願いします!:bow_tone1:

★テスト記述する際のお決まり事項
・Class という名前のクラスのテストは、ClassTest という名前のクラスに記述
・ClassTest は、(ほとんどの場合) PHPUnit\Framework\TestCase 
・テストは、test* という名前のパブリックメソッドとなります。(なのでtestSample()やtestA()という書き方になる)
・テストメソッドの中で assertSame() のようなアサーションメソッド (アサーション を参照ください) を使用して、期待される値と実際の値が等しいことを確かめます。

★ユニットテストを書くそもそもの目的
バグの発見と修正や コードのリファクタリングを開発者がやりやすくすること。
ソフトウェアのドキュメントとしての役割を果たすこと。
私的には同じバグを繰り返さないために書くものという感じで理解しています。

★どこをテストすべきか
ひとつのユニットテストがカバーするのは、 通常はひとつの関数やメソッド内の特定のルートだけとなる

上記内容はほぼ以下URLからの参考です。
https://phpunit.readthedocs.io/ja/latest/writing-tests-for-phpunit.html

様々なチェックメソッドは以下
https://phpunit.readthedocs.io/ja/latest/assertions.html#appendixes-assertions

では早速

環境作成

以下のようにファイルを作成して、『docker-compose.yml』ある場所でdocker-compose up --build -dを叩きます。
そうすると、<<ipアドレス>>:81以下のように表示されています。
※ポート(81の部分)は同じですがIpは各環境で違います。ちなみに私のipは$ docker-machine ip defaultで調べられます。意味は『defaultとして作成したドッカーマシンのip』

全体の構成参考画像

スクリーンショット 2021-02-28 19.49.46.png
index.php
<?php
require_once 'sample.php';

$sample = new sample\Sample();

echo $sample->a();
sample.php
<?php
namespace sample;

class Sample
{
    public function a()
    {
        return 'あ';
    }

    public function i()
    {
        return 'い';
    }
}
SampleTest.php
<?php
use PHPUnit\Framework\TestCase;

require_once 'sample.php';

class SampleTest extends TestCase
{
    public function testSample()
    {
        $sample = new sample\Sample();

        //同じ値、同じ型でない場合エラー
        $this->assertSame($sample->a(), 'あ');
    }
}
docker-compose.yml
version: '3'
services:
  apach:
    container_name: sample-apache #作成されていると出来ないので新しく作成する場合ここの名前を変更
    build: ./  #Dockerfileを使用してコンテナ作成
    ports:
      - 81:80
    volumes:
      - ./html:/var/www/html # コンテナ var/www/html の中に/htmlの中身を入れる
From php:7.4-apache

RUN apt-get update \
&& apt-get install -y \
zip \
unzip

WORKDIR /var/www/html
スクリーンショット 2021-02-28 19.51.59.png

※以下に最終完成バージョンも含めてあげているので、もしよかったら参考にしていただければと思います。

コンテナに入ってもう一仕事する

:sun_with_face:以下コマンド今使用しているコンテナIDの確認を行う
$ docker ps

:sun_with_face:コンテナ内に以下コマンドで入る、 ※『1999d283b4f9』は上記で出力したコンテナIDを使用してください
$ docker exec -i -t 1999d283b4f9 bash

:sun_with_face:コンテナ内で以下コマンドうちcomposerのインストール
$ curl -sS https://getcomposer.org/installer | php

:sun_with_face:composerのインストールが無事行えたら以下コマンドで『PHPUnit』のインストール
$ php composer.phar require --dev phpunit/phpunit
参考:https://phpunit.readthedocs.io/ja/latest/installation.html

:sun_with_face:全ての準備は出来た!テストコードは書いてあるので、以下コマンドをコンテナ内で実行しテスト実行してみる
$ vendor/bin/phpunit SampleTest.php
以下のようになればテスト行えたことが分かる

スクリーンショット 2021-02-28 20.03.20.png

テスト実行結果参考以下URL
https://phpunit.readthedocs.io/ja/latest/textui.html

表示 意味
. テストが成功した際に表示されます。
F アサーション失敗
E テストメソッドで何かしら失敗
I テストがふかんぜんまたは未実装

最後に

以上終わりです!
書き足すことあるかもですが最初に書いたものは削除しない予定です!

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerでPHPUnitを少しさわってみる

はじめに

Dockerをある程度使える前提で書いていきます。

これはあくまで、
『DockerでPHPUnitを少しさわってPHPのテスト少し試してみたい!』方向けです。

よろしくお願いします!:bow_tone1:

後、あ、これも!これも書いておきたい!っと思ったら書き足していく予定です!

では早速

環境作成

以下のようにファイルを作成して、『docker-compose.yml』ある場所でdocker-compose up --build -dを叩きます。
そうすると、<<ipアドレス>>:81以下のように表示されています。
※ポート(81の部分)は同じですがIpは各環境で違います。ちなみに私のipは$ docker-machine ip defaultで調べられます。意味は『defaultとして作成したドッカーマシンのip』

スクリーンショット 2021-02-28 19.51.59.png

※以下に最終完成バージョンも含めてあげているので、もしよかったら参考にしていただければと思います。

index.php
<?php
require_once 'sample.php';

$sample = new sample\Sample();

echo $sample->a();
sample.php
<?php
namespace sample;

class Sample
{
    public function a()
    {
        return 'あ';
    }

    public function i()
    {
        return 'い';
    }
}
SampleTest.php
<?php
use PHPUnit\Framework\TestCase;

require_once 'sample.php';

class SampleTest extends TestCase
{
    public function testSample()
    {
        $sample = new sample\Sample();

        //同じ値、同じ型でない場合エラー
        $this->assertSame($sample->a(), 'あ');
    }
}
docker-compose.yml
version: '3'
services:
  apach:
    container_name: sample-apache #作成されていると出来ないので新しく作成する場合ここの名前を変更
    build: ./  #apachのDockerfileを使用してコンテナ作成
    ports:
      - 81:80
    volumes:
      - ./html:/var/www/html # コンテナ var/www/html の中に/apach/htmlの中身を入れる
From php:7.4-apache

RUN apt-get update \
&& apt-get install -y \
zip \
unzip

WORKDIR /var/www/html

コンテナに入ってもう一仕事する

・以下コマンド今使用しているコンテナIDの確認を行う
$ docker ps

・コンテナ内に以下コマンドで入る、 ※『1999d283b4f9』は上記で出力したコンテナIDを使用してください
$ docker exec -i -t 1999d283b4f9 bash

・コンテナ内で以下コマンドうちcomposerのインストール
$ curl -sS https://getcomposer.org/installer | php

・composerのインストールが無事行えたら以下コマンドで『PHPUnit』のインストール
$ php composer.phar require --dev phpunit/phpunit
参考:https://phpunit.readthedocs.io/ja/latest/installation.html

・全ての準備は出来た!テストコードは書いてあるので、以下コマンドをコンテナ内で実行しテスト実行してみる
$ vendor/bin/phpunit SampleTest.php
以下のようになればテスト行えたことが分かる

スクリーンショット 2021-02-28 20.03.20.png

最後に

以上終わりです!
書き足すことあるかもですが最初に書いたものは削除しない予定です!

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerでPHPUnitを少しさわってみる(Composer使用)

はじめに

Dockerをある程度使える前提で書いていきます。

これはあくまで、
『DockerでPHPUnitを少しさわってPHPのテスト少し試してみたい!』方向けです。

よろしくお願いします!:bow_tone1:

後、あ、これも!これも書いておきたい!っと思ったら書き足していく予定です!

では早速

環境作成

以下のようにファイルを作成して、『docker-compose.yml』ある場所でdocker-compose up --build -dを叩きます。
そうすると、<<ipアドレス>>:81以下のように表示されています。
※ポート(81の部分)は同じですがIpは各環境で違います。ちなみに私のipは$ docker-machine ip defaultで調べられます。意味は『defaultとして作成したドッカーマシンのip』

スクリーンショット 2021-02-28 19.51.59.png

※以下に最終完成バージョンも含めてあげているので、もしよかったら参考にしていただければと思います。

全体の構成参考画像

スクリーンショット 2021-02-28 19.49.46.png
index.php
<?php
require_once 'sample.php';

$sample = new sample\Sample();

echo $sample->a();
sample.php
<?php
namespace sample;

class Sample
{
    public function a()
    {
        return 'あ';
    }

    public function i()
    {
        return 'い';
    }
}
SampleTest.php
<?php
use PHPUnit\Framework\TestCase;

require_once 'sample.php';

class SampleTest extends TestCase
{
    public function testSample()
    {
        $sample = new sample\Sample();

        //同じ値、同じ型でない場合エラー
        $this->assertSame($sample->a(), 'あ');
    }
}
docker-compose.yml
version: '3'
services:
  apach:
    container_name: sample-apache #作成されていると出来ないので新しく作成する場合ここの名前を変更
    build: ./  #apachのDockerfileを使用してコンテナ作成
    ports:
      - 81:80
    volumes:
      - ./html:/var/www/html # コンテナ var/www/html の中に/apach/htmlの中身を入れる
From php:7.4-apache

RUN apt-get update \
&& apt-get install -y \
zip \
unzip

WORKDIR /var/www/html

コンテナに入ってもう一仕事する

:sun_with_face:以下コマンド今使用しているコンテナIDの確認を行う
$ docker ps

:sun_with_face:コンテナ内に以下コマンドで入る、 ※『1999d283b4f9』は上記で出力したコンテナIDを使用してください
$ docker exec -i -t 1999d283b4f9 bash

:sun_with_face:コンテナ内で以下コマンドうちcomposerのインストール
$ curl -sS https://getcomposer.org/installer | php

:sun_with_face:composerのインストールが無事行えたら以下コマンドで『PHPUnit』のインストール
$ php composer.phar require --dev phpunit/phpunit
参考:https://phpunit.readthedocs.io/ja/latest/installation.html

:sun_with_face:全ての準備は出来た!テストコードは書いてあるので、以下コマンドをコンテナ内で実行しテスト実行してみる
$ vendor/bin/phpunit SampleTest.php
以下のようになればテスト行えたことが分かる

スクリーンショット 2021-02-28 20.03.20.png

最後に

以上終わりです!
書き足すことあるかもですが最初に書いたものは削除しない予定です!

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel カート機能実装入門】〜後編〜 sessionを使用したカート機能実装の完全解説!!

0. はじめに

※こちらの記事は前後篇の後編です!前編はこちらをクリック!!!
*本記事ではLaravel 5.8での開発を解説しています

ECサイトの作成をしていてこんな事を感じた経験はないでしょうか。
「セッションを用いたカート機能実装の実装が難しすぎる!」
過去の自分や現在進行形で困っている方へ向けて
その悩みを解決するためカート機能実装の網羅的な解説記事を作りました。
お役に立てれば幸いです!

前後編の長文となりますのでご了承下さい。

目次

  • 0.はじめに
    • 本記事の対象になりうる方
    • 概要
  • 1.各分野で必要となる基礎知識
  • 2.要求定義と要件定義
  • 3.機能設計
  • 4.大まかな流れ
  • 5.実際の挙動と画面遷移のと処理の流れ(Gif動画)
  • 6.関連コード
  • 7. 各実装の詳細説明
    • 7-1 解説-addCart-メソッド
    • ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    • ↑ここまでが前編
    • ↓これ以降は後編
    • ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    • 7-2 ※解説の前の補足事項 〜ModelとEloquent、DB設計、リレーションの取得〜
    • 7-3 解説-index-メソッド
    • 7-4 解説-remove-メソッド
    • 7-5 解説-addCart-メソッド

7-2 ※解説の前の補足事項 〜ModelとEloquent、DB設計、リレーションの取得〜

ここでは今回の記事の理解において必要になると思われる事項についてだけ駆け足で記述しています。
それぞれの話題だけで一記事できてしまいますのでここでの詳細な説明は割愛します。

  • Modelとは、MVCモデルにおけるDB関連の処理を担当するクラス。
  • Laravel 特有のDBデータの取り扱う方法には以下の2種類。他にはSQL文の直書きがあります。

Eloquent ORM
②クエリビルダ  参考:クエリビルダとは?Eloquentの違いと使い方
今回使用するのはEloquentなのでクエリビルダの説明は割愛し、Eloquentの説明のみにします。 

Eloquent(エロクエント)

  • Laravel 独自の ORM(Object Relational Mapping:O/Rマッパー)
    • Laravelで提供されているデータ操作の為の機能
    • データベースとModelを関連付け、柔軟なデータ操作を行う為の Laravel 独自の機能
  • 全てのEloquentモデルは Illuminate\Database\Eloquent\Model を継承する

ORM

  • オブジェクト指向のプログラム言語で作成されたオブジェクトと、リレーショナルDB(MySQLなど)を繋ぐ。
    • データベースのレコードを、プログラミング言語のオブジェクトとして扱えるようにする
  • 「オブジェクト指向のプログラム言語で作成されたプログラム」と「リレーショナルDB」の間には本来互換性がありません。
    • この様な設計思想の異なるシステム間での非互換性をインピーダンス・ミスマッチといいます。ORMはこのインピーダンスミスマッチを解消することが出来ます。
  • データベースからデータを取得したらデータをオブジェクト化します。
    • 逆にPHPのオブジェクトをそのままDBテーブルのデータに変換もする。
  • 互いに関連するデータの集合とそれらに対する手続き群をひとまとめにしたのが「オブジェクト」
    • データと処理のまとまり、ということらしいのですが、ぶっちゃけなんとなくしか理解できません。


交流電気回路分野のインピーダンスとの類似性と考察:(私見に基づくポエムです)
私自身そういった分野の学問に就学していたので馴染みある概念です。
インピーダンスをかなり大雑把に説明すると、異なる物質間の境界面を波動や波の性質を持つ情報が通過する際に現れる抵抗、もしくはフィルタの様なものです。
交流電気回路に限らず電磁波、光、音響、震動、地震、津波など全ての波、波動現象に適用されうるもので、多くの分野でアナロジが見受けられる概念となります。
ここで重要なのはインピーダンスというのは、伝搬する何か(ここでは情報、プログラム内の命令)の伝わりやすさや、伝搬途中でのベクトルを変化させる度合いを表現する指標だということです。
よく間違われるのですが、伝わりにくさを表す指標ではありません。
伝わる情報そのものの性質、伝搬するために必要な触媒の性質で伝わり易さは変化するからです。
それは物質(システム)ごとで異なるものなので、異なる物質(システム)同士で情報が伝搬する場合、必ずインピーダンス不整合(ミスマッチ)というものが発生します。
即ち、伝えたい情報が正確な形で伝わらなかったり(減衰・増幅)、意味が変わってしまう、といった現象が起きます。
コンピュータの情報のやり取りは状態(電源)のオンオフ、即ち0か1によるものに過ぎません。
その正体は電流もしくは電圧であり、これは性質として「波」なんですよね。
このアナロジが適用できるということは、電気信号と波動という一見別々の性質の様に見える現象が、実は抽象的なレイヤでは同じか非常に近いメカニズムで有ることが示されているんだ、という様に感じます。
いやーこういった別の分野同士をつなげてたり、それによるパラダイムシフトが起きたりするのは胸熱展開ですよね?そうですよね?ええ、そう言ってくれると信じてました。


私は医療従事者なので医療でも適用分野を上げてみたいと思います。
超音波検査で赤ちゃんの画像をディスプレイに描出できるのも、このインピーダンスによって生じる音波の減衰差異を数値化して映像信号に変換しているからなのです。
また水中での音速が空気中の約三倍になるのもインピーダンスの違いで起きうる現象なんですね(更にこの違いを適応した治療法に尿石や結石を超音波で破砕する治療が存在します)
こういった考え方を拡張し適用したのがここで言われる

「インピーダンスミスマッチ」

なのでしょう。
解りやすい例えでは、光の屈折ですね。
光という情報が空気と水といった異なる存在の境界面でベクトルが変化する現象が光の屈折と言われる現象です。
ここでは光を屈折することなく水の中を透過させてあげればいいというわけです。空気がLaravel、水がDBで、光はLaravelからDBへ飛んでくるテーブルデータ取得のためのプログラムですかね。これが屈折されたら困るということです。
この様に情報の入力と出力間で情報が伝わり切るような状態をインピーダンス整合が取れていると表現します。
結局ここで言いたかったのはORMはこのインピーダンス整合のための機能なんですねってことなんですけど、途中から自分の言いたいことんばかり言って熱くなっていしまいましたー!すみません(笑)
ただここで、一つの疑問が生まれます。ほんなら始めからLaravelも水ん中に居たらええんじゃないの?
つまりEloquentやクエリビルダを使用せず、

直接SQL文を直書きすればそもそも「インピーダンスミスマッチ」は起きない

のですよね。
EloquentもSQLの直書きも調べた限りではどちらにもメリット・デメリットあるから場面によって使い分けたらええやんって感じがします。どちらにも原理主義的な意見があるみたいですが、個人的には原理主義的な意見は信用ないようにしています。角度が変われば物事は違う顔をしているのが真理だと思っているからです。
また仏教が好きなので中庸的な意見が好きです。なので中庸的な意見の参考としてこちらをどうぞ。一般的な意見としてはこちらを参考にしています
ちなみに光の屈折を起こさない様にするためには水面に対して光を真正面から入射させれば屈折現象は起きません。これは、光を光線と捉え、水面に斜めに入射した時に水面に最初に触れる内側の接面と遅れて触れる外側の接面があることでの挙動の遅れがあると解れば理解できるのですが、話がズレ過ぎなので自重します…

Model

  • Eloquentでは、モデルクラスを定義しModelというクラスを継承して処理を実装します。
  • Modelとは、MVCモデルの「M」にあたる部分で、主にデータベースとの連携を行う。
  • 命名規則として、テーブル名を 単数形+アッパーキャメル にしたものがモデル名となります。またテーブル名が複数型なのに対して、モデル名は単数型にするという決まりがあります。
    • この関係性が成立する場合に、Laravelではモデルとテーブルが自動的に紐づきます
  • Laravel側でSQL文を記述することなくデータベース内のテーブルのデータを、簡単な記述で取得することが可能
  • 特定のテーブルとModelが1対1になっており、そのモデルの使用をコントローラで宣言することでコントラーラ内でそのModelに紐づくテーブルのデータが取得できます

ex. Modelとテーブルの結合例

Product.php
(抜粋)
class Product extends Model
{
    //対応するテーブルを明示し定義する。 m_products テーブルは商品情報を記録
    protected $table = 'm_products';
}

ex. controllerでのモデルの使用宣言とEloquentでのデータ取得

ProductController.php
抜粋
//モデルの使用宣言
use App\Product;
 //$sessionProductsId はセッションに保存された商品ID番号が代入されている。
 $product = Product::find($sessionProductsId);

$productの中身($sessionProductsIdが「1」だった場合)

  • 返される値はModelオブジェクトとなる
    • アロー演算子(->)で中身のプロパティが取得できる(※->については前編で解説)
    • attributesの中身は連想配列、->の後にkeyを指定し対応する値が取得できる
      • ex.$product->product_nameとすれば値は「雪の恋人」となる
      • ex.$product->descriptionとすれば値は「北海道に訪れた恋人たちの想い出」
App\Product {#142 ▼
  #table: "m_products"
  +timestamps: false
  #fillable: array:3 [▶]
  #connection: "mysql"
  #primaryKey: "id"
  #keyType: "int"
  +incrementing: true
  #with: []
  #withCount: []
  #perPage: 15
  +exists: true
  +wasRecentlyCreated: false
  #attributes: array:10 [▼
    "id" => 1
    "product_name" => "雪の恋人"
    "category_id" => 1
    "price" => 1980
    "description" => "北海道に訪れた恋人たちの想い出"
    "sale_status_id" => 1
    "product_status_id" => 1
    "regist_data" => "2021-02-01 02:06:49"
    "user_id" => 1
    "delete_flag" => ""
   ]
  #original: array:10 [▶]
  #changes: []
  #casts: []
  #dates: []
  #dateFormat: null
  #appends: []
  #dispatchesEvents: []
  #observables: []
  #relations: []
  #touches: []
  #hidden: []
  #visible: []
  #guarded: array:1 [▶]
}

$productの中身($sessionProductsIdの中身が配列や複数の値の場合)

  • Collectionオブジェクトが戻り値となる
    • 中身がすべてModelオブジェクトになっている
    • 複数のレコードを返す場合はCollection`で返される
    • getfindなどのメソッドの違いでもCollectionModelか変わる
  • 多次元配列の為Modelオブジェクトとは異なりアロー演算子(->)でプロパティを取得できない
Illuminate\Database\Eloquent\Collection {#1202 ▼
  #items: array:4 [▼
    0 => App\Product {#1205 ▶}
    1 => App\Product {#1206 ▶}
    2 => App\Product {#1207 ▶}
    3 => App\Product {#1208 ▼
      #table: "m_products"
      +timestamps: false
      #fillable: array:3 [▶]
      #connection: "mysql"
      #primaryKey: "id"
      #keyType: "int"
      +incrementing: true
      #with: []
      #withCount: []
      #perPage: 15
      +exists: true
      +wasRecentlyCreated: false
      #attributes: array:10 [▼
        "id" => 5
        "product_name" => "十勝のバターハヤシライス"
        "category_id" => 5
        "price" => 5800
        "description" => "生牛乳から作られたバターをふんだんに使用!10食セット☆"
        "sale_status_id" => 5
        "product_status_id" => 5
        "regist_data" => "2021-02-01 02:06:49"
        "user_id" => 2
        "delete_flag" => ""
      ]

DB設計について

達人に学ぶDB設計 徹底指南書 (Japanese Edition) Kindle 版. からの抜粋

・データベースを使わないシステムは、この世に存在しない。
・最初にデータがある。プログラムはその次にできる。
・データベースを制する者がシステムを制す。データベースは、システムの中心であると同時に、システム開発の中心でもある。

この様にDB設計はアプリケーションの根幹を担うところであると理解していますが、本質的な部分を実感を伴ってここで解説するには自分には全く出来ません。
多くの諸兄の方々がすでに解説記事を出されている所ですのでそちらを御覧ください。
オススメは下記の書籍です。
全く何も知らないと、辛いかもしれませんが体系的に一から学ぶという点ではとても良かったです。
達人に学ぶDB設計 徹底指南書

ここで解っておいてほしい事はDB内の各種テーブルの定義、各種テーブルの同士の関係(リレーション)、テーブル内のデータ管理の仕方などは予め厳密に設計するものであり、それが適正に定められていなければアプリケーションやシステムは期待した機能にはなってくれない、ということです。

下記ではDB設計において、設計がなされた後に、設計を元にDB作成を行う方法を紹介しますが、マイグレーションリレーションについてだけ説明します。他に部分は必要あれば適宜解説をはさみたいと思います。


データベース用語
データベースの用語を理解しよう 「テーブル」「レコード」「カラム」「フィールド」とは?
より引用


Ⅱ.データベースにおける用語
リレーショナルデータベースマネジメントシステム(RDBMS、以降単にデータベースと表記します)は、今日のウェブシステムには欠かせないものになっていて、クライアントの担当者やウェブディレクターでも、データベースについて打ち合わせする機会がよくあります。ウェブにおいてデータベースは、動的なページを生成する際に、テンプレートに差し込むデータを保管し、素早く取り出す役目を担っています。そのデータベースに関してよく使われる用語を整理してみましょう。データベースというとほとんどの人はMicrosoft Excelのような表計算ソフトの画面をイメージするので、今回もExcelとの比較で解説することにします。


Ⅱ-ⅰ.「テーブル」
シートに相当するのがテーブルです。データベースではデータの種類やプログラムの利便性を考慮して複数のテーブルを持つことが多く、Excelのブックのような構造になっているとイメージできます。
図はショッピングサイトの例です。商品部分は商品テーブル、購入者情報は顧客テーブルからというように複数のテーブルを関係(リレーション)させるので、リレーショナルデータベースといいます。
image.png160425_DBword_03.png


Ⅱ-ⅱ.「カラム」
列に相当するのが、”カラム”です。雑誌の囲み記事を”コラム”といいますが、スペルは同じです。データベースではカラムごとに、文字列(と文字数)、数値(と桁数)のように属性が定められるので、打ち合わせにおいては、これら属性のことをカラムと呼ぶ人もいます。


Ⅱ-ⅲ.「レコード」
”テーブル”と”カラム”が、データが保管される場所のことを表していた語に対して、”レコード”はデータそのもののことを指す言葉です。同時に、列である”カラム”に対して行を意味する”ロウ”と同じ意味で用いることもあります。


Ⅱ-ⅳ.「フィールド」
レコードを構成する1つ1つの要素のことです。Excelでいう”セル”に相当します。
カラム、レコード、フィールドの関係を表すと、「複数のレコードの同じフィールドを集めたものがカラム 」ということになります。
また、ユーザーインターフェイスにおいてもフィールドと言う言葉を使います。入力フォームのデータを入力するスペースのこともフィールドといいます。フォームのフィールドから入力されたデータは、(データベースを使っていれば)該当するテーブルのレコード内フィールドにだいにゅされるわけですから、フォームのフィールドとデータベースのフィールドはこの場合同じと言えます。
image.png160425_DBword_04.png

マイグレーションとは?

  • マイグレーションとは、テーブル定義を管理する仕組み
  • 下記の様なER図を元にマイグレーションファイルを作成しマイグレーションを実行します。
    • マイグレーションを実行するとファイル内で定義されたテーブル作成が容易に可能です。
    • ER図通りにファイルを作成し実行するとER図通りのカラムが作成されます。

スクリーンショット 2021-02-19 0.30.58.png
ER図を元に作成したマイグレーションファイル

2020_11_23_101254_create_m_products_table.php
<?php

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

class CreateMProductsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {

        Schema::create('m_products', function (Blueprint $table) {
            // Eloquent にデフォルトで設定されている主キー(Primary Key)はid。主キーを変更する:$primaryKey idを無効にして他のものを主キーとして設定できる。 
            // Eloquent ORMでは主キーに対する規約を設けている。①符号なしINT (unsigned int)②フィールド名はid③オートインクリメント 
            // Laravel5.8 Eloquent:利用の開始 主キー Eloquentは更にテーブルの主キーがidというカラム名であると想定しています。この規約をオーバーライドする場合は、protectedのprimaryKeyプロパティを定義してください。 

            $table->increments('id');
            // increments()で作ったカラムには裏でunsined(符号無し・整数のみ)属性が付与される auto_increment を有効にすると自動で primarykey付与  
            // 1テーブルに対しAUTO_INCREMENTカラム1つのみ。セカンダリーインデックス,またはユニークキーがあるカラムに対して有効。プライマリキー以外でも可 

            $table->string('product_name')->length(64);
            $table->integer('category_id')->unsigned()->index();
            // インデックスを作成することでテーブルとは別に検索用に最適化された状態で必要なデータだけがテーブルとは別に保持される。なのでデータ追加時の処理が重くなるというデメリットもある 

            $table->integer('price')->unsigned()->index();
            // 「UNSIGNED」属性を付加すると、0と正の整数のみを扱う。通常では負の数をカウントするために使用される格納域が
            //正の整数のみをカウントするために使用できるようになり、同じバイト数でより大きな正の整数値を記録できる。データ量が二倍に

            $table->string('description')->length(256);
            // string=varcharタイプ。可変長文字列のことを指す。varchar(m)という形で指定 mはバイト数。0~65535まで char型と異なり、末尾に空白は付かない。・末尾に空白が付いた文字列は
            そのまま格納されるメリット:指定された分だけメモリに格納されるため効率がいいデメリット:文字数が値ごとに違うため処理は遅く不定となる 

            $table->integer('sale_status_id')->unsigned()->index();;
            $table->integer('product_status_id')->unsigned()->index();;
            $table->timestamp('regist_data');
            $table->integer('user_id')->unsigned()->index();
            $table->char('delete_flag')->length(1);
            //固定長文字列のことを指す。・char(m)という形で指定する。mは文字数。0~255まで。・charcter(m)の略。・指定した文字数以下の文字を格納した時、文字列末尾
            //に必要な分の空白を付け加えて指定の長さの文字列として格納するメリット:文字数が固定のため、処理が早く一定。デメリット:文字数が必ず定まった分格納されるため、メモリを圧迫しやすい

            // 外部キー制約とは他のテーブルのデータに参照(依存)するようにカラムにつける制約のこと。参照されるのが親テーブル参照するのが子テーブルと呼ぶ。 
            // 主キーと外部キーはRDBにとって、それぞれのテーブルを関連付けるために使用するとても大切な機能。主キーと外部キーを使った制約で利用した場合、下記の制限が入る

            // 1.存在しない値を外部キーとして登録することはできない 2. 子テーブルの外部キーに値が登録されている親テーブルのレコードは削除できない 
            $table->foreign('sale_status_id')->references('id')->on('m_sales_statuses')->onDelete('cascade');
            $table->foreign('product_status_id')->references('id')->on('m_products_statuses')->onDelete('cascade');
            // CASCADE=親テーブルのレコードに対し、削除または更新を行うと、子テーブル内で同じ値を持つカラムのデータに対して削除または更新を行う 
            // RESTRICT=親テーブルのレコードに対し、削除または更新を行うとエラーとなる。設定を省略した場合 RESTRICT が設定される 
        });
    }

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

m_categoriesテーブル
スクリーンショット 2021-02-20 18.01.30.png
m_productsテーブル
スクリーンショット 2021-02-20 18.02.42.png

マイグレーション方法はこちらの記事を参考にして下さい。
Laravel入門 データベーステーブルとマイグレーション
Laravelにおけるマイグレーションのしくみ
Laravelのマイグレーション&スキーマビルダでDBのテーブルやカラムを作成する

リレーションとは?

  • 関連し合う(同じ様な属性のデータ)テーブル同士を繋げさせ、1つの情報(データ)を形成する。これをリレーション(結合)と言います。

商品情報(m_produts)テーブルと商品カテゴリー(m_categories)テーブルをリレーションさせた場合に何が出来るのか?
スクリーンショット 2021-02-19 0.30.58.png

この様なER図で表現されたテーブルのリレーションがあった場合(PK/FKに関しては割愛)

  • 結合に関するパターン(テーブル同士の関係)は、下記のような記述をModelに定義する事でリレーションを行う事が出来ます。
    • これによってproductオブジェクト、及びcategoryオブジェクトをEloquentで取得した際に、両オブジェクトからリレーション先の情報を取得できるようになります。
    • テーブル同士の関係は以下のように多数ありますが説明は割愛します。 Laravel 5.8 Eloquent:リレーション
      • 1対1/1対多/多対多
      • Has One Through/Has Many Through
      • 1対1(ポリモーフィック)/1対多(ポリモーフィック)/多対多(ポリモーフィック)
Product.php
(リレーションメソッドcategoryの抜粋)
class Product extends Model
{
    // Productモデルにcategoryモデルのリレーション設定
    public function category()
    {
        return $this->belongsTo('App\Category');
    }
}
Category.php
(リレーションメソッドproductsの抜粋)
class Category extends Model
{
    // categoryモデルにProductモデルのリレーション設定
    public function products()
    {
        return $this->hasMany('App\Product');
    }
}

リレーションを行いうことで何が出来るのでしょうか?
先程$productの中身を見た時、Modelオブジェクトが格納されていました。
その中の値をアロー演算子で取得できることも説明しましたね。
それと同様に、リレーションを行いうことでリレーション先の値も取得できるよになります。
Eloquentproductオブジェクトを取得するには下記の記述が必要でした。 

$product = Product::find($sessionProductsId);

$sessionProductsIdの中身は複数の配列とします。中身は上記に記載してありますので忘れた方は再確認して下さい。
戻り値がcollectionオブジェクトになりますのでアロー演算子ではプロパティやリレーション先の情報の取得はできません。foreachで回すか、特定の値をヘルパー関数で取得する必要があります。
下記ではforeachでの例を記載します。

$sessionProductsId内のIDで商品情報取得、foreach内で各Productのcategoryを取得
$product = Product::find($sessionProductsId);

foreach ($product as $pro) {
    echo $pro->category->category_name;
}

出力結果:菓子類 果物類 生鮮食品 レトルト類 

①ループ処理によって各ProductModelオブジェクトにアクセス出来るようにします($pro)
②各Modelオブジェクトのリレーション先にアクセスする($pro->category)
categoryオブジェクトにアクセス出来ます。


リレーションオブジェクト取得
 ➡ $pro->category$pro->category()とするとリレーションオブジェクトを取得します。
 ➡ Illuminate\Database\Eloquent\Relations\BelongsToBelongsToオブジェクトです。
 ➡ category()とすることでリレーションメソッドの使用となります。中身は割愛します。
 ➡ このあたりの詳細は後述の 解説-storeメソッド- で触れます。

③リレーション先の値を取得する(->category_name)

これでリレーション先の値の取得ができました!わりかし簡単かと思います。
この他、ここではN + 1問題の発生が問題となります。
この問題に関して該当箇所で詳しく説明したいと思います。

7-3 ❏解説-indexメソッド-❏

大まかな流れのうち以下の③〜⑥について、今から解説致します
①商品詳細画面から商品IDと注文個数をrequestオブジェクトへ保存
②controllerで上記データをrequestオブジェクトから取り出しセッションへ保存
③controllerで上記で保存したセッション情報を取得!
④セッションデータを用いてDBから任意の値を引き出す
⑤取得したデータから必要な値を取り出す
⑥取得した情報をviewに渡す
⑦カート内商品を個別に削除できるようにする
⑧カート内商品をテーブルに保存

ー処理の流れー
1. セッションからユーザIDを取得しDB検索をかける
2. removeメソッドでの配列削除時の配列連番抜け対策
3. セッションそのものではなく、セッションを取得し格納した変数の存在確認
4. true ➡ 必要な要素を抽出(データの加工)し、それを元にDBからデータ取得
 ・上記、3. の変数に格納された配列の中身をループで加工(リファレンス使用)
 ・カートリスト画面へ(viewに情報を渡す)
5. false ➡ 「カート内商品が有りません」画面に遷移

ProductController.php
(抜粋)
public function index(Request $request)
    {
        //渡されたセッション情報をkey(名前)を用いそれぞれ取得、変数に代入
        $sessionUser = User::find($request->session()->get('users_id'));

        //removeメソッドでの配列削除時の配列連番抜け対策
        if ($request->session()->has('cartData')) {
            $cartData = array_values($request->session()->get('cartData'));
        }
  • セッションから取得した各種情報を元にDBから情報を取得し変数に格納します。

①セッションに保存されているユーザIDで現在ログインしているユーザの情報を取得
②ここが他のメソッドと連動する部分になります
➡ カート内商品を個別に削除するためのメソッドとしてremoveメソッドを作成しています。
詳細は後で後述しますがこのメソッドはセッション内に保存されている商品情報(配列)を削除します。
すると配列のindex番号が歯抜けになる現象が起きます。
具体的には下記の左にある様な配列が有って index番号2を削除したとしましょう。
すると右のような配列構成となります。

array:4 [                                 array:3 [               
  0 => array:2 []                           0 => array:2 []
  1 => array:2 []                           1 => array:2 []
  2 => array:2 []                           3 => array:2 [
  3 => array:2 [                              "session_products_id" => "6"
    "session_products_id" => "6"               "session_quantity" => "1"
    "session_quantity" => "1"                ]
  ]                                        ]
]

2番の配列は削除され、index番号は歯抜け状態になります。
このままでセッションデータを取得、viewに情報を渡しforeachで表示しようとすると下記のエラーが発生します。
空の配列に対して要素の取得を行おうとする、もしくは
配列で存在しない位置のインデックスを参照したときに出るエラーとなります。
スクリーンショット 2021-02-14 15.06.03.png

このエラーを回避するにはこのindex番号の抜けを無くして連番になるようにすれば良いわけです。
この処理をしてくれるのがarray_values();関数になります。

array_values() は、配列 array から全ての値を取り出し、数値添字をつけた配列を返します。

ProductController.php
//removeメソッドでの配列削除時の配列連番抜け対策
if ($request->session()->has('cartData')) {
    $cartData = array_values($request->session()->get('cartData'));
}

このコードの意味は
「セッションの中にcartDataという値が有った場合、その値を取得しindex番号の歯抜けを連番になるように処理した後に、変数に格納する」
になります。
そしてそうではなかった場合の処理は以下になります。

ProductController.php
if (!empty($cartData)) {
   $sessionProductsId = array_column($cartData, 'session_products_id');
   $product = Product::with('category')->find($sessionProductsId);

emptyは変数の中身が空かどうか判定します。空であればtrue判定となります。
!emptyは判定が逆になるようにしています。空であればfalse判定となります。
$cartDataに情報が格納されていればtrueになるように意図しています。
つまり$cartDataに商品情報が格納されていれば以下の処理をして下さい、という記述になります。
参考:PHP 型の比較表

その後の処理は以下になります。
①商品ID「のみ」を変数から抽出
②①を元にDBから商品情報を取得、その際リレーション先の情報も同時に取得

array_column();を用い配列から、必要な値だけを抽出した配列に変換します。
array_column($cartData, 'session_products_id');
第一引数に対象となる「配列」を、第二引数に抽出したい値のkeyを指定します。
ここではセッションに保存された商品IDの値のみで配列を作成しています。デバックで確認してみましょう!

ProductController.php
if (!empty($cartData)) {
   $sessionProductsId = array_column($cartData, 'session_products_id');
   dd($sessionProductsId);


$cartDaraのデバック結果)                    ($sessionProductsIdのデバック結果)
array:4 [                                array:4 [
  0 => array:2 [                           0 => "2"
    "session_products_id" => "2"            1 => "4"
    "session_quantity" => "2"               2 => "5"
  ]                                         3 => "6"
  1 => array:2 [                         ]
    "session_products_id" => "4"          
    "session_quantity" => "3"             商品IDのみの配列が作成されました
  ]                                       indexに名前を付けて連想配列にしたい
  2 => array:2 [                         場合は第三引数を指定して下さい
    "session_products_id" => "5"          詳しくは下記の参考を御覧ください
    "session_quantity" => 2               今回は特に必要無いので配列のままです
  ]
  3 => array:2 [
    "session_products_id" => "6"
    "session_quantity" => "1"
  ]
]

第三引数をしていすると、新しい配列のキー名を指定できます:多次元配列から特定カラムの配列を作る


引数・関数と戻り値、欲しい値の求め方イメージとして料理で捉えると解りやすいかもしれません。料理の材料が引数。関数は全自動調理器です。結果としての戻り値は出来上がった料理になります。関数に引数を渡すと戻り値が返るというのは、食材を全自動調理器にいれると料理ができあがったみたいなイメージで良いと思います。①こういう料理を作りたい(結果)②何で作れる(関数)③必要な材料は?(引数)の順で考えると思考もしやすいし検索もしやすいかもしれません。材料がなければ作れば良いのです。

②①を元にDBから商品情報を取得、その際リレーション先の情報も同時に取得
$product = Product::with('category')->find($sessionProductsId);

  • このコードはProduct.phpというModelを用いて、DBから$sessionProductsId内に存在する商品IDに該当する商品情報を取得(find-探してくる-)
  • その際にProduct.phpというModelを介してCategory.phpというModelからも情報を同時に取得する。with('category')->の部分です。これを「リレーションの取得」といいます。

 
では次にこのindexメソッドの中で一番重要な部分を解説したいと思います。
viewに商品情報を表示させるための処理です。

ProductController.php
            foreach ($cartData as $index => &$data) {
                //二次元目の配列を指定している$dataに'product〜'key生成 Modelオブジェクト内の各カラムを代入
                //&で参照渡し 仮引数($data)の変更で実引数($cartData)を更新する
                $data['product_name'] = $product[$index]->product_name;
                $data['category_name'] = $product[$index]['category']->category_name;
                $data['price'] = $product[$index]->price;
                //商品小計の配列作成し、配列の追加
                $data['itemPrice'] = $data['price'] * $data['session_quantity'];
            }
            unset($data);

            return view('products.cartlist', compact('sessionUser', 'cartData', 'totalPrice'));

        } else {

            return view('products.no_cart_list',  ['user' => Auth::user()]);
        }

foreach文で多次元連想配列の内容を追加・変更する(リファレンス渡し)

ここでしていることは
foreach文で多次元連想配列の内容を追加・変更するです。
解説していきます。

foreach ($cartData as $index => &$data) 

この部分では&によるリファレンス渡しというものを行っています。
コメントアウト部分では「参照渡し」と記載していますが、正確にはPHPの呼び方でリファレンス渡しと呼びます。
参考:リファレンス渡し
まずはこの処理の前後で配列がどう変化したのかを見てもらいましょう

変化前
array:4 [
  0 => array:2 [
    "session_products_id" => "1"
    "session_quantity" => 3
  ]
  1 => array:2 [
    "session_products_id" => "2"
    "session_quantity" => 2
  ]
  2 => array:2 [
    "session_products_id" => "3"
    "session_quantity" => 5
  ]
  3 => array:2 [
    "session_products_id" => "5"
    "session_quantity" => "1"
  ]
]
変化後
array:4 [
  0 => array:6 [
    "session_products_id" => "1"
    "session_quantity" => 3
    "product_name" => "雪の恋人"
    "category_name" => "菓子類"
    "price" => 1980
    "itemPrice" => 5940
  ]
  1 => array:6 [
    "session_products_id" => "2"
    "session_quantity" => 2
    "product_name" => "ハブマンゴー"
    "category_name" => "果物類"
    "price" => 8800
    "itemPrice" => 17600
  ]
  2 => array:6 [
    "session_products_id" => "3"
    "session_quantity" => 5
    "product_name" => "一本マグロ"
    "category_name" => "生鮮食品"
    "price" => 1800000
    "itemPrice" => 9000000
  ]
  3 => & array:6 [
    "session_products_id" => "5"
    "session_quantity" => "1"
    "product_name" => "十勝のバターハヤシライス"
    "category_name" => "レトルト類"
    "price" => 5800
    "itemPrice" => 5800
  ]
]

それぞれの連想配列の要素にproduct_name category_name price itemPrice
keyとした値が代入されていますね。
もともと無かった連想配列を作成し、keyとした値を代入しました。
これがリファレンス渡しになります。

リファレンス渡しを簡単に言うと、変数や配列を上書きするための仕組みになります。
変数と配列は基本的に値渡し、オブジェクトは初めからリファレンス型なので&がなくてもリファレンスされます。

オブジェクトは初めからリファレンス型なので&がなくてもリファレンスされます。

ここは「ちがう!」という意見も拝読した時がありました。正直な所、今の自分では判別できません。
今の所は関係ないので放置してますが、いつ何処でどんな形でこの問題が自分も前に現れるか楽しみです。
参考:PHPのオブジェクトは参照渡しではないし、変数リファレンスは全然別物

「変数の実体はメモリ上に格納された領域」という話があります。
難しい言い方ですが、簡単に言えばコンピュータの記録領域に変数の内容を保存する領域があるという話です。
領域には番号がありメモリ番地といいます。住所みたいなものですね。
・値渡し (call by value) ➡ 変数の値をコピーする渡し方
・参照渡し (call by reference) ➡ 変数が格納されているメモリ領域(番地)を共有する

メモリ上に格納されている変数を変数にコピーするのが値渡し。オリジナルの変数の内容は変更できません
変数の情報が格納されている場所を共有するのがリファレンス渡し。ここにはオリジナルの変数の値が格納されているのでここの内容を変更すればオリジナルの値を変更できます。
イメージですが値渡しが新築で、リファレンス渡しがリフォームです。
新築は他の一軒家の間取りを真似して作られいます。住所も変わるので保存しているメモリ領域(番地)も変わります。
リフォームは場所は変わらず中身を変える感じです。住所も変わらないのでメモリ番地も変わりません。

参考:
Rubyist Magazine -値渡しと参照渡しの違いを理解する-
【PHP】全ボクが実務で泣いた「値渡し」と「参照渡し」を完!全!理!解!

値渡しとリファレンス渡しの違い
(値渡し)                          (リファレンス渡し)
$a = 1;                          $a = 1;
$b = $a;   //変数aを代入           $b = &$a;  //変数aを&をつけて代入
$a = 2;    //変数aの値を変更        $a = 2;    //変数aの値を変更
echo $b;                         echo $b;

出力結果                          出力結果
1                                2


$a = [1, 2, 3];                  $a = [1, 2, 3];
$b = $a;     //配列を代入          $b = &$a;    //配列を代入
$a[1] = 22;  //配列aを変更         $a[1] = 22;  //配列aを変更
dd($b);                          dd($b);

出力結果                          出力結果
array:3 [                       array:3 [
  0 => 1                           0 => 1
  1 => 2 //$b[1]の値は変わらない       1 => 22  //$b[1]の値が変わる
  2 => 3                           2 => 3
 ]                                ]

最後に忘れていけないのはこの部分です!

unset($data);

foreachが終了しても$dataには配列の最後の要素に対する参照が設定されたままのため(3 => & array:6の部分)、その後のプログラムで間違って要素の値を書き換えないように変数の割り当てを明示的に解除する必要があります。
別の用途に$dataを使おうとして、何も考えずに何かを代入すると$arrayの最終要素が上書きされてしまうわけです。
これをしないと、その後のプログラムで値を書き換えてしまい思わぬバグを生む可能性があります。
unsetすると&が外れます。

そしてその後のviewへ値を渡す部分とif文の条件以外だった場合の処理部分

return view('products.cartlist', compact('sessionUser', 'cartData', 'totalPrice'));

        } else {

            return view('products.no_cart_list',  ['user' => Auth::user()]);
        }

まずif文の条件以外だった場合の処理部分の説明です。
もともとの条件として

if ($request->session()->has('cartData'))

がありました。この意味は「カート内に商品があればtrue」ということですので
条件外というのは「カート内に商品がない場合false」とする条件になります。
その場合表示される画面はこちらです。

スクリーンショット 2021-02-21 16.32.28.png

ここで必要な情報はログインユーザ情報なので
return view('products.no_cart_list', ['user' => Auth::user()]);
の部分で画面遷移先を指定し、ログインユーザ情報をviewに渡しています。
ここではセッションのデータからユーザ情報は取得できません。
セッションにユーザ情報を保存するためには商品詳細画面からユーザIDをフォームから送信する必要がありました(前編参照)。
今回、商品がカート内にないという状態ですから、商品IDもユーザIDもrequest オブジェクトの中に存在しないのでそもそもセッションに保存し取得することも出来ません。

viewには「セッション内のユーザIDで取得したユーザ情報」、「カート内の商品情報」、そして「商品の合計金額(小計)」を渡します。
これでBladeファイルで変数として各値を使用することが出来ます。
ここでの渡し方ですが、compactメソッドを使用しています。基本的な配列で渡す方法と比較してみましょう。

Laravelでviewに値を渡すやり方

・配列で渡す

return view('products.cartlist', [
        'sessionUser'=> sessionUser, 
        'cartData'=> cartData, 
        'totalPrice'=> totalPrice, 
    ]);

・compact関数で渡す

return view('products.cartlist', compact('sessionUser', 'cartData', 'totalPrice'));

compact関数の方がシンプルかつ短いですね!
すでに定義した変数と同じ名前の文字列をドット区切りで渡す。 viewに渡す値は、変数名と同じ名前になることが殆どになるかと思うので
compact関数でほとんど良いような気がします。

わざわざ配列で渡したい場合ってcontroller内での変数名とviewでの変数名を別々にした場合に限る気がするのですがどうなんでしょうか?
他の使用方法で有用なものがあれば教えて頂きたいです。

N+1問題の解消

※解説の前の補足事項 〜ModelとEloquent、DB設計、「リレーションの取得」について〜
の最後で述べたN+1問題についてここで詳しく解説したいと思います。

N+1問題の概要
各言語のFWで使用されるORMでは、あるモデルが参照している別のモデル(リレーション関係にあるテーブル)を参照するとその時点でSQLが発行されてしまいます。
これは単純にリレーションしたとしても値にアクセスした時点でデータを取得しに行く仕様からきているものです。
ループのなかでSQLを都度発行するようなコードだとクエリが膨大になり重くなることで、気が付かないうちにパフォーマンスが低下するという問題です。
SQLを実行するという処理が一番システムに負荷をかるのですが、アクセス集中などでサービスが停止したりするのもSQLの実行回数が増え捌ききれなくなりDBサーバが落ちる場合が多いそうです。
よってSQLの発行回数をいかに少なくするかは非常に重要な問題です。
※FWならではの問題になります。

では※解説の前の補足事項 〜ModelとEloquent、DB設計、「リレーションの取得」について〜
で使用したコードをもとに解説してきたいおと思います。

$sessionProductsId内のIDで商品情報取得、foreach内で各Productのcategoryを取得
$product = Product::find($sessionProductsId);

foreach ($product as $pro) {
    echo $pro->category->category_name;
}

この様なコードでSQLが何回発行されているのか?
まずはそれを確認しましょう。
下記のように\DB::enableQueryLog();を上部に
dd(\DB::getQueryLog());を下部に記述しSQLの発行ログをデバッグしてみましょう。

ちなみに「laravel sql 確認」などで検索すると他の方法も確認できますが->toSql();という方法は

toSqlメソッドが定義されているクラスは、Builderクラスになります。
以下のエラーが出ます。
Method Illuminate\Database\Eloquent\Collection::toSql does not exist.

とのことでBuilderクラス以外では使えません。参考:allメソッドを使うとtoSqlメソッドが使えない
今回取得されるのはcollectionオブジェクトなので異なる方法でクエリログを確認しています。

\DB::enableQueryLog();            
$product = Product::find($sessionProductsId);

foreach ($product as $pro) {
    echo $pro->category->category_name;
}
dd(\DB::getQueryLog());

出力結果:菓子類 果物類 生鮮食品 レトルト類


[クエリログ]
array:5 [
  0 => array:3 [
    "query" => "select * from `m_products` where `m_products`.`id` in (?, ?, ?, ?)"
    "bindings" => array:4 [
      0 => "1"
      1 => "2"
      2 => "3"
      3 => "5"
    ]
    "time" => 0.47
  ]
  1 => array:3 [
    "query" => "select * from `m_categories` where `m_categories`.`id` = ? limit 1"
    "bindings" => array:1 [
      0 => 1
    ]
    "time" => 0.82
  ]
  2 => array:3 [
    "query" => "select * from `m_categories` where `m_categories`.`id` = ? limit 1"
    "bindings" => array:1 [
      0 => 2
    ]
    "time" => 0.22
  ]
  3 => array:3 [
    "query" => "select * from `m_categories` where `m_categories`.`id` = ? limit 1"
    "bindings" => array:1 [
      0 => 3
    ]
    "time" => 0.22
  ]
  4 => array:3 [
    "query" => "select * from ```m_categories` where `m_categories`.`id` = ? limit 1"
    "bindings" => array:1 [
      0 => 5
    ]
    "time" => 0.17
  ]
]
]

この様に、N+1問題のN(m_categories)+1(m_products)にそれぞれ対応します。リレーション先(m_categories)を参照する度にSQLを発行しDBにアクセスしています。
これを解消する方法はちゃんと用意されています。

EagerLord

Eloquentでデータを取得後にループで回して値にアクセスしている部分は、その数だけクエリが発行されてることになります。
この、N+1問題を防ぐ為に関連するデータを一括取得を行うのがEagerロードになります。
Eagerロードを行うには、with()メソッドを使います。

全Productを取得し、m_categoryテーブルをEagerLordしておく
$product = Product::with('category')->find($sessionProductsId);

with(リレーションメソッド名)の部分がEagerLoadです。
こうすることで、次の 2 クエリしか発行されないようになります。
N+1問題は
①親モデル(Product.php)のオブジェクト生成時にはリレーション先の情報がなく
②アロー演算子で子モデル(Category.php)を繋げた時(->category)に初めてリレーションデータを取得
③よってリレーションプロパティにアクセスする度にSQLが発行される

以下の様にすれば解決します
「事前にリレーション先の情報を含めた親モデル(Product.php)のオブジェクト生成を行えばいい」

それがEagerLordwith(リレーションメソッド名)です!やることは簡単ですね。


余談:N+1問題におけるORMの重たさについて
以下はこちらのサイトの引用です!N+1問題におけるORMの重たさについて
直接SQLを実行した場合、ORMを利用した場合と比べて、一桁高速です。N+1回のクエリを実行していても、クエリ2回だけのORMよりも高速です。
つまり、この程度の単純なクエリの場合、ORMの処理時間のほとんどはRDBの呼び出しではなく、SQLの組み立てやモデルオブジェクトの構築に費やされているのです。
このような仕組みを理解していれば、この処理をチューニングする場合でも機械的にN+1問題対策として prefetch_related を適用するのではなく、できるだけORMレイヤーの機能を利用せずに必要な値だけを直接取得する、などの手段を思いつくのではないでしょうか。

array:2 [
  0 => array:3 [
    "query" => "select * from `m_products` where `m_products`.`id` in (?, ?, ?, ?)"
    "bindings" => array:4 [
      0 => "1"
      1 => "2"
      2 => "3"
      3 => "5"
    ]
    "time" => 0.48
  ]
  1 => array:3 [
    "query" => "select * from `m_categories` where `m_categories`.`id` in (1, 2, 3, 5)"
    "bindings" => []
    "time" => 0.48
  ]
]

EagerLordの種類

複数のリレーション先情報を取得したい場合→引数を配列に

$product = Product::with(['category', 'user'])->find($sessionProductsId);

リレーション先の、その先のリレーション情報取得(ネスト)→ドット記法

$product = Product::with('category.user')->find($sessionProductsId);

7-4 ❏解説-removeメソッド-❏

ProductController.php
    /* 
    |--------------------------------------------------------------------------
    | カート内商品の削除
    |--------------------------------------------------------------------------
    */
    public function remove(Request $request)
    {
        //session情報の取得(product_idと個数の2次元配列)
        $sessionCartData = $request->session()->get('cartData');

        //削除ボタンから受け取ったproduct_idと個数を2次元配列に
        $removeCartItem = [
            ['session_products_id' => $request->product_id, 
            'session_quantity' => $request->product_quantity]
        ];

        //sessionデータと削除対象データを比較、重複部分を削除し残りの配列を抽出
        $removeCompletedCartData = array_udiff($sessionCartData, $removeCartItem, function ($sessionCartData, $removeCartItem) {
            $result1 = $sessionCartData['session_products_id'] - $removeCartItem['session_products_id'];
            $result2 = $sessionCartData['session_quantity'] - $removeCartItem['session_quantity'];
            return $result1 + $result2;
        });

        //上記の抽出情報でcartDataを上書き処理
        $request->session()->put('cartData', $removeCompletedCartData);
        //上書き後のsession再取得
        $cartData = $request->session()->get('cartData');

        //session情報があればtrue
        if ($request->session()->has('cartData')) {
            return redirect()->route('cartlist.index');
         }

        return view('products.no_cart_list', ['user' => Auth::user()]);
    }

Feb-21-2021 17-36-24.gif

大まかな流れのうち以下の⑦について、今から解説致します
①商品詳細画面から商品IDと注文個数をrequestオブジェクトへ保存
②controllerで上記データをrequestオブジェクトから取り出しセッションへ保存
③controllerで上記で保存したセッション情報を取得
④セッションデータを用いてDBから任意の値を引き出す
⑤取得したデータから必要な値を取り出す
⑥取得した情報をviewに渡す
⑦カート内商品を個別に削除できるようにする
⑧カート内商品をテーブルに保存

ーremoveメソッドの処理内容ー
1.セッション情報の取得
2.削除商品のデータを2次元連想配列(1次元ではなく)として作成
3.array_udiff関数で多次元連想配列の加工
4.加工した多次元連想配列でセッションデータを上書き
5.商品削除によってカート内が空になっていないか判定し処理

ProductController.php
 public function remove(Request $request)
    {
        //session情報の取得(product_idと個数の2次元配列)
       $sessionCartData = $request->session()->get('cartData');

        //削除ボタンから受け取ったproduct_idと個数を2次元配列に
       $removeCartItem = [
            ['session_products_id' => $request->product_id, 
            'session_quantity' => $request->product_quantity]
        ];

①ここはこれまでと同じですね。
②ここは少し特徴があります。配列作成時にわざわざ2次元の連想配列にしています。
これから説明する「3.array_udiff関数で多次元連想配列の加工」に必要な処理だからです。

この値はviewにあるformの削除ボタンから商品IDと個数をrequestオブジェクトに格納した値です。
前編参照)。

cartlist.blade.php
(抜粋)
{!! Form::open(['route' => ['itemRemove', 'method' => 'post', $data['session_products_id']]]) !!}
    {{ Form::submit('削除', ['name' => 'delete_products_id', 'class' => 'btn btn-danger']) }}
    {{ Form::hidden('product_id', $data['session_products_id']) }}
    {{ Form::hidden('product_quantity', $data['session_quantity']) }}
{!! Form::close() !!}
web.php
Route::post('productInfo/addCart/cartListRemove', 'ProductController@remove')->name('itemRemove');

array_udiff関数で多次元連想配列の加工

参考:array_udiff
参考:二つの多次元配列を比較して、片方にだけ含まれるものを抽出する方法
array_udiff関数は、自分で作成したfunctionを用いて、引数に当てられた配列に関しての差分を計算してくれます。

ProductController.php
//sessionデータと削除対象データを比較、重複部分を削除し残りの配列を抽出
$removeCompletedCartData = array_udiff($sessionCartData, $removeCartItem, function ($sessionCartData, $removeCartItem) {
    $result1 = $sessionCartData['session_products_id'] - $removeCartItem['session_products_id'];
    $result2 = $sessionCartData['session_quantity'] - $removeCartItem['session_quantity'];
    return $result1 + $result2;
});

ここでの引数は

array_udiff($sessionCartData, $removeCartItem

functionの内容は

$result1 = $sessionCartData['session_products_id'] - $removeCartItem['session_products_id'];
$result2 = $sessionCartData['session_quantity'] - $removeCartItem['session_quantity'];
    return $result1 + $result2;


実はかなり苦労しました…。


着想もそうですがそれを実現する方法を想像することも難しかったです。

ここで大事だった着想・発想
1.配列の差分を計算する
2.必要な関数を見極める
3.計算結果をどう活かすのか

ここに着手した時点で前編で解説したドット記法で「深い階層にアクセス」する方法を知らなかったので
直接セッションデータを編集する方法を知らなかったのです…。
ドット記法で行えば
$request->session()->forget('cartData.' . $index);
のような感じで直接、削除したいデータにアクセスして削除できたのでしょう…。
あえて苦悩の証として残してます(´;ω;`)
愛着も有るんですよね(笑)

ここでしていることはコメントの通りなのですが
sessionデータと削除対象データを比較、重複部分を削除し残りの配列を抽出
です。

ここでは
・元々セッション内に保存されている商品情報を収めた2次元連想配列

viewでユーザが削除ボタンでリクエストした商品情報、をもとに作成した2次元連想配列

値の「重複部分」を引き算しています。
これは$sessionCartData$removeCartItem双方に格納されたsession_products_idsession_quantityの値が、お互いに両方とも等しい場合を意味しています。
個数だけを見て重複と判断されてしまっては「指定した商品を消したい」という要求が実現しません。

id=1id=2の商品の個数がそれぞれ1だった場合、削除されるべきはどちらになるのでしょうか?
個数の重複だけを基準にしてしまってはどちらが消えるのか解らなくなってしまいます。
なので個数と商品IDの双方が重複している配列を削除出来るように指定しなくてはいけません。
それがこのarray_udiff関数内で指定されている内容になります。

こういった商品IDと個数2つの値が対応し合う形の配列構造だからこそ実装できる処理となります。
この配列構造は商品をカート内に入れるためのメソッドであるaddCartメソッドで定義されています。

また、商品IDと商品個数は紐付いた情報にしたいので配列として変数に格納します。
そうでないと後々の処理の中で商品と個数を連動させた処理がものすごく面倒になります。
参考:本記事の前編

また実際の引き算では更に多次元連想配列を細分化して計算しています。
計算式では1次元の連想配列に分解しています。

$result1 = $sessionCartData['session_products_id'] - $removeCartItem['session_products_id'];
$result2 = $sessionCartData['session_quantity'] - $removeCartItem['session_quantity'];

この様に個々の値を別々に計算しています。
それでは実際の計算結果を見てみましょう。まずは計算のもとになる各配列の中身です。
ここで多次元構造を見て下さい。

②ここは少し特徴があります。配列作成時にわざわざ2次元の連想配列にしています。

と先ほどお話ししました。その理由は以下になります。
①計算の際のデータの取り扱い方を同じに出来るので、同じ多次元構造にしたかった
array_udiff関数は「配列同士の計算にしか使えない」
➡ もし$removeCartItemを作成時に2次元でなく1次元連想配列にした場合、計算時での配列細分化の際に配列の次元が合わなくなる。

dd($sessionCartData);                      dd($removeCartItem);

array:4 [                                 array:1 [
  0 => array:2 [                            0 => array:2 [
    "session_products_id" => "1"               "session_products_id" => "1"
    "session_quantity" => "1"                  "session_quantity" => "1"
  ]                                          ]
  1 => array:2 [                          ]
    "session_products_id" => "2"
    "session_quantity" => "1"
  ]
  2 => array:2 [
    "session_products_id" => "3"
    "session_quantity" => "1"
  ]
  3 => array:2 [
    "session_products_id" => "4"
    "session_quantity" => "1"
  ]
]

ここでの計算は以下の連想配列が対象になります。2つの2次元連想配列の重複部分ですね。
計算式では1次元の連想配列に細分化して計算しています。
$removeCartItemの値はユーザが削除したい商品情報です。

ここで

計算式では1次元の連想配列に細分化して計算しています。

という点をもう少し詳しく解説したいと思います。
細分化せずに変数内に格納された2次元連想配列同士で引き算するとどうなるでしょうか?
$result1 = $sessionCartData - $removeCartItem;
このような形ですね。すると

Unsupported operand types = 「サポートされていないオペランド型」

というエラーが吐かれてしまいました。
オペランド (operand ) とは「被演算子」のことです。

プログラムの式は
$a + $b;
のように書きますが、これを言い換えるとこうなります。
オペランド 演算子 オペランド;
ここから Unsupported operand types というエラーは、式のオペランド同士の組み合わせが良くない、ということがわかります。
参考:[PHP] Uncaught Error: Unsupported operand types

phpで型が違う変数同士の加算で起こるエラーの様ですが、どうも今回違うみたいでした。
左右のデバック内容が全く同じ「変数内に格納された2次元連想配列同士」でも発生したからです…。
他の可能性も考えました。

phpは「動的型付け」といわれる言語で、変数の型が勝手にころころ変わってしまう言語です。
動的型付けの対義語として、静的型付けがあり、例を挙げるとjavaなどです。
以下のコードは、javaでは(基本的に)エラーになり、phpでは当たり前に実現できるものです。

$hoge = 'a/b/c';       //$hogeはstring
$hoge = explode('/', $hoge);  //$hogeはarray ['a','b','c']

javaでも無理やり同じことをするには、Object型として変数を定義の上、そのとき何型とみなすかをその都度キャストにて明示する必要があります。
これは、「静的型付け言語では、変数の型が勝手に変わらないから、プログラマがいちいち手動で変えなければいけない」わけです。

public class test
{
public static void main(String...args)
{
Object hoge = "a/b/c";
hoge = ((String)hoge).split("/");
for(String elem : (String[])hoge)
System.out.println(elem);
}
}

両者を比較してみてください。動的型付けでは、プログラマが型を意識しなくてもコードが書きやすい反面、場合によっては型が勝手に変わって矛盾を生むことによるエラーが起きやすいです。
ご質問における「不思議なこと」の正体は、「型が勝手に変わる」という動的型付けの性質そのものです。
一方静的型付けでは、プログラマが型を意識しなければいけませんが、型が変わることがないので、ご質問のような「不思議なこと」が起こりません。
参考:PHP Fatal error: Unsupported operand types

うーん、今回これでもなさそうだなぁ…と頭をひねっているとリファレンスにこういった記述を見つけました。

データの比較にコールバック関数を用い、配列の差を計算します。 この関数は array_diff() と異なり、 データの比較に内部関数を利用します。

データの比較にコールバック関数を用い、配列の差を計算します。
配列の差を計算します。
配列の差を
「配列」…

「・・・・・・!!!!これや…!!これやったんや答えは!もろたで工藤!」

そうなんです。「配列」なんです、扱えるのは。ここには多次元配列が入っています。エラー出るわけです。
なので細分化して「配列」にしました。以下の形ですね。
$sessionCartData['session_products_id'] - $removeCartItem['session_products_id'];

それでは計算結果を見ていきましょう。以下が計算対象になる連想配列です。

dd($sessionCartData);                      dd($removeCartItem);


  0 => array:2 [                            0 => array:2 [
    "session_products_id" => "1"               "session_products_id" => "1"
    "session_quantity" => "1"                  "session_quantity" => "1"
  ]                                          ]

計算結果です。

dd($removeCompletedCartData);

array:3 [
  1 => array:2 [
    "session_products_id" => "2"
    "session_quantity" => "1"
  ]
  2 => array:2 [
    "session_products_id" => "3"
    "session_quantity" => "1"
  ]
  3 => array:2 [
    "session_products_id" => "4"
    "session_quantity" => "1"
  ]
]

$sessionCartDataindex番号が無くなっています。
これにて計算終了ですがこのままではあきません。

❏解説-indexメソッド-❏での

  1. removeメソッドでの配列削除時の配列連番抜け対策

で行っている処理が必要になるのです。必要があれば遡って確認してくださいませ。


書いてて気づきましたがこの書き方良くないですね
書いてて気づきましたがその処理、ここで上書きする前に実行すべきですね!
単一のメソッド内で処理が完結するし、コードの流れも解りやすいですね!
オブジェクト指向にしろ構造化プログラミングにしろ、この手のコンセプトの目指すところは
「いかにコード間の依存関係を減らして、メンテナンスしやすいプログラムを書くか」
なんだろうなと理解してるのですが、そういった観点からみると「コード間の依存関係」をつくってしまったなと感じますね。

ではそれ以降の処理の部分ですがここはindexメソッドで解説したのとほとんど同じになりますが大切な部分があります。
上記の計算結果である$removeCompletedCartDataでセッション内の
cartaDataを上書き(putメソッド)せねばなりません。その後再取得ですね。


上書きしないとどうなる?(余談)
削除直後は消えるのですが再読み込みすると復活します。
以前Twitterに上げた動画のリンク貼ります。興味あれば見てみて下さい。
ツイートした日は誕生日なのですがめっちゃ苦しんでました。
人生で一番スッキリしない誕生日でした(笑)削除しても復活する商品達(Twitter)

        //上記の抽出情報でcartDataを上書き処理
        $request->session()->put('cartData', $removeCompletedCartData);
        //上書き後のsession再取得
        $cartData = $request->session()->get('cartData');

        //session情報があればtrue
        if ($request->session()->has('cartData')) {
            return redirect()->route('cartlist.index');
         }

        return view('products.no_cart_list', ['user' => Auth::user()]);
    }

再取得して、セッションデータがあればindexメソッドへ処理が流れます。
商品がすべてなくなれば、以下の画面を表示します。
スクリーンショット 2021-02-28 17.16.25.png

7-5 ❏解説-storeメソッド-❏

ProductController.php
    /*
    |--------------------------------------------------------------------------
    | カート内商品注文確定(DB登録)
    |--------------------------------------------------------------------------
    */
    public function store(Request $request)
    {
        $cartData = $request->session()->get('cartData');
        $$carbonNow = Carbon::now();

        //オブジェクト生成
        $order = new \App\Order;
        //指定値をオブジェクト代入
        $order->user_id = Auth::user()->id;
        $order->order_date = $now;
        $order->order_number = rand();
        //認証済みのユーザーのみDBへ保存
        Auth::user()->orders()->save($order);

        //Qrderテーブルの カラム「order_number」が「$order->order_number」の最新のレコードを一つ取得
        $savedOrder = Order::where('order_number', $order->order_number)->latest()->first();
        //上記Collectionから id の値だけを取得した配列に変換
        $savedOrderId = $savedOrder->pluck('id')->toArray();

        //注文詳細情報保存を注文数分繰り返す 1回のリクエストを複数カラムに分けDB登録
        foreach ($cartData as $data) {
            //注文詳細情報に関わるオブジェクト生成
            $orderDetail = new \App\OrderDetail;
            $orderDetail->product_id = $data['session_products_id'];
            $orderDetail->order_id = $savedOrderId[0];
            $orderDetail->shipment_status_id = 3;
            $orderDetail->order_quantity = $data['session_quantity'];
            $orderDetail->shipment_date = $now;
            Auth::user()->orderDetails()->save($orderDetail);
        }

        //session削除
        $request->session()->forget('cartData');
        return view('products/purchase_completed', compact('order'));
    }

大まかな流れのうち以下の⑧について、今から解説致します
①商品詳細画面から商品IDと注文個数をrequestオブジェクトへ保存
②controllerで上記データをrequestオブジェクトから取り出しセッションへ保存
③controllerで上記で保存したセッション情報を取得
④セッションデータを用いてDBから任意の値を引き出す
⑤取得したデータから必要な値を取り出す
⑥取得した情報をviewに渡す
⑦カート内商品を個別に削除できるようにする
⑧カート内商品をテーブルに保存

ーstoreメソッドの処理内容ー
1. t_ordersテーブル(注文情報)への保存
1-1.セッション情報の取得
1-2.保存したいデータに対応するモデルのオブジェクトを作成する
1-3.保存したい値をオブジェクトに格納する
1-4.saveメソッドでDBの各テーブルに値を保存
カラム
・ id(主キー)
・ user_id
・ order_date
・ order_number

2.t_order_detailsテーブル(注文詳細情報)への保存
2-1.t_ordersテーブルとt_order_detailsテーブルの指定するカラムを紐付けるた値を取得する
2-2.保存したいデータに対応するモデルのオブジェクトを作成する
2-3.保存したい値をオブジェクトに格納する
2-4.注文詳細情報保存を注文数分繰り返す(foreach内でsaveメソッド実行)
カラム
・ id
・ product_id
・ order_id(外部キー)
・ shipment_status_id
・ order_quantity
・ shipment_date

それでは一つ一つ見ていきましょう。

t_ordersテーブル(注文情報)への保存

public function store(Request $request)
    {
       $cartData = $request->session()->get('cartData');
       $carbonNow = Carbon::now();

        //オブジェクト生成
       $order = new \App\Order;
        //指定値をオブジェクト代入
        $order->user_id = Auth::user()->id;
        $order->order_date = $now;
        $order->order_number = rand();
        //認証済みのユーザーのみDBへ保存
        Auth::user()->orders()->save($order);

①セッション取得しています。
②変数にCarbonオブジェクトを格納し、オブジェクト変数を作成する
③EloquentでDBの対応テーブルへデータを保存する

①はもう説明不要ですね
②日付操作に便利ということでしたが今回この恩恵を感じる場面はなかったのです。
が、使いたかったので使いました!現在の日時が取得できます!

Carbon(カーボン)はPHPに標準実装されているDateTimeクラスを継承したクラスで、Laravelなどのメジャーフレームワークでも採用されている日時を扱うクラスです。CarbonはDateTimeクラスの不便な部分を拡張しているので、時間の比較、時間の加算/減算をより便利に使うことができます。
参考:【第1回】PHPで日付や時刻を扱うときに便利なライブラリ「Carbon」

③については下記の流れ

③-1. 対象となるモデルのオブジェクト生成➡変数に代入(オブジェクト変数という)
③-2. オブジェクトの属性(#attributes)に値を代入  
③-3. saveメソッドで新しいレコードをDBに挿入する()

モデルオブジェクト($order)の属性に値を代入する($order->user_id = Auth::user()->id;)
それでは下記の商品を保存してみましょう!
スクリーンショット 2021-02-25 19.44.46.png

③-1. まず注文情報(ユーザID・注文日・注文番号)のオブジェクト生成します

  • 注文情報テーブル(t_ordersテーブル)のオブジェクト生成($order = new \App\Order;)
  • カラムは、「ユーザID」「注文日」「注文番号」の3つです
この時点でのオブジェクト変数`$order`のデバック内容
App\Order {#1177 ▼
  #table: "t_orders"
  +timestamps: false                        
  #fillable: array:3 [▼
    0 => "user_id"
    1 => "order_date"
    2 => "order_number"
  ]
~省略~
  #attributes: []     まだオブジェクトには何も代入されていない
~省略~
  ]
}

③-2. オブジェクトの属性(#attributes)に値を代入

  • 下記の実行
    • $order->user_id = Auth::user()->id;
    • $order->order_date = $now;
    • $order->order_number = rand();
属性代入後のオブジェクト変数`$order`のデバック内容
(抜粋)
#attributes: array:3 [▼
    "user_id" => 1
    "order_date" => Carbon\Carbon @1614254126 {#1182▼
~省略~
      date: 2021-02-25 19:50:36.828250 Asia/Tokyo (+09:00)
    }
    "order_number" => 187331179
  ]

オブジェクトの属性にそれぞれの値が代入されましたね!

③-3. saveメソッドで新しいレコードをDBに挿入する

//認証済みのユーザーのみDBへ保存可能
Auth::user()->orders()->save($order);

やっていることはコメントどおりなのですが、具体的に何をしているのか見てみましょう。
これはUserモデルとリレーションしているOrdersモデルの関係性を用いてDBにレコードの挿入をしています。
認証していないユーザにDBへのレコード保存をさせないためです。
※(今回は実装しませんでしたがPoliyを用いて認可機能も実装させるべきだと思います)
参考:よくわかる認証と認可

いま誰が注文しようとしているのか?誰に注文させてよいのか?
これをプログラムする必要があります。
この「注文」と「誰が注文したのか」を注文時に判別出来るように条件付をするのです。
その条件が
「いま認証されているユーザだけが注文できる」
となります。正確には
「いま認証されているユーザだけがt_ordersテーブルの上記の3つのカラムに値を保存できる」
とでもいうのでしょうか。

ここで出てくるのがリレーションです。
ここでリレーションしているモデルとは勿論、UserモデルとOrdersモデルです。

User.php
(抜粋)
public function orders()
    {
        return $this->hasMany('App\Order');
    }

・一人のユーザーが複数の注文を所持するのでhasMany(わたしもつたくさん、の意味)を用い、メソッド名は複数形のordes
・反対に一つの注文は一人のユーザからしか生じないのでbelongsTo(〜に属する、の意味)を用いメソッド名は単数形のuser

Order.php
(抜粋)
public function user()
    {
        return $this->belongsTo('App\User');
    }

リレーションさせることで
「認証済みのユーザ」と「注文情報」を紐付け出来るようになります。
これで認証済みユーザのみがDBに保存できるプログラムを書く準備ができました。
それではコードを再び見ていきましょう。
Auth::user()->orders()->save($order);

これまでのリレーション先の値を取得したかった場合の記述方法
上記のUserモデルとリレーションしているOrdersモデルの関係の場合
リレーション先の値を取得する場合の記述は下記のようになります。
そして返ってくるのはCollectionオブジェクトです。各モデルのプロパティを取得したい場合
上記でもありましたが、このオブジェクト変数はCollectionなので繰り返し処理すれば良いです。

認証済みユーザの全Order取得
$user = \App\User::find(Auth::user()->id);
$orders = $user->orders;

foreach($orders as $order) {
    $userOrder = $oreder->order_date;
}
$ordersのデバック内容
Illuminate\Database\Eloquent\Collection {#1232 ▼
  #items: array:30 [▼
    0 => App\Order {#1233 ▶}
    1 => App\Order {#1234 ▶}
~省略~

$userOrderのデバック内容
"2021-02-25 21:28:56"

ここでコードを比較してみます
Auth::user()->orders()->save($order);

$user->orders;
注目してほしいのはordersの後ですね。
上記では()がない形でした。この場合、動的プロパティ(値)というものを呼び出しています。
上記のような複数のModelオブジェクトが格納されいるCollectionオブジェクトや
単体としてのModelオブジェクトが返ってきますね。

ではリレーションメソッドであるorders()の場合なにが返ってくるのでしょう。
return $this->hasMany('App\Order');がメソッドの内容ですが、この内容だと
下記のような「HasManyオブジェクト」というものを返します。

Illuminate\Database\Eloquent\Relations\HasMany {#1197 ▼
  #foreignKey: "t_orders.user_id"
  #localKey: "id"
  #query: Illuminate\Database\Eloquent\Builder {#1194 ▼
    #query: Illuminate\Database\Query\Builder {#1195 ▶}
    #model: App\Order {#1193 ▶}
    #eagerLoad: []
    #localMacros: []
    #onDelete: null
    #passthru: array:17 [▶]
    #scopes: []
    #removedScopes: []
  }
  #parent: App\User {#1200 ▶}
  #related: App\Order {#1193 ▶}
}

ここで重要なのはHasManyオブジェクトなどのEloquentリレーションオブジェクトは
クエリビルダとしても動作する
ということです。おそらく、この記述の部分で解るようにBuilderクラスをラップしているからだと思います。
#query: Illuminate\Database\Eloquent\Builder {#1194 ▼
#query: Illuminate\Database\Query\Builder {#1195 ▶

再びコードです。
Auth::user()->orders()->save($order);

クエリビルダは Laravel で SQL の記法を書きやすくしたものです。
->save($order)といった記法をメソッドチェーン的に繋げることが可能になり、これによってDBへのレコードの挿入が可能になります。

saveメソッドの第一引数には保存先を指定しないといけません。
ここが抜けると何処に保存するのかわからなくなりますし、引数が足りませんと怒られます。

スクリーンショット 2021-02-25 22.12.28.png

ちなみに、リレーションオブジェクトからは勿論プロパティの取得は出来ないです。
#attributesになんの値も入っていないので加工しようが繰り返し処理しようがないものはないのです。
値を取得したければリレーションメソッドを使用せずに、動的プロパティを取得しましょう。

それではsaveメソッド実行後の$order内にある#attributesの変化を見てみましょう!

#attributes: array:4 [▼
    "user_id" => 1
    "order_date" => Carbon\Carbon @1614254126 {#1182 ▶}
    "order_number" => 187331179
    "id" => 27
  ]

$orderの各属性に代入した値(上記の$orderのデバック内容参照)が代入されていますね。
それに加え
"id" => 27
という連想配列もあります。
これはt_ordersテーブルの主キーです。オートインクリメントなので注文数(処理数)に合わせてidも正比例して増加します。
ややこしいのですが、注文数=id番号数ではないのですが、大して重要ではないので理由は割愛します。

(ここらへんがちんぷんかんぷんな方は、マイグレーションファイルのコメントアウト部分や参考先のサイトを見てくださいませ。)

この値でt_ordersテーブルとt_order_detailsテーブルの指定したカラムを紐付けますが、解説は次でします!

これにて
Auth::user()->orders()->save($order);
「認証済みユーザのみがDBに保存できる」ようになりました!

ちなみにですがAuth::user()->orders->save($order);では勿論保存できません。
スクリーンショット 2021-02-26 23.54.10.png
動的プロパティの中身はモデルオブジェクトだったりするのですが、その中にはsaveメソッドはないんですね。
上記のhasManyオブジェクトには有るんですよね、saveメソッドが。
このオブジェクト内の、継承しているなにかのクラスがsaveメソッドを持っているのだと思いますが、はっきりはわかりません。
Builderクラスの中のどこかに継承されているのでしょうかね?

それでは続きの解説といきましょう!もう少しでおしまいです!
処理の流れの再確認です

t_order_detailsテーブル(注文詳細情報)への保存

2-1.t_ordersテーブルとt_order_detailsテーブルの指定したカラムを紐付けた値を取得する
2-2.保存したいデータに対応するモデルのオブジェクトを作成する
2-3.保存したい値をオブジェクトに格納する
2-4.注文詳細情報保存を注文数分繰り返す(foreach)
カラム
・ id
・ product_id
・ order_id(外部キー)
・ shipment_status_id
・ order_quantity
・ shipment_date

2-1.t_ordersテーブルとt_order_detailsテーブルのレコードを紐付けるための値を取得する
これは上記の
t_ordersテーブルの主キーであるidだけを取得する必要があります

該当するデータだけ取得したい場合where()で指定することで可能です。
第一引数には条件付けを行うカラム、第二引数には条件となる値を渡します。

t_ordersテーブルのカラム「order_number」が「$order->order_number」という
レコードを取得したい場合、
つまり今t_ordersテーブルに保存したorder_numberに該当するレコードを取得したい場合、
下記のような記述となります。

//Orderテーブルの カラム「order_number」が「$order->order_number」の最新のレコードを一つ取得
$savedOrder = Order::where('order_number', $order->order_number)->latest()->first();

//上記Collectionから id の値だけを取得した配列に変換
$savedOrderId = $savedOrder->pluck('id')->toArray();

$savedOrderのデバック内容を見てみましょう!

(抜粋)
#attributes: array:4 [▼
    "user_id" => 1
    "order_date" => Carbon\Carbon @1614254126 {#1182 ▶}
    "order_number" => 187331179
    "id" => 27
  ]

先程保存されたレコードが取得できました!

②このコードは上記のレコードから、主keyであるidだけを抽出し配列に変換しています。
plunkメソッドでkeyidの値を抽出しtoArrayメソッドで配列化しています。
配列化前はCollectionオブジェクトです。このままでは配列の値を$savedOrderId[0]の様な記述で取り出せません。
$savedOrderId[0]という形で配列の値を取得できるようにするためにtoArrayメソッドで配列化します。

$savedOrder->pluck('id')の配列化前後のデバック内容
(配列化前)
Illuminate\Support\Collection {#1194 ▼
  #items: array:1 [▼
    0 => 27
  ]
}

(配列化後)
array:1 [
  0 => 27        
]

このidは先程も言ったように、
t_ordersテーブルとt_order_detailsテーブルの指定したカラムを紐付けた値
となります。

続きの処理を見ていきましょう!

//注文詳細情報保存を注文数分繰り返す 1回のリクエストを複数カラムに分け商品を個別にDB登録
foreach ($cartData as $data) {
    //注文詳細情報に関わるオブジェクト生成
    $orderDetail = new \App\OrderDetail;
    $orderDetail->product_id = $data['session_products_id'];
    $orderDetail->order_id = $savedOrderId[0];
    $orderDetail->shipment_status_id = 3;
    $orderDetail->order_quantity = $data['session_quantity'];
    $orderDetail->shipment_date = $now;
    Auth::user()->orderDetails()->save($orderDetail);
    }

一度の購入で複数の商品が有った場合、商品を個別にDBに保存する必要があるので繰り返し処理をします。
indexメソッドでの処理方法で途中まで同じです。必要な値を各々に代入しています。
保存処理も先程説明した部分と同じになります。

複数のテーブルのカラムを紐付ける:外部キー制約

例として、一度の注文で作成される各テーブルのレコードを見てみましょう。
下記のようなテーブル構造に値が保存されます。

・t_ordersテーブル = 「注文情報」

id(主キー) user_id order_date order_number
1 2 2021-02-25 20:55:26 187331179

・t_order_detailsテーブル = 「注文詳細情報」

id product_id order_id(外部キー) shipment_status_id order_quantity shipment_date
1 1 1 3 4 2021-02-26 14:05:12
2 2 1 3 10 2021-02-26 14:05:12
3 4 1 3 3 2021-02-26 14:05:12
4 6 1 3 6 2021-02-26 14:05:12

ここで注目してほしいのは、「注文情報」のidカラムとorder_idです。
ここの値は連動していて両方とも「1」になっています。
両テーブルでこの値は同一のものとなります。これは外部キー制約という機能で成立しています。

このテーブルは有るユーザの買い物と、その買い物の詳細情報で分割されています。
「誰がいつ買い物をしたのか?」              注文情報
「どんな買い物をして、現在の状態(発送の有無)は?」   注文詳細情報

注文詳細情報テーブルでは「誰が」「いつ買ったのか?」、がわかりません。
注文情報テーブルでは「誰が」「いつ」この買物をしたのか、がわかりません。
なので両テーブルで指定されたカラム(id=order_id)は双方のテーブルからお互いを参照するためのカラムとなります。

注文詳細情報テーブルから「誰が」「いつ買ったのか?」を知りたければ、order_idからidを辿れば知ることが出来ます。
その逆に、注文情報テーブルでは「誰が」「いつ」この買物をしたのか知りたければidからorder_idを辿れば知ることが出来ます。

両テーブルで指定されたカラム(id=order_id)によって両テーブルの関連性が保たれています。
もし、指定されたカラムの値が片方だけ変化してしまった場合、もう片方もそれに連動して変化しなくてはなりません。
(更新 or 削除 or 変更不可)
片方だけ変化してしまった場合、テーブル間のデータの整合性が失われてしまいます。
それをDBそのものに保証させるのが外部キー制約で、この外部キー制約によってDBのデータの整合性は保たれます。


外部キー制約とは
以下のマイグレーションファイルはt_order_detailsテーブル構造を定義しています。
このマイグレーションファイルをmagraiteすることでt_order_detailsテーブルが作成されます。

スクリーンショット 2021-02-27 20.56.42.png


外部キー制約とは

外部キー制約は下記のような挙動を定義することが出来ます。
①親テーブルのレコードに対して、削除または更新を行うと、子テーブル内で同じ値を持つカラムのデータに対して削除または更新を行う
②=親テーブルのレコードに対し、削除または更新を行うとエラーとなる

・他のテーブルのデータに参照(依存)するようにカラムにつける制約のこと。
  ・参照されるのが親テーブル参照するのが子テーブルと呼ぶ(親テーブル = t_ordersテーブル)
  ・外部キーは主キーと結びつく
・主キーと外部キーはRDBにとって、それぞれのテーブルを関連付けるために使用するとても大切な機能。
  ・RDB = リレーショナル・データベース
  ・主キーと外部キーを使った制約で利用した場合、下記の制限が入る
     ・1. 存在しない値を外部キーとして登録することはできない
     ・2. 子テーブルの外部キーに値が登録されている親テーブルのレコードは削除できない
・各テーブルの主キーと外部キー、それぞれの値が連動することでテーブル間の値の整合性が保たれる。

 
・それでは上記の知識を踏まえてこのコードの解説です。

①$table->foreign('order_id')②->references('id')->on('t_orders')③->onDelete('cascade');



①. $table->foreign('order_id')
➡ このテーブル( t_orderDetail テーブル)の order_id カラムを外部キーに設定
②. ->references('id')->on('t_orders')
➡ t_orders テーブル(親テーボォ)の id を参照します(に依存します)。
➡ t_orders テーブル(親)の主キー id カラムと t_orderDetail テーブル(子)の外部キー order_id カラムが紐づく
③. ->onDelete('cascade');
➡ 親テーブルのレコードに対して削除を行うと、子テーブル内で同じ値を持つカラムのデータに対し削除を行う


これで2つのカラムが紐づき、テーブル間でのデータの整合性が保たれました。
どちらかの値が削除された場合、両方削除され片方だけ残る様な状況が防げますね。
そうならないような保証をDBにさせるのが外部キー制約の役割です。
制約の仕方何種類かありますが、ここでは紹介しないので興味あれば調べてみて下さい。

それでは最後の処理です。

//session削除
$request->session()->forget('cartData');
return view('products/purchase_completed', compact('order'));

注文内容をDBに保存したら、商品情報を保存しているセッション情報は不要になります!
削除しましょう!

そして最後に購入完了画面を表示させます!お客様に、問い合わせ用の注文番号を表示させるために
viewにオブジェクト変数orderを渡します。

<p class="h1">注文番号:{{ $order->order_number }}</p>





これにて終了です!最後まで読んで下さった方、お疲れさまです!長くてすみません(´;ω;`)有難うございました!!
※訂正部分などありましたら教えていただくと泣いて喜びます。むしろお願いしますm(_ _)m
スクリーンショット 2021-02-28 13.57.55.png

参考にさせていただいたサイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】route関数を使用する場合のSSL化対策

SSL化する前の状態

bladeテンプレートのformのaction属性をroute関数で記載。
route関数についてはこちらの記事で説明しています。

<form method="post" action="{{ route('profile.create') }}">

上記の記述だと、URLがhttp://・・・になってしまいます。
SSL化されていませんので遷移先の画面では遷移先では下記画面が表示されます。
bladeエラー.png

ちなみに、「このまま送信」ボタンをクリックするとエラーになります。
エラーの原因は、action属性で指定したroute先にリダイレクトされているため(GETリクエストのため)です。

(formのaction属性にパスを直で指定した場合は、URLはhttps://・・・でした。なのでroute関数を使用しない場合は、今回のSSL化対策はしなくても大丈夫だと思います。)

SSL化対策

App/Providers/AppServiceProvider.phpに以下を追記する必要があります。

App/Providers/AppServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Routing\UrlGenerator;


class AppServiceProvider extends ServiceProvider
    public function boot(UrlGenerator $url)
    {
        $url->forceScheme('https');
    }
}

追記した後に、再度URLを確認するとhttps://・・・になっていました。

おわりに

今回、route関数を使用した時のSSL化対策を記載しました。似たような関数でurl関数がありますが、こちらもSSL化対策が必要だそうです。対策する場合は、下記記事が参考になると思います。
Laravelで作ったサービスをSSL化した時にやったことと参考記事一覧

参考

LaravelのSSL化対策

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】Formの項目が多い場合にsessionに保存したエラーメッセージが表示されない問題

背景

LaravelでRequestFormを使ったバリデーションを実装中、

空のFormを送信するとバリデーションにひっかかって正しくリダイレクトされるが、

FormRequestに設定したエラーメッセージが表示されない問題が発生。

原因を切り分けてみるとFormRequestの設定に問題はなく、対象のformの項目数を減らすと正しくエラーメッセージが表示されたので、

項目が一定数以上あるフォームで発生するのは分かったのですが、

根本的な原因が分からずに詰まってしまったのでその原因と解決策をメモ。

結論

/config/session.php'driver'の項目にfileを指定することで解決しました。
.envの環境変数で指定している場合は対象の値をfileに変更すると設定できます。
(※Dockerを利用している場合はDockerfileの環境変数の設定も確認)

結論(詳細)

こちらに似たような質問と答えが載ってました。
stackoverflow - Laravel form validation issues (session size limit?)

RequestFormのエラーメッセージはsessionに保存されてviewに渡されており、

sessionドライバーにcookieを指定しているとcookieの容量の上限で保存できないのが原因のため、sessionドライバーにfileを使うと解決するとのことでした。

sessionドライバーの指定は/config/session.php'driver'の項目で指定できるので、cookieを使っているとをfileに書き換えて読み込み直すと変更できます。

/config/session.php
<?php

use Illuminate\Support\Str;

return [

    /*
    |--------------------------------------------------------------------------
    | Default Session Driver
    |--------------------------------------------------------------------------
    |
    | This option controls the default session "driver" that will be used on
    | requests. By default, we will use the lightweight native driver but
    | you may specify any of the other wonderful drivers provided here.
    |
    | Supported: "file", "cookie", "database", "apc",
    |            "memcached", "redis", "dynamodb", "array"
    |
    */

    'driver' => env('SESSION_DRIVER', 'file'),

    /* ~~~~~ 省略 ~~~~~~ */
];

.envの環境変数で指定してる場合はそちらの値(僕の場合はSESSION_DRIVER)にfileを指定すると変更できます。

SESSION_DRIVER=file

余談

僕の環境の場合、上記のサイトのように/config/session.php.envSESSION_DRIVERの値はどちらもfileを指定していたのですが、Dockerfileの環境変数の設定でSESSION_DRIVERcookieを指定していたため、反映されずに詰まってました。

引用

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP 基礎文法

前提

PHPの復習的備忘録となります。
間違い等あればご指摘ください。

基礎

基礎
<?php  
  // コード記述
> // 閉じタグはHTMLと同時記載しない場合は不要

//  〜コメント
#  〜コメント(行末まで)
/*  範囲コメント  */

// 変数
$変数名 = 変数;

// 出力
echo 'hello world' . PHP_EOL;
// . ピリオドで連結 PHP_EOLは改行
// " ' を使い分け('使用時は”で囲む)
$w = 'world';
echo "hello $w" . PHP_EOL; // hello world
echo "It's \"Friday\".\tHello $w " . PHP_EOL;
// 内部に変数使用時には " "で囲む
// " => \"   タブ=> \t
ヒアドキュメント
$text = <<<EOT   // 'EOT':変数不可  "EOT",EOT:変数可

  // 長めのテキスト(改行や字下げも保持)

EOT;   // 終端記号(改行必須、コメント不可)...文字自体は自由
計算
echo 10 + 3 . PHP_EOL; // 13
echo 10 - 3 . PHP_EOL;  // 7
echo 10 * 3 . PHP_EOL; // 30
echo 10 / 3 . PHP_EOL; // 3.333...
echo 10 % 3 . PHP_EOL;  // 1
echo 10 ** 3 . PHP_EOL; // 1000 乗算
echo (2 + 10) * 3 . PHP_EOL; // ()優先
echo 2 +'3' . PHP_EOL; // 5 
//文字列でも数値っぽいものは数値化して計算する

// 自己代入
$price += 100;
$price -= 100;
// ±1
$price++;
$price--;

定数
// 大文字で定義、どちらか2パターン
define('NAME','tomo');
const NAME = 'tomo';
データ型
文字列 string 'hello', "world"
整数 int 2, 4, -10
浮動小数点 float 2.5, -4.2
null null
真偽値 bool true,false
配列 array [3,6,2]
オブジェクト object new Object()
型確認
var_dump($変数)  // 型(数値or文字数)文字列

// キャスト(型変換)
// 値の前に()で型指定
$a = (float)20;      
$b = (string)2.5; 
var_dump($a);  // float(20)
var_dump($b);  // string(3) "2.5"

if文
if (条件①){
  // 条件①がtrueの場合の処理
} else if (条件②) {
  // 条件②がtrueの場合の処理
} else {
  // どれも当てはまらない場合の処理
}      // if 文は末尾 ; 不要

比較演算子、真偽値、論理演算子
>= // 以上
<= // 以下
> // より大きい
< // より小さい
=== // 等しい
!== // 異なる

$X = 3 ;
if ( $X ) {  ...      
// 単一文字,数字でも可
// false , ±0, ± 0.0, '0' , ' ' , null , [ ] , はfalse, 以外はtrue
// 条件式や値は真偽値として評価される

&& and  // かつ
|| or   // もしくは
!     // 〜ではない
switch文
switch (①:変数など){
  case :
    // ①が❶の場合の処理
    break;   // breakを入れない場合、条件が2つ重なる
  case :
    // ①が❷の場合の処理
    break;
  case :
  case :    // 複数条件時はcaseを重ねる
    // ①が❸または❹の場合の処理
    break;
  default:
  // ①がどれにもあてはまらない場合の処理
    break;
}
for文
// for (定義; 条件 ; 変更){}
for ($i = 1 ; $i <= 3 ; $i++){
   // $iを1と定義して、$iが3以下の条件になるまで処理、処理後$i++
   規定回数 ループ処理
}
while文
while (条件①){
  条件が満たされるまで ループ処理
  条件①の変化 // 状態変化を付けないと無限ループ
}

do{
  1回処理を行った上で条件が満たされるまで ループ処理
  条件①の変化 
} while(条件①);   // ; が必要

スキップ・中断
for ($i = 1; $i <= 10; $i++) {
  if ($i === 4){
     if ($i % 3 === 0 ){
     continue;   // 処理をスキップ(当てはまる場合はその処理だけ飛ばす)
     break; // 処理を中断(当てはまる場合、その時点で処理を止める)
  }
  echo $i . PHP_EOL;
}
関数・引数・return
function 関数名(仮引数 = 'デフォルト値')
{
  // 繰り返し使用したい処理  (引数込)
  return 返したい処理;  // return時点で処理終了、値が戻る。
}

関数名(実引数);  // 呼び出し 
スコープ
$変数A = 1.1;  //グローバルスコープ 

function sum($a, $b, $c)
{
  global $変数A;  // global記述で外定義の変数使用できるが非推奨
  $変数A = 1.08;  // ローカルスコープ  ...内部で定義し直す
  return ($a + $b + $c)*$変数A; // 関数の外で定義された変数だと使用不可
}
echo sum(100, 200, 300) + sum(300, 400, 500) . PHP_EOL; 
 // 1944 ローカルスコープ が適用

無名関数
// function sum()のように名称定義しない関数

$sum = function ($a, $b, $c) { 
  return $a + $b + $c;
};  // 関数代入の場合 末尾 ; 必要
echo $sum(100,300,500) . PHP_EOL;  // 900
条件演算子
function sum($a, $b, $c) 
{
  $total =  $a + $b + $c;
  // if ($total < 0){
  //   return 0;
  // } else {
  //   return $total;
  // }

// こちらに書き換え可
  return $total < 0 ? 0 :$total;
}
echo sum(100,300,500) . PHP_EOL;   // 900
echo sum(-1000,300,500) . PHP_EOL;   // 0
型指定
declare(strict_types=1);  // 厳密な型付け設定

// 引数の型付けは値の前に型名をいれる
// : のあとに返り値の型も指定できるがない場合は void
function showPoint(string $name, int $point): void  
{
  echo $name . ': ' . $point . PHP_EOL;
}

// showPoint('tomo',20);
// showPoint('tomo','tomotomo');  // 型指定しない場合でも処理してしまう
// 引数の型指定でエラーにできるが
showPoint('tomo','6');  // 文字列でも数値っぽい場合は処理できる
// 処理させたくない場合はdeclare〜で厳密な型付け設定に。
null渡し
declare(strict_types=1);

function getPoint(int $point): ?string    // null or string という設定
{
  return $point >= 100 ? 'Good' : null ;
}

echo getPoint(150) . PHP_EOL; // Good
echo getPoint(40) . PHP_EOL; // nullでエラーになるので、
// 「?型」 設定でnullも渡せるようにする 引数でも使用可
配列・キー
// 同種データをまとめたもの
$scores = [   
  30,   
  60,
  100,  // 最後も , を入れると管理しやすい
  ];      
$scores[0] = 50; // 代入で変更可

$scores = [
  'first' => 30,  // 添字ではなくキーで管理可
  'second' => 60,
  'third' => 100,
];
$scores['second']   // 60
foreach
// $scores の要素を$scoreに繰り返し代入して処理
foreach ($scores as $score){
  echo $score . PHP_EOL;
}
// key も出力可能
foreach ($scores as $key => $score){
  echo $key . '--' . $score . PHP_EOL;
}
要素展開
// 配列内配列
$配列名 = [,,[③、④],];
$配列名[0] => 
$配列名[2][0] => 

// 配列に別配列の要素を展開する場合 
$配列A = [a,b,c];
$配列B = [,,...$配列A, ]; // [①,②,a,b,c,③]

可変長引数
//可変長引数 引数が幾つでも可
// function sum($a, $b, $c) ではなく
function sum(...$numbers) 
{
  // return $a + $b + $c;
  $total = 0;
  forEach ($numbers as $number){
    $total += $number;
  }
  return $total;
}

複数返り値
function getNum(...$numbers)
{
  $total = 0;
  foreach ($numbers as $number) {
    $total += $number;
  }
  return [$total, $total / count($numbers)];   // 返り値を配列代入
}

list($sum, $average) = getNum(1,2,3); // こちらか
[$sum, $average] = getNum(1,2,3);   // こちらのパターンで代入

echo $sum . PHP_EOL;  // 6
echo $average . PHP_EOL;  // 2

参考

ドットインストール

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP8(Laravel8)でデスクトップアプリケーションを作る方法

今回はPHP8,Laravel8にてデスクトップアプリケーションを作る方法を紹介します。

phpdesktop( https://github.com/cztomczak/phpdesktop )を使うことによってWEB技術によってデスクトップアプリケーションを作ることが可能になります。

早速、起動するまでの手順を書いていきます。
ちなみに自分はwindows環境です。

phpdesktopをダウンロードする

githubのreadmeにOSごとの最新バージョンのReleaseへのリンクがあるので自分の環境に合わせたものをダウンロードして解凍します。

キャプチャ.PNG

中身はこんな感じ

キャプチャ2.PNG

phpのバージョンを8.0にする

 そのままだと、PHPのバージョンが古くてLaravel8が使えません。
 Laravel8では最低でもバージョン7.3以上が必要です。
 今回はphp8にしたいと思いますがバージョ使いたいバージョンでも大丈夫です。

 PHPのダウンロードページに行き、使うバージョンをダウンロードします。
 
 Windowsの場合はhttps://windows.php.net/download#php-8.0 にあるx64 Non Thread Safeでないと行けないので注意!

 そして、phpdesktopフォルダ内にあるphpフォルダの中身をダウンロードしたものに置き換えます。

Laravel8を用意する

 laravel8のprojectを用意します。
 自分はwindows環境なのですが、windowsにcomposerやら何やら入れるのがめんどくさかったので
vagrantのcentos7上で色々やってます。
 予め環境が用意されている場合はwindows上でやっても問題ないかと思います。

 まず、gitからlaravel8を落としてくる
デフォルトブランチが8.xなのでそのままで。

[root@localhost vagrant]# git clone https://github.com/laravel/laravel

laravelフォルダに行きcomposerで色々インストールする

[root@localhost vagrant]# cd laravel/
[root@localhost laravel]# composer install

.env ファイルを用意する

[root@localhost laravel]# cp .env.example .env

アプリケーションのkeyを発行する

[root@localhost laravel]# php artisan key:generate

これでOK

phpdesktopの設定

phpdesktopのwwwフォルダ内に

laravelフォルダ内のものを全部移動させます。

settings.jsonをいじる

"web_server": {
    ...
    "www_directory": "www/public",
    ...
    "404_handler": "/index.php"

phpdesktop-chrome.exeを実行すると

キャプチャ3.PNG

起動できました!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel 6* ui で新規ユーザー登録後のリダイレクト先をデフォルトから変更

ド初心者です。認識の違いや問題点等ありましたら、どうぞご連絡ください。。。お願いします

やりたいこと


Laravel uiを使って新規登録したユーザーを任意のURLにリダイレクトしたい

これでいけた!

登録系をつかさどるRegisterControllerの中に、

RouteServiceProviderをuse。


そして、同コントローラの中に、、、

protected $redirectTo = RouteServiceProvider::HOME;

を追加!
<?php
namespace App\Http\Controllers\Auth;

use App\User;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Foundation\Auth\RegistersUsers;
use App\Providers\RouteServiceProvider;//追加

class RegisterController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Register Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles the registration of new users as well as their
    | validation and creation. By default this controller uses a trait to
    | provide this functionality without requiring any additional code.
    |
    */

    use RegistersUsers;

    /**
     * Where to redirect users after registration.
     *
     * @var string
     */
    protected $redirectTo = RouteServiceProvider::HOME;//追加
//以下省略

RouteServiceProviderをuseして、その中にあるHOMEに定義してあるリダイレクト先へ飛べるようにするわけですね

んじゃあ、今度はそのリダイレクト先を任意の場所に指定してあげればOK

というわけで、

RouteServiceProviderに行って指定したいリダイレクト先を追加してあげよう!

app/Providers/RouteServiceProvider

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;

class RouteServiceProvider extends ServiceProvider
{
    /**
     * This namespace is applied to your controller routes.
     *
     * In addition, it is set as the URL generator's root namespace.
     *
     * @var string
     */
    public const HOME = '/';//任意のリダイレクト先を追加

    protected $namespace = 'App\Http\Controllers';
//以下省略

定数HOMEを作ってあげて、そこにリダイレクト先を指定すればOK。
public const HOME = '/';とすることで、今回は「/」というURLを設定しました。

一旦は以上とさせていただきます・・・

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】名前付きルートの使い方について

名前付きルートとは

名前付きルートは特定のルートへのURLを生成したり、リダイレクトしたりする場合に便利です。ルート定義にnameメソッドをチェーンすることで、そのルートに名前がつけられます。
公式より

ルート定義に名前を付ければ、命名したルート名を指定することでURLの呼び出しができます。
つまり、コントローラーやビューの中でURLを呼び出すときは直にパスを書く必要はありません。ルート名を指定することでURLを呼び出すことができます。

使い方

名前付きルートの設定、呼び出しの方についてまとめました。

名前付きルートの設定

かなりシンプルです。各ルート定義の行末にnameメソッドでルート名を付けるだけです。
(※ルート名は一意である必要があります)

web.php
// ルート名の書き方
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
Route::get('/user/{id}/profile', [App\Http\Controllers\UserProfileController::class, 'show'])->name('profile.show');
Route::post('/user/{id}/profile', [App\Http\Controllers\UserProfileController::class, 'create'])->name('profile.create');

設定が出来たら、あとは呼び出し側でルート名を指定します。

Controllerで名前付きルートの呼び出し

// URLの生成
$url = route('home');

// リダイレクトの生成
return redirect()->route('profile.show', ['id' => 1]);

名前付きルートでパラメータを定義している場合は、route関数の第2引数でパラメーターを指定する必要があります。指定されたパラメーターは自動的にURLの正しい場所へ埋め込まれます。

Viewで名前付きルートの呼び出し

// formでのルート指定
<form method="post" action="{{ route('profile.create', ['id' => 1]) }}">

// hrefでのルート指定
<a href="{{ route('profile.show', ['id' => 1]) }}">プロフィールを見る</a>

注意点

注意すべき点は、ルート名は常に一意である必要があります。
重複があると、あとに命名されたルートが優先されてしまいます。

web.php
// 重複のあるルート
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('profile');
Route::get('/user/{id}/profile', [App\Http\Controllers\UserProfileController::class, 'show'])->name('profile');
// リダイレクトでルート名を呼び出し
return redirect()->route('profile');

この場合、リダイレクトで呼ばれるルートは
Route::get('/user/{id}/profile', [App\Http\Controllers\UserProfileController::class, 'show'])->name('profile')になります。呼び出しの時にパラメータを渡していないので、エラーが生じます。

メリット

名前付きルートを利用するメリットは挙げてみました。

  • URLを指定する時に、長々とパスを書く必要がなくなりますのでタイプミスを回避できます。
  • パス変更があっても、変更箇所はルート定義ファイルのweb.phpのみ。影響範囲が小さく済みます。

参考

Laravel 7.x ルーティング

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel8のdatetime型をjson出力するとYYYY-MM-DDT00:00:00.000000Z(ISO8601)で出力される

はじめに

モデルからdatetime型のカラムを取得し、jsonで出力すると
日付がYYYY-MM-DDT00:00:00.000000Z(ISO8601)で出力される。
調べてみるとlaravel7から変更された仕様とのこと。
■参考URL
https://laravel.com/docs/8.x/eloquent-serialization#date-serialization

楽観排他チェックに更新日を使っているため、ISO8601では困る。
共通で日付を変換できるように修正する。

対応方針

\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Concerns\HasAttributes.php
    /**
     * Prepare a date for array / JSON serialization.
     *
     * @param  \DateTimeInterface  $date
     * @return string
     */
    protected function serializeDate(DateTimeInterface $date)
    {
        // return Carbon::instance($date)->toJSON();
        return $date->format('Y-m-d H:i:s');
    }

よくはないがフレームワーク内を修正。

終わり

参考サイトではserializeDateをオーバーライドすると書いてあるが、
どうすればいいかわからず。
とりあえずの対処方法を残します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む