今まで、WEBアプリはjsp/servletばかりで開発していました。
今回はSPAで簡単なCRUDアプリを作成するために必要な勉強をしました。
その際に一連の情報がまとまっていると便利かと思い、備忘録として記していきたいと思います。
改善点・誤記等ありましたら、コメントにてご指摘いただけると幸いです。
目次
やりたいこと
ローカル環境下で社員の一覧取得が行えるWEBアプリケーションの作成を目標とします。
使用する環境については、以下の通りとなります。
- Ubuntu 20.04.5 LTS(Windows10 WSL2)
- react@18.2.0
- mui@5.10.16
- axios@1.2.0
 
- PHP 8.1.13
- PostgreSQL 15.1
環境構築
事前準備
今回はLinux上で環境を構築します。
windowsで環境構築する場合は、WSLを通してLinuxを利用します。
※WSLについては、下記リンクをご参考ください。
React
React基本パッケージインストール
0. パッケージの更新
後述Node.js、npmをインストールする前にパッケージの更新を実施します。
$ sudo apt update
$ sudo apt upgrade -y
1. Node.jsのインストール
Node.jsがインストールされているか確認します。
$ node -v
インストールされていない(バージョン情報が表示されない)場合、以下の通りインストールを実施します。
※バージョンが表示される場合は、「2. npmのインストール」へ進みます。
$ sudo apt install nodejs
インストールができていることを確認します。
$ node -v
v17.9.0
2. npmのインストール
npmがインストールされているか確認します。
$ npm -v
インストールされていない(バージョン情報が表示されない)場合、以下の通りインストールを実施します。
※バージョンが表示される場合は、「Reactプロジェクトの作成」へ進みます。
sudo apt install npm
インストールができていることを確認します。
$ npm -v
8.5.5
Reactプロジェクトの作成
Create React Appを使用して、プロジェクトを作成します。
※Create React Appについては、React公式サイトを参照してください。
コマンドはnpx create-react-app [プロジェクト名]です。
プロジェクトでJavaScriptとTypeScriptのどちらを使用するか設定することができますが、今回はJavaScriptを選択します。
※デフォルトがJavaScript、オプションに--typescriptを付与することでTypeScriptとなります。
$ npx create-react-app emp_web
プロジェクトのディレクトリが作成されていることを確認します。
$ ls
emp_web
Reactローカルサーバー立ち上げ
作成したディレクトリに移動し、ローカルサーバーを立ち上げます。
$ npm start
http://localhost:3000/にアクセスし、Reactのhelloページが表示されていれば立ち上げ完了です。

React用ライブラリインストール
基本のパッケージ以外に今回のCRUDアプリに必要なライブラリをインストールします。
1. MUIのインストール
MUIはUIコンポーネントを内包したフレームワークです。
Reactに相性の良いCSSフレームワークを探していたところ、おすすめとして「MUI」が上がっていたため今回は採用しました。
詳細は下記MUI公式サイトをご参照ください。
MUIをインストールする対象プロジェクトのディレクトリで以下の通り実施します。
$ npm install --save @mui/material @mui/icons-material
$ npm install @emotion/react @emotion/styled @mui/styled-engine
ライブラリがインストールされていることを確認します。
$ npm list --depth=0
emp_web@0.1.0 [プロジェクトのパス]/emp_web
├── @emotion/react@11.10.5
├── @emotion/styled@11.10.5
├── @mui/icons-material@5.10.16
├── @mui/material@5.10.16
├── @mui/styled-engine@5.10.16
2. Axiosのインストール
AxiosはAPI通信を簡単に行うことができるライブラリです。
PHPで作成するバックエンド側のアプリケーションとの通信に使用します。
Axiosをインストールする対象プロジェクトのディレクトリで以下の通り実施します。
$ npm install axios
ライブラリがインストールされていることを確認します。
$ npm list --depth=0
emp_web@0.1.0 [プロジェクトのパス]/emp_web
├── axios@1.2.0
PHP
PHP基本パッケージインストール
1. PHPがすでにインストールされているか確認
$ php -v
インストールされていた場合は、「4. Composerのインストール」へ進みます。
2. パッケージの更新
$ sudo apt -y install software-properties-common
$ sudo add-apt-repository ppa:ondrej/php
$ sudo apt-get update
3. PHPのインストール
$ sudo apt install php8.1 php8.1-mbstring php8.1-dom
PHPがインストールされていることを確認します。
$ php -v
PHP 8.1.13 (cli) (built: Nov 26 2022 14:07:36) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.13, Copyright (c) Zend Technologies
    with Zend OPcache v8.1.13, Copyright (c), by Zend Technologies
