20201127のlaravelに関する記事は18件です。

Terraformを用いてEC2およびRDSを立ち上げ、Laravelの環境を構築する

構成

Terraformで ap-northeast-1リージョンにVPC、1つのパブリックサブネットおよび2つのプライベートサブネットを作成した。さらにパブリックサブネット内にEC2インスタンスを立て、プライベートサブネットにはRDS for MySQLを設置した。ここまでをTerraformで行った。
EC2インスタンス内にはSSH接続でログインし、Laravelの環境を構築した。
ec2-rds-nginx-laravel.jpg
>>

TerraformでEC2、RDSを構築する

IAMユーザーを作成する

AWSコンソールからIAMダッシュボードへ移動する。
1
ユーザーを追加を選択する。
2
ユーザー名を入力し、プログラムによるアクセスを選択する。
3
既存のポリシーを直接アタッチを選択し、AdministratorAccess を選択する。
4
内容を確認し、ユーザーの作成を選択する。
5
アクセスキーIDシークレットアクセスキーを控える。
6

~/.aws/credentialsaws_access_key_id および aws_secret_access_key を記述する。

% mkdir ~/.aws
% (echo "[default]"; echo "aws_access_key_id = xxxxxxxxxxxxxxxxxxxx"; echo "aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") >> ~/.aws/credentials

この状態になる。

~/.aws/credentials
[default]
aws_access_key_id = xxxxxxxxxxxxxxxxxxxx
aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Terraformのファイルを用意する

ファイルの構成

sample
├── aws_db_instance.tf
├── aws_db_parameter_group.tf
├── aws_db_subnet_group.tf
├── aws_instance.tf
├── aws_internet_gateway.tf
├── aws_route_table.tf
├── aws_route_table_association.tf
├── aws_security_group.tf
├── aws_subnet.tf
├── aws_vpc.tf
├── main.tf
├── secret.tfvars
└── terraform.tfvars

リソースごとにファイルを分けた。

今回は13ファイル用意した。

ファイルの内容

main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.0"
    }
  }
}
# Configure the AWS Provider
provider "aws" {
  region = "ap-northeast-1"
}
# variables
variable "app" { type = string }
variable "region" { type = string }
variable "vpc_cidr_block" { type = string }
variable "subnet_cidr_block1" { type = string }
variable "subnet_cidr_block10" { type = string }
variable "subnet_cidr_block11" { type = string }
variable "ssh_cidr_blocks" {
  type = list
  description = "allowed SSH connection"
  default = ["0.0.0.0/0"]
}
variable "db_name" { type = string }
variable "db_username" { type = string }
variable "db_password" { type = string }
aws_vpc.tf
# Create a VPC
resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr_block
  tags = { Name = var.app }
}
aws_subnet.tf
# Create a subnet
resource "aws_subnet" "public" {
  vpc_id = aws_vpc.main.id
  cidr_block = var.subnet_cidr_block1
  availability_zone = "${var.region}a"
  tags = { Name = "${var.app}-public-subnet" }
}
resource "aws_subnet" "private1" {
  vpc_id = aws_vpc.main.id
  cidr_block = var.subnet_cidr_block10
  availability_zone = "${var.region}a"
  tags = { Name = "${var.app}-private1-subnet" }
}
resource "aws_subnet" "private2" {
  vpc_id = aws_vpc.main.id
  cidr_block = var.subnet_cidr_block11
  availability_zone = "${var.region}c"
  tags = { Name = "${var.app}-private2-subnet" }
}
aws_internet_gateway.tf
# Provides a resource to create a VPC Internet Gateway.
resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id
  tags = { Name = var.app }
}
aws_route_table.tf
# Provides a resource to create a VPC routing table.
resource "aws_route_table" "public_route" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }
  tags = { Name = "${var.app}-public-route" }
}
aws_route_table_association.tf
# Provides a resource to create an association between a route table and a subnet or a route table and an internet gateway.
resource "aws_route_table_association" "public_route" {
  subnet_id = aws_subnet.public.id
  route_table_id = aws_route_table.public_route.id
}
aws_security_group.tf
# Provides a security group resource.
resource "aws_security_group" "instance" {
  name = var.app
  description = "secutity group for instanse of ${var.app}"
  vpc_id = aws_vpc.main.id
  ingress {
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port = 22
    to_port = 22
    protocol = "tcp"
    cidr_blocks = var.ssh_cidr_blocks
  }
  egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "${var.app}-instance-security-group" }
}

resource "aws_security_group" "db_instance" {
  name = "${var.app}-db"
  description = "security group for db instance of ${var.app}"
  vpc_id = aws_vpc.main.id
  ingress {
    from_port = 3306
    to_port = 3306
    protocol = "tcp"
    security_groups = [aws_security_group.instance.id]
  }
  egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
  }
  tags = { Name = "${var.app}-db-instance-security-group" }
}
aws_instance.tf
# Provides an EC2 instance resource.
resource "aws_instance" "web" {
  ami = data.aws_ssm_parameter.amzn2_ami.value
  instance_type = "t2.micro"
  associate_public_ip_address = true
  subnet_id = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.instance.id]
  key_name = "sample"
  tags = { Name = var.app }
}
# Provides an SSM Parameter data source.
data "aws_ssm_parameter" "amzn2_ami" {
  name = "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
}

aws_ssm_parameter のData SourceでAmazon Linux 2のAMI IDを取得している。
この記事が参考になった。

本記事内では key_namesample としている。

後ほど sample という名前のキーペアを作成する

aws_db_subnet_group.tf
# Provides an RDS DB subnet group resource.
resource "aws_db_subnet_group" "default" {
  name = "${var.app}-db-subnet-group"
  description = "${var.app}-db-subnet-group"
  subnet_ids = [ aws_subnet.private1.id, aws_subnet.private2.id ]
  tags = { Name = "${var.app}-db-subnet-group" }
}
aws_db_parameter_group.tf
# Provides an RDS DB parameter group resource.
resource "aws_db_parameter_group" "default" {
  name_prefix = var.app
  family = "mysql8.0"
  description = "${var.app} parameter group for mysql8.0"
  parameter {
    name = "time_zone"
    value = "Asia/Tokyo"
  }
  tags = { Name = var.app }
}
aws_db_instance.tf
# Provides an RDS instance resource.
resource "aws_db_instance" "default" {
  allocated_storage = 20
  storage_type = "gp2"
  engine = "mysql"
  engine_version = "8.0"
  instance_class = "db.t2.micro"
  db_subnet_group_name = aws_db_subnet_group.default.id
  vpc_security_group_ids  = [ aws_security_group.db_instance.id ]
  parameter_group_name = aws_db_parameter_group.default.id
  name = var.db_name
  username = var.db_username
  password = var.db_password
  skip_final_snapshot = true
  tags = { Name = "${var.app}-rds" }
}
変数ファイル
terraform.tfvars
app = "sample"
region = "ap-northeast-1"
vpc_cidr_block = "10.1.0.0/16"
subnet_cidr_block1 = "10.1.1.0/24"
subnet_cidr_block10 = "10.1.10.0/24"
subnet_cidr_block11 = "10.1.11.0/24"

terraform.tfvars ファイルを作って変数に値を渡す。

secret.tfvars
ssh_cidr_blocks = ["x.x.x.x/32"] // ← allowed SSH connection
db_name = "sample"
db_username = "xxxxxx"
db_password = "xxxxxx"

secret.tfvars などのように terraform.tfvars 以外の名前を使う場合は -var-file を使う。
クレデンシャル情報を書く場合は、同ファイルをバージョン管理の対象外としておく。
今回は secret.tfvars にクレデンシャル情報を書いた。
db_namedb_username および db_password の値は Laravelの環境変数を設定する際に使用する

terraform を実行する

% terraform init

terraformの作業ディレクトリを初期化している。

% terraform plan -var-file="secret.tfvars"

terraform plan 差分を確認する。
-var-file="secret.tfvars" で変数を渡している。
terraform.tfvars に書いた変数は -var-file 無しで渡せる。

% terraform apply -var-file="secret.tfvars"

terraform apply でAWSにサービスを構築する。

これでAWS上で下図の状態になる。
ec2-rds.jpg
あとはSSH接続し、EC2インスタンス内に必要なものをインストールしていく。

もし構築したものをすべて壊すときは terraform destroy -var-file="secret.tfvars" を実行する。

SHH接続用のキーペアを作成する

AWSコンソールからEC2ダッシュボードへ移動する。
1
キーペア を選択する。
2
キーペアを作成 を選択する。
3
キーペアを作成する。
4
キーペアが作成される。
5
キーペアを作成すると秘密鍵がダウンロードされる。
秘密鍵は ~/.ssh へ移動させて、権限を変更してユーザーのみ読み込み可能にしておく。

% mv ~/Downloads/sample.pem ~/.ssh/
% chmod 400 ~/.ssh/sample.pem 

Amazon EC2 キーペアと Linux インスタンス | オプション 1: Amazon EC2 を使用してキーペアを作成する - Amazon Elastic Compute Cloud

EC2インスタンス内にLaravelの環境を構築する

AWSコンソールからEC2ダッシュボードへ移動する。
1
インスタンスを選択し、パブリックIPアドレスをコピーする。

SSHクライアントでEC2インスタンスに接続する

% ssh -i ~/.ssh/sample.pem ec2-user@x.x.x.x

SSH接続でEC2にログインする。

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
No packages needed for security; 3 packages available
Run "sudo yum update" to apply all updates.

nginxとphpをインストールする

