Laravelに入門したい!(Laravel APIサーバー+React SPA)vol.2
前回:Laravelに入門したい!(Laravel APIサーバー+React SPA)vol.1
今回は、ログイン/ログアウト/ユーザーデータ取得/メールベリファイ/パスワード忘れを確認してみる。
ログイン
前回同様、テスト用のコンポーネントを作っていく
import { Box, Button, Stack, TextField } from "@mui/material"; import axios from "axios"; import React from "react"; export default function TestLogin(): JSX.Element { const handleLogin = React.useCallback((e: React.FormEvent<HTMLFormElement>) => { e.preventDefault() const formData = new FormData(e.currentTarget) axios.post("/login", { "email": formData.get("email"), "password": formData.get("password"), }, { withCredentials: true }).then(res => { console.log(res.data) }).catch(e => { console.error(e) }) }, []) return ( <Box p={2} > <Box component={"form"} p={3} onSubmit={handleLogin} > <Stack spacing={2} > <TextField label="email" name="email" type="email" size='small' required /> <TextField label="password" name="password" type='password' size='small' required /> <Button variant='outlined' fullWidth type='submit' >Login!</Button> </Stack> </Box> </Box> ) }
ログインするだけ!結果はコンソールに表示。
ログアウト
import { Box, Button } from "@mui/material"; import axios from "axios"; import React from "react"; export default function TestLogout(): JSX.Element { const handleLogout = React.useCallback((e: React.MouseEvent<HTMLButtonElement>) => { e.preventDefault() axios.post("http://localhost:8000/logout", undefined, { withCredentials: true }).then(res => { console.log(res.data) }).catch(e => { console.error(e) }) }, []) return ( <Box p={3} > <Button variant='outlined' fullWidth onClick={handleLogout} >Logout!</Button> </Box> ) }
こちらもログアウトするだけ。結果はコンソールに表示。
ユーザーデータ取得
import { Box, Button, Paper, Typography } from "@mui/material"; import axios from "axios"; import React from "react"; interface UserData { id: number name: string email: string created_at: string updated_at: string email_vefified_at: string } export default function TestUser(): JSX.Element { const [userdata, setUserdata] = React.useState<UserData>() const handleUser = React.useCallback((e: React.MouseEvent<HTMLButtonElement>) => { e.preventDefault() axios.get("http://localhost:8000/api/user", { withCredentials: true }).then(res => { console.log(res.data) setUserdata(res.data) }).catch(e => { console.error(e) setUserdata(undefined) }) }, []) return ( <Box p={3} > <Button variant='outlined' fullWidth onClick={handleUser} >User!</Button> <Paper variant="outlined" sx={{ mt: 2 }} > {userdata ? ( <Box p={2} textAlign={"left"} > <Typography>id: {userdata?.id}</Typography> <Typography>name: {userdata?.name}</Typography> <Typography>email: {userdata?.email}</Typography> <Typography>created_at: {userdata?.created_at}</Typography> <Typography>updated_at: {userdata?.updated_at}</Typography> <Typography>email_vefified_at: {userdata?.email_vefified_at}</Typography> </Box> ) : ( <Box p={2} textAlign={"left"} > <Typography>not login</Typography> </Box> )} </Paper> </Box> ) }
ユーザーデータを取得して表示する。
メールベリファイ
import { Box, Button } from "@mui/material"; import axios from "axios"; import React from "react"; export default function TestEmailVerificationNotification(): JSX.Element { const handleEmailVerificationNotification = React.useCallback((e: React.MouseEvent<HTMLButtonElement>) => { e.preventDefault() axios.post("http://localhost:8000/email/verification-notification", undefined, { withCredentials: true }).then(res => { console.log(res.data) }).catch(e => { console.error(e) }) }, []) return ( <Box p={2} > <Button variant='outlined' fullWidth onClick={handleEmailVerificationNotification} >EmailVerificationNotification!</Button> </Box> ) }
メールベリファイ通知を送るだけ。結果はコンソールに表示。
パスワード忘れ・リセット
import { Box, Button, Stack, TextField } from "@mui/material"; import axios from "axios"; import React from "react"; export default function TestForgotPassword(): JSX.Element { const handleForgotPassword = React.useCallback((e: React.FormEvent<HTMLFormElement>) => { e.preventDefault() const formData = new FormData(e.currentTarget) axios.post("http://localhost:8000/forgot-password", { "email" : formData.get("email") }, { withCredentials: true }).then(res => { console.log(res.data) }).catch(e => { console.error(e) }) }, []) return ( <Box component={"form"} p={3} onSubmit={handleForgotPassword} > <Stack spacing={2} > <TextField label="email" name="email" type="email" size='small' required /> <Button variant='outlined' fullWidth type='submit' >ForgotPassword!</Button> </Stack> </Box> ) }
パスワードを忘れたよーを送信するだけ。結果はコンソールに表示。
バックエンドからメールが送信される。
import { Box, Button, Stack, TextField } from "@mui/material"; import React from "react"; import axios from "axios"; export default function TestResetPassword(): JSX.Element { const handleResetPassword = React.useCallback((e: React.FormEvent<HTMLFormElement>) => { e.preventDefault() const formData = new FormData(e.currentTarget) axios.post("http://localhost:8000/reset-password", { token: formData.get("token"), email: formData.get("email"), password: formData.get("password"), password_confirmation: formData.get("password_confirmation"), }, { withCredentials: true }).then(res => { console.log(res.data) }).catch(e => { console.error(e) }) }, []) return ( <Box component={"form"} p={3} onSubmit={handleResetPassword} > <Stack spacing={2} > <TextField label="token" name="token" type="text" size='small' required /> <TextField label="email" name="email" type="email" size='small' required /> <TextField label="password" name="password" type="password" size='small' required /> <TextField label="pasword(confirm)" name="password_confirmation" type='password' size='small' required /> <Button variant='outlined' fullWidth type='submit' >ResetPassword!</Button> </Stack> </Box> ) }
パスワード忘れたよーのメールに記載されたtokenを使って、パスワードをリセットする。結果はコンソールに表示。
各コンポーネントを表示
import { Paper, Stack } from '@mui/material'; import './App.css'; import TestCSRF from './test/TestCSRF'; import TestCreateUser from './test/TestCreateUser'; import TestEmailVerificationNotification from './test/TestEmailVerificationNotification'; import TestForgotPassword from './test/TestForgotPassword'; import TestLogin from './test/TestLogin'; import TestLogout from './test/TestLogout'; import TestResetPassword from './test/TestResetPassword'; import TestUser from './test/TestUser'; function App() { return ( <div className="App"> <Stack spacing={3} p={3} mx={"auto"} maxWidth={"sm"} > <Paper variant='outlined' > <TestCSRF /> </Paper> <Paper variant='outlined' > <TestCreateUser /> </Paper> <Paper variant='outlined' > <TestLogin /> </Paper> <Paper variant='outlined' > <TestLogout /> </Paper> <Paper variant='outlined' > <TestUser /> </Paper> <Paper variant='outlined' > <TestEmailVerificationNotification /> </Paper> <Paper variant='outlined' > <TestForgotPassword /> </Paper> <Paper variant='outlined' > <TestResetPassword /> </Paper> </Stack> </div> ); } export default App;
表示してみる
表示するとこんな感じ
もうちょっと見やすく作ればよかったと思う後悔はおいといて、これで、デフォルトで追加されているルートはだいたい利用できる。
残ったルートは、
GET|HEAD /
POST _ignition/execute-solution
GET|HEAD _ignition/health-check
POST _ignition/update-config
GET|HEAD verify-email/{id}/{hash}
くらい。
「/」はバージョンを表示させるだけのルート
「_ignition/・・・」はデバッグ用のルート
「verify-email/{id}/{hash}」はメールベリファイ用のルート(メールに載るURL)
動作を確認してみる
長々と確認したことを書いてみる。
まずは、ログインしているかしていないのかよくわからない状態なので、「USER!」ボタンを押してみる
「not login」のままの場合と、ユーザーデータが表示される場合がある。
これは、ユーザーを作成したあとすぐに「USER!」ボタンを押すと、ユーザーデータが表示される模様。
おそらくユーザー作成時、自動的にログインする仕様になっているということであろう。
ログインできていない場合は、emailとpasswordを入力して、「LOGIN!」を押してみる。
そのあと、「USER!」ボタンを押すと、ユーザーデータが表示される。
あれ、、、?CSRFトークンは?と思ったけど、ログインできていなくても、「/api/user」にアクセスしたときに、CSRFトークンがセットされている模様。
試しに、バージョンを取得する「/」にアクセスしてみた場合もCSRFトークンがセットされていたので、
単純にGETの場合は、CSRFトークンがセットされるのかな?
とにかく、ログインできたので、そのままログアウトを試す。
「LOGOUT!」ボタンからの「USER!」ボタンで、「not login」が表示される。
「EmailVerificationNotification!」ボタンはログインが必要なので、ログインしてから、「EmailVerificationNotification!」ボタンを押す。
すると、backendがメールを送信する。メールの確認はMailDevを使う。docker-compose.ymlをLaravelに入門したい!(Laravel APIサーバー+React SPA)vol.1で設定したものと同じにしてあれば、http://localhost:1080/ で確認ができる。
メールのVerify Email Addressをクリックすると、メール確認ができる。
メールベリファイが完了したら、ユーザーデータを取得したとき「email_verified_at」にメールベリファイ完了日時が入る。
パスワード忘れによるパスワードリセットは、ログアウトしている状態である必要があるので、いったんログアウトする。
そのあと、emailを入力して、「FORGOT PASSWORD!」をクリックすると、メールが送信される。(ログインしている場合は、/dashboardにリダイレクト、メールアドレスが見つからないときは、「422」エラー)
本当は、メールのリンクをクリックするとtokenをSPA内で取得するようにすべきだけど、今回は、そんなことしていないので、メールに記載されているURL「http://localhost:3000/password-reset/〇〇?email=△△」の〇〇の部分がtokenなので、そこをコピペして、token、email、password、password(confirm)を入力して、「RESETPASSWORD!」をクリック。
新しいパスワードでログインができるか確認してみる。
ここまでやってみて、、、
パスワードリセットが最初から使えるのはありがたい!
でも、メールアドレスが見つからないとき「422」返す仕様とか、やたら、dashboardにリダイレクトしたがる仕様とか、(あたりまえだけど)実際に使うにはいろいろとカスタマイズしないといけないなと。
あと、個人的には、正常完了時、dataに何も入っていないので、{status: “ok”}くらいは返しときたいと思ったりする。
次回は、ここから仕様を変更していこうかな。
Laravelに入門したい!(Laravel APIサーバー+React SPA)vol.1
やっぱ、PHPはLaravelなのか!?
フルスタックのフレームワークはあまり乗り気ではないけれど、
そんなことを思ってしまう今日この頃。。。
今回はLaravelに入門してみたい。
とりあえず、LaravelでAPIサーバーを作ってReactでSPA作って、パスワード認証する感じのものを作っていこうと思う。
まずは、ユーザー登録機能まで。
環境
・PHP:8.1
・Laravel:10.x
・データベース:mysql8
とりあえずLaravel
まずはLaravelのプロジェクトを新規作成
場所は「/backend」にする。
$ composer create-project laravel/laravel backend
この状態からでもいろいろできそうだけど、今回は、Laravelをバックエンドのみで使いたい。
ということで、スターターキットを使ってみる。
$ cd backend /backend $ composer require laravel/breeze --dev /backend $ php artisan breeze:install api
ないものが追加されていく感じなのかなと思っていたら、いろいろと不必要なものも削除してくれているじゃないか。
ありがたい話です。
データベース準備
docker composeで準備してみる。
データベースサーバーはmysql8
ついでに、phpmyadminとmaildevも入れておく。
ファイルを作成「/docker-compose.yml」
volumes: local-dev-db: services: app_db: image: mysql:8 environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword} MYSQL_DATABASE: ${DATABASE_NAME:-app_db} MYSQL_USER: ${DATABASE_USER:-app_db} MYSQL_PASSWORD: ${DATABASE_USER_PASSWORD:-password} TZ: "Asia/Tokyo" ports: - "3306:3306" volumes: - local-dev-db:/var/lib/mysql phpmyadmin: image: phpmyadmin environment: PMA_ARBITRARY: 1 PMA_HOST: app_db PMA_USER: root PMA_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword} MEMORY_LIMIT: 512M UPLOAD_LIMIT: 512M ports: - "8080:80" maildev: image: maildev/maildev ports: - "1080:1080" - "1025:1025"
必要であれば、「/.env」を作成
MYSQL_ROOT_PASSWORD=password_for_mysql_root_user DATABASE_NAME=application_database_name DATABASE_USER=application_database_user DATABASE_USER_PASSWORD=password_for_application_database_user
dockerを立ち上げる
/ $ docker compose up -d
laravelのデータベースとメールサーバーの設定をdocker環境に合わせる
.... DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=application_database_user_laravel_api DB_USERNAME=application_database_user DB_PASSWORD=password_for_application_database_user .... MAIL_MAILER=smtp MAIL_HOST=127.0.0.1 MAIL_PORT=1025 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_NAME="${APP_NAME}" ...
いったんデータベースのマイグレーションをしておく。
/backend $ php artisan migrate
バックエンド側はいったん終了
SPA側を準備
Create Reactで作成しておく。ついでにmuiとaxiosを入れておく。
(fetchだとうまく動作しなかった。。。多分設定不十分なのだけど、まだちゃんと調査していない。)
階層は「/frontend」
/ $ npx create-react-app frontend --template typescript / $ cd frontend /frontend $ npm i @mui/material @emotion/react @emotion/styled @mui/icons-material axios
次は、動作確認用のフォームを作っていく。
ユーザー登録用フォーム
/frontend/spa/src/test/TestCreateUser.tsx を作成
import { Box, Button, Stack, TextField } from "@mui/material"; import React from "react"; import axios from "axios"; export default function TestCreateUser(): JSX.Element { const handleCreate = React.useCallback((e: React.FormEvent<HTMLFormElement>) => { e.preventDefault() const formData = new FormData(e.currentTarget) axios.post("http://localhost:8000/register", { "name": formData.get("name"), "email": formData.get("email"), "password": formData.get("password"), "password_confirmation": formData.get("password_confirmation") }, { withCredentials: true }).then(res => { console.log(res.data) }).catch(e => { console.error(e) }) }, []) return ( <Box p={2} > <Box component={"form"} p={3} onSubmit={handleCreate} > <Stack spacing={2} > <TextField label="name" name="name" size='small' required /> <TextField label="email" name="email" type="email" size='small' required /> <TextField label="password" name="password" type='password' size='small' required /> <TextField label="pasword(confirm)" name="password_confirmation" type='password' size='small' required /> <Button variant='outlined' fullWidth type='submit' >Create!</Button> </Stack> </Box> </Box> ) }
これだけだと、419エラーが出るので、あらかじめcsrf-cookieを取得できるようにする。
(動作の確認がメインなので、今回は手動でとれるようにする。普通はこんなことしないけどね)
/frontend/spa/src/test/TestCSRF.tsx を作成
import { Box, Button } from "@mui/material"; import React from "react"; import axios from "axios"; export default function TestCSRF(): JSX.Element { const handleCSRF = React.useCallback((e: React.MouseEvent<HTMLButtonElement>) => { e.preventDefault() axios.get("http://localhost:8000/sanctum/csrf-cookie", { withCredentials: true }).then(res => { console.log(res.data) }).catch(e => { console.error(e) }) }, []) return ( <Box p={2} > <Button variant='outlined' fullWidth onClick={handleCSRF} >CSRF!</Button> </Box> ) }
frontend/spa/src/App.tsx を書き換える
import { Paper, Stack } from '@mui/material'; import './App.css'; import TestCSRF from './test/TestCSRF'; import TestCreateUser from './test/TestCreateUser'; function App() { return ( <div className="App"> <Stack spacing={3} p={3} mx={"auto"} maxWidth={"sm"} > <Paper variant='outlined' > <TestCSRF /> </Paper> <Paper variant='outlined' > <TestCreateUser /> </Paper> </Stack> </div> ); } export default App;
動作を確認してみる
バックエンドとフロントエンドを起動する
/ $ cd backend /backend $ php artisan serve / $ cd frontend /frontend $ npm run start
画面はこんな感じ
試しに「name」「email」「password」「pasword(confirm)」を入力して、「Create!」ボタンをクリックすると、
「419」エラーが返ってくる。レスポンスのエラーメッセージを見てみると、「CSRF token mismatch.」と書いてある。(ブラウザのデベロッパーツールから確認)
次に上の、「CSRF!」ボタンをクリックして、「Create!」ボタンをクリックすると、
「204 No Content」が返ってくる。(ブラウザのデベロッパーツールから確認)
dockerで用意しておいた、phpmyadmin(http://localhost:8080)からデータベースを確認してみると、
usersテーブルにデータが追加されている。
ここまでやってみて、、、
自分で作ると何かと面倒なユーザー登録機能。Laravelで作ると結構簡単にできるんだな。
あとは、ログインやパスワードリセットも用意されているようなので、そちらも確認していく!
そのあと、細かい動作を確認して、カスタマイズを確認して、、、やることはまだたくさん。。。
[Shopify]注文合計金額メモ
最近Shopifyを扱うようになった。。。
Shopifyはいろいろ面倒なことを考える必要がないので、ありがたい。
(EC Cubeみたいに際限なくカスタマイズできないからね笑)
ということで、注文データの合計金額について勘違いをしていたのでメモ。。。
order.total_price
合計金額、、、ではあるのだが、、、
The total price is calculated before refunds are applied. Use order.total_net_amount to output the total minus any refunds.
https://shopify.dev/api/liquid/objects/order#order-total_price
とあるように、払い戻しが適用される前の合計金額。
order.total_net_amount
上にもあるように、払い戻し後の合計金額を表示させたい場合は、order.total_net_amountを使う必要がある。
The amount is calculated after refunds are applied, so is equal to
https://shopify.dev/api/liquid/objects/order#order-total_net_amountorder.total_price
minusorder.total_refunded_amount
.
ただ、返金処理が完了していない場合は、反映されない模様。
order.total_refunded_amount
払い戻し金額だけを表示したい場合はこれを使う。
ただ、返金処理が完了していない場合は、反映されない模様。
order.total_discounts
クーポンコードで割引があった場合の割引額合計がここに入ってくる。
が、、、order.total_priceには割引前の金額が入っているようなので、割引後の合計金額を表示させたい場合は、
{{ order.total_price | minus : order.total_discounts | money_with_currency }}
のようにする。
ギフトカードを使った場合も、order.total_priceには利用前の金額が入っている。
もっとも、ギフトカードは金券扱いだと思うので、当たり前といえば当たり前ではある、、、。
[EC-CUBE 4]問い合わせフォームの複製
EC-Cube4.xは問い合わせフォームが一つしか実装されていない。
もう一つフォームを実装する場合の方法。
今回は、「app/Customize/」にコードを作成して、
新たに「http://〇〇○.△△△/contact2」にデフォルトと同じ問い合わせフォームを作成してみる。
また、問い合わせメールの送信時や問い合わせ画面表示時などのアクションは使用しない。
[EC-CUBE 4]コントローラを利用するページを追加してみる
EC-CUBE4でページを追加するには、
「管理ページ」から、「コンテンツ管理」→「ページ管理」より、「新規作成」から行えるが、
ここからだと、(多分)コントローラで何か機能を追加するようなページは作成できなさそう(?)
今回は、こちらの機能を利用せずに、カスタマイズしてコントローラを利用するページを追加してみる。
作成ページは
http://〇〇.□ □/sample
とし、コンテンツ管理からもアクセスできるようにしたい!
[EC-CUBE 4]商品情報の項目を追加する
商品を登録するとき、項目がーーーー、、、ということがあると思う。
そんなときに、カスタマイズで対応する方法。
ちなみに、そんな機能のプラグインもある模様。
今回は、商品情報に「メーカー名」を追加
続きを読む »[EC-CUBE 4]インストール
EC-CUBE4のインストールに関するメモ
今回は、2019年4月16日現在最新の4.0.2を使用。
EC-CUBE official:https://www.ec-cube.net/
EC-CUBE 4.0 開発ドキュメント・マニュアル:http://doc4.ec-cube.net/
[symfony3.x]インストール(Symfony Installer)
PHPのフレームワークの一つ、「symfony」をインストールしてみるメモ。
今回は、現在(2019年4月)時点の最終長期サポートバージョンである、3.4をインストールしてみる。
LTSである3.4は3年(2020年11月)までのサポート予定で、5.5.9以降のPHPが利用可能。
4.xは7.1.3以降のPHPが必要なので注意
・symfony公式「https://symfony.com」
続きを読む »