20201020のPHPに関する記事は20件です。

[Codewars] Build Tower

概要

Codewarsの問題 Build Tower の回答の復習とベストプラクティスをまとめる個人メモです。

問題

数値が渡ってくるので、以下のように*の数値分の階層のピラミッドを作成する問題です。

Build Tower by the following given argument:
number of floors (integer and always greater than 0).
Tower block is represented as *

[
  '  *  ', 
  ' *** ', 
  '*****'
]

[
  '     *     ', 
  '    ***    ', 
  '   *****   ', 
  '  *******  ', 
  ' ********* ', 
  '***********'
]

回答

規則性として、最大のアスタリスクの数は$n * 2 -1、アスタリスクの数が$i * 2 -1、空白の数が*の最大値 - *の数 / 2と定義できます。
数を決めた後、str_repeatを使用しピラミッドを表現させます。

function tower_builder(int $n): array {
  $max = $n * 2 - 1;
  $array = [];
  for($i=1;$i <= $n;$i++){
    $asterisk = $i * 2 - 1;
    $empty = ($max - $asterisk) / 2;
    $result = str_repeat(" ", $empty) . str_repeat("*", $asterisk) . str_repeat(" ", $empty);
    array_push($array, $result);
  }
  return $array;
}

ベストプラクティス

str_padを使用し文字列を埋めています。
STR_PAD_BOTHを引数に取ることで、第一引数の値が真ん中にくるようになっています。
https://www.php.net/manual/ja/function.str-pad.php