$ amazon-linux-extras 

利用可能なパッケージを確認する。
今回はnginx1php7.4を利用する。

$ sudo amazon-linux-extras install -y php7.4 nginx1

インストールする。
必要なパッケージも同時にインストールされる。

$ sudo systemctl start nginx php-fpm

nginxおよびphp-fpmを起動させる。
nginxを起動させてから http://x.x.x.x にアクセスすると
screenshot
このように表示される。

$ systemctl status nginx php-fpm

で起動出来ているかどうかの確認ができる。

必要なパッケージのインストール

Laravel 8.x インストールによると、

以下の要件を満たす必要があります。

PHP >= 7.3
BCMath PHP拡張
Ctype PHP拡張
Fileinfo PHP拡張
JSON PHP拡張
Mbstring PHP拡張
OpenSSL PHP拡張
PDO PHP拡張
Tokenizer PHP拡張
XML PHP拡張

従って、必要なパッケージをインストールする。

$ sudo yum install -y php-bcmath php-ctype php-fileinfo php-json php-mbstring php-openssl php-pdo php-tokenizer php-xml

Composerをインストールする

$ curl -sS https://getcomposer.org/installer | php

Composerをインストールする。

$ sudo mv composer.phar /usr/local/bin/composer

移動させてパスを通す。

nginxとphpの設定

nginxの設定

$ sudo vi /etc/nginx/conf.d/default.conf 

/etc/nginx/conf.d/default.conf を作成し、以下の内容に変更する。

