Laravelに入門したい!(Laravel APIサーバー+React SPA)vol.1

やっぱ、PHPはLaravelなのか!?

フルスタックのフレームワークはあまり乗り気ではないけれど、
そんなことを思ってしまう今日この頃。。。

今回はLaravelに入門してみたい。

とりあえず、LaravelでAPIサーバーを作ってReactでSPA作って、パスワード認証する感じのものを作っていこうと思う。

まずは、ユーザー登録機能まで。

環境
・PHP:8.1
・Laravel:10.x
・データベース:mysql8

とりあえずLaravel

まずはLaravelのプロジェクトを新規作成

場所は「/backend」にする。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ composer create-project laravel/laravel backend
$ composer create-project laravel/laravel backend
$ composer create-project laravel/laravel backend

この状態からでもいろいろできそうだけど、今回は、Laravelをバックエンドのみで使いたい。

ということで、スターターキットを使ってみる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ cd backend
/backend $ composer require laravel/breeze --dev
/backend $ php artisan breeze:install api
$ cd backend /backend $ composer require laravel/breeze --dev /backend $ php artisan breeze:install api
$ cd backend
/backend $ composer require laravel/breeze --dev
/backend $ php artisan breeze:install api

ないものが追加されていく感じなのかなと思っていたら、いろいろと不必要なものも削除してくれているじゃないか。
ありがたい話です。

データベース準備

docker composeで準備してみる。

データベースサーバーはmysql8
ついでに、phpmyadminとmaildevも入れておく。

ファイルを作成「/docker-compose.yml」

/docker-compose.yml
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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"
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"
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」を作成

/.env
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
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
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を立ち上げる

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
/ $ docker compose up -d
/ $ docker compose up -d
/ $ docker compose up -d

laravelのデータベースとメールサーバーの設定をdocker環境に合わせる

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
....
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}"
...
.... 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}" ...
....

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}"

...

いったんデータベースのマイグレーションをしておく。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
/backend $ php artisan migrate
/backend $ php artisan migrate
/backend $ php artisan migrate

バックエンド側はいったん終了

SPA側を準備

Create Reactで作成しておく。ついでにmuiとaxiosを入れておく。
(fetchだとうまく動作しなかった。。。多分設定不十分なのだけど、まだちゃんと調査していない。)

階層は「/frontend」

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
/ $ npx create-react-app frontend --template typescript
/ $ cd frontend
/frontend $ npm i @mui/material @emotion/react @emotion/styled @mui/icons-material axios
/ $ npx create-react-app frontend --template typescript / $ cd frontend /frontend $ npm i @mui/material @emotion/react @emotion/styled @mui/icons-material axios
/ $ 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 を作成

frontend/spa/src/test/TestCreateUser.tsx
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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>
)
}
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> ) }
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 を作成

frontend/spa/src/test/TestCSRF.tsx
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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>
)
}
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> ) }
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 を書き換える

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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;
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;
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;

動作を確認してみる

バックエンドとフロントエンドを起動する

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
/ $ cd backend
/backend $ php artisan serve
/ $ cd frontend
/frontend $ npm run start
/ $ cd backend /backend $ php artisan serve / $ cd frontend /frontend $ npm run start
/ $ 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で作ると結構簡単にできるんだな。

あとは、ログインやパスワードリセットも用意されているようなので、そちらも確認していく!
そのあと、細かい動作を確認して、カスタマイズを確認して、、、やることはまだたくさん。。。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

*