function tower_builder(int $n): array {
    $pad = $n * 2 - 1;
    $x = 1;

    $arr = [];
    while ($n --> 0) {
        $arr[] = str_pad(str_repeat('*', $x), $pad, ' ', STR_PAD_BOTH);
        $x += 2;
    }

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

phpで簡単パイプライン関数

関数型プログラミングを学習する中でパイプライン演算子に出会い、惚れてしまいました。phpで似たようなことができないかと思い、今回の実装を行いました。

まだまだ若輩者ですのでアドバイス等いただけると嬉しいです。

前提

  1. 単一引数の関数を対象とする
  2. 中置記法が使えないので |> の形は諦める
  3. array_mapみたく不格好でもいいからとりあえずpiplineぽいことがしたい

ソース(github)

<?php

function array_pipeline($data, array $functions)
{
    $recursive = function ($data, $count = 0) use ($functions, &$recursive)
    {
        if ($count === count($functions)) return $data;
        return $recursive($functions[$count]($data), $count+1);
    };
    return $recursive($data);
}

実装のポイント

  • phpは「'関数名'()」で関数を呼べるので、関数名をarrayで受け取ることにした
  • 関数内関数を無名関数にしないと再帰的に関数を定義してしまい、エラーになる
  • 無名関数を再帰的に実行するにはuseで変数を渡すときに参照渡しにする必要がある

使用例

<?php

require 'pipeline.php';

function double($data)
{
    return $data * 2;
}

function addTwo($data)
{
    return $data + 2;
}

echo array_pipeline(5, ['double', 'addTwo']);
// 12

echo "\n";

echo array_pipeline(5, ['addTwo', 'double']);
// 14

今後の展望

  • array_pipeline($data, [['add', 3], ['minus', 7]]); のようにして単一引数の関数という制約を外したい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel/PHP タグ付き投稿の検索機能(orWhereHasと無名関数、クロージャ)

やりたいこと

ジャズライブの口コミをタグ付きで投稿できるサイトを、チームで開発しています。
口コミ投稿をキーワード検索するコードをネットを参考にしながら書いたのですが、一部読み解けなかった部分を整理したいと思います。

仕様

  • キーワードが入力されると、そのキーワードを含む投稿を検索しビューに表示する。
  • 検索対象は、投稿の(1)タイトル、(2)本文、(3)投稿につけられたタグ。
  • 投稿のテーブル、タグのテーブルはEloquentでModelを利用している。
  • 投稿のテーブルとタグのテーブルが中間テーブルを挟んで多対多のリレーションでつながっている。

実装してみた

コントローラの実装例

ルート、ビューなどは省略させていただきます。もし見せて!という方いたらコメントください(いないと思いますが汗)。

SearchController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Review;
use App\Tag;

class SearchController extends Controller
{
    public function search(Request $request)
    {
        $keyword = $request->input('keyword');
        if (!empty($keyword))
        {
            $reviews = Review::where('title', 'like', '%' . $keyword . '%')
            ->orWhere('text', 'like', '%' . $keyword . '%')
            ->orWhereHas('tags', function ($query) use ($keyword){
                $query->where('tag_name', 'like', '%' . $keyword . '%');
            })
            ->paginate(10);
        } else {
            return redirect('/');
        }

        return view('reviews.search', [
            'reviews' => $reviews
        ]);
    }
}

orWhereHasからの2行がよくわからない!!ココを解明したい!!

orWhereHasとは?

            ->orWhereHas('tags', function ($query) use ($keyword){
                $query->where('tag_name', 'like', '%' . $keyword . '%');

orWhereHasはEloquentのメソッド、関数。わかってたふりしてましたが、案外うまく説明できなかったので整理。公式ドキュメント→Eloquent:存在するリレーションのクエリ
orは、2つ以上の条件をつけて「AもしくはB」としたい時に、2つめ以降の条件のメソッドにつける言葉。
Whereは、SQLのわかる方なら想像しやすいかもですが、問い合わせに条件をつけるもの。例えばidがホニャララなもの、とかcreated_atがいついつ以前のものといった条件が想定できるかと思います。
Hasはあるモデルからリレーション先のテーブルに対してレコードを探してくれるメソッド。リレーション名を第一パラメータに渡します。例えば、以下のようにするとタグの付いたレビューを取得するという命令になります。

$reviews = Review::has('tags')->get();

まとめるとorWhereHasは、リレーション先にレコードを(Has)、条件付きで(Where)、並列の条件(or)として探しにいく時に便利なEloquentのメソッドです。

orWhereHasの第二パラメータの関数は何?

            ->orWhereHas('tags', function ($query) use ($keyword){
                $query->where('tag_name', 'like', '%' . $keyword . '%');

このfunctionから始まる2行は、無名関数、またの名をクロージャというやつみたいです。公式→PHP 無名関数。名前ないですものね。無名関数はどういう時に使うかというと、プログラムの中でその場で一度しか使わないものに適切みたいです。ちなみに、Closureというクラスで実装されているそうです。紐解いていきます。

順番前後しますが、use ($keyword)は、その関数の親のスコープから変数$keyordを引き継ぐ時にuseで渡すようです。
($query)は変数ではなく引数。通常の引数を受け付けています。この無名関数の中では、「tag_nameに$keywordを含んだレコードを」という条件設定をしているwhereメソッドにアクセスしています。

総合して一言でこの第2パラメータを説明すると、親スコープの$keywordを引き継いで作成する、whereという条件設定をする関数という感じでしょうか。

最後に

もしご指摘などあれば、バシバシお願い申し上げます!

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

【Laravel】public/css内のCSSを読み込まないエラー 〜 net::ERR_CONNECTION_CLOSED〜

【背景】

AWSからVagrantにローカル環境を変えて、開発を行う。
Viewを作成時に、コンパイルしたcssを読み込もうしても、Viewに反映されず行き詰まる。

【状況の確認】

Bladeファイルのheadタグ内にCSSを読み込む記述がされているか確認。

 <link href="{{ secure_asset('css/app.css') }}" rel="stylesheet">
 <link href="{{ secure_asset('css/style.css') }}" rel="stylesheet">

次にchromeの検証ツールでエラー内容を確認。エラーは検証ツール画面の右上の設定マークの横にある赤いバツ❌をクリックします。
スクリーンショット 2020-09-07 17.47.18.png
net::ERR_CONNECTION_CLOSED と表示されています。
どうやら接続ができない様子。Javascriptの方もできていない。

【解決法】

Blade内のsecure_の部分を消す。

 <link href="{{ asset('css/app.css') }}" rel="stylesheet">
 <link href="{{ asset('css/style.css') }}" rel="stylesheet">

同ファイル内のJavascriptの部分も直します。

 <script src="{{ asset('js/app.js') }}" defer></script>

これでブラウザで確認すればCSSが反映されていると思います。

【原因】

Web環境のhttpsを使った環境だとsecure_を付ける必要がある。
今回、開発環境をAWSからローカル環境のVagrantに変えたので、記述を変える必要があったようです。

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

php 画像 削除

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

php slimのRequestオブジェクトからリクエストURLの絶対URLを取得する

困ったこと

PHPのSlim フレームワークで開発をしていてリクエストの絶対URLが取得したかった。

TL;DR

ストリングでキャストするとでてくる。

(string)$this->request->getUri();

経緯

slimで開発をしていて、ちょっとリクエストされたURIをセッションに保存して、別の処理をした後に、セッションから読み出して、リダイレクトで戻りたかった。

PSR-7のRequestオブジェクトが、こんな感じで、これを自分で結合していくのは面倒。

何かよい方法あるはずって探した。

‌‌> $this->request->getUri()
< Slim\Psr7\Uri::__set_state(array(
   'scheme' => 'https',
   'user' => '',
   'password' => '',
   'host' => 'example.com',
   'port' => 443,
   'path' => '/oauth2/authorize',
   'query' => 'state=xxxxxx',
   'fragment' => '',
))

キャストするだけだった(笑)

> ‌‌(string)$this->request->getUri()
< https://example.com/oauth2/authorize?state=xxxxx

参考文献

https://discourse.slimframework.com/t/building-the-full-request-url/626

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

PHPからPDOを使ってSQLServerのストアドプロシージャを呼び出す際にOUTPUTが効かなくなる現象への対応

ドはまりしてしまったので、後学のためにメモを残します。

事象

次のようなスクリプトで、データ投入用のテーブルおよびプロシージャを作成します。

-- データ投入用のテーブル作成
create table test_table (
  col1 varchar(50),
  col2 int,
);

-- プロシージャ作成
create procedure sample
  @in varchar(50),
  @out int OUTPUT
as
  set @out = 567;
  insert into test_table values (@in, @out);
;

PDOから次のようなスクリプトでプロシージャを実行します。

$pdo = new PDO($dsn, $username, $password);
$stmt = $pdo->prepare('{CALL sample (?, ?)}'); // 'EXEC sample ?, ?'でもOK
$in = 'xyz';
$stmt->bindParam(1, $in, PDO::PARAM_STR);
$stmt->bindParam(2, $out, PDO::PARAM_INT|PDO::PARAM_INPUT_OUTPUT);
$stmt->execute();
echo $out;

プロシージャ内でset @out = 567と記述したので$outには567が設定されており、結果として567が出力される想定でしたが、結果が異なりました。

プロシージャがうまく動いているかどうか確認するため、作成したテーブルに格納されたデータを取得したところ、想定通りのデータが入っており、特に問題なさそうでした。

select * from test_table; -- 「'xyz', 567」が取得できる

ここで、プロシージャ内のinsert文をコメントアウトしたところ、想定通りのOUTPUT変数の使い方が出来ました。

-- プロシージャ作成
alter procedure sample
  @in varchar(50),
  @out int OUTPUT
as
  set @out = 567;
  -- insert into test_table values (@in, @out);
;
$pdo = new PDO($dsn, $username, $password);
$stmt = $pdo->prepare('{CALL sample (?, ?)}'); // 'EXEC sample ?, ?'でもOK
$in = 'xyz';
$stmt->bindParam(1, $in, PDO::PARAM_STR);
$stmt->bindParam(2, $out, PDO::PARAM_INT|PDO::PARAM_INPUT_OUTPUT);
$stmt->execute();
echo $out; //「567」が出力される

解決策

プロシージャ内で SET NOCOUNT ON の記述を入れることで、PHP側の変数に値が入るようになります。

-- プロシージャ作成
alter procedure sample
  @in varchar(50),
  @out int OUTPUT
as
  set @out = 567
  SET NOCOUNT ON
  insert into test_table values (@in, @out)
;
$pdo = new PDO($dsn, $username, $password);
$stmt = $pdo->prepare('{CALL sample (?, ?)}'); // 'EXEC sample ?, ?'でもOK
$in = 'xyz';
$stmt->bindParam(1, $in, PDO::PARAM_STR);
$stmt->bindParam(2, $out, PDO::PARAM_INT|PDO::PARAM_INPUT_OUTPUT);
$stmt->execute();
echo $out; //「567」が出力される

SET NOCOUNT ONとは

https://docs.microsoft.com/ja-jp/sql/t-sql/statements/set-nocount-transact-sql?view=sql-server-ver15

Transact-SQL ステートメントまたはストアド プロシージャで処理された行数を示すメッセージが結果セットの一部として返されないようにします。

参考

この記事のおかげで助かりました。
https://trentrichardson.com/making-sense-of-stored-procedures-with-php-pdo-and-sqlsrv.html

You also may notice the empty result set at position 0. This is the insert query (not the first select query as it is simply assigning a value). To suppress these you should use "SET NOCOUNT ON" in your stored procedure. At that point insert, update and deletes will not return these empty sets.

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

PHP 多次元配列

初めてのPHPの投稿になります。
今回はPHPの多次元配列を備忘録として投稿させて頂きます。

多次元配列とは

配列の中に配列を定義することを指します。
また、横と縦に並んでいることをイメージするとよりわかりやすいでしょう。
これを多次元配列」と呼び、よくプログラムコードでも使用される配列の形です。

考え方

下記にて分かりやすい図がありましたので参照して下さい。
横のHairetu (0) (1) (2)から数えます。
次に縦のHairetu (0) (1) (2) (3)から数えます。
これでどこの要素を取り出したいかを指定できます。

“スクリーンショット” 2020-10-19 22.15.57.jpg

サンプルコードを掲載します。

sample.php
こちらの配列は分かりやすい様に記述したものです。
    0   1   2
0[["1","2","3"],
1["A","B","C"],
2["a","b","c"]];

<?php
$member = [["1","2","3"],["A","B","C"],["a","b","c"]];
echo ($member[2][2]);//c
echo ($member[1][1]);//B
echo ($member [0][0])//0
?>

多次元配列を最初見たときは何これと思いましたがよくよく考えたら分かりやすかったです。

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

php クラス

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

Laravelで超シンプルなToDoアプリを作成する

はじめに

Laravelで超シンプルなToDoアプリを作成する手順を紹介します。
ToDoアプリは、CRUD(Create, Read, Update, Delete)の操作を兼ね揃えた最もシンプルなアプリの一つと言えます。
そのため、学習したい言語やフレームワークを用いてToDoアプリを作成することで、その言語やフレームワークを用いたデータの作成・読み取り・更新・削除といった基本操作を一通り学習することができます。

Laravelを用いてToDoアプリを作成するチュートリアルは公式のものも含めて既にいくつかあると思いますが、この記事では超シンプル・必要最低限の機能だけに絞ったToDoアプリを作成する手順を紹介します。
まずは最低限のToDoアプリを作成してみてLaravelのCRUDアプリ作成の勘所を押さえ、そこから自分が欲しいと思う機能を少しずつ加えながら学習を更に進めるのが良いと思います。

作成するアプリの概要

画面構成

超シンプルにしてLaravelによるCRUD操作に内容をフォーカスしたいので、CSSはほぼ設定していません。
見た目はイケてませんが、そこは機能を実現してから各自の好みで装飾してください。

タスク一覧ページ

タスクの一覧を表示するためのページです。
このページからタスク作成ページ・タスク編集ページ・タスク削除ページへ遷移することができます。

image.png

このページのURLは、アプリのルート(/)にします。

タスク作成ページ

新しいタスクを作成するためのページです。
タスクの情報をフォームに入力し、"追加"ボタンを押すことで新しいタスクを作成することができます。

image.png

このページのURLは/create-pageにします。
また、追加ボタンが押されて実際にタスクを追加するAPIのURLは/createとします。

タスク編集ページ

選択したタスクの内容を編集するためのページです。
選択したタスクの内容が予めフォームに入力されているので、編集したい箇所を書き換えて"修正"ボタンを押すことで内容を書き換えることができます。

image.png

このページのURLは/edit-pageにします。
また、修正ボタンが押されて実際にタスクを修正するAPIのURLは/editとします。

タスク削除ページ

選択したタスクを削除するためのページです。
選択したタスクの内容が表示されるので、確認の上で"削除"ボタンを推すことでタスクを削除することができます。

image.png

このページのURLは/delete-pageにします。
また、削除ボタンが押されて実際にタスクを修正するAPIのURLは/deleteとします。

URL設計

このアプリで使用するURLは以下のとおりです。

URL HTTPリクエストメソッド 用途
/ GET アプリのトップページをレスポンスで返す
/create-page GET タスクの作成ページをレスポンスで返す
/create POST タスクの作成を実行する
/edit-page GET タスクの編集ページをレスポンスで返す
/edit POST タスクの編集を実行する
/delete-page GET タスクの削除ページをレスポンスで返す
/delete POST タスクの削除を実行する

テーブル構成

このアプリは、作成したタスクをデータベース(SQLite)に保存します。
使用するテーブルは"todos"テーブルひとつだけです。

本格的なアプリのデータベースとしてはSQLiteは力不足ですが、LaravelによるCRUDアプリの作成方法にフォーカスしたいので、まずは使うまでの準備が超簡単なSQLiteで慣れましょう。
作成したアプリをサーバで動かしたいといったステップまで進んだら、mysqlやpostgresなどのもう少し本格的なデータベースに切り替えることを検討すればよいと思います。

todosテーブルの構成

todosテーブルは以下のようなカラムを持ちます。

カラムの論理名 カラムの物理名
id id
タスクの名前 task_name
タスクの説明 task_description
担当者の名前 assign_person_name
見積時間(h) estimate_hour

開発環境を整える

PHPとcomposerのインストール

こちらのページを参考にさせていただきました。
Macの場合でもほぼ変わりありませんので、composerのインストールまで済ませておきましょう。
Laravelチュートリアル1 – 環境構築編(Windows)

上記のチュートリアルですが、2以降も大変参考になるので、Laravel初学者は一通り手を動かしてみることをお勧めします。
Laravelチュートリアル2 – ルーティングを理解しよう
Laravelチュートリアル3 -コントローラーで簡単な処理を書いてみよう
Laravelチュートリアル4 -データベースを使用して処理を書いてみよう
Laravelチュートリアル5 – フォームを扱った処理を書いてみよう

SQLite3のインストール

初期データを作成するためにSQLite3も使用します。
以下のページを参考にSQLite3もインストールしておきましょう。
Laravelでsqliteをデータベースとして使用する方法

開発の流れ

超シンプルなToDoアプリを作る流れを順を追って解説します。

Laravelプロジェクトの作成

Laravelのプロジェクトを作成したいディレクトリをターミナルで開き、以下のコマンドを実行します。

composer create-project laravel/laravel=5.7 laravel_todolist

Laravelプロジェクトの作成が成功したら、以下のコマンドで"laravel_todolist"ディレクトリに入り、開発サーバを起動します。

cd laravel_todolist
php artisan serve

http://127.0.0.1:8000へアクセスして、Laravelと表示さてれいれば、開発サーバの起動は成功しています。

データベースを用意

sqliteファイルの作成

作成した"laravel_todolist"ディレクトリで以下のコマンドを実行してsqliteファイルを作成します。

touch database/database.sqlite

LaravelプロジェクトのDB接続情報を書き換え

"laravel_todolist"ディレクトリにある.envファイルをvi等で開いて内容を書き換えます。

vi .env

書き換え対象は以下の行です。

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

SQLiteを使用するために以下のような内容に書き換えて保存します。
#の行はSQLiteでは使わないのでコメントアウトしています)

DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=homestead
# DB_USERNAME=homestead
# DB_PASSWORD=secret

マイグレーションの実行

"laravel_todolist"ディレクトリで以下のコマンドを実行し、マイグレーションを実行してテーブルを作成します。

php artisan migrate

Laravelはデフォルトでusersテーブルとpassword_resetsテーブルのマイグレーションファイルを持っているので、migrateコマンドを実行することでusersテーブルとpassword_resetsテーブルがデータベースに作成されます。

SQLite3による接続確認

"laravel_todolist"ディレクトリで以下のコマンドを実行してデータベースに接続します。

sqlite3 database/database.sqlite

接続ができると、ターミナルが以下のような表示になります。
この画面でSQLを入力・実行することができます。

SQLite version 3.28.0 2019-04-15 14:49:49
Enter ".help" for usage hints.
sqlite>

.tableというコマンドを入力することでテーブルの一覧を見ることができます。
migrateコマンドで作成したusersテーブルとpassword_resetsテーブルが存在することを確認してください。
(migrateを実行した際に、マイグレーションの履歴を保持するためのmigrationsというテーブルも自動的に作成されています)

sqlite> .table
migrations       password_resets  users

以上を確認できたらSQLite3を終了させます。
SQLite3を終了させるにはCtrl+Dを入力します。

ToDoテーブルの作成

機能を開発する前に、データを格納するtodosテーブルをデータベースに作成しておきます。

マイグレーションファイルの作成

"laravel_todolist"ディレクトリで以下のコマンドを実行します。

php artisan make:migration create_todos_table --create=todos

マイグレーションを実行してtodosテーブルを作成

"laravel_todolist/database/migrations"ディレクトリにYYYY_MM_DD_hhmmss_create_todo_table.phpといったフォーマットの名前でファイルが作成されます。これがマイグレーションファイルです。
このファイルをviで開きます。

YYYY_MM_DD_hhmmss_create_todo_table.php
<?php                                                                                                              

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

class CreateTodoTable extends Migration

   /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('todos', function (Blueprint $table) {
            $table->increments('id');
            $table->timestamps();
        });
    }

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

upファンクションに定義されているのが、マイグレーションが実行された際にデータベースに作成されるtodosテーブルの情報です。todosテーブルには以下の情報を加えたいと思います。

カラムの論理名 カラムの物理名
id id
タスクの名前 task_name
タスクの説明 task_description
担当者の名前 assign_person_name
見積時間(h) estimate_hour

upファンクションを以下のように書き換えます。

YYYY_MM_DD_hhmmss_create_todo_table.php
<?php                                                                                                              

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

class CreateTodoTable extends Migration

   /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('todos', function (Blueprint $table) {
            $table->increments('id');
            $table->string('task_name');  // 追加
            $table->text('task_description');  // 追加
            $table->string('assign_person_name');  // 追加
            $table->string('estimate_hour');  // 追加
            $table->timestamps();
        });
    }

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

修正が終わったら、ファイルを保存します。
"laravel_todolist"ディレクトリに戻り、以下のコマンドでマイグレーションを実行してください。

php artisan migrate

SQLite3でdatabase.sqliteを開き、todosテーブルが作成されていることを確認します。

sqlite3 database/database.sqlite
SQLite version 3.28.0 2019-04-15 14:49:49
Enter ".help" for usage hints.
sqlite> .table
migrations       password_resets  todos            users          
sqlite> 

SQLite3で初期データを登録

引き続き、SQLite3で初期データを登録します。
以下のSQLを実行します。

sqlite> INSERT INTO todos(task_name, task_description, assign_person_name, estimate_hour) VALUES("テスト", "テストです", "テストさん", "1");

以下のSQLを実行して、初期データが登録されたことを確認してください。

sqlite> SELECT * FROM todos;
1|テスト|テストです|テストさん|1||

Todoモデルクラスを実装

todosテーブルをLaravelから操作するために、Todoモデルクラスを実装します。

"laravel_todolist"ディレクトリで以下のコマンドを実行して、Todoモデルクラスを生成します。

php artisan make:model Todo

"laravel_todolist/app/Todo.php"というファイルが生成されるので、vi等で開き以下の行を追加して保存します。
ID列を$guardedで指定したので、ID列にはデータの作成時や更新時に値が入らないようになります。(ID列には、データ作成時に自動で連番を入れたいのでこのような指定を行っておきます)

Todo.php
<?php                                                                                                              

namespace App;

use Illuminate\Database\Eloquent\Model;

class Todo extends Model
{
    protected $guarded = ['id'];  // 追加
}

タスク一覧ページを実装

CRUDのR(Read)に該当する機能を実装します。

ルーティングの設定

"laravel_todolist/routes/web.php"をvi等で開きます。

web.php
<?php                                                                                                              

/*
|--------------------------------------------------------------------------
| 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');
});

/へのルーティングが予め書かれていますが、ここを以下のように書き換えます。

web.php
<?php                                                                                                              

/*
|--------------------------------------------------------------------------
| 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('/', 'TodolistFormController@index');

これで、/を呼び出されたときに、TodolistFormControllerクラスのindexファンクションが実行される指定になりました。

コントローラの作成

web.phpを書き換えましたが、まだTodolistFormControllerクラスが無いので実装しましょう。

"laravel_todolist"ディレクトリで以下のコマンドを実行してTodolistFormControllerクラスを作成します。

php artisan make:controller TodolistFormController

"laravel_todolist/app/Http/Controllers/TodolistFormController.php"というファイルが作成されるので、このファイルをvi等で開きます。

TodolistFormController.php
<?php                                                                                                              

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TodolistFormController extends Controller
{
    //
}

まだ何もファンクションが無い状態なので、ここにタスク一覧を表示するindexファンクションを追加します。

TodolistFormController.php
<?php                                                                                                              

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\ToDo;  // 追加

class TodolistFormController extends Controller
{
     // タスクを一覧で表示
     public function index()
     {
         $todos = Todo::orderBy('id', 'asc')->get();
         return view('todo_list', [
             "todos" => $todos
         ]);
     }
}

use App\ToDo;でTodoモデルクラスを読み込んでいることに注意してください。
ToDoモデルクラスのget()ファンクションを呼び出すことで、todosテーブルの値を取り出すことが出来ます。
また、orderBy()ファンクションを使用することでデータのソートも行っています。

returnでは"todo_list"ビューを使用し、$todosデータを渡すように定義しています。
次は"todo_list"ビューを作成しましょう。

ビューの作成

ビューファイルはartisanコマンドで作成できないので、touchコマンド等で直接ファイルを作成します。
"laravel_todolist/resources/views/"ディレクトリに"todo_list.blade.php"というファイルを作成し、vi等で開いて以下のように実装します。

todo_list.blade.php
<h1>ToDo List</h1>                                                                                                 
<div>
    <h2>タスクの一覧</h2>
    <a href="/create-page">タスクを追加</a>
    <table border="1">
        <tr>
            <th>タスクの名前</th>
            <th>タスクの説明</th>
            <th>担当者の名前</th>
            <th>見積時間(h)</th>
            <th colspan="2">操作</th>
        </tr>
        @foreach($todos as $todo)
        <tr>
            <td>{{$todo->task_name}}</td>
            <td>{{$todo->task_description}}</td>
            <td>{{$todo->assign_person_name}}</td>
            <td>{{$todo->estimate_hour}}</td>
            <td><a href="/edit-page/{{$todo->id}}">編集</a></td>
            <td><a href="/delete-page/{{$todo->id}}">削除</a></td>
        </tr>
        @endforeach
    </table>
</div>

見るべきポイントとしては@foreach($todos as $todo)でタスクの数だけ繰り返し表示している箇所です。コントローラからタスクの一覧が$todosという名前で渡されてくるので、これをforeachでループさせながら全件表示しています。

また、まだタスクの追加・編集・削除機能は実装していませんが、<a>タグによるリンクは先に実装してしまいます。
編集と削除のURLはどのタスクを対象とするのかをIDで判別するため、URLに{{$todo->id}}を渡しています。

動作確認

"laravel_todolist"ディレクトリで以下のコマンドを実行し、Laravelの開発サーバを起動します。

php artisan serve

ブラウザでhttp://localhost:8000/を開き、SQLiteで登録しておいた初期データが表示されることを確認してください。

image.png

これで、タスク一覧ページの実装が出来ました。
試しに、SQLite3でINSERTのSQLを実行して初期データの件数を増やすと、タスク一覧ページに表示されるタスクの件数も増えていくはずです。
次は、タスクを追加するページを実装します。

タスク追加ページを実装

CRUDのC(Create)に該当する機能を実装します。

ルーティングの設定

"laravel_todolist/routes/web.php"を編集します。
タスク追加ページを表示する/create-pageと、追加ボタンが押されたら実際にタスクを追加する/createへのルーティングを追加します。
/create-pageはGETメソッドの指定で、/createはPOSTメソッドの指定になる点に注意してください。

web.php
<?php                                                                                                              

/*
|--------------------------------------------------------------------------
| 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('/', 'TodolistFormController@index');
Route::get('/create-page', 'TodolistFormController@createPage');
Route::post('/create', 'TodolistFormController@create');

コントローラの作成

"laravel_todolist/app/Http/Controllers/TodolistFormController.php"を開き、TodolistFormControllerの実装に処理を追加します。

createPageファンクションの追加

"createPage"ファンクションはタスク作成ページを返すだけでよいので、"todo_create"ビューを返しているだけです。

TodolistFormController.php
    // ここより上は省略

    // タスク作成画面を表示
    public function createPage()
    {
        return view('todo_create');
    }

    // 以下省略

createファンクションの追加

"create"ファンクションはタスク作成ページで「作成」ボタンを押されたら実行されます。タスク作成ページで入力された情報がPOSTリクエストのパラメータとして渡ってくるので、その情報をもとに新しいタスクを作成しています。

Todoモデルクラスをnewしてインスタンスを生成し、タスクの情報をパラメータに詰めてsave()ファンクションを呼び出すことでtodosテーブルにレコードが保存されます。

処理の最後でreturn redirect('/')することで、タスク登録後に/ページへリダイレクトされます。

TodolistFormController.php
    // ここより上は省略

    // タスクを登録
    public function create(Request $request)
    {
        $todo = new Todo();
        $todo->task_name = $request->task_name;
        $todo->task_description = $request->task_description;
        $todo->assign_person_name = $request->assign_person_name;
        $todo->estimate_hour = $request->estimate_hour;
        $todo->save();

        return redirect('/');
    }

    // 以下省略

ビューの作成

タスク追加画面が必要なので作成します。
"laravel_todolist/resources/views/"ディレクトリに"todo_create.blade.php"というファイルを作成し、vi等で開いて以下のように実装します。

todo_create.blade.php
<h1>ToDo List</h1>                                                                                                 
<div>
    <h2>タスクを追加</h2>
    <form method="POST" action="/create">
        @csrf
        <p>
            タスクの名前<input type="text" name="task_name">
        </p>
        <p>
            タスクの説明<input type="text" name="task_description">
        </p>
        <p>
            担当者の名前<input type="text" name="assign_person_name">
        </p>
        <p>
            見積時間(h)<input type="number" name="estimate_hour">
        </p>
        <input type="submit" name="create" value="追加">
    </form>
    <a href="/">戻る</a>
</div>

<form method="POST" action="/create">としてあるので、<input type="submit" name="create" value="追加">ボタンが押されたら/createのPOSTメソッドへリクエストが飛ぶことになります。

動作確認

"laravel_todolist"ディレクトリでphp artisan serveコマンドでテストサーバを起動し、以下の手順で新しいタスクを追加できることを確認します。

image.png

タスク編集ページを実装

CRUDのU(Update)に該当する機能を実装します。

ルーティングの設定

"laravel_todolist/routes/web.php"を編集します。
タスク編集ページを表示する/edit-pageと、修正ボタンが押されたら実際にタスクを更新する/editへのルーティングを追加します。

/edit-pageはGETメソッドの指定で、/editはPOSTメソッドの指定になる点はタスク作成ページと同様です。
ただし/edit-pageへのルーティングは、「どのタスクを編集するのか」という情報をURLパラメータに加えたいため、'/edit-page/{id}'という指定になっている点に注意です。

web.php
<?php                                                                                                              

/*
|--------------------------------------------------------------------------
| 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('/', 'TodolistFormController@index');
Route::get('/create-page', 'TodolistFormController@createPage');
Route::post('/create', 'TodolistFormController@create');
Route::get('/edit-page/{id}', 'TodolistFormController@editPage');
Route::post('/edit', 'TodolistFormController@edit');

コントローラの作成

"laravel_todolist/app/Http/Controllers/TodolistFormController.php"を開き、TodolistFormControllerの実装に処理を追加します。

editPageファンクションの追加

"editPage"ファンクションは、編集対象のタスクのIDがURLパラメータの$idで渡ってくるので、$idを引数として受け付けます。
Todoモデルクラスのfindファンクションを用いるとテーブルのキーを用いた検索ができるので、引数で受け取った$idを使って対象のタスクを取得し、"todo_edit"ビューと合わせて返しています。

TodolistFormController.php
    // ここより上は省略

    // タスク編集画面を表示
    public function editPage($id)
    {
        $todo = Todo::find($id);
        return view('todo_edit', [
            "todo" => $todo
        ]);
    }

    // 以下省略

editファンクションの追加

"edit"ファンクションはタスク編集ページで「修正」ボタンを押されたら実行されます。タスク修正ページで入力された情報がPOSTリクエストのパラメータとして渡ってくるので、その情報をもとにタスクの情報を更新しています。

まず、Todoモデルクラスのfindファンクションを使って、リクエストパラメータに入っている$idのタスクをデータベースのtodosテーブルから取得します。
そして、updateファンクションを使って、取得したタスクの内容を書き換えます。

処理の最後でreturn redirect('/')することで、タスク編集後に/ページへリダイレクトされます。

TodolistFormController.php
    // ここより上は省略

    // タスクを更新
    public function edit(Request $request)
    {
        Todo::find($request->id)->update([
            'task_name' => $request->task_name,
            'task_description' => $request->task_description,
            'assign_person_name' => $request->assign_person_name,
            'estimate_hour' => $request->estimate_hour
        ]);
        return redirect('/');
    }

    // 以下省略

ビューの作成

タスク編集画面が必要なので作成します。
"laravel_todolist/resources/views/"ディレクトリに"todo_edit.blade.php"というファイルを作成し、vi等で開いて以下のように実装します。

todo_edit.blade.php
<h1>ToDo List</h1>                                                                                                 
<div>
    <h2>タスクの修正</h2>
    <form method="POST" action="/edit">
        @csrf
        <input type="hidden" name="id" value="{{$todo->id}}">
        <p>
            タスクの名前<input type="text" name="task_name" value="{{$todo->task_name}}">
        </p>
         <p>
             タスクの説明<input type="text" name="task_description" value="{{$todo->task_description}}">
         </p>
         <p>
             担当者の名前<input type="text" name="assign_person_name" value="{{$todo->assign_person_name}}">
         </p>
         <p>
             見積時間(h) <input type="number" name="estimate_hour" value="{{$todo->estimate_hour}}">
         </p>
         <input type="submit" name="edit" value="修正">
     </form>
     <a href="/">戻る</a>
 </div>

<form method="POST" action="/edit">としてあるので、<input type="submit" name="edit" value="修正">ボタンが押されたら/editのPOSTメソッドへリクエストが飛ぶことになります。
また、各inputのvalueには、コントローラから渡された$todoデータに格納されているタスクの情報が入ります。これによって、タスク編集画面を開いたときに、タスクの情報が予めフォームに入力された状態になります。

動作確認

"laravel_todolist"ディレクトリでphp artisan serveコマンドでテストサーバを起動し、以下の手順でタスクの編集ができることを確認します。

image.png

タスク削除ページを実装

CRUDのD(Delete)に該当する機能を実装します。

ルーティングの設定

"laravel_todolist/routes/web.php"を編集します。
タスク削除ページを表示する/delete-pageと、修正ボタンが押されたら実際にタスクを更新する/deleteへのルーティングを追加します。

/delete-pageはGETメソッドの指定で、/deleteはPOSTメソッドの指定になる点はタスク編集ページと同様です。/delete-pageへのルーティングについても、「どのタスクを削除するのか」という情報をURLパラメータに加えたいため、'/delete-page/{id}'という指定になっています。
ただし、/deleteはPOSTメソッドの指定で'/delete/{id}'となっている点は注意です。これは、削除ページではタスクの内容を確認するだけでフォームへの入力は行わないため、リクエストパラメータを使うよりもURLパラメータにIDを仕込んだほうがシンプルだと考えたためです。

web.php
<?php                                                                                                              

/*
|--------------------------------------------------------------------------
| 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('/', 'TodolistFormController@index');
Route::get('/create-page', 'TodolistFormController@createPage');
Route::post('/create', 'TodolistFormController@create');
Route::get('/edit-page/{id}', 'TodolistFormController@editPage');
Route::post('/edit', 'TodolistFormController@edit');
Route::get('/delete-page/{id}', 'TodolistFormController@deletePage');
Route::post('/delete/{id}', 'TodolistFormController@delete');

コントローラの作成

"laravel_todolist/app/Http/Controllers/TodolistFormController.php"を開き、TodolistFormControllerの実装に処理を追加します。

deletePageファンクションの追加

"deletePage"ファンクションは、削除対象のタスクのIDがURLパラメータの$idで渡ってくるので、$idを引数として受け付けます。
Todoモデルクラスのfindファンクションで対象のタスクを取得し、"todo_delete"ビューと合わせて返しています。

TodolistFormController.php
    // ここより上は省略

    // タスク削除画面を表示
    public function deletePage($id)
    {
        $todo = Todo::find($id);
        return view('todo_delete', [
            "todo" => $todo
        ]);
    }

    // 以下省略

deleteファンクションの追加

"delete"ファンクションはタスク削除ページで「削除」ボタンを押されたら実行されます。選択されたタスクのIDがURLパラメータとして渡ってくるので、その情報をもとにタスクの情報を削除しています。

まず、Todoモデルクラスのfindファンクションを使って、URLパラメータに入っている$idのタスクをデータベースのtodosテーブルから取得します。
そして、deleteファンクションを使って、取得したタスクをtodosテーブルから削除しています。

処理の最後でreturn redirect('/')することで、タスク編集後に/ページへリダイレクトされます。

TodolistFormController.php
    // ここより上は省略

    // タスクを削除
    public function delete(Request $id)
    {
        Todo::find($id)->delete();
        return redirect('/');
    }

    // 以下省略

ビューの作成

"laravel_todolist/resources/views/"ディレクトリに"todo_delete.blade.php"というファイルを作成し、vi等で開いて以下のように実装します。

todo_delete.blade.php
<h1>ToDo List</h1>                                                                                                 
<div>
    <h2>タスクを削除</h2>
    <form method="POST" action="/delete/{{$todo->id}}">
        @csrf
        <p>
            タスクの名前{{$todo->task_name}}
        </p>
        <p>
            タスクの説明{{$todo->task_description}}
        </p>
        <p>
            担当者の名前{{$todo->assign_person_name}}
        </p>
        <p>
            見積時間(h) {{$todo->estimate_hour}}
        <p>
        <input type="submit" name="delete" value="削除">
    </form>
    <a href="/">戻る</a>
</div>

<form method="POST" action="/delete">としてあるので、<input type="submit" name="delete" value="削除">ボタンが押されたら/deleteのPOSTメソッドへリクエストが飛ぶことになります。
削除対象のタスクの情報はコントローラから$todoという名前のデータで渡されてきているので、$todoから値を取り出してHTML上に直接(inputタグを使わずに)表示しています。

動作確認

"laravel_todolist"ディレクトリでphp artisan serveコマンドでテストサーバを起動し、以下の手順でタスクの削除ができることを確認します。

image.png

その他の機能を実装

以上で、超シンプルなToDoアプリのCRUD(Create, Read, Update, Delete)の機能が実現できました。
このままでもまぁまぁ使えるのですが、もう少し完成度を上げるために以下の2点の改善を行います。

  1. タスクに設定した見積時間の合計を表示する
  2. Validationの実装(タスク作成時と編集時に入力内容をチェックする)

見積時間の合計を表示

個々のタスクの見積時間はタスク一覧ページで表示されますが、タスクの数が増えてくるといちいち合計を手計算しなければいけないのは面倒です。
そこで、全てのタスクの見積時間を合計し、タスク一覧ページに表示しましょう。

コントローラの修正

"laravel_todolist/app/Http/Controllers/TodolistFormController.php"を開き、TodolistFormControllerのindexファンクションに処理を追加します。

TodolistFormController.php
    // ここより上は省略

    // タスクを一覧で表示
    public function index()
    {
        $todos = Todo::orderBy('id', 'asc')->get();

        // 見積時間の合計を求める
        $estimate_hour_sum = 0;
        foreach ($todos as $todo){
            $estimate_hour_sum += $todo->estimate_hour;
        }

        return view('todo_list', [                                                                                 
            "todos" => $todos,
            "estimate_hour_sum" => $estimate_hour_sum
        ]);
    }

    // 以下省略

新たに$estimate_hour_sumという変数を定義し、foreachで全タスクを回してestimate_hourを合算します。そして最後に、ビューを返す際のデータにestimate_hour_sumも加えてあげます。

ビューの修正

"laravel_todolist/resources/views/todo_list.blade.php"を開いて以下のように修正します。

todo_list.blade.php
 <h1>ToDo List</h1>
 <div>
     <h2>タスクの一覧</h2>
     <a href="/create-page">タスクを追加</a>
     <table border="1">
        <tr>
            <th>タスクの名前</th>
            <th>タスクの説明</th>
            <th>担当者の名前</th>
            <th>見積時間(h)</th>
            <th colspan="2">操作</th>
        </tr>
        @foreach($todos as $todo)
        <tr>
            <td>{{$todo->task_name}}</td>
            <td>{{$todo->task_description}}</td>
            <td>{{$todo->assign_person_name}}</td>
            <td>{{$todo->estimate_hour}}</td>
            <td><a href="/edit-page/{{$todo->id}}">編集</a></td>
            <td><a href="/delete-page/{{$todo->id}}">削除</a></td>
        </tr>                                                                                                      
        @endforeach
    </table>
    <p>
        見積時間の合計(h){{$estimate_hour_sum}}
    </p>
</div>

一番下に見積時間の合計を追加しています。

Validationの実装

現状では、タスク作成やタスク編集で、フォームの値が空の状態だった場合、QueryExceptionが発生してしまいます。

image.png

これは、todosテーブルの各カラムについてNULLを許容していないためですが、ユーザがタスク作成ページやタスク編集ページではフォームの値を埋めた状態でボタンを押してもらえればこのExceptionは回避できます。
そのような機能を実現させたい場合、Validationを実装することでフォームに入力されたデータを先にチェックし、異常なデータが有る場合は処理を中断させることができます。

コントローラの修正

"laravel_todolist/app/Http/Controllers/TodolistFormController.php"を開き、TodolistFormControllerのcreateファンクションに処理を追加します。

TodolistFormController.php
    // ここより上は省略

    // タスクを登録
    public function create(Request $request)
    {
        $validator = $request->validate([
            'task_name' => 'required',
            'task_description' => 'required',
            'assign_person_name' => 'required',
            'estimate_hour' => 'required'
        ]);

        $todo = new Todo();
        $todo->task_name = $request->task_name;
        $todo->task_description = $request->task_description;
        $todo->assign_person_name = $request->assign_person_name;
        $todo->estimate_hour = $request->estimate_hour;
        $todo->save();

        return redirect('/');
    }

    // 以下省略

Todoモデルクラスをnewする前に、HTTPリクエストで渡されてきたパラメータを$validatorを使ってチェックしています。
チェックのルールは各パラメータで個別に設定することができ、今回の場合は全てのパラメータについてrequiredを指定して必須としています。

Validationルールは他にも色々あるので、以下の公式ガイドの「使用可能なバリデーションルール」を参照してみてください。
Laravel 5.7 バリデーション

ビューの修正

"laravel_todolist/resources/views/todo_create.blade.php"を開いて以下のように修正します。

todo_create.blade.php
<h1>ToDo List</h1>                                                                                                 
<div>
    <h2>タスクを追加</h2>
    <form method="POST" action="/create">
        @csrf
        @if ($errors->any())
            <ul>
            @foreach ($errors->all() as $error)
               <li style="color:red">{{$error}}</li>
            @endforeach
            </ul>
        @endif
        <p>
            タスクの名前<input type="text" name="task_name">
        </p>
        <p>
            タスクの説明<input type="text" name="task_description">
        </p>
        <p>
            担当者の名前<input type="text" name="assign_person_name">
        </p>
        <p>
            見積時間(h)<input type="number" name="estimate_hour">
        </p>
        <input type="submit" name="create" value="追加">
    </form>
    <a href="/">戻る</a>
</div>

@if ($errors->any())で、Validationエラーがあった場合のエラーメッセージ表示領域を定義しています。
コントローラ側で$validatorのチェックルールに引っかかった場合、自動的にビューが再表示されます。その際に$errorsにエラー内容が配列で格納されているので、foreachループで回して全てのエラーメッセージを<li>タグでリスト表示しています。

日本語メッセージの表示

日本語メッセージファイルのインストール

ここまででもValidationチェックとエラーメッセージの表示はできるのですが、エラーメッセージが英文表記となっていてユーザフレンドリーとは言い難い状態です。
そのため、まずは公式の手順に沿って日本語メッセージのインストールを行います。

Laravel 5.7 validation.php言語ファイル

"laravel_todolist"ディレクトリで以下のコマンドを実行します。

php -r "copy('https://readouble.com/laravel/5.7/ja/install-ja-lang-files.php', 'install-ja-lang.php');"
php -f install-ja-lang.php
php -r "unlink('install-ja-lang.php');"

"laravel_todolist/resources/lang/ja"というディレクトリが新たに作られ、その配下にあるvalidation.phpがValidationの日本語メッセージです。

Laravelプロジェクトの言語設定を変更

"laravel_todolist/config/app.php"を開き、localeというキーの値をenからjaに変更して保存します。

app.php
    // ここより上は省略

    /*
    |--------------------------------------------------------------------------
    | Application Locale Configuration
    |--------------------------------------------------------------------------
    |
    | The application locale determines the default locale that will be used
    | by the translation service provider. You are free to set this value
    | to any of the locales which will be supported by the application.
    |
    */

    'locale' => 'ja',

    // 以下省略
カスタムバリデーション属性名の設定

ここまででメッセージのフォーム名以外は日本語になりますが、まだフォーム名はリクエストパラメータのキー名がそのまま表示されてしまいます。ここを日本語名に変換するには、カスタムバリデーション属性名を設定する必要があります。
"laravel_todolist/resources/lang/ja/validation.php"を開いて以下のように修正します。

validation.php
    // ここより上は省略

    /*
    |--------------------------------------------------------------------------
    | カスタムバリデーション属性名
    |--------------------------------------------------------------------------
    |
    | 以下の言語行は、例えば"email"の代わりに「メールアドレス」のように、
    | 読み手にフレンドリーな表現でプレースホルダーを置き換えるために指定する
    | 言語行です。これはメッセージをよりきれいに表示するために役に立ちます。
    |
    */

    'attributes' => [
        'task_name' => 'タスクの名前',
        'task_description' => 'タスクの説明',
        'assign_person_name' => '担当者の名前',
        'estimate_hour' => '見積時間',
    ],
];
動作確認

タスク追加画面でフォームの値を入力せずに追加ボタンを押すと、以下のようにValidationメッセージが表示されるようになります。

image.png

同様のExceptionはタスク編集画面でも起こりうるので、タスク編集画面とコントローラのeditファンクションにもValidationを導入しておくと良いでしょう。

ToDoアプリの機能追加例

超シンプルなToDoアプリの作成紹介は以上となります。
ここからは、各々がToDoアプリをベースにしながら付帯機能の開発を進めて、Laravelによるアプリ開発の理解を深めるのがよいと思います。
いくつか機能追加の例を示しておきます。

見積時間(h)のデータ型を文字型から数値型に変える

現状では見積時間(h)はマイグレーションファイルでStringとして定義されているため、todosテーブル上でも文字型として定義されています。
しかし、時間は数字であったほうが集計等で扱いやすいので、文字型ではなく数字型の方が良いはずです。

マイグレーションファイルを追加し、estimate_hourの定義を数値型に変更する対応をしてみると、開発途中でデータ型を変更する練習になると思います。

担当者の名前をセレクトボックスから選択できるようにする

現状では担当者の名前はテキストボックスに直接入力していますが、ここは予め決められた名前の中から選択できる方が使い勝手が良いはずです。

担当者の名前をセレクトボックスから選択できるようにするには、ざっと考えただけでも以下のような対応が必要になってきそうです。

  • 担当者の名前をデータベースに登録しておきたい
    • 担当者の名前のデータベースが必要
    • 担当者の名前のCRUD機能が必要
  • タスク登録時に担当者の名前をデータベースから取得したい
    • タスクをデータベースに保存する際の担当者の名前は、Valueではなくidを保存する
  • タスク編集時に担当者の名前をデータベースから取得したい
    • タスク登録時と似ているが、セレクトボックスには予めidに対応するvalueを選択した状態にしておきたい
  • その他考慮すべきこと
    • 担当者の名前を削除するときに、その担当者のタスクが残っている場合にどうするか?

これは、ToDoモデルに加えて、新たに担当者のモデル(AssignPersonモデル)が必要となり、モデル間の関係も考慮する練習になると思います。

ログイン機能の実装

現状ではこのToDoアプリはログイン機能を備えていないため、URLを知っていれば誰でもアクセスしてデータを操作できてしまいます。これではセキュリティ面で実用的では無いため、ログイン機能を備えるべきでしょう。

Laravelは標準でAuthの機能を備えているので、公式ガイドを参考にしながらログイン機能を追加してみるとより実用的なアプリになると思います。
Laravel 5.7 認証

また、ユーザに対して「管理者」「一般」といった権限レベルを追加し、タスクの削除ができるのは管理者のみといった権限設定を行えるようにするのも学習になると思います。

排他制御の実装

ログイン機能を追加したら、実用性を上げるためにデータ更新時の排他制御も実現させたいところです。
現状では、複数のアカウントから同時にタスクの編集を行われた場合、一方の編集内容は破棄されてしまうという問題があります。
処理の流れを示すと以下のようになります。

排他制御が行われていない場合の処理の流れ

  1. Aさんがタスク1のタスク編集画面を開く
  2. Bさんがタスク1のタスク編集画面を開く
  3. Aさんがタスク1の内容を書き換えて保存する
  4. Bさんがタスク1の内容を書き換えて保存する ※Aさんの編集内容はここには反映されていない
  5. タスク1はBさんの編集内容で保存されるため、Aさんの編集内容は破棄されてしまう

このような問題を回避するためには、排他制御としてデータ更新時の楽観ロック(更新前に読み込んだデータが、他のユーザによって先に更新されていないかチェックする仕組み)を実装する必要があります。
排他制御を実装している場合、処理の流れは以下のようになります。

排他制御が行われる場合の処理の流れ

  1. Aさんがタスク1のタスク編集画面を開く
  2. Bさんがタスク1のタスク編集画面を開く
  3. Aさんがタスク1の内容を書き換えて保存しようとしたときに、他の人が先に書き換え・保存していないかチェックする
  4. 他の人によって書き換え・保存されていないので、Aさんの書き換え内容は保存される
  5. Bさんがタスク1の内容を書き換えて保存しようとしたときに、他の人が先に書き換え・保存していないかチェックする
  6. Aさんによって書き換え・保存されているので、Bさんの書き換えは失敗する

複数アカウントでデータが操作されるアプリであれば排他制御は避けて通れないので、良い練習になると思います

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

PHP 7(.4)のDockerコンテナ(fpm-alpine)にphp-mecabを導入してPHPでMeCabを使う

DockerのPHPイメージにMeCabとmecab-ipadic-NEologdを導入して、PHPから使えるように設定する方法を紹介します?

PHPフレームワークやWebサーバーを導入せず、PHPでMeCabを使えるかどうかまで確認します。ここで導入できたら必要なものを追加しましょう✨

PHPのイメージは次を用いて確認しています。

Dockerfile
FROM php:7.4.12-fpm-alpine

docker-composeの設定

起動・終了を簡単にするためdocker-composeを用います。次の docker-compose.yml をプロジェクトのルートに作成します。

  • PHPコンテナにMeCabをインストールしてカスタムでコンテナを作るため、新規作成するDockerfileの場所をbuilddockerfileで指定します
  • volumes にはPHPでMeCabを使うサンプルスクリプトを配置するホスト側のディレクトリとコンテナ側のディレクトリをマウントします
  • PHPのコンテナを起動させ続けるために tty をtrueにしておきます
docker-compose.yml
version: '3'

services:
  php-mecab:
    build:
      context: ./
      dockerfile: Dockerfile
    volumes:
      - ./src:/app
    tty: true

MeCabとmecab-ipadic-NEologdを導入する

同じくプロジェクトのルートにDockerfile を作成して、PHPのコンテナにMeCabとmecab-ipadic-NEologdを導入します。

古い記事ですが、PHP7.4.12の現在もこちらの記事と同じ方法で構築できます。
https://qiita.com/nownabe/items/4171776aec1f05de9f28

Dockerfile
FROM php:7.4.12-fpm-alpine

## ここから
# https://qiita.com/nownabe/items/4171776aec1f05de9f28#dockerfile
RUN apk add --update --no-cache build-base

ENV MECAB_VERSION 0.996
ENV IPADIC_VERSION 2.7.0-20070801
ENV mecab_url https://drive.google.com/uc?export=download&id=0B4y35FiV1wh7cENtOXlicTFaRUE
ENV ipadic_url https://drive.google.com/uc?export=download&id=0B4y35FiV1wh7MWVlSDBCSXZMTXM
ENV build_deps 'curl git bash file sudo openssh'
ENV dependencies 'openssl'

RUN apk add --update --no-cache ${build_deps} \
  # Install dependencies
  && apk add --update --no-cache ${dependencies} \
  # Install MeCab
  && curl -SL -o mecab-${MECAB_VERSION}.tar.gz ${mecab_url} \
  && tar zxf mecab-${MECAB_VERSION}.tar.gz \
  && cd mecab-${MECAB_VERSION} \
  && ./configure --enable-utf8-only --with-charset=utf8 \
  && make \
  && make install \
  && cd \
  # Install IPA dic
  && curl -SL -o mecab-ipadic-${IPADIC_VERSION}.tar.gz ${ipadic_url} \
  && tar zxf mecab-ipadic-${IPADIC_VERSION}.tar.gz \
  && cd mecab-ipadic-${IPADIC_VERSION} \
  && ./configure --with-charset=utf8 \
  && make \
  && make install \
  && cd \
  # Install Neologd
  && git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git \
  && mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n -y \
  # Clean up
  && apk del ${build_deps} \
  && rm -rf \
    mecab-${MECAB_VERSION}* \
    mecab-${IPADIC_VERSION}* \
    mecab-ipadic-neologd

## ここまで

php-mecabを導入する

MeCabをPHPで使う場合はphp-mecabモジュールを導入する必要があります。Dockerfileを作るポイントとしては次のとおりです。

  • autoconf を導入しないとphp-mecabのビルドに必要な configure が生成されない
  • php-config, mecab-config/usr/local/bin/ 以下に配置されている

このポイントを踏まえてphp-mecabを導入するため Dockerfileに追記すると 次のようになります。

Dockerfile
# ...
## ここまで

## ここから追記する
ENV build_deps_phpmecab 'git autoconf'
COPY php.mecab.ini /usr/local/etc/php/conf.d/
RUN apk add --update --no-cache ${build_deps_phpmecab} \ 
  && git clone https://github.com/rsky/php-mecab.git \
  && cd ./php-mecab/mecab \
  && phpize \
  && ./configure --with-php-config=/usr/local/bin/php-config --with-mecab=/usr/local/bin/mecab-config \
  && make \
  && make test \
  && make install \
  # Clean up
  && cd  \
  && rm -rf php-mecab \
  && apk del ${build_deps_phpmecab}

# ▼▼このコマンドが最後に来るようにしてください
RUN docker-php-ext-install pdo pdo_mysql

php.mecab.ini をCOPYしていますが、次の手順でこのファイルを作成します。

PHPからMeCabモジュールを読み込む

導入したphp-mecabのモジュールをPHPで有効にするため、PHPの設定ファイル(php.mecab.ini)を作成してmecab.soを指定します。

php.mecab.ini
extension=mecab.so

最終的には次のファイル構成になります。

$ tree
.
├── Dockerfile
├── docker-compose.yml
└── php.mecab.ini

PHPでMeCabを使う

PHP7向けに MeCab_Tagger から \MeCab\Tagger に変更されています。 バックスラッシュもそのまま使います。

sample_php_mecab.php
<?php
        $str     = 'センテンススプリングでも文春でもどっちでもいいだろ';
        $options = array('-d', '/usr/local/lib/mecab/dic/mecab-ipadic-neologd');
        $mecab = new \MeCab\Tagger($options);
        $nodes = $mecab->parseToNode($str);
        echo $str . "\n";
        foreach ($nodes as $n) {
            $stat = $n->getStat();
            // 単語かどうか
            if ($stat != 0) {
                continue;
            }
            $features = explode(',', $n->getFeature());
            if ($features[0] !== '名詞') {
                continue;
            }
            // 単語
            echo "Surface: " . $n->getSurface() . "\n";
        }
docker-compose up --build -d php-mecab
docker-compose exec php-mecab /bin/sh
php /app/sample_php_mecab.php

これでMeCabが使えるPHPのコンテナが作成できます。
この構成ではWebブラウザで表示して確認することはできないので、

  • Laravelなどのフレームワークを導入して開発用サーバーを立ち上げる
  • 別途NginxなどのWebサーバーのコンテナを導入して、このPHPコンテナで動作しているFPMと接続させる

などの作業は必要です。

参考

Alpine LinuxでMeCab with NEologd - Qiita
https://qiita.com/nownabe/items/4171776aec1f05de9f28

phpでmecab - Qiita
https://qiita.com/kojirock5260/items/1787d70b9cfca3e43d12

phpizeでエラー : てるてる坊主
http://teru2-bo2.blogspot.com/2012/05/phpize.html

PHP7.0+mecabでハマった事 - Qiita
https://qiita.com/murota/items/3c76cc3ca0fd819dbac7

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

Laravel8 CRUD処理を使った投稿アプリを作成する その4 投稿一覧ページの作成

目的

  • 投稿された内容の一覧を表示するページの作成を行う

実施環境

  • 筆者の実施環境を記載する。
  • ハードウェア環境
項目 情報
OS macOS Catalina(10.15.5)
ハードウェア MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)
プロセッサ 2 GHz クアッドコアIntel Core i5
メモリ 32 GB 3733 MHz LPDDR4
グラフィックス Intel Iris Plus Graphics 1536 MB
  • ソフトウェア環境
項目 情報 備考
PHP バージョン 7.4.8 Homebrewを用いてこちらの方法で導入→Mac HomebrewでPHPをインストールする
Laravel バージョン 8.6.0 commposerを用いてこちらの方法で導入→Mac Laravelの環境構築を行う
MySQLバージョン 8.0.19 for osx10.13 on x86_64 Homwbrewを用いてこちらの方法で導入→Mac HomebrewでMySQLをインストールする

前提条件

  • その1の記事の内容が完了していること。

前提情報

概要

  1. ルーティング情報の記載
  2. コントローラファイルの記載
  3. ビューファイルの作成と記載
  4. 確認

詳細

  1. ルーティングの記載

    1. laravel8_crudディレクトリで下記コマンドを実行してルーティングファイルを開く。

      $ vi routes/web.php 
      
    2. 開いたファイルに下記の行を追記する。

      laravel8_crud/routes/web.php
      Route::get('/output', [ContentController::class, 'output'])->name('output');
      
    3. 追記後のルーティングファイルの内容を下記に記載する。

      laravel8_crud/routes/web.php
      <?php
      
      use Illuminate\Support\Facades\Route;
      use App\Http\Controllers\ContentController;
      
      /*
      |--------------------------------------------------------------------------
      | 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', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
      
      Route::get('/input', [ContentController::class, 'input'])->name('input');
      Route::post('/save', [ContentController::class, 'save'])->name('save');
      // 下記を追記する
      Route::get('/output', [ContentController::class, 'output'])->name('output');
      
  2. コントローラファイルの記載

    1. laravel8_crudディレクトリで下記コマンドを実行して作成したコントローラファイルを開く。

      $ vi app/Http/Controllers/ContentController.php 
      
    2. 下記の内容をクラス内に追記する。

      laravel8_crud/app/Http/Controllers/ContentController.php
      public function output()
      {
          $contents_get_query = Content::select('*');
          $items = $contents_get_query->get();
      
          return view('contents.output', [
              'items' => $items,
          ]);
      }
      
    3. すでに記載されている投稿内容を保存するアクション内のリダイレクトの記載を変更する。

      laravel8_crud/app/Http/Controllers/ContentController.php
      public function save(Request $request)
      {
          $input_content = new Content();
          $input_content->content = $request['content'];
          $input_content->save();
          //  下記を修正する
          return redirect(route('output'));
      }
      
    4. 両内容の追記後のコントローラファイルの内容を下記に記載する。

      laravel8_crud/app/Http/Controllers/ContentController.php
      <?php
      
      namespace App\Http\Controllers;
      
      use Illuminate\Http\Request;
      use App\Models\Content;
      
      class ContentController extends Controller
      {
          public function input()
          {
              return view('contents.input');
          }
      
          public function save(Request $request)
          {
              $input_content = new Content();
              $input_content->content = $request['content'];
              $input_content->save();
              //  下記を修正する
              return redirect(route('output'));
          }
      
          // 下記を追記する
          public function output()
          {
              $contents_get_query = Content::select('*');
              $items = $contents_get_query->get();
      
              return view('contents.output', [
                  'items' => $items,
              ]);
          }
          // 上記までを追記する
      }
      
  3. ビューファイルの作成と記載

    1. laravel8_crudディレクトリで下記コマンドを実行してビューファイルを作成する。

      vi resources/views/contents/output.blade.php
      
    2. 作成して開いたビューファイルに下記の内容を追記する。

      laravel8_crud/resources/views/contents/output.blade.php
      <h1>output</h1>
      
      @foreach ($items as $item)
          <hr>
          <p>{{$item['content']}}</p>
      @endforeach
      
  4. 確認

    1. laravel8_crudディレクトリで下記コマンドを実行してローカルサーバを起動する。

      $ php artisan serve
      
    2. ブラウザで下記にアクセスする。

    3. 下記の様に投稿内容が表示されることを確認する。

      127_0_0_1_8000_output_と_Zoomミーティング.png

    4. ブラウザで下記にアクセスする。

    5. テキストボックスに何かしらの文字列を入力して「送信」をクリックする。

      127_0_0_1_8000_input.png

    6. 下記の画面にリダイレクトし、入力内容が表示されれば本記事の作業は完了である。

      127_0_0_1_8000_output.png

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

Carbon で1週間の日付と曜日を取得する

PHPのCarbonで、日付と曜日(日本語)のリストを取得する方法をシェアします。

実行環境

  • Laravel Framework 6.18.23
  • "nesbot/carbon": "2.0"

取得したい結果

array: [
    0 => "10/18 (日)"
    1 => "10/19 (月)"
    2 => "10/20 (火)"
    3 => "10/21 (水)"
    4 => "10/22 (木)"
    5 => "10/23 (金)"
    6 => "10/24 (土)"
]

ソースコード

Laravelの config/app.php'locale' => 'ja' の設定しておく。

$numOfDays = 7; //日付を取得する日数

$dt = new Carbon();
$format = 'MM/DD (ddd)';

$week[0] = $dt->today()->isoFormat($format);

//Carbonのインスタンスが上書きされないようにcopy()して日付を加算
for ($i=1; $i < $numOfDays ; $i++) {
  $week[$i] = $dt->copy()->addDay($i)->isoFormat($format);
}

dump($week);

参考

https://carbon.nesbot.com/docs/
https://uiuifree.com/blog/develop/php-carbon-ja-weekday/

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

Moodle 3.9 マニュアル - バルクユーザーアクション

原文

バルクユーザーアクション

管理>サイト管理>ユーザー>アカウント>一括ユーザーアクションの一括ユーザーアクション機能では、管理者がフィルターを作成してユーザーを選択し、以下のいずれかのアクションを実行することができます。

電子メールベースの自己登録で作成された、ユーザーによってまだ確認されていないユーザー アカウントの確認
・メッセージを送信する(メッセージングが必要です)
・ユーザーアカウントの削除
・ページにユーザーの一覧を表示する
・ユーザーデータをテキスト、ODS、またはExcelファイル形式でダウンロードできます。
・ユーザーにパスワードを強制的に変更させる
・ユーザーをコホートに追加する

バルクユーザーアクションを実行するには

・ユーザーを知っている場合は、利用可能なリストからユーザーを選択するか、「もっと表示」をクリックすると、より多くのフィルターオプションが表示されます。

頻繁に使用されるフィルタは、「デフォルトユーザーフィルタ」(userfiltersdefault) (サイト管理 / ユーザー / アカウント / ユーザー管理) で選択することができます。

・ユーザーは、フルネーム、姓、名、メールアドレス、都市/町、国、確認済み、初回アクセス、最終アクセス、最終ログイン、ユーザー名、認証に基づいてフィルタリングすることができます。(下のスクリーンショット1)
・カスタムプロファイルフィールドを設定している場合は、"プロファイル "ドロップダウンボックスからも利用可能です(以下のスクリーンショット2)。
・リストからユーザーを選択します。
・ドロップダウンメニューからアクションを選択します(下記スクリーンショット3)
・フィルタの追加ボタンをクリックします。
・左側のボックスで必要なユーザーを選択し、右側のボックスに移動します。
・選択したユーザーのドロップダウンボックスから、実行するアクションを選択し、[Go] をクリックします。
300px-Bulkuserfilter.png
(1) 「詳細を表示」をクリックします。
300px-customfilter.png
(2)カスタムプロファイルフィールドによるフィルタリング
300px-Bulkuser.png
(3) バルクユーザーアクションの選択
も参照してください。

video
バルクユーザーのアクションを実演するビデオ

カテゴリ | アカウント | サイト管理

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

Moodle 3.9 マニュアル - カテゴリ:登録

原文

カテゴリ:登録

コース内のロール (通常は学生) にユーザーを割り当てるプロセスである登録 (または登録) に関するドキュメントページのインデックス。
カテゴリ「登録」のページ

全60ページのうち、以下のページがこのカテゴリに該当します。

B

Banner LMB

C

Capabilities/enrol/category:config
Capabilities/enrol/category:synchronised
Capabilities/enrol/cohort:config
Capabilities/enrol/database:config
Capabilities/enrol/guest:config
Capabilities/enrol/imsenterprise:config
Capabilities/enrol/ldap:manage
Capabilities/enrol/manual:config
Capabilities/enrol/manual:enrol
Capabilities/enrol/manual:manage
Capabilities/enrol/manual:unenrol
Capabilities/enrol/manual:unenrolself
Capabilities/enrol/meta:config
Capabilities/enrol/meta:selectaslinked
Capabilities/enrol/mnet:config
Capabilities/enrol/paypal:config
Capabilities/enrol/paypal:manage
Capabilities/enrol/paypal:unenrol
Capabilities/enrol/paypal:unenrolself
Capabilities/enrol/self:config
Capabilities/enrol/self:holdkey
Capabilities/enrol/self:manage
Capabilities/enrol/self:unenrol
Capabilities/enrol/self:unenrolself
Capabilities/moodle/course:enrolconfig
Capabilities/moodle/course:enrolreview
Category enrolments
Cohort sync
Course enrolment
Course meta link

E

Enrolment FAQ
Enrolment key
Enrolment methods
Enrolment plugins
Development:Enrolment plugins
Development:Enrolment usage overview
Enrolments
error/moodle/disableunenroll
External database enrolment

F

Flat file

G

Guest access

I

IMS Enterprise

K

Keyholder role

L

LDAP enrolment

M

Manual enrolment
Metacourse examples of use
MNet
MNet remote enrolments

N

Development:New enrolments in 2.0

O

Openlml enrolment plugin
OSS enrolment plugin
Other users

P

Participants
PayPal enrolment
Profile field enrolment
Publish as LTI tool

R

Development:Role archetypes

S

Self enrolment

U

Unenrolment

メインページ

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

Moodle 3.9 マニュアル - 新規ユーザーの追加

原文

新規ユーザーの追加

内容

1 新規ユーザーの追加
2 一般
2.1 ユーザー名
2.2 認証方法
2.3 一時停止されたアカウント
2.4 パスワードを生成してユーザに通知する
2.5 パスワード
2.6 ファーストネーム
2.7 ラストネーム
2.8 メールアドレス
2.9 電子メールの表示
2.10 電子メールのフォーマット
2.11 メールダイジェストタイプ
2.12 フォーラムの自動購読
2.13 フォーラムのトラッキング
2.14 テキストを編集する場合
2.15 スクリーンリーダー
2.16 市町村
2.17 国
2.18 タイムゾーン
2.19 説明
3 ユーザーの画像
4 関心事
5 オプション

1 新規ユーザーの追加

管理者またはマネージャ(またはmoodle/user:createケイパビリティを持つ他のユーザ)は、サイト管理 > ユーザ > アカウント > 新しいユーザの追加で新しいユーザアカウントを作成することができます (ユーザを一括で追加するには、ユーザのアップロードを参照してください)。
26manualreg.png
26manualreg.png

2 一般

2.1 ユーザー名

ユーザはこのユーザ名を使用してMoodleインスタンスにログインします。このユーザ名は一意である必要があります。これは変更することができます。ユーザ名には小文字のアルファベット、数字、ハイフン '-'、アンダースコア '_'、ピリオド '.'、またはアット文字 '@' のみを含めることができます - あなたが「ユーザ名に拡張文字を許可する」を有効にしていない限り (サイト管理 > セキュリティ > サイトポリシー)

2.2 認証方法

この設定は、ユーザの指定したパスワードが正しいかどうかをMoodleがどのようにチェックするかを指定します。

管理者によって作成されたアカウントは手動アカウントメソッドを使用し、ユーザ自身が電子メールサインアップメソッドを使用して作成したアカウントは電子メールベースの自己登録メソッドを使用します。

2.3 一時停止されたアカウント

停止されたユーザーアカウントはログインやウェブサービスの利用ができず、送信メッセージはすべて破棄されます。

2.4 パスワードを生成してユーザーに通知する

Moodleは一時的なパスワードを生成し、ログインおよびパスワードの変更方法をユーザにEメールで通知します。(以下のスクリーンショットのような) 電子メールメッセージは、サイト管理 > 言語 > 言語のカスタマイズで変更することができます。moodle.php」と文字列識別子newusernewpasswordtextを選択してください。
300px-26emailpassword.png
新規ユーザーメール
300px-26changepasswordmessage.png
デフォルトのメッセージを変更する方法

2.5 パスワード

これはユーザーのパスワードです。サイトポリシーのパスワードポリシーに従います。ユーザーは、ユーザーメニューの「ユーザー」>「環境設定」からパスワードを変更することができます。

2.6 ファーストネーム

ユーザーのファーストネームです。メッセージ、フォーラムの投稿、参加者リスト、レポートなど、ユーザーに関する情報がページ上に表示される場所では、姓と一緒に表示されます。

2.7 ラストネーム

ユーザーの苗字です。メッセージ、フォーラムの投稿、参加者リスト、レポートなど、ユーザーに関する情報がページ上に表示される場所では、ファーストネームと一緒に表示されます。

2.8 メールアドレス

パスワードリセット通知、フォーラムダイジェスト、その他のメッセージはMoodleサイトからこのメールアドレスに送信されます。

3.0の新機能。サイト管理 > プラグイン > 認証 > 認証の管理で「同じメールアドレスを持つアカウントを許可する」設定が有効になっている場合、メールアドレスの重複を許可することができます。

2.9 電子メールの表示

この設定では、ユーザーのメールアドレスを誰が見ることができるかを制御します。

2.10 電子メールのフォーマット

この設定は、Moodleがユーザにテキストのみのメールを送信するように使用することができます。

2.11 メールダイジェストタイプ

この設定では、購読しているフォーラムの新しい投稿ごとにメールを受信するか、新しい投稿を1日1回ダイジェストで送信するか、どのタイプのダイジェストを受信するかを設定します。

・ダイジェストなし(個別のメールを受信する)。
・コンプリート(1日1回のダイジェスト)または
・お題(投稿トピックのみを含む毎日1回のダイジェスト

2.12 フォーラムの自動配信

ユーザーがフォーラムを購読すると、指定した通りに新しい投稿がダイジェストで送信されます。この設定では、ユーザーがフォーラムに自動的に購読するか、各フォーラムの購読ボタンを手動でクリックする必要があるかを設定します。

2.13 フォーラムのトラッキング

この設定では、ユーザーが最後に訪れてから書き込まれた新しい投稿をハイライト表示するかどうかを指定します。

2.14 テキストを編集する場合

この設定は、ユーザーがWYSIWYGテキストエディタを表示するか、単なるテキストボックスを表示するかを指定します。

2.15 スクリーンリーダー

この設定を有効にすると、Moodleの表示が改善され、スクリーンリーダーとの互換性が向上します。

2.16 市町村

ユーザーの市区町村

2.17 国

ユーザーの国

注: 都市と国の既定値は、管理 > サイト管理 > 位置情報 > 位置情報の設定で設定できます。

2.18 タイムゾーン

この設定はメッセージや課題/小テストの締切日の時間をユーザーの現地時間に合わせて調整するために使用します。

2.19 説明

他のユーザーが見ることができるユーザーに関する情報。

3 ユーザーの画像

ユーザの写真は、ユーザがフォーラムなどのMoodleアクティビティに投稿したコンテンツの横にユーザ名の横に表示することができます。詳細はユーザの写真を参照してください。

4 関心事

興味関心のリストは、似たような興味を持つユーザーをつなぐ方法として利用することができます。タグはサイト上で有効にする必要があります。

5 オプション

標準インストールには、いくつかのオプションフィールドがあります。これらには以下が含まれます。Webページ、ICA番号、Skype ID、AIM ID、Yahoo ID、MSN ID、ID番号、機関名、部署名、電話番号、携帯電話番号、住所。

サイト管理者は、より多くのカスタムフィールドを追加したり、これらの「オプション」フィールドをオフにしたりすることができます。

カテゴリ アカウント サイト管理

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

DateTime::formatメソッドに指定するフォーマットを無意味に長くすると,不具合を引き起こしやすい.

TL;DR

  • index.htmlという文字列をフォーマットとして \DateTime::format() メソッドに引き渡したところ,i801Asia/Tokyox.013108Saturday という出力が生成された.
  • format() メソッドに指定するフォーマットは,必要最低限とする.変換されない文字列と誤認して、n, eなどを指定してしまうと面倒なことになる.
  • 自分の想定通りに動くか,細かい単位で動作確認・テストを行う.

環境

  • PHP7.2
  • Ubuntu18.04 LTS

経緯

今日の日付情報を保持した \DateTime クラスからディレクトリパス(文字列)を format() メソッドを用いて生成し,それをキーとして指定してS3にファイルをアップロードするという処理にて,予期せぬ挙動が発生しました.
DateTime
具体的には,

  • 2020/08/index.html

というパスにファイルを自動でアップロードしたかったものが

  • 2020/08/i801Asia/Tokyox.013108Saturday

というパスにファイルがアップロードされてしまい,自動生成されるはずの目的のページが生成されないという意図せぬ挙動となりました.

CLIで実行して確認

\DateTime 型のインスタンスを取得し, format() 関数にフォーマットを引き渡して呼び出してやれば,渡したフォーマットに応じた文字列が返ります.

php > $date = new \DateTime();
php > echo $date->format('Y')
php > ;
2020
php > echo $date->format('m');
08

今回の問題箇所についても実行してみます.

php > echo $date->format('Y/m/index.html');
2020/08/33801Asia/Tokyox.013108Saturday

ああ...

下記のように変更を行い,正常に動作するようになりました.

php > echo $date->format('Y/m/').'index.html';
2020/08/index.html

教訓

  • format() メソッドに指定するフォーマットは,必要最低限とする.変換されない文字列だからといってむやみに詰め込まない.
  • 自分の想定通りに動くか,細かい単位でチェックを行う.
  • var_dump() さんありがとう.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【実装編】Laravel 8(+jetstream, fortify)でマルチログイン

はじめに

この記事ではLaravel8でマルチログインの実装をしていきます。
動作環境、詳しい仕様は前回の記事をご覧ください。
前回でUserのログイン機能とAdminモデル、テーブルの作成まで完了しました。
なお、本プロジェクトはGitHubにアップしてありますのでご参考ください。

マルチログイン実装

  • 一度ルーティングを確認してみる
    • php artisan route:listでルーティングを確認してみると、すでにUserの認証に必要なルーティングが設定されています。これはLaravel Fortify内部の/{your-workspace-root}/multi-auth/vendor/laravel/fortify/routes/routes.phpで定義されています。Userの認証関係のルーティングはこちらに全て任せることにしましょう。
    • login/logout処理に関してはAuthenticatedSessionControllerが担っていますので、Adminの認証もこちらを参考にしていきます。
+--------+----------+----------------------------------+---------------------------------+---------------------------------------------------------------------------------+---------------------------------------------------------+
| Domain | Method   | URI                              | Name                            | Action                                                                          | Middleware                                              |
+--------+----------+----------------------------------+---------------------------------+---------------------------------------------------------------------------------+---------------------------------------------------------+
|        | POST     | login                            |                                 | Laravel\Fortify\Http\Controllers\AuthenticatedSessionController@store           | App\Http\Middleware\EncryptCookies                      |
|        |          |                                  |                                 |                                                                                 | Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse |
|        |          |                                  |                                 |                                                                                 | Illuminate\Session\Middleware\StartSession              |
|        |          |                                  |                                 |                                                                                 | Laravel\Jetstream\Http\Middleware\AuthenticateSession   |
|        |          |                                  |                                 |                                                                                 | Illuminate\View\Middleware\ShareErrorsFromSession       |
|        |          |                                  |                                 |                                                                                 | App\Http\Middleware\VerifyCsrfToken                     |
|        |          |                                  |                                 |                                                                                 | Illuminate\Routing\Middleware\SubstituteBindings        |
|        |          |                                  |                                 |                                                                                 | App\Http\Middleware\RedirectIfAuthenticated             |
|        | GET|HEAD | login                            | login                           | Laravel\Fortify\Http\Controllers\AuthenticatedSessionController@create          | App\Http\Middleware\EncryptCookies                      |
multi-auth/vendor/laravel/fortify/routes/routes.php
Route::group(['middleware' => config('fortify.middleware', ['web'])], function () {
    // Authentication...
    Route::get('/login', [AuthenticatedSessionController::class, 'create'])
        ->middleware(['guest'])
        ->name('login');

    $limiter = config('fortify.limiters.login');

    Route::post('/login', [AuthenticatedSessionController::class, 'store'])
        ->middleware(array_filter([
            'guest',
            $limiter ? 'throttle:'.$limiter : null,
        ]));

    Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])
        ->name('logout');
~~省略~~
multi-auth/vendor/laravel/fortify/src/Http/Controllers/AuthenticatedSessionController.php
<?php

namespace Laravel\Fortify\Http\Controllers;

use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Routing\Pipeline;
use Laravel\Fortify\Actions\AttemptToAuthenticate;
use Laravel\Fortify\Actions\EnsureLoginIsNotThrottled;
use Laravel\Fortify\Actions\PrepareAuthenticatedSession;
use Laravel\Fortify\Actions\RedirectIfTwoFactorAuthenticatable;
use Laravel\Fortify\Contracts\LoginResponse;
use Laravel\Fortify\Contracts\LoginViewResponse;
use Laravel\Fortify\Contracts\LogoutResponse;
use Laravel\Fortify\Fortify;
use Laravel\Fortify\Http\Requests\LoginRequest;

class AuthenticatedSessionController extends Controller
{
    /**
     * The guard implementation.
     *
     * @var \Illuminate\Contracts\Auth\StatefulGuard
     */
    protected $guard;

    /**
     * Create a new controller instance.
     *
     * @param  \Illuminate\Contracts\Auth\StatefulGuard
     * @return void
     */
    public function __construct(StatefulGuard $guard)
    {
        $this->guard = $guard;
    }

    /**
     * Show the login view.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Laravel\Fortify\Contracts\LoginViewResponse
     */
    public function create(Request $request): LoginViewResponse
    {
        return app(LoginViewResponse::class);
    }

    /**
     * Attempt to authenticate a new session.
     *
     * @param  \Laravel\Fortify\Http\Requests\LoginRequest  $request
     * @return mixed
     */
    public function store(LoginRequest $request)
    {
        return $this->loginPipeline($request)->then(function ($request) {
            return app(LoginResponse::class);
        });
    }

    /**
     * Get the authentication pipeline instance.
     *
     * @param  \Laravel\Fortify\Http\Requests\LoginRequest  $request
     * @return \Illuminate\Pipeline\Pipeline
     */
    protected function loginPipeline(LoginRequest $request)
    {
        if (Fortify::$authenticateThroughCallback) {
            return (new Pipeline(app()))->send($request)->through(array_filter(
                call_user_func(Fortify::$authenticateThroughCallback, $request)
            ));
        }

        if (is_array(config('fortify.pipelines.login'))) {
            return (new Pipeline(app()))->send($request)->through(array_filter(
                config('fortify.pipelines.login')
            ));
        }

        return (new Pipeline(app()))->send($request)->through(array_filter([
            config('fortify.limiters.login') ? null : EnsureLoginIsNotThrottled::class,
            RedirectIfTwoFactorAuthenticatable::class,
            AttemptToAuthenticate::class,
            PrepareAuthenticatedSession::class,
        ]));
    }

    /**
     * Destroy an authenticated session.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Laravel\Fortify\Contracts\LogoutResponse
     */
    public function destroy(Request $request): LogoutResponse
    {
        $this->guard->logout();

        $request->session()->invalidate();

        $request->session()->regenerateToken();

        return app(LogoutResponse::class);
    }
}