/etc/nginx/conf.d/default.conf
server {
    listen 80;
    server_name _;
    root /var/www/sample/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    index index.php;

    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass unix:/run/php-fpm/php-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

内容はデプロイ 8.x Laravelを参考にしている。

php-fpmの設定

$ sudo vi /etc/php-fpm.d/www.conf

/etc/php-fpm.d/www.conf の以下の値を変更する。

/etc/php-fpm.d/www.conf
user = nginx
group = nginx

listen = /run/php-fpm/php-fpm.sock

listen.owner = nginx
listen.group = nginx
listen.mode = 0660
$ sudo systemctl restart nginx php-fpm

再起動する。

$ sudo mkdir /var/www && sudo chmod 777 /var/www

/var/www ディレクトリを作成し、全権限を与える。

Laravelをインストールする

$ composer create-project --prefer-dist laravel/laravel /var/www/sample

インストールする。

$ sudo chmod -R 777 /var/www/sample/{storage,bootstrap/cache/}

/var/www/sample/storage/および/var/www/sample/bootstrap/cache/の権限を変更し、全権限を与える。

データベースの環境変数を設定する

$ vi /var/www/sample/.env
/var/www/sample/.env
DB_CONNECTION=mysql
DB_HOST=エンドポイント
DB_PORT=3306
DB_DATABASE=データベース名
DB_USERNAME=ユーザー名
DB_PASSWORD=パスワード

エンドポイントはRDSダッシュボードから確認する。

1
2
3
4
データベース名ユーザー名、およびパスワードterraformに変数で渡した値

マイグレーション

$ cd /var/www/laravel
$ php artisan migrate:fresh --seed

成功すれば、LaravelとMySQLの接続も問題ない。
EC2インスタンス内にも必要なものが揃い、目的の構成の状態になった。

npmを使うためには

$ curl -sL https://rpm.nodesource.com/setup_14.x | sudo bash - && sudo yum install -y nodejs

インストールする。

gitが必要な場合

$ sudo yum install -y git

インストールする。

mysqlにログインする場合

$ sudo yum install -y mysql

インストールする。

$ mysql -h エンドポイント -u ユーザー名 -p

ログインする。

あとがき

もともと ECS、ECRを使って下図のような構成にしようと思っていました。

screenshot

しかし、おそらくタスク定義に問題があり、ローカルでは問題無いもののAWS上ではnginxとphp-fpmの接続がうまくいきませんでした。
どなたかわかる方が居りましたら教えてください。

参考

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

LaravelでSQL実行計画を取得(explain)する

何がしたいか

Laravel8 でexplainメソッドが追加されました。

User::where('id',1)->explain()->dump();
DB::table('id',1)->where('id',1)->explain()->dump();

https://www.amitmerchant.com/using-mysql-explain-for-queries-in-laravel-8/

Laravel8未満でも使いたいなーと思いやってみることに

環境

下記環境で動作を確認してます。
Laravel 7.x

やり方

macroで拡張できるので、メソッドを追加して、マージされたプルリクの内容をAppServiceProviderにペタリ

AppServiceProvider
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;


    public function boot(): void
    {
        QueryBuilder::macro('explain', function () {
            $sql = $this->toSql();

            $bindings = $this->getBindings();

            $explanation = $this->getConnection()->select('EXPLAIN ' . $sql, $bindings);

            return collect($explanation);
        });
        EloquentBuilder::macro('explain', function () {
            $sql = $this->toSql();

            $bindings = $this->getBindings();

            $explanation = $this->getConnection()->select('EXPLAIN ' . $sql, $bindings);

            return collect($explanation);
        });
    }

これでとれるようになった。

User::where('id',1)->explain()->dump();
DB::table('id',1)->where('id',1)->explain()->dump();

ログに出す

そもそも、ログ出すのに使う必要ないので、explainメソッドいらない子かもしれない...
サービスプロバイダのbootに下記を追加(参考)

AppServiceProvider
\DB::listen(function ($query) {
    if (!preg_match("/^EXPLAIN/", $query->sql)) {
        $explain = \DB::select("EXPLAIN  {$query->sql}", $query->bindings);
        \Log::info($explain);
    }
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelのscss(background)で画像を読み込ませたい

SCSSに画像を読み込ませたい

cssで画像を読み込ませたい
background: url("../../../public/img/lp/icn-check.png") no-repeat
                5px 40%;

たったこれだけに少し苦戦をしたので、アウトプットしておきたいと思います。

webpack.mix.jsを編集

まずwebpack.mix.jsを編集します。

webpack.mix.js
mix.js("resources/js/app.js", "public/js")
    .sass("resources/sass/app.scss", "public/css")
    .options({
        processCssUrls: false
    });

ポイントをそれぞれみます。

.sass("resources/sass/app.scss", "public/css")

"public/css"をつけて、cssの保存先のフォルダを指定します。
詳細
そして、oputinsを付与します。

optionsを付与
.options({
      processCssUrls: false
   });

これをつけない場合、通常下記のようになる。

css
.example {
  background: url(/images/example.png?d41d8cd98f00b204e9800998ecf8427e);
}

こうなると指定した画像名と異なるため、画像を読み込めない。
なので、processCssUrls: falseにする。

ファイルを指定する。

さあ、準備ができました。
あとは画像を指定するだけです。
ここから重要です。

scssファイルの階層は、
アプリ名/resources/sass/lp/top.scss

画像ファイルの階層は、下記に入れたとします(人によります)。
アプリ名/public/img/lp/icn-check.png

画像はresourcsフォルダの兄弟ディレクトリである、publicフォルダに入れています。
そのため、上の階層に戻す必要があります。

なので、階層を戻していきます。
アプリ名/resources/sass/lp/top.scssなので、3階層戻ります。

background: url("../../../");

これでresourcesの親階層になります。resourcesの兄弟ディレクトリである、publicフォルダを指定したいので、

background: url("../../../public/");

あとは画像を入れているフォルダを指定してやればいいので、

background: url("../../../public/img/lp/icn-check.png") no-repeat 5px 40%;

となります。

はじめはpublic/cssを読み込んでいると思っていましたが、そうではなくsassファルダから画像フォルダまでのパスを記載してやる必要がありました。

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

LaravelAdminのシャーディング対応

 本記事は、サムザップ Advent Calendar 2020 #2 の12/6の記事です。

 LaravelAdminは、いわゆる管理画面のフレームワークです。(公式: https://laravel-admin.org/docs/en/)

 シンプルながらデザインが整っていて、いちから作るよりはよい機能がそろっています。また、CRUDで作られていて、定型的な表示や操作ならモデル指定ぐらいで作れたりします。

 一方で、ゲーム開発のサーバでは負荷分散のためにシャーディングをしていることが多く、データベースとテーブルを複数組み合わせてひとつのデータを示していることがよくあります。
 LaravelAdminでは、そのままこれらのデータを扱うことができません。
 ここではそんなLaravelAdminでシャーディングしたデータを扱うためのいくつかの方法を述べてみます。

何が問題?

 LaravelAdminではGrid、Formなどいくつかの表示や操作の仕組みを提供していて、それらのクラスに対してモデルを渡したり表示内容などを記述するといった作り方をしています。
 渡すモデルは、あらかじめ生成したモデルを渡すことができるので、シャーディングされたDBやテーブルをシャーディングキーで特定することが事前にできれば、そのシャーディング先を示すモデルを扱うことはできます。
 ただシャーディングされたデータをそのままGridに渡すと、全件表示されてしまいます。たとえばユーザーIDをシャーディングキーにしているものなら、ユーザーIDを指定しても、ほかのユーザーのデータも表示されてしまいます。
 ほかにも条件による絞り込み機能など、サブ的な仕組みには、シャーディングキーを呼び出し元と揃えたい場合があり、シャーディングキーをどうやって渡すかがポイントになりがちです。

シャーディングに対応したgridの作成

 Gridは便利な機能ですが、そのままでは扱えないので、LaravelAdminのコードを改造することにしました。
 考え方として、シャーディングキーをあらかじめフィルターのように使えば、Gridの表示の際、シャーディングキーに合わせた表示がされるはずです。この仕組みを「デフォルトのフィルター」としてGridに用意してやればいいわけです。
 vendor/encore/laravel-admin/src/Grid.phpに以下のコードを追加します。

Grid.php
/**
 * default filter key
 * @var string
 */
protected $defaultFilterKey;

/**
 * default filter value
 * @var mixed
 */
protected $defaultFilterValue;


/**
 * Add default filter.
 *
 * @return void
 */
public function addDefaultFilter($key, $value)
{
    $this->defaultFilterKey = $key;
    $this->defaultFilterValue = $value;
}

 同じGrid.phpのapplyQuery()では、フィルタに基づく条件の設定などを行っています、ここでdefaultFilterKey、defaultFilterValueをwhereして、検索条件に加えます。

Grid.php
/**
 * @return array|Collection|mixed
 */
public function applyQuery()
{
    if (!empty($this->defaultFilterKey)) {
        $this->model()->where($this->defaultFilterKey, $this->defaultFilterValue);
    }

    $this->applyQuickSearch();

    $this->applyColumnFilter();

    $this->applyColumnSearch();

    $this->applySelectorQuery();
}

 使い方は次の通りです。ここでは$userIdがシャーディングキーになります。

$hogemodels = new HogeModel();

//シャードをセット。DBが決まる
$hogemodels->setShard($userId);

//Gridの初期化コールバックでシャードキーを設定。表示を$userIdのものに指定
Grid::init( function(Grid $grid) use ($userId) {
    $grid->addDefaultFilter('user_id', $userId);
});

return Admin::content(function (Content $content) use ($hogemodels, $userId) {
    //Gridの利用
    $content->body(Admin::grid($hogemodels, function (Grid $grid) use ($userId) {
        $grid->user_id('ユーザーID')->sortable();
    }));
});

シャーディングキーを渡して表示条件指定を行う

 フィルター機能にはhiddenフィールドを記述できる仕組みがあるので、それでシャーディングキーを渡すようにすると、うまく呼び出せます。
 以下のようにfilterのコールバックでhidden()を記述します。$userIdがシャーディングキーです。

public function resultview(Request $request)
{
    $userId = $request->user_id;
    $hogemodels = new LogTrophies();
    $hogemodels->setShard($userId);

    Grid::init( function(Grid $grid) use ($userId) {
        $grid->addDefaultFilter('user_id', $userId);
    });
    return Admin::content(function (Content $content) use ($hogemodels, $userId) {
        $content->body(Admin::grid($hogemodels, function (Grid $grid) use ($userId) {
            $grid->user_id('ユーザーID')->sortable();

            $grid->perPages([100, 200, 500, 1000]);
            $grid->filter(function ($filter) use ($userId) {
                $filter->disableIdFilter();
                $filter->hidden('user_id', $userId);
                $filter->equal('type', 'type');
                $filter->equal('key', 'key');
                $filter->equal('value', 'value');
            });
        }));
    });
}

シャーディングキーを渡してEditフォームを利用する

 Editフォームにはhiddenフィールドの記述方法がないので、シャーディングキーを渡すのにちょっと頭を使います。
 Editフォームを呼び出すために使うURLには、呼び出し元のURLが使われます。そこで$grid->setResourceでシャーディングキーを混ぜたURLにしてやることで、シャーディングキーを渡せます。
 大元のGridで以下のようにして

$content->body(Admin::grid($hogemodels, function (Grid $grid) use ($userId) {
    $grid->setResource('/admin/hoge/resultview/' . $userId); //Edit Action用にユーザーID渡すため
    $grid->user_id('ユーザーID')->sortable();
}));

routes.phpは以下のようにシャーディングキーであるUserIdと、テーブルのユニークIDであるidを両方取るように記述します。

routes.php
$router->get('/hoge/resultview/{userId}/{id}/edit', 'HogeController@edit');

Editの受け側には、userId、idの2つの引数を取るように記述します

public function edit($userId, $id)
{
    $hogemodels = new UserBirthdays();
    $hogemodels->setShard($userId);
    Grid::init( function(Grid $grid) use ($userId) {
        $grid->addDefaultFilter('user_id', $userId);
    });

    return Admin::content(function (Content $content) use ($hogemodels, $userId) {
        $content->header('hoge情報');
        $content->description('編集');
        $content->body(
            Admin::form($hogemodels, function (Form $form) use ($hogemodels, $userId) {
                $form->display('ユーザーID')->default($userId);
                $form->setaction('/admin/hoge/modification');
            })
        );
    });
}

まとめ

 LaravelAdminは便利なフレームワークですが、便利であるがゆえに仕組みが決まっているため、最初は作法がわからずとっつきにくいかもしれません。でも、いろいろコードを調べると、便利なものが多くあります。ぜひいろいろ試してみてください。

 明日は @hiroki_shimada の記事です。

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

Laravel × Vue.jsでハマった簡単なエラー

ターミナルで下記の様に表示された際は、このエラーの上で右クリック+許可するだけ

ESLint is disabled since its execution has not been approved or denied yet. Use the light bulb menu to open the approval dialog. 

google先生に頼りすぎていたせいか、思考力が低下していた。気をつけよう・・・

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

Laravelバリデーション integerとnumericの動作の違い

  • Laravel6.x
  • PHP7.4

紛らわしいので調べました。

integer の挙動

入力値が整数で構成された文字列か数値であることをバリデートします。

※実際の判定文はこちら

// vendor\laravel\framework\src\Illuminate\Validation\Concerns\ValidatesAttributes.php
public function validateInteger($attribute, $value)
{
    return filter_var($value, FILTER_VALIDATE_INT) !== false;
}

numericの挙動

入力値が数字または数値形式の文字列であることをバリデートします。

※実際の判定文はこちら

// vendor\laravel\framework\src\Illuminate\Validation\Concerns\ValidatesAttributes.php
public function validateNumeric($attribute, $value)
{
    return is_numeric($value);
}

比較

  • ○:バリデーションチェックを通過する
  • ×:バリデーションチェックに引っかかりエラーになる
検証値 integer numeric regex:/^[0-9]+$/i
'0'
'123'
'0123' × 注意!
'+1' ○ 注意! ×
'-1' ○ 注意! ×
'0.1' × ×
'1.1' × ×
'113-0012' × × ×
'090-111-222' × × ×
'123' × × ×

おまけ 動作確認用

<?php
$list = ['0', '123', '0123', '+1', '-1', '0.1', '1.1', '114-0012', '090-2222-3333', '123'];
foreach ($list as $value) {
    echo "$value ";
    if (filter_var($value, FILTER_VALIDATE_INT) !== false) {
        echo " |integer: ○";
    } else {
        echo " |integer: ×";
    }    

    if (is_numeric($value)) {
        echo " |numeric: ○";
    } else {
        echo " |numeric: ×";
    }

    if (preg_match("/^[0-9]+$/i", $value)) {
        echo " |regex: ○";
    } else {
        echo " |regex: ×";
    }

    echo "\n";
}
?>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP/Laravel用語集

はじめに

PHP/Laravelに関する基本的な用語を簡単にまとめました。
特に初学者の場合、用語の意味や意図が分かっていないまま学習をしていると「今何をしてるんだ?」となりがちなので、迷子にならない為にも用語の意味を知っておくことが重要だと思い記事にしました。

私自身、勉強中の身ですので間違っている箇所などありましたら、Twitter(@Tii_engineering)のDMなどでご指摘いただけたら幸いです。
随時更新予定です。


MVCフレームワーク

Model(モデル)、View(ビュー)、Controller(コントローラー)の各機能に分けて整理し、これらのパーツを作ることで開発を行う。

Routing(ルーティング)

アクセスを設定している情報(ルート)を管理する機能。
特定のアドレスにアクセスした時、どの処理を呼び出して実行するかを管理するもの。
「〇〇というアドレスにアクセスしたら、××という処理を呼び出す」という関連付けを行なっている。

Model(モデル)

データ処理全般を担当。データベースアクセスに関する処理全般を扱う。

View(ビュー)

画面表示を担当。
Controllerの指示によって アクセスしてきたユーザーのブラウザに表示するデータを生成する。(HTMLファイルを出力するところのようなイメージ)

Controller(コントローラー)

全体の制御を担当。
必要に応じてModelからデータを取得したり、Viewを利用して画面表示を作成したりする。

Blade

シンプルにHTMLファイルを書くように書くことのできるテンプレートエンジン。
通常のPHPスクリプトファイルを記述するより効率的に作ることができる。
テンプレートを継承し新たなテンプレートを定義したり、レイアウトの一部をセクションとしてはめ込むことができる。

パス(path:通り道)

ファイルやフォルダの場所。
(ファイル名を含む場合と含まない場合がある。)

Migration(マイグレーション)

データベースのバージョン管理機能。
データベースのテーブルを作成・削除等する機能も持つ。

Validation(バリデーション)

Modelでデータを保存する前に、フォームからデータを送信されてきた値が正しい形式で書かれているかどうか、データの不備をあらかじめ防ぐために検証する仕組み

Auth

facade (ファサード)という機能で、ユーザーを認証しログインを管理する機能を自動生成してくれる機能。

composer

Laravelを構成するたくさんの公開ライブラリを矛盾がない状態で管理するツール。
ライブラリ同士は相互に複雑な依存関係がるが、これらの依存関係の管理を一手に引き受けてくれるのがcomposer。

Webpack

CSSやJavaScriptなど複数に分割されているファイルを1つにまとめることができる。
ソースコードを圧縮し、接続速度が早くなる。

Laravel Mix

WebpackやsassをLaravel用に使いやすくオーバーラップしているツール。
Webpackやsassはそれぞれ環境構築などを行うのに時間がかかるがLaravel Mixを使えば設定ファイルに数行記述するだけで、自動的に圧縮等の処理ができる。

GET

指定したURLの内容を取り出すための要求で、最も基本的なHTTPメソッド。
ブラウザからURLを入力してwebページを開くときには、GETメソッドのHTTPリクエストを送っている。

POST

URLに対して情報を要求するだけでなく、クライアントからさまざまなデータを送信することができる。
主にデータを更新するような処理に使われています。

テーブル

分類されたデータベースの種類ごとの単位。
データベースは複数のテーブルを保持し、テーブルごとにデータを管理している。

カラム

データベースの列に相当するもの。「属性」を意味する。

レコード

データそのものを意味する。

インスタンス

データベースとユーザーの間を仲介する役割がある。
インスタンスを介してデータベースを操作することで、データの安全性やパフォーマンスの向上につながる。インスタンスは『データベースにアクセスするための過程』であり、データベースは『データを格納したファイル』という関係性。

スキーマ

データベースの設計図
必要なデータの洗い出しや、そのデータの格納整理のルールを決める。(全体を理解するために割り当てられた『構造』)
データベースでは、そのような作業を『スキーマを定義する』と呼ぶ。

Eloquent ORM

ORMとは、レコードをオブジェクトとして扱えるようにするための仕組み。
Laravelには「Eloquent」というORMが内臓されている。
このEloquentを使って、モデルとデータベースとのやり取りを行うようにしています。

リレーション

テーブルの関連付け。
「Aさんが投稿した記事」というように、ユーザーと記事のテーブルを関連付けることを言う。

デプロイ

公開用のサーバーを用意し、そのサーバーに作成したアプリケーションのインストールを行い本稼働させること。

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

Laravel6 Unitテスト時に「Maximum function nesting level~」のエラーが出た

目的

  • しょーもないミスで時間を費やしてしまったので反省の意味も含めてエラー原因などをメモ的にまとめる

前提情報

  • LaravelのUnitテストを実行したところ「Error: Maximum function nesting level of '256' reached, aborting!」というエラーが出た。
  • テストではServiceを呼び出し、紐付いたRepositoryからデータを取得する事ができるかを確認していた。

原因

  • Serviceクラスの依存注入でそのServiceクラス自体を依存注入してしまっていた。
  • さらにServiceクラス内のメソッドでそのメソッドを呼び出す処理を記載していた。
  • わかりにくいと思うので下記に図を記載する。

    • 理想

      mermaid-diagram-20201127145744.png

    • 今回

      mermaid-diagram-20201127145729.png

  • 上記でなんとなくご理解いただけたと思うがUnitテストで呼び出しているServiceクラスのメソッドが自身を呼び出すようになってしまっていた。

  • 当該ループが256回に達したため、エラーを出力して処理が停止した。

改善策

  • ServiceクラスでRepositoryクラスが依存注入できているかを確認する。
  • 下記画像の流れを意識しながら実装する。

    mermaid-diagram-20201127145744.png

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

【Laravel】routesのweb.phpでRouteが未定義になるのはVSCodeの拡張機能が原因だった

【Laravel】routesのweb.phpでRouteが未定義になるのはVSCodeの拡張機能が原因だった

使用環境

  • windows 10 Home(COREi7)
  • XAMPP 7.3.18
  • Laravel 6
  • VSCode

背景

Laravelの学習に取り組んでいるとルーティングを行うroutesフォルダのweb.phpでRouteが未定義だとエラーが発生していた。

原因

VSCodeの拡張機能PHP intelephenseを使用しているとこのエラーが発生する模様。

解決方法

調べてみると2パターンある模様。1つはVSCodeの拡張機能を使う方法。2つ目は定義づけする方法。

1つ目 VSCodeの拡張機能を使う

 拡張機能のintelephenseを使用し、intelephenseの設定から以下のチェックを外す。その後、VSCodeを再起動する。
詳しくは下記webサイトを参照。

intelephense.diagnostics.undefinedClassConstants
intelephense.diagnostics.undefinedConstants
intelephense.diagnostics.undefinedFunctions
intelephense.diagnostics.undefinedMethods
intelephense.diagnostics.undefinedProperties
intelephense.diagnostics.undefinedTypes
[VSCode] LaravelでRouteなどがエラーになるのを直す
VSCode拡張機能Intelephenseの更新によるUndefined type 'Route'(他クラス)のエラー

2つ目 web.phpファイル内で定義付けする

 web.phpファイル内にuse Illuminate\Support\Facades\Route;の1文を追加する。
ちなみに関連付けされたRoute.phpは次の場所にある。
作成したLaravelプロジェクトフォルダ\vendor\laravel\framework\src\Illuminate\Support\Facades

注意事項

Routeの使用自体はuse Illuminate\Support\Facades\Route;をわざわざ記述しなくても使用できるようになっている。

web.phpでuse Illuminate\Support\Facades\Routeをしなくてよい理由

App\Http\Controllers名前空間をコントローラルート登録時に毎回指定しなくても済むように、デフォルトでRouteServiceProviderが名前空間グループの中でroutes.phpファイルを読み込み、指定していることを覚えておいてください。これにより、先頭のApp\Http\Controllers名前空間を省略でき、続きの部分を指定するだけで済みます。
[公式]ルーティング 6.xLaravel

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

Laraveを触って1年経ったのでTIPS

本当は2年くらいかもしれない

確認バージョンは Laravel 7.26.1

よく使うコマンド

ネームスペースとか考慮するとコマンドから作った方が良いです。でもコピペしちゃう

make系

# マイグレーション作成
php artisan make:migration create_xxxxx_table

# モデル&ファクトリー&シーダー作成(app/)
php artisan make:model Models/Xxxx -fs

# コントローラー作成(app/Http/Controllers/)
php artisan make:controller Api/XxxxController --model=Models/Xxxx --api

# フォームリクエスト作成(app/Http/Requests/)
php artisan make:request Api/Xxxx/Store

# コマンド作成(app/Console/Commands/)
php artisan make:command Xxxx

### ide-helperが導入されていれば
# ide_helper:generate
php artisan ide-helper:generate

# ide_helper:models
php artisan ide-helper:models -N

db系

# マイグレーション実行
php artisan migrate

# マイグレーションを一つ戻す
php artisan migrate:rollback --step=1

# DB初期化&DatabaseSeeder実行
php artisan migrate:fresh --seed

# シーダー実行
php artisan db:seed --class=UserSeeder

キャッシュを消す

# 全消し
php artisan optimize:clear

# optimize:clearの中身の実態は下記コマンド
php artisan view:clear && \
php artisan cache:clear && \
php artisan route:clear && \
php artisan config:clear && \
php artisan clear-compiled

ちなみに間違ってもローカル開発環境でphp artisan config:cacheは使わないよう注意してください。
phpunitを走らせた際にキャッシュ化した.envの環境変数が使われてしまいます。
間違えた場合は php artisan config:clearしてください。

公式にも書いてる!
https://readouble.com/laravel/7.x/ja/configuration.html

Factory

公式Faker
https://github.com/fzaninotto/Faker

日本語ソース
https://github.com/fzaninotto/Faker/blob/master/src/Faker/Provider/ja_JP

# ランダム英数字生成(unique使っても衝突してしまう場合)
$faker->unique()->regexify('[a-zA-Z0-9]{1,20}')

Routing

ルーディングの書き方について

個人的にはHTTPメソッドごとにアクションを指定した方がいいかと思います。
Route::resourceを使ってデフォルトアクションを指定できるんですが、ワナがあります。
下記のように公開・下書きで投稿の処理を丸ごと分けたいなどした時に下記のようなルーティングになるかと思います。

api.php

Route::resource('publish/posts', 'Publish\PostController')->only([
    'index',
    'store',
    'show',
    'update',
    'destroy',
]);

Route::resource('draft/posts', 'Draft\PostController')->only([
    'index',
    'store',
    'show',
    'update',
    'destroy',
]);

処理的には問題ないですが、php artisan route:cache(本番環境での高速化に必要)した際にname被りでエラーになります。
なのでシンプルにメソッドごとに書いた方が良いかと思います(いずれresourceで表現できないURIの方が多くなるので)

Route::get('/publish/posts', 'Publish\PostController@index');
Route::post('/publish/posts', 'Publish\PostController@store');
Route::get('/publish/posts/{id}', 'Publish\PostController@show');
Route::patch('/publish/posts/{id}', 'Publish\PostController@update');
Route::delete('/publish/posts/{id}', 'Publish\PostController@destroy');

Route::get('/draft/posts', 'Draft\PostController@index');
Route::post('/draft/posts', 'Draft\PostController@store');
Route::get('/draft/posts/{id}', 'Draft\PostController@show');
Route::patch('/draft/posts/{id}', 'Draft\PostController@update');
Route::delete('/draft/posts/{id}', 'Draft\PostController@destroy');

Controller

  • ルートモデルバインディングはFatControllerになる危険性があるので、使用は注意

FormRequest

  • exists使う際にはdeleted_atを考慮する必要あり
  • min、max、sizeなどは型指定で動きが変わるので注意
  • existsの逆はuniqueを使う
    public function rules()
    {
        return [
            'id'    => 'required|integer|exists:posts,id,deleted_at,NULL',
            'str1'  => 'required|string|max:10',   // 10文字以内
            'str2'  => 'required|string|min:10',   // 10文字以上
            'str3'  => 'required|string|size:10',  // 10文字固定
            'num1'  => 'required|integer|max:10',  // 10以内
            'num2'  => 'required|integer|min:10',  // 10以上
            'num3'  => 'required|integer|size:10', // 10固定
        ];
    }

Eloquent

Model

  • 論理削除を有効にしたい場合はuse SoftDeletes
  • 全モデルでguardedにidを指定しておくと良い。予期せぬキーの登録・更新を防げる。
  • created_atカラムを作成しなかった場合はCREATED_AT = null
  • updated_atカラムを作成しなかった場合はUPDATED_AT = null
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Xxxxx extends Model
{
    use SoftDeletes;

    const CREATED_AT = null;
    const UPDATED_AT = null;

    protected $guarded = [
        'id',
    ];
}

Relation

こんなモデルがあったとして

class User extends Authenticatable
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}
class Post extends Model
{
    use SoftDeletes;

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}
class Comment extends Model
{
    const UPDATED_AT = null;

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

リレーションメソッドでアクセスした時

  • hasOneの戻り値は Model or null
  • hasManyの戻り値はCollection
  • belongsToの戻り値は Model or null

QueryBuilder

  • クエリビルダの場合はjoinするテーブルのdeleted_atが機能しない
    User::join('posts', 'users.id', '=', 'posts.user_id')->get();
    select * from users inner join posts on users.id = posts.user_id
  • joinされるテーブルがModelの場合はdeleted_atが機能する
    Post::join('comments', 'posts.id', '=', 'comments.post_id')->get();
    select * from posts inner join comments on posts.id = comments.post_id where posts.deleted_at is null

Relation Query

hasで流れるSQL

    User::has('posts.comments')->get();
[2020-12-05 23:39:48] local.DEBUG: SQL {"time":"12.82 ms","sql":"select * from `users` where exists (select * from `posts` where `users`.`id` = `posts`.`user_id` and exists (select * from `comments` where `posts`.`id` = `comments`.`post_id`) and `posts`.`deleted_at` is null)"} 
select * from users
where exists (
    select * from posts
    where users.id = posts.user_id
    and exists (
        select * from comments
        where posts.id = comments.post_id
    )
    and posts.deleted_at is null
)

joinで流れるSQL

    User::join('posts', 'posts.user_id', '=', 'users.id')
        ->join('comments', 'comments.post_id', '=', 'posts.id')
        ->whereNull('posts.deleted_at')
        ->get();
[2020-12-06 00:00:13] local.DEBUG: SQL {"time":"12.11 ms","sql":"select * from `users` inner join `posts` on `posts`.`user_id` = `users`.`id` inner join `comments` on `comments`.`post_id` = `posts`.`id` where `posts`.`deleted_at` is null"} 
select * from users
inner join posts on posts.user_id = users.id
inner join comments on comments.post_id = posts.id
where posts.deleted_at is null

withで流れるSQL

    User::with('posts.comments')->get();
[2020-12-05 23:49:37] local.DEBUG: SQL {"time":"11.54 ms","sql":"select * from `users`"} 
[2020-12-05 23:49:37] local.DEBUG: SQL {"time":"0.70 ms","sql":"select * from `posts` where `posts`.`user_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20) and `posts`.`deleted_at` is null"} 
[2020-12-05 23:49:37] local.DEBUG: SQL {"time":"0.41 ms","sql":"select * from `comments` where `comments`.`post_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)"} 
select * from users
select * from posts where posts.user_id in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20) and posts.deleted_at is null
select * from comments where comments.post_id in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Console

複数時間でタスクスケジューリングしたい場合(8:30、11:30、18:30など)はcron形式にする。

app/Console/Kernel.php

->cron('30 8,11,18 * * *');
コンソール出力時にログにも残す

トレイトを作成してコマンドクラスでuse PrependsOutput;してあげればOK
普通に$this->info()$this->error()するだけでログに残せます。
テスト時に実行時間を出力していない理由は、コンソール出力値もテストしているためです。

app/Console/Commands/PrependsOutput.php

<?php

namespace App\Console\Commands;

use Carbon\CarbonImmutable;

trait PrependsOutput
{
    /**
     * コンソール出力に追加
     *
     * @param  string  $string
     * @param  string|null  $style
     * @param  int|string|null  $verbosity
     * @return void
     */
    public function line($string, $style = null, $verbosity = null)
    {
        if (\App::environment() === 'testing') {
            parent::line($string, $style, $verbosity);
        } else {
            parent::line(CarbonImmutable::now()->format('[Y-m-d H:i:s] ').$string, $style, $verbosity);
        }
        logger(CarbonImmutable::now()->format('[Y-m-d H:i:s] ').$string);
    }
}

PHPUnitテスト

  • テストが遅い時はxdebugを無効にしてみてください。5倍くらい違う!
# テスト実行
./vendor/bin/phpunit

# 対象のファイルでテスト実行
./vendor/bin/phpunit tests/Feature/XxxxxTest.php

# 対象のファイルの対象の関数でテスト実行
./vendor/bin/phpunit tests/Feature/XxxxxTest.php --filter=xxxxxxxx

# テスト結果をログに残す
./vendor/bin/phpunit --testdox-text=test.txt

他いろいろ

実行SQLを確認したい

下記を参考にしてちょっと直しました
LaravelでSQL文をlaravel.logに出力する

下記をregisterに追加

app/Providers/AppServiceProvider.php

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        // SQL Log
        \DB::listen(function ($query) {
            $sql = preg_replace('/"(.*?)"/', "'$1'", $query->sql);
            for ($i = 0; $i < count($query->bindings); $i++) {
                $bindValue = $query->bindings[$i];
                if (is_bool($bindValue)) {
                    $bindValue = $bindValue ? 'true' : 'false';
                } else {
                    $bindValue = "'".(string)$bindValue."'";
                }
                $sql = preg_replace("/\?/", $bindValue, $sql, 1);
            }
            \Log::debug("SQL", ["time" => sprintf("%.2f ms", $query->time), "sql" => $sql]);
        });
    }

Laravelログ+Slackにもログを残す

  1. stackのchannelsにslackを追加
  2. slackのlevelをcritical → debugに変更
  3. envのLOG_SLACK_WEBHOOK_URLにslackのwebhookUrlを指定

config/logging.php

    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => [
                'single',
                'slack', // 追加
            ],
            'ignore_exceptions' => false,
        ],
        
        'slack' => [
            'driver' => 'slack',
            'url' => env('LOG_SLACK_WEBHOOK_URL'),
            'username' => 'Laravel Log',
            'emoji' => ':boom:',
            'level' => 'debug', // critical → debugに変更
        ],

.env

LOG_SLACK_WEBHOOK_URL=xxxxxx

stackのchannelsには複数のdriverが使えるので、fatalエラーと単純なログをチャンネルごとに分けると便利です

    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => [
                'single',
                'slack-critical',
                'slack-debug',
            ],
            'ignore_exceptions' => false,
        ],
        
        'slack-critical' => [
            'driver' => 'slack',
            'url' => env('LOG_SLACK_WEBHOOK_URL'),
            'username' => 'Laravel critical Log',
            'emoji' => ':boom:',
            'channel' => env('LOG_SLACK_CHANNEL_ALERT'),
            'level' => 'critical',
        ],
        'slack-debug' => [
            'driver' => 'slack',
            'url' => env('LOG_SLACK_WEBHOOK_URL'),
            'username' => 'Laravel debug Log',
            'emoji' => ':memo:',
            'channel' => env('LOG_SLACK_CHANNEL_DEBUG'),
            'level' => 'debug',
        ],
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelを触って1年経ったのでTIPS