4. Composerのインストール
Laravelを使用したPHPプロジェクトを作成する際に必要となるComporserをインストールします。
公式サイトの「Download」に記載の通りコマンドを実行します。
$ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
$ php -r "if (hash_file('sha384', 'composer-setup.php') === '55ce33d7678c5a611085589f1f3ddf8b3c52d662cd01d4ba75c0ee0459970c2200a51f492d557530c71c15d8dba01eae') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
$ php composer-setup.php
$ php -r "unlink('composer-setup.php');"
composerコマンドを利用できるようにPATH配下のディレクトリに格納します。
その後、移動したしたファイルを実行できるよう、権限を付与します。
$ sudo mv composer.phar /usr/local/bin/composer
$ sudo chmod +x /usr/local/bin/composer
Composerがインストールされていることを確認します。
$ composer -V
Composer version 2.3.5 2022-04-13 16:43:00
5. php-curlのインストール
Comporserでプロジェクトを作成する際、必要となるモジュールです。
php-curlがインストールされているか確認します。
$ php -m
インストールされていない(出力にcurlが含まれていない)場合、以下の通りインストールを実施します。
※バージョンが表示される場合は、「6. php-pgsqlのインストール」へ進みます。
$ sudo apt-get install php8.1-curl
PHPインストールされているモジュールの一覧にcurlが含まれていることを確認します。
$ php -m
6. php-pgsqlのインストール
PHPからPostgreSQLへデータベースアクセスをする際に必要となるモジュールです。
以下の通りインストールを実施します。
sudo apt-get install php8.1-pgsql
PHPインストールされているモジュールの一覧にpgsqlが含まれていることを確認します。
$ php -m
PHPプロジェクトの作成
Composerを使用して、プロジェクトを作成します。
オプションを設定することでフレームワークにLaravelを使用することができます。
コマンドはcomposer create-project laravel/laravel --prefer-dist [プロジェクト名]です。
$ composer create-project laravel/laravel --prefer-dist emp_ap
プロジェクトのディレクトリが作成されていることを確認します。
$ ls
emp_ap
PHPローカルサーバー立ち上げ
作成したディレクトリに移動し、ローカルサーバーを立ち上げます。
$ php artisan serve
http://localhost:8000にアクセスし、Laravelのhelloページが表示されていれば立ち上げ完了です。
PostgreSQL環境構築
PostgreSQLインストール
公式サイトを参考にインストールします。
$ sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
$ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
$ sudo apt-get update
$ sudo apt-get -y install postgresql
PostgreSQLがインストールされていることを確認します。
$ psql --version
psql (PostgreSQL) 15.1 (Ubuntu 15.1-1.pgdg20.04+1)
PostgreSQL
PostgreSQL準備
1. PostgreSQLサーバー立ち上げ
$ sudo service postgresql start
// ※サーバーを止める場合
$ sudo service postgresql stop
2. PostgreSQL操作コンソール起動
linuxユーザをpostgresに切り替えます。
$ sudo su - postgres
操作コンソールを起動します。
$ psql
psql (15.1 (Ubuntu 15.1-1.pgdg20.04+1))
"help"でヘルプを表示します。
postgres=#
3. postgresユーザのパスワードを設定
最初に、postgresユーザにパスワードを与えます。
ALTER ROLE postgres PASSWORD '[任意のパスワード]';
4. 今回に必要なユーザとデータベースを作成
以下の通り、ユーザーを作成します。
また、ログイン・データベース作成の権限を与えます。
CREATE ROLE emp_sys_user;
ALTER ROLE emp_sys_user PASSWORD '[任意のパスワード]';
ALTER ROLE emp_sys_user LOGIN;
ALTER ROLE emp_sys_user CREATEDB;
今回使用するデータベースを作成します。
CREATE DATABASE emp_sys_db WITH OWNER emp_sys_user;
作成したユーザemp_sys_userでデータベースemp_sys_dbに接続し、データベース・ユーザが作成されていることを確認します。
\c emp_sys_db - emp_sys_user
※以下のエラーが出力された場合はこちらのサイトを参照してpeer認証からパスワード認証へ変更します。
FATAL:  ユーザー"emp_sys_user"で対向(peer)認証に失敗しました
※一度ユーザーを登録すると、linuxのカレントユーザがpostgresでなくても、psqlコマンドから直接操作コンソールへ作成したユーザを使用して起動可能です。
コマンドはpsql -d [データベース名] -U [ユーザー名]
$ psql -d emp_sys_db -U emp_sys_user
ユーザー emp_sys_user のパスワード:
psql (15.1 (Ubuntu 15.1-1.pgdg20.04+1))
"help"でヘルプを表示します。
emp_sys_db=>
実装
環境の構築が完了したので、続いて実装していきます。
今回はMUIの公式サンプルコードにある「Dashboard」をベースに進めていきます。
※MUI公式のDashboardデモページ
メインページ作成
MUIサンプルコードダウンロード
GitHubにサンプルコードがあるので、ダウンロードしてきます。
上記リポジトリには、js版、ts版の二種類ありますが今回はjsでプロジェクトを作成しているため、.jsのファイルを使用します。
サンプルコードを配置
上記で作成したReactプロジェクトのディレクトリに、以下の通り格納用のディレクトリdashboardを作成します。
emp_web/src/dashboard
作成したディレクトリに以下のファイルを配置します。
- Dashboard.js
- Deposits.js
- listItems.js
- Orders.js
- Title.js
サンプルコードの表示
配置したソースを適用するためにemp_web/src/App.jsを修正します。
※Reactの基本的な知識については、公式のチュートリアルを参照してください。
+import Dashboard from './dashboard/Dashboard'
-import logo from './logo.svg';
-import './App.css';
function App() {
  return (
+    <Dashboard/>
-    <div className="App">
-      <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.js</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
-      </header>
-    </div>
  );
}
export default App;
また、今回のライブラリ構成ではchartコンポーネントは使用できないためDashboardコンポーネントから削除します。
import * as React from 'react';
import { styled, createTheme, ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import MuiDrawer from '@mui/material/Drawer';
import Box from '@mui/material/Box';
import MuiAppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import List from '@mui/material/List';
import Typography from '@mui/material/Typography';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import Badge from '@mui/material/Badge';
import Container from '@mui/material/Container';
import Grid from '@mui/material/Grid';
import Paper from '@mui/material/Paper';
import Link from '@mui/material/Link';
import MenuIcon from '@mui/icons-material/Menu';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import NotificationsIcon from '@mui/icons-material/Notifications';
import { mainListItems, secondaryListItems } from './listItems';
-import Chart from './Chart';
import Deposits from './Deposits';
import Orders from './Orders';
/*~中略~*/
function DashboardContent() {
  const [open, setOpen] = React.useState(true);
  const toggleDrawer = () => {
    setOpen(!open);
  };
  return (
/*~中略~*/
          <Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
            <Grid container spacing={3}>
-              {/* Chart */}
-              <Grid item xs={12} md={8} lg={9}>
-                <Paper
-                  sx={{
-                    p: 2,
-                    display: "flex",
-                    flexDirection: "column",
-                    height: 240,
-                  }}
-                >
-                  <Chart />
-                </Paper>
-              </Grid>
              {/* Recent Deposits */}
              <Grid item xs={12} md={4} lg={3}>
                <Paper
                  sx={{
                    p: 2,
                    display: "flex",
                    flexDirection: "column",
                    height: 240,
                  }}
                >
                  <Deposits />
                </Paper>
              </Grid>
              {/* Recent Orders */}
              <Grid item xs={12}>
                <Paper sx={{ p: 2, display: "flex", flexDirection: "column" }}>
                  <Orders />
                </Paper>
              </Grid>
            </Grid>
            <Copyright sx={{ pt: 4 }} />
          </Container>
  );
}
ローカルサーバーを起動し、画面が出力されることを確認します。
※すでにサーバーを起動している場合は、修正し保存した時点でホットデプロイが走るためすぐに確認ができます。
$ npm start
サンプルコードを修正
不要なレイアウトを削除し、シンプルなメインページとします。
import * as React from "react";
import { styled, createTheme, ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import MuiDrawer from "@mui/material/Drawer";
import Box from "@mui/material/Box";
import MuiAppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import List from "@mui/material/List";
import Typography from "@mui/material/Typography";
import Divider from "@mui/material/Divider";
import IconButton from "@mui/material/IconButton";
import Badge from "@mui/material/Badge";
import Container from "@mui/material/Container";
import Grid from "@mui/material/Grid";
import Paper from "@mui/material/Paper";
-import Link from "@mui/material/Link";
import MenuIcon from "@mui/icons-material/Menu";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import NotificationsIcon from "@mui/icons-material/Notifications";
import { mainListItems, secondaryListItems } from "./listItems";
-import Deposits from "./Deposits";
import Orders from "./Orders";
-function Copyright(props) {
-  return (
-    <Typography
-      variant="body2"
-      color="text.secondary"
-      align="center"
-      {...props}
-    >
-      {"Copyright © "}
-      <Link color="inherit" href="https://mui.com/">
-        Your Website
-      </Link>{" "}
-      {new Date().getFullYear()}
-      {"."}
-    </Typography>
-  );
-}
const drawerWidth = 240;
const AppBar = styled(MuiAppBar, {
  shouldForwardProp: (prop) => prop !== "open",
})(({ theme, open }) => ({
  zIndex: theme.zIndex.drawer + 1,
  transition: theme.transitions.create(["width", "margin"], {
    easing: theme.transitions.easing.sharp,
    duration: theme.transitions.duration.leavingScreen,
  }),
  ...(open && {
    marginLeft: drawerWidth,
    width: `calc(100% - ${drawerWidth}px)`,
    transition: theme.transitions.create(["width", "margin"], {
      easing: theme.transitions.easing.sharp,
      duration: theme.transitions.duration.enteringScreen,
    }),
  }),
}));
const Drawer = styled(MuiDrawer, {
  shouldForwardProp: (prop) => prop !== "open",
})(({ theme, open }) => ({
  "& .MuiDrawer-paper": {
    position: "relative",
    whiteSpace: "nowrap",
    width: drawerWidth,
    transition: theme.transitions.create("width", {
      easing: theme.transitions.easing.sharp,
      duration: theme.transitions.duration.enteringScreen,
    }),
    boxSizing: "border-box",
    ...(!open && {
      overflowX: "hidden",
      transition: theme.transitions.create("width", {
        easing: theme.transitions.easing.sharp,
        duration: theme.transitions.duration.leavingScreen,
      }),
      width: theme.spacing(7),
      [theme.breakpoints.up("sm")]: {
        width: theme.spacing(9),
      },
    }),
  },
}));
const mdTheme = createTheme();
function DashboardContent() {
  const [open, setOpen] = React.useState(true);
  const toggleDrawer = () => {
    setOpen(!open);
  };
  return (
    <ThemeProvider theme={mdTheme}>
      <Box sx={{ display: "flex" }}>
        <CssBaseline />
        <AppBar position="absolute" open={open}>
          <Toolbar
            sx={{
              pr: "24px", // keep right padding when drawer closed
            }}
          >
            <IconButton
              edge="start"
              color="inherit"
              aria-label="open drawer"
              onClick={toggleDrawer}
              sx={{
                marginRight: "36px",
                ...(open && { display: "none" }),
              }}
            >
              <MenuIcon />
            </IconButton>
            <Typography
              component="h1"
              variant="h6"
              color="inherit"
              noWrap
              sx={{ flexGrow: 1 }}
            >
              Dashboard
            </Typography>
            <IconButton color="inherit">
              <Badge badgeContent={4} color="secondary">
                <NotificationsIcon />
              </Badge>
            </IconButton>
          </Toolbar>
        </AppBar>
        <Drawer variant="permanent" open={open}>
          <Toolbar
            sx={{
              display: "flex",
              alignItems: "center",
              justifyContent: "flex-end",
              px: [1],
            }}
          >
            <IconButton onClick={toggleDrawer}>
              <ChevronLeftIcon />
            </IconButton>
          </Toolbar>
          <Divider />
          <List component="nav">
            {mainListItems}
-            <Divider sx={{ my: 1 }} />
-            {secondaryListItems}
          </List>
        </Drawer>
        <Box
          component="main"
          sx={{
            backgroundColor: (theme) =>
              theme.palette.mode === "light"
                ? theme.palette.grey[100]
                : theme.palette.grey[900],
            flexGrow: 1,
            height: "100vh",
            overflow: "auto",
          }}
        >
          <Toolbar />
          <Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
            <Grid container spacing={3}>
-              {/* Recent Deposits */}
-              <Grid item xs={12} md={4} lg={3}>
-                <Paper
-                  sx={{
-                    p: 2,
-                    display: "flex",
-                    flexDirection: "column",
-                    height: 240,
-                  }}
-                >
-                  <Deposits />
-                </Paper>
-             </Grid>
              {/* Recent Orders */}
              <Grid item xs={12}>
                <Paper sx={{ p: 2, display: "flex", flexDirection: "column" }}>
                  <Orders />
                </Paper>
              </Grid>
            </Grid>
            <Copyright sx={{ pt: 4 }} />
          </Container>
        </Box>
      </Box>
    </ThemeProvider>
  );
}
export default function Dashboard() {
  return <DashboardContent />;
}
サイドバーのリストも減らします。
import * as React from 'react';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
-import ListSubheader from '@mui/material/ListSubheader';
import DashboardIcon from '@mui/icons-material/Dashboard';
-import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
-import PeopleIcon from '@mui/icons-material/People';
-import BarChartIcon from '@mui/icons-material/BarChart';
-import LayersIcon from '@mui/icons-material/Layers';
-import AssignmentIcon from '@mui/icons-material/Assignment';
export const mainListItems = (
  <React.Fragment>
    <ListItemButton>
      <ListItemIcon>
        <DashboardIcon />
      </ListItemIcon>
      <ListItemText primary="Dashboard" />
    </ListItemButton>
-    <ListItemButton>
-      <ListItemIcon>
-        <ShoppingCartIcon />
-      </ListItemIcon>
-      <ListItemText primary="Orders" />
-    </ListItemButton>
-    <ListItemButton>
-      <ListItemIcon>
-        <PeopleIcon />
-      </ListItemIcon>
-      <ListItemText primary="Customers" />
-    </ListItemButton>
-    <ListItemButton>
-      <ListItemIcon>
-        <BarChartIcon />
-      </ListItemIcon>
-      <ListItemText primary="Reports" />
-    </ListItemButton>
-    <ListItemButton>
-      <ListItemIcon>
-        <LayersIcon />
-      </ListItemIcon>
-      <ListItemText primary="Integrations" />
-    </ListItemButton>
  </React.Fragment>
);
-export const secondaryListItems = (
-  <React.Fragment>
-    <ListSubheader component="div" inset>
-      Saved reports
-    </ListSubheader>
-    <ListItemButton>
-      <ListItemIcon>
-        <AssignmentIcon />
-      </ListItemIcon>
-      <ListItemText primary="Current month" />
-    </ListItemButton>
-    <ListItemButton>
-      <ListItemIcon>
-        <AssignmentIcon />
-      </ListItemIcon>
-      <ListItemText primary="Last quarter" />
-    </ListItemButton>
-    <ListItemButton>
-      <ListItemIcon>
-        <AssignmentIcon />
-      </ListItemIcon>
-      <ListItemText primary="Year-end sale" />
-    </ListItemButton>
-  </React.Fragment>
-); 
Orderのコンポーネントは、後ほど社員一覧として改造するため残しておきます。
社員一覧取得処理事前準備
社員テーブル作成
今回のCRUD処理を行うテーブルを作成します。
前述で作成したemp_sys_dbに以下の通りテーブルを登録します。
※Laravelで連携するテーブルの名称は末尾にsを付けるようにします。
CREATE TABLE employees (
    employee_id CHAR(4) PRIMARY KEY,
    employee_name VARCHAR(60) NOT NULL,
    employee_mail VARCHAR(254)
);
続いて、作成したemployeesテーブルに初期データの投入を行います。
INSERT INTO employees VALUES ('0001', '田中 太郎', 'tanaka_taro@example.com');
INSERT INTO employees VALUES ('0002', '伊藤 花子', 'ito_hanako@example.com');
データが登録されていることを確認します。
SELECT * FROM employees;
employee_id | employee_name |      employee_mail
-------------+---------------+-------------------------
 0001        | 田中 太郎     | tanaka_taro@example.com
 0002        | 伊藤 花子     | ito_hanako@example.com
(2 行)
一覧取得処理作成
バックエンド
まずは、DBへ接続するための設定を行います。
emp_ap/.envにDB接続情報を記述します。
~中略~
-DB_CONNECTION=mysql
-DB_HOST=127.0.0.1
-DB_PORT=3306
-DB_DATABASE=laravel
-DB_USERNAME=root
-DB_PASSWORD=
+LOG_CHANNEL=stack
+DB_CONNECTION=pgsql
+DB_HOST=127.0.0.1
+DB_PORT=5432
+DB_DATABASE=emp_sys_db
+DB_USERNAME=emp_sys_user
+DB_PASSWORD=[設定したパスワード]
作成したテーブルemployeesと連携するためのモデル・リソース・コントローラを作成します。
モデル作成
モデルについての詳細な説明は下記リンクをご参照ください。
PHPプロジェクトのディレクトリ内でコマンドを実行します。
モデルの作成コマンドはphp artisan make:model [テーブル名]です。
モデル名は、連携するテーブル名の単数形とします。
$ php artisan make:model Employee
   INFO  Model [app/Models/Employee.php] created successfully.
emp_ap/app/Models/配下にEmployee.phpが作成されていることを確認します。
リソース作成
リソースは取得したテーブル定義と、フロントエンドに引き渡すパラメータ(json)との紐づけを行うものです。
PHPプロジェクトのディレクトリ内でコマンドを実行します。
モデルの作成コマンドはphp artisan make:resource [テーブル名]Resourceです。
$ php artisan make:resource EmployeeResource
   INFO  Resource [app/Http/Resources/EmployeeResource.php] created successfully.
emp_ap/app/Http/Resources/配下にEmployeeResource.phpが作成されていることを確認します。
今回のテーブルレイアウトに合わせるためEmployeeResource.phpを修正します。
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class EmployeeResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
     */
    public function toArray($request)
    {
-        return parent::toArray($request);
+        return [
+            'employee_id' => $this->employee_id,
+            'employee_name' => $this->employee_name,
+            'employee_mail' => $this->employee_mail,
+        ];
    }
}
コントローラ作成
コントローラについての詳細な説明は下記リンクをご参照ください。
PHPプロジェクトのディレクトリ内でコマンドを実行します。
モデルの作成コマンドはphp artisan make:controller [テーブル名]Controllerです。
$ php artisan make:controller EmployeeController
   INFO  Controller [app/Http/Controllers/EmployeeController.php] created successfully.
emp_ap/app/Http/Controllers/配下にEmployeeController.phpが作成されていることを確認します。
employeesテーブルからデータを取得するメソッドをEmployeeController.phpに追加します。
※Laravelを用いたDB取得方法については以下リンクをご参照ください。
<?php
namespace App\Http\Controllers;
-use Illuminate\Http\Request;
use App\Http\Resources\EmployeeResource;
use App\Models\Employee;
class EmployeeController extends Controller
{
    //
+    public function getEmployeeAll()
+    {
+        return EmployeeResource::collection(
+            Employee::get()
+        );
+    }
}
ルートの設定
上記のコントローラをAPIとして呼び出せるようにemp_ap/routes/api.phpルートを設定します。
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
-Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
-    return $request->user();
-});
+Route::group( ['middleware' => 'api'], function(){
+    Route::get('getemployeeall', 'App\Http\Controllers\EmployeeController@getEmployeeAll');
+});
API連携で社員情報を取得できるようになったのでhttp://127.0.0.1:8000/api/getemployeeallへアクセスして取得できることを確認します。
※ローカルサーバーを起動していない場合は起動します。