基本的にFortifyの各クラスを参考に実装していきます。それでは作成していきましょう。

AdminLoginResponseクラスの作成

  • multi-auth/app/Responsesディレクトリを作成し、その中にAdminLoginResponseを作成
    • AdminLoginResponseは/{your-workspace-root}/multi-auth/vendor/laravel/fortify/src/Http/Responses/LoginResponse.phpからコピペして一部修正します。
muti-auth/app/Responses/AdminLoginResponse.php
<?php

namespace App\Responses;

use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;

class AdminLoginResponse implements LoginResponseContract
{
    /**
     * Create an HTTP response that represents the object.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function toResponse($request)
    {
        return $request->wantsJson()
            ? response()->json(['two_factor' => false])
            : redirect()->intended('admin/dashboard'); // ログイン後に遷移させたいリダイレクト先を指定
    }
}

login.blade.phpの修正

  • Userと同じBladeを使用しますがAdmin用に分けても構いません。今回は簡易にするために、同じBladeですがguard変数があるかどうかでactionのURLを分けています。
multi-auth/resources/views/auth/login.blade.php
<x-guest-layout>
    <x-jet-authentication-card>
        <x-slot name="logo">
            <x-jet-authentication-card-logo />
        </x-slot>

        <x-jet-validation-errors class="mb-4" />

        @if (session('status'))
        <div class="mb-4 font-medium text-sm text-green-600">
            {{ session('status') }}
        </div>
        @endif

        <!-- guard変数がセットされているかによってPOST先を変更 -->
        <form method="POST" action="{{ isset($guard) ? route('admin.login') : route('login') }}">
            @csrf

            <div>
                <x-jet-label for="email" value="{{ __('Email') }}" />
                <x-jet-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')"
                    required autofocus />
            </div>

            <div class="mt-4">
                <x-jet-label for="password" value="{{ __('Password') }}" />
                <x-jet-input id="password" class="block mt-1 w-full" type="password" name="password" required
                    autocomplete="current-password" />
            </div>

            <div class="block mt-4">
                <label for="remember_me" class="flex items-center">
                    <input id="remember_me" type="checkbox" class="form-checkbox" name="remember">
                    <span class="ml-2 text-sm text-gray-600">{{ __('Remember me') }}</span>
                </label>
            </div>

            <div class="flex items-center justify-end mt-4">
                @if (Route::has('password.request'))
                <a class="underline text-sm text-gray-600 hover:text-gray-900" href="{{ route('password.request') }}">
                    {{ __('Forgot your password?') }}
                </a>
                @endif

                <x-jet-button class="ml-4">
                    {{ __('Login') }}
                </x-jet-button>
            </div>
        </form>
    </x-jet-authentication-card>
</x-guest-layout>

AttemptToAuthenticateクラスをapp/Actions/Admin配下に作成

認証処理を実施するクラスです。

  • {your-workspace-root}/multi-auth/vendor/laravel/fortify/src/Actions/AttemptToAuthenticate.phpからコピペして一部修正します。
multi-auth/app/Actions/Admin/AttemptToAuthenticate.php
<?php

namespace App\Actions\Admin;

use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Fortify;
use Laravel\Fortify\LoginRateLimiter;

class AttemptToAuthenticate
{
    /**
     * The guard implementation.
     *
     * @var \Illuminate\Contracts\Auth\StatefulGuard
     */
    protected $guard;