本当は2年くらいかもしれない

確認バージョンは Laravel 7.26.1

よく使うコマンド

ネームスペースとか考慮するとコマンドから作った方が良いです。でもコピペしちゃう

make系

# マイグレーション作成
php artisan make:migration create_xxxxx_table

# モデル&ファクトリー&シーダー作成(app/)
php artisan make:model Models/Xxxx -fs

# コントローラー作成(app/Http/Controllers/)
php artisan make:controller Api/XxxxController --model=Models/Xxxx --api

# フォームリクエスト作成(app/Http/Requests/)
php artisan make:request Api/Xxxx/Store

# コマンド作成(app/Console/Commands/)
php artisan make:command Xxxx

### ide-helperが導入されていれば
# ide_helper:generate
php artisan ide-helper:generate

# ide_helper:models
php artisan ide-helper:models -N

db系

# マイグレーション実行
php artisan migrate

# マイグレーションを一つ戻す
php artisan migrate:rollback --step=1

# DB初期化&DatabaseSeeder実行
php artisan migrate:fresh --seed

# シーダー実行
php artisan db:seed --class=UserSeeder

キャッシュを消す

# 全消し
php artisan optimize:clear

# optimize:clearの中身の実態は下記コマンド
php artisan view:clear && \
php artisan cache:clear && \
php artisan route:clear && \
php artisan config:clear && \
php artisan clear-compiled