以上でバックエンド側は完了です。
フロントエンド
一覧出力用コンポーネント作成
サンプルソースのOrderコンポーネントを修正して作成していきます。
APIの呼び出しや不要なものの削除や文言の修正等を以下のとおり実施します。
import * as React from 'react';
-import Link from '@mui/material/Link';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Title from './Title';
+import { useState, useEffect } from "react";
+import axios from "axios";
-// Generate Order Data
-function createData(id, date, name, shipTo, paymentMethod, amount) {
-  return { id, date, name, shipTo, paymentMethod, amount };
-}
-const rows = [
-  createData(
-    0,
-    '16 Mar, 2019',
-    'Elvis Presley',
-    'Tupelo, MS',
-    'VISA ⠀•••• 3719',
-    312.44,
-  ),
-  createData(
-    1,
-    '16 Mar, 2019',
-    'Paul McCartney',
-    'London, UK',
-    'VISA ⠀•••• 2574',
-    866.99,
-  ),
-  createData(2, '16 Mar, 2019', 'Tom Scholz', 'Boston, MA', 'MC ⠀•••• 1253', 100.81),
-  createData(
-    3,
-    '16 Mar, 2019',
-    'Michael Jackson',
-    'Gary, IN',
-    'AMEX ⠀•••• 2000',
-    654.39,
-  ),
-  createData(
-    4,
-    '15 Mar, 2019',
-    'Bruce Springsteen',
-    'Long Branch, NJ',
-    'VISA ⠀•••• 5919',
-    212.79,
-  ),
-];
-function preventDefault(event) {
-  event.preventDefault();
-}
export default function Orders() {
+  const [rows, setRows] = useState([]);
+  useEffect(() => {
+    axios
+      .get("http://127.0.0.1:8000/api/getemployeeall")
+      .then((res) => {
+        setRows(res.data.data);
+      })
+      .catch((e) => {
+        console.log(e);
+      });
+  }, []);
  return (
    <React.Fragment>
+      <Title>Employee List</Title>
-      <Title>Recent Orders</Title>
      <Table size="small">
        <TableHead>
          <TableRow>
+            <TableCell>社員ID</TableCell>
+            <TableCell>名前</TableCell>
+            <TableCell>メールアドレス</TableCell>
-            <TableCell>Date</TableCell>
-            <TableCell>Name</TableCell>
-            <TableCell>Ship To</TableCell>
-            <TableCell>Payment Method</TableCell>
-            <TableCell align="right">Sale Amount</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {rows.map((row) => (
+           <TableRow key={row.employee_id}>
-           <TableRow key={row.id}>
+             <TableCell>{row.employee_id}</TableCell>
+             <TableCell>{row.employee_name}</TableCell>
+             <TableCell>{row.employee_mail}</TableCell>
-             <TableCell>{row.id}</TableCell>
-             <TableCell>{row.name}</TableCell>
-             <TableCell>{row.shipTo}</TableCell>
-             <TableCell>{row.paymentMethod}</TableCell>
-             <TableCell align="right">{`$${row.amount}`}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
-      <Link color="primary" href="#" onClick={preventDefault} sx={{ mt: 3 }}>
-        See more orders
-      </Link>
    </React.Fragment>
  );
}
修正内容の説明
上記で修正した、不要項目の削除や、文言修正以外の説明をしていきます。
取得データの定義
画面へ出力する可変項目について以下の通り定義します。
const [rows, setRows] = useState([]);
[{変数}, {Setメソッド}]として定義します。
変数の値を変更する場合はSetメソッドを通して設定します。
API呼び出し
useEffectを使用して、初回レンダリング時にAPIの呼び出しを実施しています。
APIの呼び出しには、ライブラリaxiosを使用しています。
axios.get()にバックエンド側で設定したAPIのURLを設定します。
最後に、APIからのレスポンスを定義したsetRowsを使用してrowsに格納しています。
  useEffect(() => {
    axios
      .get("http://127.0.0.1:8000/api/getemployeeall")
      .then((res) => {
        setRows(res.data.data);
      })
      .catch((e) => {
        console.log(e);
      });
  }, []);
useEffectの詳しい説明はReact公式サイトをご確認ください。
画面上のテーブル項目へバインド
上記で設定したrowsをテーブルへバインドしています。
<TableBody>
  {rows.map((row) => (
    <TableRow key={row.employee_id}>
      <TableCell>{row.employee_id}</TableCell>
      <TableCell>{row.employee_name}</TableCell>
      <TableCell>{row.employee_mail}</TableCell>
    </TableRow>
  ))}
</TableBody>
確認
http://localhost:3000/にアクセスし、テーブルに登録されている社員情報がリストに表示されていれば完成です。

最後に
当初は、登録や更新の機能も記載していく予定でしたが、初投稿のこともあり、思っていたよりも執筆に時間がかかりデータの取得までとなってしまいました。
もし機会があれば、一通りのCRUDアプリ作成について執筆してみたいと思います。
また、今回はMUIのサンプルをそのまま使用しましたが、さらに細かくコンポーネントを分けたりしても面白いかもしれません。
他にもreact-routerを使用したルーティングや、ダークモードの実装などもMUIを用いることで簡単にできたのでこちらも書いてみたいです。
【余談】
今回はあまり紹介できませんでしたが、CSSフレームワークのMUIではbootstrapのようなブロック式でのレイアウトデザインや、一定の画面サイズを設定して表示/非表示の切り替えができるなど、レスポンシブなレイアウト作成も比較的容易に実装できる印象でした。
また、最後にMUIのText Fieldについて。
このコンポーネントは基本的なMUIのテキストボックスとなるのですが、ラベルとプレースホルダーが一体となっています。
この構造が一般的に使いやすいのかはさておき、エンジニアが担当するようなレイアウト作成の際に発生する、「ラベルがあるせいで項目の頭がそろわない」、「プレースホルダーだけだと、入力後に項目の内容が識別できない」などの細々した悩みがなくなり、個人的には画期的だなと思いました。
他にも便利なコンポーネントはたくさんあり、動きは公式サイトから確認できるのでぜひ一度見てみてください。
参考リンク