    /**
     * The login rate limiter instance.
     *
     * @var \Laravel\Fortify\LoginRateLimiter
     */
    protected $limiter;

    /**
     * Create a new controller instance.
     *
     * @param  \Illuminate\Contracts\Auth\StatefulGuard  $guard
     * @param  \Laravel\Fortify\LoginRateLimiter  $limiter
     * @return void
     */
    public function __construct(StatefulGuard $guard, LoginRateLimiter $limiter)
    {
        $this->guard = $guard;
        $this->limiter = $limiter;
    }

    /**
     * Handle the incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  callable  $next
     * @return mixed
     */
    public function handle($request, $next)
    {
        if (Fortify::$authenticateUsingCallback) {
            return $this->handleUsingCustomCallback($request, $next);
        }

        if ($this->guard->attempt(
            $request->only(Fortify::username(), 'password'),
            $request->filled('remember')
        )) {
            return $next($request);
        }

        $this->throwFailedAuthenticationException($request);
    }

    /**
     * Attempt to authenticate using a custom callback.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  callable  $next
     * @return mixed
     */
    protected function handleUsingCustomCallback($request, $next)
    {
        $user = call_user_func(Fortify::$authenticateUsingCallback, $request);

        if (!$user) {
            return $this->throwFailedAuthenticationException($request);
        }

        $this->guard->login($user, $request->filled('remember'));

        return $next($request);
    }