ちなみに間違ってもローカル開発環境でphp artisan config:cacheは使わないよう注意してください。
phpunitを走らせた際にキャッシュ化した.envの環境変数が使われてしまいます。
間違えた場合は php artisan config:clearしてください。

公式にも書いてる!
https://readouble.com/laravel/7.x/ja/configuration.html

Factory

公式Faker
https://github.com/fzaninotto/Faker

日本語ソース
https://github.com/fzaninotto/Faker/blob/master/src/Faker/Provider/ja_JP

# ランダム英数字生成(unique使っても衝突してしまう場合)
$faker->unique()->regexify('[a-zA-Z0-9]{1,20}')

Routing

ルーディングの書き方について

個人的にはHTTPメソッドごとにアクションを指定した方がいいかと思います。
Route::resourceを使ってデフォルトアクションを指定できるんですが、ワナがあります。
下記のように公開・下書きで投稿の処理を丸ごと分けたいなどした時に下記のようなルーティングになるかと思います。

api.php

Route::resource('publish/posts', 'Publish\PostController')->only([
    'index',
    'store',
    'show',
    'update',
    'destroy',
]);

Route::resource('draft/posts', 'Draft\PostController')->only([
    'index',
    'store',
    'show',
    'update',
    'destroy',
]);

処理的には問題ないですが、php artisan route:cache(本番環境での高速化に必要)した際にname被りでエラーになります。
なのでシンプルにメソッドごとに書いた方が良いかと思います(いずれresourceで表現できないURIの方が多くなるので)

Route::get('/publish/posts', 'Publish\PostController@index');
Route::post('/publish/posts', 'Publish\PostController@store');
Route::get('/publish/posts/{id}', 'Publish\PostController@show');
Route::patch('/publish/posts/{id}', 'Publish\PostController@update');
Route::delete('/publish/posts/{id}', 'Publish\PostController@destroy');

Route::get('/draft/posts', 'Draft\PostController@index');
Route::post('/draft/posts', 'Draft\PostController@store');
Route::get('/draft/posts/{id}', 'Draft\PostController@show');
Route::patch('/draft/posts/{id}', 'Draft\PostController@update');
Route::delete('/draft/posts/{id}', 'Draft\PostController@destroy');

Controller

  • ルートモデルバインディングはFatControllerになる危険性があるので、使用は注意

FormRequest

  • exists使う際にはdeleted_atを考慮する必要あり
  • min、max、sizeなどは型指定で動きが変わるので注意
  • existsの逆はuniqueを使う
    public function rules()
    {
        return [
            'id'    => 'required|integer|exists:posts,id,deleted_at,NULL',
            'str1'  => 'required|string|max:10',   // 10文字以内
            'str2'  => 'required|string|min:10',   // 10文字以上
            'str3'  => 'required|string|size:10',  // 10文字固定
            'num1'  => 'required|integer|max:10',  // 10以内
            'num2'  => 'required|integer|min:10',  // 10以上
            'num3'  => 'required|integer|size:10', // 10固定
        ];
    }

Eloquent

Model

  • 論理削除を有効にしたい場合はuse SoftDeletes
  • 全モデルでguardedにidを指定しておくと良い。予期せぬキーの登録・更新を防げる。
  • created_atカラムを作成しなかった場合はCREATED_AT = null
  • updated_atカラムを作成しなかった場合はUPDATED_AT = null
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Xxxxx extends Model
{
    use SoftDeletes;

    const CREATED_AT = null;
    const UPDATED_AT = null;

    protected $guarded = [
        'id',
    ];
}

Relation

こんなモデルがあったとして

class User extends Authenticatable
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}
class Post extends Model
{
    use SoftDeletes;

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}
class Comment extends Model
{
    const UPDATED_AT = null;

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

リレーションメソッドでアクセスした時

  • hasOneの戻り値は Model or null
  • hasManyの戻り値はCollection
  • belongsToの戻り値は Model or null

QueryBuilder

  • クエリビルダの場合はjoinするテーブルのdeleted_atが機能しない
    User::join('posts', 'users.id', '=', 'posts.user_id')->get();
    select * from users inner join posts on users.id = posts.user_id
  • joinされるテーブルがModelの場合はdeleted_atが機能する
    Post::join('comments', 'posts.id', '=', 'comments.post_id')->get();
    select * from posts inner join comments on posts.id = comments.post_id where posts.deleted_at is null

Relation Query

hasで流れるSQL

    User::has('posts.comments')->get();
[2020-12-05 23:39:48] local.DEBUG: SQL {"time":"12.82 ms","sql":"select * from `users` where exists (select * from `posts` where `users`.`id` = `posts`.`user_id` and exists (select * from `comments` where `posts`.`id` = `comments`.`post_id`) and `posts`.`deleted_at` is null)"} 
select * from users
where exists (
    select * from posts
    where users.id = posts.user_id
    and exists (
        select * from comments
        where posts.id = comments.post_id
    )
    and posts.deleted_at is null
)

joinで流れるSQL

    User::join('posts', 'posts.user_id', '=', 'users.id')
        ->join('comments', 'comments.post_id', '=', 'posts.id')
        ->whereNull('posts.deleted_at')
        ->get();
[2020-12-06 00:00:13] local.DEBUG: SQL {"time":"12.11 ms","sql":"select * from `users` inner join `posts` on `posts`.`user_id` = `users`.`id` inner join `comments` on `comments`.`post_id` = `posts`.`id` where `posts`.`deleted_at` is null"} 
select * from users
inner join posts on posts.user_id = users.id
inner join comments on comments.post_id = posts.id
where posts.deleted_at is null

withで流れるSQL

    User::with('posts.comments')->get();
[2020-12-05 23:49:37] local.DEBUG: SQL {"time":"11.54 ms","sql":"select * from `users`"} 
[2020-12-05 23:49:37] local.DEBUG: SQL {"time":"0.70 ms","sql":"select * from `posts` where `posts`.`user_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20) and `posts`.`deleted_at` is null"} 
[2020-12-05 23:49:37] local.DEBUG: SQL {"time":"0.41 ms","sql":"select * from `comments` where `comments`.`post_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)"} 
select * from users
select * from posts where posts.user_id in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20) and posts.deleted_at is null
select * from comments where comments.post_id in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Console

複数時間でタスクスケジューリングしたい場合(8:30、11:30、18:30など)はcron形式にする。

app/Console/Kernel.php

->cron('30 8,11,18 * * *');
コンソール出力時にログにも残す

トレイトを作成してコマンドクラスでuse PrependsOutput;してあげればOK
普通に$this->info()$this->error()するだけでログに残せます。
テスト時に実行時間を出力していない理由は、コンソール出力値もテストしているためです。

app/Console/Commands/PrependsOutput.php

<?php

namespace App\Console\Commands;

use Carbon\CarbonImmutable;

trait PrependsOutput
{
    /**
     * コンソール出力に追加
     *
     * @param  string  $string
     * @param  string|null  $style
     * @param  int|string|null  $verbosity
     * @return void
     */
    public function line($string, $style = null, $verbosity = null)
    {
        if (\App::environment() === 'testing') {
            parent::line($string, $style, $verbosity);
        } else {
            parent::line(CarbonImmutable::now()->format('[Y-m-d H:i:s] ').$string, $style, $verbosity);
        }
        logger(CarbonImmutable::now()->format('[Y-m-d H:i:s] ').$string);
    }
}

PHPUnitテスト

  • テストが遅い時はxdebugを無効にしてみてください。5倍くらい違う!
# テスト実行
./vendor/bin/phpunit

# 対象のファイルでテスト実行
./vendor/bin/phpunit tests/Feature/XxxxxTest.php

# 対象のファイルの対象の関数でテスト実行
./vendor/bin/phpunit tests/Feature/XxxxxTest.php --filter=xxxxxxxx

# テスト結果をログに残す
./vendor/bin/phpunit --testdox-text=test.txt

他いろいろ

実行SQLを確認したい

下記を参考にしてちょっと直しました
LaravelでSQL文をlaravel.logに出力する

下記をregisterに追加

app/Providers/AppServiceProvider.php

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        // SQL Log
        \DB::listen(function ($query) {
            $sql = preg_replace('/"(.*?)"/', "'$1'", $query->sql);
            for ($i = 0; $i < count($query->bindings); $i++) {
                $bindValue = $query->bindings[$i];
                if (is_bool($bindValue)) {
                    $bindValue = $bindValue ? 'true' : 'false';
                } else {
                    $bindValue = "'".(string)$bindValue."'";
                }
                $sql = preg_replace("/\?/", $bindValue, $sql, 1);
            }
            \Log::debug("SQL", ["time" => sprintf("%.2f ms", $query->time), "sql" => $sql]);
        });
    }

Laravelログ+Slackにもログを残す

  1. stackのchannelsにslackを追加
  2. slackのlevelをcritical → debugに変更
  3. envのLOG_SLACK_WEBHOOK_URLにslackのwebhookUrlを指定

config/logging.php

    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => [
                'single',
                'slack', // 追加
            ],
            'ignore_exceptions' => false,
        ],
        
        'slack' => [
            'driver' => 'slack',
            'url' => env('LOG_SLACK_WEBHOOK_URL'),
            'username' => 'Laravel Log',
            'emoji' => ':boom:',
            'level' => 'debug', // critical → debugに変更
        ],

.env

LOG_SLACK_WEBHOOK_URL=xxxxxx

stackのchannelsには複数のdriverが使えるので、fatalエラーと単純なログをチャンネルごとに分けると便利です

    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => [
                'single',
                'slack-critical',
                'slack-debug',
            ],
            'ignore_exceptions' => false,
        ],
        
        'slack-critical' => [
            'driver' => 'slack',
            'url' => env('LOG_SLACK_WEBHOOK_URL'),
            'username' => 'Laravel critical Log',
            'emoji' => ':boom:',
            'channel' => env('LOG_SLACK_CHANNEL_ALERT'),
            'level' => 'critical',
        ],
        'slack-debug' => [
            'driver' => 'slack',
            'url' => env('LOG_SLACK_WEBHOOK_URL'),
            'username' => 'Laravel debug Log',
            'emoji' => ':memo:',
            'channel' => env('LOG_SLACK_CHANNEL_DEBUG'),
            'level' => 'debug',
        ],
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelでapiを叩いたときにjsonが返ってこない問題