    /**
     * Throw a failed authentication validation exception.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return void
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    protected function throwFailedAuthenticationException($request)
    {
        $this->limiter->increment($request);

        throw ValidationException::withMessages([
            Fortify::username() => [trans('auth.failed')],
        ]);
    }
}

AdminLoginServiceProviderクラスの作成

  • 先ほど作成したAttemptToAuthenticateクラスと後ほど作成するLoginControllerで使用するguardをDIするためにAdminLoginServiceProviderクラスを作成します。
$ php artisan make:provider AdminLoginServiceProvider
Provider created successfully.
multi-auth/app/Providers/AdminLoginServiceProvider.php
<?php

namespace App\Providers;

use App\Http\Controllers\Auth\LoginController;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Support\Facades\Auth;
use App\Actions\Admin\AttemptToAuthenticate;
use Illuminate\Support\ServiceProvider;

class AdminLoginServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app
            ->when([LoginController::class, AttemptToAuthenticate::class])
            ->needs(StatefulGuard::class)
            ->give(function () {
                return Auth::guard('admin');
            });
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

app.phpを修正

作成したAdminLoginServiceProviderが反映されるようにapp.phpを修正します。

multi-auth/config/app.php
~~省略~~
        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
        App\Providers\FortifyServiceProvider::class,
        App\Providers\JetstreamServiceProvider::class,
        App\Providers\AdminLoginServiceProvider::class, // この行を追加

Admin認証用のLoginControllerを作成

  • ログイン画面表示や認証、ログアウトなどのアクションを受けるLoginControllerをapp/Http/Controllers/Auth配下に作成します。
  • {your-workspace-root}/multi-auth/vendor/laravel/fortify/src/Http/Controllers/AuthenticatedSessionController.phpを参考に、先ほどまでで作った各クラスを利用するように修正します。
$ php artisan make:controller Auth/LoginController
Controller created successfully.
multi-auth/app/Http/Controllers/Auth/LoginController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Http\Request;
use Illuminate\Routing\Pipeline;
use App\Actions\Admin\AttemptToAuthenticate;
use Laravel\Fortify\Actions\PrepareAuthenticatedSession;
use App\Responses\AdminLoginResponse;
use Laravel\Fortify\Contracts\LogoutResponse;
use Laravel\Fortify\Http\Requests\LoginRequest;

class LoginController extends Controller
{
    /**
     * The guard implementation.
     *
     * @var \Illuminate\Contracts\Auth\StatefulGuard
     */
    protected $guard;