確認時のLaravelバージョンは7.26.1

apiを叩いたときにjsonが返ってこない問題

結論から言うとapiを叩く際に下記ヘッダーを付与しなければならない

Accept: application/json
or
X-Requested-With: XMLHttpRequest

個人的にはX-Requested-With: XMLHttpRequestで良いかと思います。view側でaxios使用時に使われているので

resources/js/bootstrap.js

window.axios = require('axios');

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

原因調べてみた

ソースを追っていくとHandler::expectsJsonがfalseになってしまうのが原因

1. app/Exceptions/Handler::render
2. vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler::render
3. vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler::expectsJson

Handler::expectsJsonの実装を見るとこんな感じ

    public function expectsJson()
    {
        return ($this->ajax() && ! $this->pjax() && $this->acceptsAnyContentType()) || $this->wantsJson();
    }

各メソッドを引っ張ってくるとこんな感じ

    public function ajax()
    {
        return $this->isXmlHttpRequest();
    }

    public function isXmlHttpRequest()
    {
        return 'XMLHttpRequest' == $this->headers->get('X-Requested-With');
    }

    public function pjax()
    {
        return $this->headers->get('X-PJAX') == true;
    }

    public function acceptsAnyContentType()
    {
        $acceptable = $this->getAcceptableContentTypes();

        return count($acceptable) === 0 || (
            isset($acceptable[0]) && ($acceptable[0] === '*/*' || $acceptable[0] === '*')
        );
    }

    public function getAcceptableContentTypes()
    {
        if (null !== $this->acceptableContentTypes) {
            return $this->acceptableContentTypes;
        }

        return $this->acceptableContentTypes = array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all());
    }

    public function wantsJson()
    {
        $acceptable = $this->getAcceptableContentTypes();

        return isset($acceptable[0]) && Str::contains($acceptable[0], ['/json', '+json']);
    }

これ外部のapiクライアント使った時のハマりポイントですね
公式ドキュメントに乗っけて欲しい。。。

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

【Laravel】validationのエラーメッセージ「The given data was invalid.」をカスタマイズする

確認バージョンは Laravel 7.26.1

FormRequestのバリデーションを追って見るとこんな感じ

1. config/app.phpのproviders
2. Illuminate\Foundation\Providers\FoundationServiceProvider::class
3. vendor/laravel/framework/src/Illuminate/Foundation/Providers/FormRequestServiceProvider::boot
4. vendor/laravel/framework/src/Illuminate/Validation/ValidatesWhenResolvedTrait::validateResolved
5. vendor/laravel/framework/src/Illuminate/Validation/ValidatesWhenResolvedTrait::failedValidation
6. vendor/laravel/framework/src/Illuminate/Validation/ValidationException::__construct

vendor/laravel/framework/src/Illuminate/Validation/ValidationException::__construct
でexceptionのメッセージをThe given data was invalid.でハードコーディングしているのが問題である

なのでapp/Exceptions/Handler.phpをカスタマイズする。
HandlerはIlluminate\Foundation\Exceptions\Handlerを継承しているので、そのソースを参考に下記メソッドをオーバーライド

    /**
     * Convert a validation exception into a JSON response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Illuminate\Validation\ValidationException  $exception
     * @return \Illuminate\Http\JsonResponse
     */
    protected function invalidJson($request, ValidationException $exception)
    {
        return response()->json([
            'message' => $exception->getMessage() === 'The given data was invalid.' ? '指定されたデータは無効でした。' : $exception->getMessage(),
            'errors' => $exception->errors(),
        ], $exception->status);
    }

うーん…ハードコーディングしてる方が悪いと思う。

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

【Laravel】Eloquentのhasは1階層と2階層両方書く必要はない

確認バージョンは Laravel 7.26.1

例えばこんなモデルがあったとすると

class User extends Authenticatable
{
    /**
     * Relations
     */
    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}
class Post extends Model
{
    use SoftDeletes;

    /**
     * Relations
     */
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}
class Comment extends Model
{
    const UPDATED_AT = null;

    /**
     * Relations
     */
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

一階層のhas

    User::has('posts')->get();
select * from users
where exists (
    select * from posts
    where users.id = posts.user_id
    and posts.deleted_at is null
)

二階層のhas

    User::has('posts.comments')->get();
select * from users
where exists (
    select * from posts
    where users.id = posts.user_id
    and exists (
        select * from comments
        where posts.id = comments.post_id
    )
    and posts.deleted_at is null
)

一階層&二階層のhas

    User::has('posts')
        ->has('posts.comments')
        ->get();
select * from users
where exists (
    select * from posts
    where users.id = posts.user_id
    and posts.deleted_at is null
)
and exists (
    select * from posts
    where users.id = posts.user_id
    and exists (
        select * from comments
        where posts.id = comments.post_id
    )
    and posts.deleted_at is null
)

上記の通り、一階層と二階層両方書く必要はないです!

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

Laravel 7 Socialiteを使ってLINE認証を実装してみる。

はじめに

Laravel7でLINE認証機能を実装しました。
記事自体はポチポチあるけど、古い記事が多いしやり方も色々あってなんかうまくいかない...。
少し手間取りましたが、無事実装できたので備忘録として纏めます。

偉大な参考記事

ほとんど下記URLの作成者様のコーディングを参考にして書いております。
https://tdomy.com/2020/08/how-to-use-laravel-socialite/

上記記事に加えてLINEのDevelopersの詳細等を補足して説明します。
上記記事ではLaravel6っぽいです。
私のはLaravel7でやってみます。

実装環境

・XAMPP
・Windows10
・PHP 7.4.11
・Laravel 7.31

LINE Developersで諸々登録

まずLINE Developersに登録してアプリで使うidやらなんやらを取得します。

登録したら早速プロバイダーを作成してみましょう。名前は何でもオッケーです。
LINEプロバイダー作成.png.jpg

そしたらLINEログインチャンネルを作成しましょう。

LINE選択画面.png.jpg

チャネル作成の画面でチャネル名だのなんだのを設定できます。
ここでの名前は自分の覚えやすい名前等で登録します。

・チャネルタイプはLINE Loginを選択
・アプリの種類は該当のものにチェック
その他は埋めるところは埋めましょう。

チャネル.png.jpg

チャネルを作成できたら上記画面のようになります。
このページでチャネルIDとチャネルシークレットは後々使用します。
ここのLINEログインをクリックしてコールバックURLを設定しましょう。
URLはhttps://localhost/アプリ名/public/login/line/callback みたいな感じで。

それでは次はログインで使用するLINEアカウントに権限をつけましょう。
LINE Developersで登録したアカウントを今回ログインで使用するものと同じであればやらなくておkです。
ビジネスアカウントとかで登録している場合は下記をしないとエラーになるのでやりましょう。

{DBE7073A-DD54-4C25-B1A1-8CF5592ACD98}.png.jpg
権限はAdminにしてください。

これで準備オッケー!
それではLaravelに移りましょー。

Socialiteを導入

Laravelのcreateからauthの導入は省きます。
それでは下記コマンドでsocialiteを入れましょう。

$composer require laravel/socialite

これでfacebookやgoogleとかは使えるのですが、
残念ながらLINEは対象外...。
ということで追加でLINE専用プロパイダーを入れましょう。

$ composer require socialiteproviders/line

これでプロパイダーはオッケー。次は環境変数ですね。

環境変数を入れる

先ほどLINE Developersで取得したIDだのを記載します。

config/service.php
'line' => [
      'client_id'     => env('LINE_CLIENT_ID'),
      'client_secret' => env('LINE_CLIENT_SECRET'),
      'redirect'      => env('LINE_CLIENT_CALLBACK'),
  ],
.env
LINE_CLIENT_ID=ここにclientIDを入れる
LINE_CLIENT_SECRET=ここにシークレットIDを入れる
LINE_CLIENT_CALLBACK=/login/line/callback

こんな感じでオッケーです。

プロパイダー追加の記述

LINEのように別途でプロパイダーを追加した場合は下記の記述をします。

config/app.php
'providers' => [

    // デフォルトでいろいろ書かれてます

    SocialiteProviders\Manager\ServiceProvider::class, // ここを追記
];

もういっちょ

app/Providers/EventServiceProvider.php
protected $listen = [

    // デフォルトでなんかいろいろ書かれてる

    \SocialiteProviders\Manager\SocialiteWasCalled::class => [
        'SocialiteProviders\\Line\\LineExtendSocialite@handle', //ここの二行を追記
    ],
];

よっしゃこれでSocialiteを使う準備オッケー!

Usersテーブルを変更する

LaravelではデフォルトでUsersテーブルがありますが、
今回のユーザー新規登録ではname,provider,provided_user_idで登録させるとします。

migrations/2014_10_12_000000_create_users_table.php
<?php

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

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
          $table->bigIncrements('id');
          $table->string('name');
          $table->enum('provider', ['line', 'twitter']);
          $table->string('provided_user_id');
          $table->timestamps();

          $table->unique(['provider', 'provided_user_id']);
        });
    }

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

編集し終えたら忘れずにmigrateします。

Userモデルを実装

Userモデルもデフォルトであるのでそれを使います。

app/User.php
<?php

namespace App;

use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Illuminate\Auth\Authenticatable;

class User extends Model implements AuthenticatableContract
{
    use Authenticatable, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'provider', 'provided_user_id',
    ];

    /**
     * Get the password for the user.
     *
     * @return string
     */
    public function getAuthPassword()
    {
        // We don't use password login.
        return '';
    }

    /**
     * Get the column name for the "remember me" token.
     *
     * @return string
     */
    public function getRememberTokenName()
    {
        // We don't use this.
        return '';
    }
}

これでモデルもオッケーです。
次はコントローラー記述です。

コントローラー実装

Authを作ったときに自動でLoginControllerが作られるのでそれを使います。

app/Http/Controllers/Auth/LoginController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Support\Facades\Auth; //追記
use Laravel\Socialite\Facades\Socialite; //追記
use App\User; //追記


class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    use AuthenticatesUsers;

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = RouteServiceProvider::HOME;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }

    /**ここから下が追記 */
    /**
     * Redirect the user to the provider authentication page.
     *
     * @param string $provider
     * @return \Illuminate\Http\Response
     */
    public function redirectToProvider($provider)
    {
        return Socialite::driver($provider)->redirect();
    }

    /**
     * Obtain the user information from the provider.
     *
     * @param string $provider
     * @return \Illuminate\Http\Response
     */
    public function handleProviderCallback($provider)
    {
        $provided_user = Socialite::driver($provider)->user();

        $user = User::where('provider', $provider)
            ->where('provided_user_id', $provided_user->id)
            ->first();

        if ($user === null) {
            // redirect confirm
            $user = User::create([
              'name'               => $provided_user->name,
              'provider'           => $provider,
              'provided_user_id'   => $provided_user->id,
            ]);
        }

        Auth::login($user);

        return redirect()->route('home');
    }

    /**
     * Log the user out of the application.
     *
     * @return \Illuminate\Http\Response
     */
    public function logout()
    {
        Auth::logout();

        return redirect()->route('home');
    }
}

return redirect先のパスは各自好きなrouteを指定してください。
今回はデフォルトでルート付けされているhomeにしています。

ルーティングを作る

さて、最後にloginとlogoutに関するルーティングをして終わりです!

routes/web.php
<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Auth::routes();

Route::get('/home', 'HomeController@index')->name('home');

Route::get('/login/{provider}', 'Auth\LoginController@redirectToProvider');
Route::get('/login/{provider}/callback', 'Auth\LoginController@handleProviderCallback');
Route::get('/logout', 'Auth\LoginController@logout')->name('logout');

これでルート付けもできた!ということでLineログインまで飛んでみましょう。
今回はURLで飛んでみます。
https://locallhost/アプリ名/public/login/line で飛んでみると....。

ログイン.png.jpg

ここでメアドとパスワードを入れてみる。

ログイン後.png.jpg

You are logged in!!!!!!!!!!!!!!!!!
やったー!ログインできた!ついでにMySQLを見るとしっかりidが追加されてました!

感想

Laravel7の記事がとても少ないし、LINEのプロパイダー記事も古い記事が多く割と躓きました。
参考記事の方はlaravel6だったのですが、同じ方法で7もできました。
他のプロパイダーを追加した場合の記事も完成次第投稿したいと思います。

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

Docker でphp8とlaravel8を動かす

はじめに

php8が公開されたので触ってみたい興味でDockerでの環境を作ってみました。
公式のDocker hubにはまだrc版しかなかったので、今回はrc版で作ってます。

github

使い方

↑のgithubからソースをクローンして、ディレクトリに移動してください。
入ったディレクトリで

make set-up

を入力したら環境が出来上がると思います。
終わったらhttp://127.0.0.1:8000/ にアクセスして↓の様になっていれば完成です。
スクリーンショット 2020-11-27 10.24.49.png

詳しい説明はそのうち書くかもしれません

おわりに

今回の記事を参考にphp8をどんどん触っていけたらいいなと思います。
間違いが合ったりしたら、コメントやPRお願いします!

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

Laravel 6.x 非同期通信(Ajax) 【JavaScript】 【jQuery】 【axios】 【Vue.js】 各記述方法 ~jQuery編~

制作環境

Windows 10
Laravel : 6.18.35
Laravel/ui : 1.0
Laravel-mix : 5.0.1
Bootstrap : 4.0.0
axios : 0.19
Vue : 2.5.17
XAMPP
PHP : 7.4.3
Visual Studio Code

はじめに

この記事はプログラミングをはじめたばかりの素人が、できたことをメモするのに利用しています。
内容には誤りがあるかもしれません。

【2020/11/27 古い書き方での記述だったので、書き方を変更しました。】

前回のつづきになります。

関連記事

Laravel 6.x 非同期通信(Ajax) 【JavaScript】 【jQuery】 【axios】 【Vue.js】 各記述方法 ~事前準備編~
Laravel 6.x 非同期通信(Ajax) 【JavaScript】 【jQuery】 【axios】 【Vue.js】 各記述方法 ~JavaScript編~

今回作成するもの

非同期通信(Ajax)を利用し、自動で仕入れ先が入力される機能を実装します。
前回JavaScriptで作成した内容を、jQueryで作成したいと思います。

以下作成条件

  • 記述にはjQueryを使用します。
  • 数字が4桁入力されたら自動で仕入れ先を検索し、反映します。
  • 検索するのにクリック操作を必要としません。inputに入力されたら検索します。
  • スクリプトの読み込みにはLaravel-mixを使用します。
  • 前回作成したtest.jsを編集して使用するで、必要ならtest2.js等コピーを作成してバックアップしてください。

完成イメージ

image2.jpg

スクリプトの作成

resources>js内のtest.jsを開き、以下のように記述します。
※前回のJavaScriptの内容は全て削除してください。

以下の記述方法は古い書き方だったので、記述を変更しました。

記述変更前

test.js
$(function() {
    // inputのid="code"(業者コード)の要素を取得し、inputイベントを監視します
    // 業者コードに値が入力されたら以下の内容を実行します
    $('#code').on('input', function() {

        // 入力された値を変数traderCodeに代入します
        let traderCode = $('#code').val();

        // 入力された値が4桁未満の時は検索させないようにします
        if (traderCode.length < 4) {
            return;
        }

        // 非同期通信を開始します
        $.ajax({
            // アクセスするurlを設定します
            url: '/form_search?trader_code=' + traderCode,
            // アクセスの方法を設定します
            type: 'GET',
            // コントローラから受け取ったデータ(検索結果)をdataに代入し以下の処理を実行します
            success: function(data) {
                 // 受け取ったデータ(検索結果)を仕入れ先のvalueに反映します
                $('#supplier').val(data[0].trader_name);
            }
        })
    })
})

記述変更後

test.js
$(function() {
    // inputのid="code"(業者コード)の要素を取得し、inputイベントを監視します
    // 業者コードに値が入力されたら以下の内容を実行します
    $('#code').on('input', function() {

        // 入力された値を変数traderCodeに代入します
        let traderCode = $('#code').val();

        // 入力された値が4桁未満の時は検索させないようにします
        if (traderCode.length < 4) {
            return;
        }

        // 非同期通信を開始します
        $.ajax({
            // アクセスするurlを設定します
            url: '/form_search?trader_code=' + traderCode,
            // アクセスの方法を設定します
            type: 'GET',
        // コントローラから受け取ったデータ(検索結果)をdataに代入し以下の処理を実行します
        // successの代わりにdoneを使用します
        }).done(function(data) {
            // 受け取ったデータ(検索結果)を仕入れ先のvalueに反映します
            $('#supplier').val(data[0].trader_name);
        })
    })
})

コンパイル

ターミナルを起動し、以下を実行してください。
※node.jsが必要です。インストールされていない方は、インストールを行ってください。

node.js
https://nodejs.org/ja/

npm run dev

確認

その他はJavaScriptの時に作成したものがそのまま使えるので、実際にJavaScriptの時と同じ動作をするか確認してみてください。

$.ajaxについて

console.log$.ajaxを確認すると、以下のようなデータが返ってきているのがわかります。

response.jpg

responseJSONresponseTextに目的のデータがあるのがわりますが、responseTextの方はJSON.parseされていないので、業者名がおかしいです。

success: function(data)について この書き方は古いので、doneを使用した記述にしてください。

前回のJavaScriptではresponseTextを使用してコントローラからデータを受け取りましたが、今回は引数に返り値を代入したい変数名を入れるだけで、データを受け取れます。
dataの部分は好きな値で大丈夫です。

console.log(data)で確認すると、欲しい値が代入されているのがわかります。

log.jpg

.done(function(data)について

.doneを指定すると同じように値を取得できます。

成功はdoneを使用し、失敗はfailを使うことで条件の分岐ができます。

今回はこれで終了です。
次回はaxiosで同じ事をしてみます。

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

switch文[PHP]

switch文をPHPで書く

if, elseifによる分岐が多く複雑な場合、switch文で書き換えるとシンプルで読みやすいコードにできる。switch(式)の(式)がcaseの値と一致したとき、そのブロックが実行される。caseのどれにも一致しなかった時、defaultのブロックが実行される
1470803893312.png

break

break命令は現在のブロックから脱出するための命令。break命令がないと、後ろに続くcaseブロックが続けて実行されてしまう

    switch($remainder) {
      case 0:
        echo "大吉です。";
        break;

      case 1:
        echo "中吉です。";
        break;

      case 2:
        echo "小吉です。";
        break;

      default:
        echo "凶です。";
        break;
    }

以上!

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