    /**
     * Create a new controller instance.
     *
     * @param  \Illuminate\Contracts\Auth\StatefulGuard
     * @return void
     */
    public function __construct(StatefulGuard $guard)
    {
        $this->guard = $guard;
    }

    /**
     * Show the login view.
     *
     * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
     */
    public function create()
    {
        return view('auth.login', ['guard' => 'admin']);
    }

    /**
     * Attempt to authenticate a new session.
     *
     * @param  \Laravel\Fortify\Http\Requests\LoginRequest  $request
     * @return mixed
     */
    public function store(LoginRequest $request)
    {
        return $this->loginPipeline($request)->then(function ($request) {
            return app(AdminLoginResponse::class);
        });
    }

    /**
     * Get the authentication pipeline instance.
     *
     * @param  \Laravel\Fortify\Http\Requests\LoginRequest  $request
     * @return \Illuminate\Pipeline\Pipeline
     */
    protected function loginPipeline(LoginRequest $request)
    {
        return (new Pipeline(app()))->send($request)->through(array_filter([
            AttemptToAuthenticate::class,
            PrepareAuthenticatedSession::class,
        ]));
    }

    /**
     * Destroy an authenticated session.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Laravel\Fortify\Contracts\LogoutResponse
     */
    public function destroy(Request $request): LogoutResponse
    {
        $this->guard->logout();

        $request->session()->invalidate();

        $request->session()->regenerateToken();

        return app(LogoutResponse::class);
    }
}

AdminDashboardControllerとadmin/dashboard.blade.phpの作成

  • 管理者ログイン後のダッシュボード表示用にAdminDashboardControllerとadmin/dashboard.blade.phpを作成します。
  • admin/dashboard.blade.phpはdashboard.blade.phpからコピペして一部編集します。
$ php artisan make:controller AdminDashboardController
Controller created successfully.
multi-auth/app/Http/Controllers/AdminDashboardController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class AdminDashboardController extends Controller
{
    public function index()
    {
        return view('admin.dashboard');
    }
}
/multi-auth/resources/views/admin/dashboard.blade.php
<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            <!-- わかりやすいようにここを修正-->
            管理者ダッシュボード
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
                <x-jet-welcome />
            </div>
        </div>
    </div>
</x-app-layout>

未ログイン時の挙動を追記

  • User、Adminともに認証が必要なページ(今回はdashboard, admin/dashboard)にアクセスした場合、それぞれのログインページにリダイレクトさせるためにAuthenticate.phpに追記します。
multi-auth/app/Http/Middleware/Authenticate.php
<?php

namespace App\Http\Middleware;

use Illuminate\Auth\Middleware\Authenticate as Middleware;

class Authenticate extends Middleware
{
    /**
     * Get the path the user should be redirected to when they are not authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return string|null
     */
    protected function redirectTo($request)
    {
        if (!$request->expectsJson()) {
            if ($request->is('admin/*')) {
                return route('admin.login');
            }
            return route('login');
        }
    }
}

web.phpの修正

  • 管理者ログインとダッシュボード用のルーティングを追記します。
multi-auth/routes/web.php
<?php

use App\Http\Controllers\AdminDashboardController;
use App\Http\Controllers\Auth\LoginController;
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');
});

Route::prefix('admin')->group(function () {
    Route::get('login', [LoginController::class, 'create'])->name('admin.login');
    Route::post('login', [LoginController::class, 'store']);

    Route::middleware('auth:admin')->group(function () {
        Route::get('dashboard', [AdminDashboardController::class, 'index']);
    });
});

Route::middleware(['auth:web', 'verified'])->get('/dashboard', function () {
    return view('dashboard');
})->name('dashboard');

auth.phpの修正

  • 最後にauth.phpを修正し、Admin用の認証ガードであるadminを定義します。
  • ファイルを修正した後はphp artisan config:cacheで設定値を反映させましょう。
multi-auth/config/auth.php
~~省略~~
    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'admin' => [
            'driver' => 'session',
            'provider' => 'admins',
        ],

        'api' => [
            'driver' => 'token',
            'provider' => 'users',
            'hash' => false,
        ],
    ],
~~省略~~
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Models\Admin::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],
~~省略~~
    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],

        'admins' => [
            'provider' => 'admins',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

ビルトインサーバーを起動して(php artisan serve)管理者ログインができるか確認

スクリーンショット 2020-10-19 19.41.51.png
スクリーンショット 2020-10-19 19.42.00.png

無事に管理者ログインできました:tada:

最後に

Fortifyではかなり多くのクラスが登場し、なおかつDIがかなり使われているため最初はなかなかコードを追うのが大変でしたが、少しずつ読み解いていくことで新しい認証スキャフォールドについて理解を深めることができました。Laravelはいいぞ。

誤字・脱字、間違った実装などありましたらコメント等いただけますと幸いです?
それでは良いLaravelライフを?

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

【準備編】Laravel 8(+jetstream, fortify)でマルチログイン

はじめに

Laravel8から標準のユーザー認証機能がlaravel/uiからLaravel Jetstream + Laravel Fortifyに変更されました。新しい認証機能もlaravel/uiと同様に導入自体は非常に簡単で、かつ多機能(プロフィール管理、チーム管理、二段階認証などなど)、必要に応じてconfigファイルで利用する機能を選択することが可能です。その一方で認証処理の実装が変わっていることもあり、拡張するには少し手間がかかります(ました)。ドキュメントでは下記のように言及されていますが、

Laravel Jetstreamが用意する認証スカフォールドを使う必要はないことに留意してください。このスカフォールドを使用しない選択をした場合、Laravelの認証クラスを直接使用し、ユーザーの認証管理を行う必要があります。

せっかくならば新しい認証機能使いたいぞ!
ということで、本記事では一般ユーザーと管理者ユーザーでログイン画面、ログイン後の画面を分けるマルチログインをLaravel JetstreamとLaravel Fortifyを使って実装していきます。
なお、Laravel Fortifyの詳しい処理を深く追ったり説明したり、ということはこの記事ではしません。

動作環境

本記事では全てローカルで動作させます

  • PHP 7.4.11
  • MySQL 8.0.21
  • Laravel Framework 8.10.0

マルチログインの仕様

  • 一般ユーザー(User)、管理者(Admin)でログインユーザーとページを分ける
  • ルーティングは以下とする(本記事ではドメインはlocalhost:8000として扱います)
    • 一般ユーザーログインページ
      • localhost:8000/login
    • 一般ユーザートップページ
      • localhost:8000/dashboard
    • 管理者ログインページ
      • localhost:8000/admin/login
    • 管理者トップページ
      • localhost:8000/admin/dashboard
  • 両ユーザー共に未ログインの時にはトップページを表示することはできない(ログインページにリダイレクトさせる)

プロジェクトの準備

  • プロジェクトの生成(今回はmulti-authというプロジェクトにしています)
    • Blade + livewireを使用するのでWhich Jetstream stack do you prefer?は0を選択
    • teamsは使用しないのでWill your application use teams?はnoを選択
$ laravel new multi-auth


    |     |         |
    |,---.|--- ,---.|--- ,---.,---.,---.,-.-.
    ||---'|    `---.|    |    |---',---|| | |
`---'`---'`---'`---'`---'`    `---'`---^` ' '


Which Jetstream stack do you prefer?
  [0] livewire
  [1] inertia
 > 0

 Will your application use teams? (yes/no) [no]:
 > no

Installing laravel/laravel (v8.1.0)
  - Installing laravel/laravel (v8.1.0): Downloading (100%)         
Created project in /{your-workspace-root}/multi-auth
~~省略~~
Application ready! Build something amazing.
  • プロジェクトが生成されたらアセットのビルドを実行
$ cd multi-auth
$ npm install && npm run dev
  • .envのDB_DATABASEと同じ名前のDBを、ローカルのMySQLで作成してmigrationを実行
.env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=multi_auth ← これ
DB_USERNAME=root
DB_PASSWORD=
$ php artisan migrate

Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (42.86ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (19.50ms)
Migrating: 2014_10_12_200000_add_two_factor_columns_to_users_table
Migrated:  2014_10_12_200000_add_two_factor_columns_to_users_table (22.99ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (25.19ms)
Migrating: 2019_12_14_000001_create_personal_access_tokens_table
Migrated:  2019_12_14_000001_create_personal_access_tokens_table (39.09ms)
Migrating: 2020_10_14_073012_create_sessions_table
Migrated:  2020_10_14_073012_create_sessions_table (52.85ms)
  • Adminモデルの作成とmigrationを実行
    • create_users_table.phpからupメソッドの中身をコピペ
$ php artisan make:model Admin -m
Model created successfully.
Created Migration: 2020_10_14_075325_create_admins_table
create_admins_table.php
~~省略~~
    public function up()
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->foreignId('current_team_id')->nullable();
            $table->text('profile_photo_path')->nullable();
            $table->timestamps();
        });
    }
$ php artisan migrate
Migrating: 2020_10_14_075325_create_admins_table
Migrated:  2020_10_14_075325_create_admins_table (31.63ms)
  • Adminモデルの内容を修正(User.phpからコピペ)
Admin.php
<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Jetstream\HasProfilePhoto;
use Laravel\Sanctum\HasApiTokens;

class Admin extends Authenticatable
{
    use HasApiTokens;
    use HasFactory;
    use HasProfilePhoto;
    use Notifiable;
    use TwoFactorAuthenticatable;

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

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
        'two_factor_recovery_codes',
        'two_factor_secret',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    /**
     * The accessors to append to the model's array form.
     *
     * @var array
     */
    protected $appends = [
        'profile_photo_url',
    ];
}

  • 一般ユーザーと管理者をシーダーで生成できるようにする
$ php artisan make:seeder UserSeeder
Seeder created successfully.
$ php artisan make:seeder AdminSeeder
Seeder created successfully.
UserSeeder.php
<?php

namespace Database\Seeders;

use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;

class UserSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        User::create([
            'name' => '一般ユーザー',
            'email' => 'user@user.user',
            'password' => Hash::make('password'),
        ]);
    }
}
AdminSeeder.php
<?php

namespace Database\Seeders;

use App\Models\Admin;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;

class AdminSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Admin::create([
            'name' => '管理者',
            'email' => 'admin@admin.admin',
            'password' => Hash::make('password')
        ]);
    }
}
DatabaseSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call(UserSeeder::class);
        $this->call(AdminSeeder::class);
    }
}
$ php artisan db:seed 
Seeding: Database\Seeders\UserSeeder
Seeded:  Database\Seeders\UserSeeder (250.64ms)
Seeding: Database\Seeders\AdminSeeder
Seeded:  Database\Seeders\AdminSeeder (106.95ms)
Database seeding completed successfully.
  • ビルトインサーバーを起動してブラウザでlocalhost:8000を表示
    • localhost:8000/loginにて一般ユーザーでログインできるか確認
$ php artisan serve

Starting Laravel development server: http://127.0.0.1:8000
[Mon Oct 1 00:00:00 0000] PHP 7.4.11 Development Server (http://127.0.0.1:8000) started

スクリーンショット 2020-10-19 14.54.04.png

スクリーンショット 2020-10-19 14.53.37.png

無事ログインできれば準備編は終了

実装編に続きます

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

Laravel7 でCSSが読み込めない

Laravel 7.25.0です。

CSSの場所:
Laravel/public/css/app.css
HTMLの場所
Laravel/resources/views/layouts.blade.php

問題点

何故かCSSが読み込めなかった。

解決

<link rel="stylesheet" href="/css/app.css">

<link rel="stylesheet" href="{{ asset('css/app.css') }}">

に変えたら読み込めました。

原因

不明です。(すみません)

参考

https://teratail.com/questions/126308

おしまい。

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