Categories: phpプログラム

Laravelで実装するシンプルな販売管理システムの全貌:基礎から実装まで徹底解説

Laravelは、シンプルかつ柔軟なWebアプリケーションフレームワークとして多くの開発現場で利用されています。本記事では、販売管理システムを題材に、Laravelを使った基本的なCRUDの実装から関連付けまで一連の流れをサンプルコード付きで解説します。初めてLaravelでの開発を行う方にもわかりやすいように、各ステップを丁寧に紹介しているので、ぜひ最後までご覧ください。

. システム概要

今回のサンプルでは、以下のような簡易的な販売管理システムを構築します。

  • 商品管理(Products):商品の登録・編集・削除・一覧表示
  • 顧客管理(Customers):顧客の登録・編集・削除・一覧表示
  • 受注管理(Orders, OrderDetails):受注の登録・編集・削除・一覧表示
    • 受注に紐づく商品と数量を管理(OrderDetails)

これらの機能を通して、LaravelにおけるCRUD操作、モデル間のリレーション、Bladeによる画面作成の流れを学びます。


2. 開発環境の準備

  • PHP: 8.0以上 (Laravel 9以上を想定)
  • Composer: 2系推奨
  • Laravel: 9系または10系
  • Database: MySQLまたはSQLiteなどを使用

Laravelプロジェクトの新規作成は以下のコマンドで行えます。

composer create-project laravel/laravel sales-management

プロジェクト作成後、.env ファイルでデータベース接続設定を行います。

envコピーする編集するDB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=sales_management
DB_USERNAME=root
DB_PASSWORD=secret

3. データベース設計

本記事で利用するテーブル構成は以下のとおりです。

  1. products テーブル
    • id (PK)
    • name (商品名)
    • price (価格)
    • created_at / updated_at
  2. customers テーブル
    • id (PK)
    • name (顧客名)
    • email (メールアドレス)
    • created_at / updated_at
  3. orders テーブル
    • id (PK)
    • customer_id (FK: customers.id)
    • order_date (受注日)
    • created_at / updated_at
  4. order_details テーブル
    • id (PK)
    • order_id (FK: orders.id)
    • product_id (FK: products.id)
    • quantity (数量)
    • created_at / updated_at

4. Migrationファイルの作成

以下のようにMigrationを用意します(既にプロジェクトに含まれる database/migrations フォルダに作成)。

1. CreateProductsTable

<?php

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

return new class extends Migration
{
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->integer('price');
$table->timestamps();
});
}

public function down()
{
Schema::dropIfExists('products');
}
};

2. CreateCustomersTable

<?php

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

return new class extends Migration
{
public function up()
{
Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamps();
});
}

public function down()
{
Schema::dropIfExists('customers');
}
};

3. CreateOrdersTable

<?php

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

return new class extends Migration
{
public function up()
{
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('customer_id')->constrained('customers')->onDelete('cascade');
$table->date('order_date');
$table->timestamps();
});
}

public function down()
{
Schema::dropIfExists('orders');
}
};

4. CreateOrderDetailsTable

<?php

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

return new class extends Migration
{
public function up()
{
Schema::create('order_details', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')->constrained('orders')->onDelete('cascade');
$table->foreignId('product_id')->constrained('products')->onDelete('cascade');
$table->integer('quantity');
$table->timestamps();
});
}

public function down()
{
Schema::dropIfExists('order_details');
}
};

作成したら、以下のコマンドでテーブルを作成します。

php artisan migrate

5. Modelの作成

1. Product.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
use HasFactory;

protected $fillable = [
'name',
'price'
];

public function orderDetails()
{
return $this->hasMany(OrderDetail::class);
}
}

2. Customer.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Customer extends Model
{
use HasFactory;

protected $fillable = [
'name',
'email'
];

public function orders()
{
return $this->hasMany(Order::class);
}
}

3. Order.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
use HasFactory;

protected $fillable = [
'customer_id',
'order_date'
];

public function customer()
{
return $this->belongsTo(Customer::class);
}

public function orderDetails()
{
return $this->hasMany(OrderDetail::class);
}
}

4. OrderDetail.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class OrderDetail extends Model
{
use HasFactory;

protected $fillable = [
'order_id',
'product_id',
'quantity'
];

public function order()
{
return $this->belongsTo(Order::class);
}

public function product()
{
return $this->belongsTo(Product::class);
}
}

6. Controllerの作成

1. ProductController

<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\Request;

class ProductController extends Controller
{
public function index()
{
$products = Product::all();
return view('products.index', compact('products'));
}

public function create()
{
return view('products.create');
}

public function store(Request $request)
{
$request->validate([
'name' => 'required',
'price' => 'required|integer|min:0'
]);

Product::create($request->all());
return redirect()->route('products.index')->with('success', '商品を登録しました');
}

public function show($id)
{
$product = Product::findOrFail($id);
return view('products.show', compact('product'));
}

public function edit($id)
{
$product = Product::findOrFail($id);
return view('products.edit', compact('product'));
}

public function update(Request $request, $id)
{
$request->validate([
'name' => 'required',
'price' => 'required|integer|min:0'
]);

$product = Product::findOrFail($id);
$product->update($request->all());
return redirect()->route('products.index')->with('success', '商品情報を更新しました');
}

public function destroy($id)
{
$product = Product::findOrFail($id);
$product->delete();
return redirect()->route('products.index')->with('success', '商品を削除しました');
}
}

2. CustomerController

<?php

namespace App\Http\Controllers;

use App\Models\Customer;
use Illuminate\Http\Request;

class CustomerController extends Controller
{
public function index()
{
$customers = Customer::all();
return view('customers.index', compact('customers'));
}

public function create()
{
return view('customers.create');
}

public function store(Request $request)
{
$request->validate([
'name' => 'required',
'email' => 'required|email|unique:customers'
]);

Customer::create($request->all());
return redirect()->route('customers.index')->with('success', '顧客を登録しました');
}

public function show($id)
{
$customer = Customer::findOrFail($id);
return view('customers.show', compact('customer'));
}

public function edit($id)
{
$customer = Customer::findOrFail($id);
return view('customers.edit', compact('customer'));
}

public function update(Request $request, $id)
{
$request->validate([
'name' => 'required',
'email' => 'required|email|unique:customers,email,' . $id
]);

$customer = Customer::findOrFail($id);
$customer->update($request->all());
return redirect()->route('customers.index')->with('success', '顧客情報を更新しました');
}

public function destroy($id)
{
$customer = Customer::findOrFail($id);
$customer->delete();
return redirect()->route('customers.index')->with('success', '顧客を削除しました');
}
}

3. OrderController

<?php

namespace App\Http\Controllers;

use App\Models\Order;
use App\Models\Customer;
use App\Models\Product;
use App\Models\OrderDetail;
use Illuminate\Http\Request;

class OrderController extends Controller
{
public function index()
{
$orders = Order::with('customer')->get();
return view('orders.index', compact('orders'));
}

public function create()
{
$customers = Customer::all();
$products = Product::all();
return view('orders.create', compact('customers', 'products'));
}

public function store(Request $request)
{
$request->validate([
'customer_id' => 'required|exists:customers,id',
'order_date' => 'required|date',
'products.*.product_id' => 'required|exists:products,id',
'products.*.quantity' => 'required|integer|min:1'
]);

// Order作成
$order = Order::create([
'customer_id' => $request->customer_id,
'order_date' => $request->order_date,
]);

// OrderDetail作成
foreach ($request->products as $p) {
OrderDetail::create([
'order_id' => $order->id,
'product_id' => $p['product_id'],
'quantity' => $p['quantity']
]);
}

return redirect()->route('orders.index')->with('success', '受注を登録しました');
}

public function show($id)
{
$order = Order::with(['customer', 'orderDetails.product'])->findOrFail($id);
return view('orders.show', compact('order'));
}

public function edit($id)
{
$order = Order::with('orderDetails')->findOrFail($id);
$customers = Customer::all();
$products = Product::all();
return view('orders.edit', compact('order', 'customers', 'products'));
}

public function update(Request $request, $id)
{
$request->validate([
'customer_id' => 'required|exists:customers,id',
'order_date' => 'required|date',
'products.*.product_id' => 'required|exists:products,id',
'products.*.quantity' => 'required|integer|min:1'
]);

// Order更新
$order = Order::findOrFail($id);
$order->update([
'customer_id' => $request->customer_id,
'order_date' => $request->order_date,
]);

// 既存のOrderDetailを削除して再度作成(簡易実装)
$order->orderDetails()->delete();
foreach ($request->products as $p) {
OrderDetail::create([
'order_id' => $order->id,
'product_id' => $p['product_id'],
'quantity' => $p['quantity']
]);
}

return redirect()->route('orders.index')->with('success', '受注情報を更新しました');
}

public function destroy($id)
{
$order = Order::findOrFail($id);
$order->delete();
return redirect()->route('orders.index')->with('success', '受注を削除しました');
}
}

7. ルーティング設定

routes/web.php にルートを追加します。

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ProductController;
use App\Http\Controllers\CustomerController;
use App\Http\Controllers\OrderController;

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

// Resourceful Routes
Route::resource('products', ProductController::class);
Route::resource('customers', CustomerController::class);
Route::resource('orders', OrderController::class);

8. Bladeテンプレートの作成

以下では簡易的に代表的なテンプレート例を示します。フォルダ構成は resources/views/ 配下に layouts, products, customers, orders フォルダを作成してそこに配置します。

1. 共通レイアウトファイル:layouts/app.blade.php

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>販売管理システム</title>
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
</head>
<body>

<nav>
<ul>
<li><a href="{{ route('products.index') }}">商品管理</a></li>
<li><a href="{{ route('customers.index') }}">顧客管理</a></li>
<li><a href="{{ route('orders.index') }}">受注管理</a></li>
</ul>
</nav>

<div class="container">
@if(session('success'))
<div style="color: green;">{{ session('success') }}</div>
@endif

@yield('content')
</div>

</body>
</html>

※CSSは public/css/app.css 等に任意で追加してください


2. 商品一覧:products/index.blade.php

@extends('layouts.app')

@section('content')
<h1>商品一覧</h1>
<a href="{{ route('products.create') }}">新規商品登録</a>

<table border="1">
<tr>
<th>ID</th>
<th>商品名</th>
<th>価格</th>
<th>操作</th>
</tr>
@foreach($products as $product)
<tr>
<td>{{ $product->id }}</td>
<td><a href="{{ route('products.show', $product->id) }}">{{ $product->name }}</a></td>
<td>{{ $product->price }}</td>
<td>
<a href="{{ route('products.edit', $product->id) }}">編集</a>
<form action="{{ route('products.destroy', $product->id) }}" method="POST" style="display:inline;">
@csrf
@method('DELETE')
<button type="submit" > </form>
</td>
</tr>
@endforeach
</table>
@endsection

3. 商品作成フォーム:products/create.blade.php

@extends('layouts.app')

@section('content')
<h1>商品登録</h1>

<form action="{{ route('products.store') }}" method="POST">
@csrf
<div>
<label>商品名</label>
<input type="text" name="name" value="{{ old('name') }}">
@error('name')
<div style="color:red;">{{ $message }}</div>
@enderror
</div>
<div>
<label>価格</label>
<input type="number" name="price" value="{{ old('price') }}">
@error('price')
<div style="color:red;">{{ $message }}</div>
@enderror
</div>
<button type="submit">登録</button>
</form>
@endsection

4. 商品編集フォーム:products/edit.blade.php

@extends('layouts.app')

@section('content')
<h1>商品編集</h1>

<form action="{{ route('products.update', $product->id) }}" method="POST">
@csrf
@method('PUT')
<div>
<label>商品名</label>
<input type="text" name="name" value="{{ old('name', $product->name) }}">
@error('name')
<div style="color:red;">{{ $message }}</div>
@enderror
</div>
<div>
<label>価格</label>
<input type="number" name="price" value="{{ old('price', $product->price) }}">
@error('price')
<div style="color:red;">{{ $message }}</div>
@enderror
</div>
<button type="submit">更新</button>
</form>
@endsection

5. 顧客一覧:customers/index.blade.php

@extends('layouts.app')

@section('content')
<h1>顧客一覧</h1>
<a href="{{ route('customers.create') }}">新規顧客登録</a>

<table border="1">
<tr>
<th>ID</th>
<th>顧客名</th>
<th>メールアドレス</th>
<th>操作</th>
</tr>
@foreach($customers as $customer)
<tr>
<td>{{ $customer->id }}</td>
<td><a href="{{ route('customers.show', $customer->id) }}">{{ $customer->name }}</a></td>
<td>{{ $customer->email }}</td>
<td>
<a href="{{ route('customers.edit', $customer->id) }}">編集</a>
<form action="{{ route('customers.destroy', $customer->id) }}" method="POST" style="display:inline;">
@csrf
@method('DELETE')
<button type="submit" > </form>
</td>
</tr>
@endforeach
</table>
@endsection

6. 顧客登録フォーム:customers/create.blade.php

@extends('layouts.app')

@section('content')
<h1>顧客登録</h1>

<form action="{{ route('customers.store') }}" method="POST">
@csrf
<div>
<label>顧客名</label>
<input type="text" name="name" value="{{ old('name') }}">
@error('name')
<div style="color:red;">{{ $message }}</div>
@enderror
</div>
<div>
<label>メールアドレス</label>
<input type="email" name="email" value="{{ old('email') }}">
@error('email')
<div style="color:red;">{{ $message }}</div>
@enderror
</div>
<button type="submit">登録</button>
</form>
@endsection

7. 顧客編集フォーム:customers/edit.blade.php

@extends('layouts.app')

@section('content')
<h1>顧客編集</h1>

<form action="{{ route('customers.update', $customer->id) }}" method="POST">
@csrf
@method('PUT')
<div>
<label>顧客名</label>
<input type="text" name="name" value="{{ old('name', $customer->name) }}">
@error('name')
<div style="color:red;">{{ $message }}</div>
@enderror
</div>
<div>
<label>メールアドレス</label>
<input type="email" name="email" value="{{ old('email', $customer->email) }}">
@error('email')
<div style="color:red;">{{ $message }}</div>
@enderror
</div>
<button type="submit">更新</button>
</form>
@endsection

8. 受注一覧:orders/index.blade.php

@extends('layouts.app')

@section('content')
<h1>受注一覧</h1>
<a href="{{ route('orders.create') }}">新規受注登録</a>

<table border="1">
<tr>
<th>ID</th>
<th>顧客名</th>
<th>受注日</th>
<th>操作</th>
</tr>
@foreach($orders as $order)
<tr>
<td>{{ $order->id }}</td>
<td>{{ $order->customer->name }}</td>
<td>{{ $order->order_date }}</td>
<td>
<a href="{{ route('orders.show', $order->id) }}">詳細</a>
<a href="{{ route('orders.edit', $order->id) }}">編集</a>
<form action="{{ route('orders.destroy', $order->id) }}" method="POST" style="display:inline;">
@csrf
@method('DELETE')
<button type="submit" > </form>
</td>
</tr>
@endforeach
</table>
@endsection

9. 受注登録フォーム:orders/create.blade.php

@extends('layouts.app')

@section('content')
<h1>新規受注登録</h1>

<form action="{{ route('orders.store') }}" method="POST">
@csrf
<div>
<label>顧客</label>
<select name="customer_id">
<option value="">選択してください</option>
@foreach($customers as $customer)
<option value="{{ $customer->id }}" {{ old('customer_id') == $customer->id ? 'selected' : '' }}>
{{ $customer->name }}
</option>
@endforeach
</select>
@error('customer_id')
<div style="color:red;">{{ $message }}</div>
@enderror
</div>

<div>
<label>受注日</label>
<input type="date" name="order_date" value="{{ old('order_date') }}">
@error('order_date')
<div style="color:red;">{{ $message }}</div>
@enderror
</div>

<h3>商品情報</h3>
<div id="product-list">
<div class="product-row">
<select name="products[0][product_id]">
<option value="">商品を選択</option>
@foreach($products as $product)
<option value="{{ $product->id }}">{{ $product->name }} (¥{{ $product->price }})</option>
@endforeach
</select>
<input type="number" name="products[0][quantity]" placeholder="数量" min="1" value="1">
</div>
</div>

<button type="button" > <button type="submit">登録</button>
</form>

<script>
let productIndex = 1;
function addProductRow() {
const productList = document.getElementById('product-list');
const newRow = document.createElement('div');
newRow.className = 'product-row';
newRow.innerHTML = `
<select name="products[${productIndex}][product_id]">
<option value="">商品を選択</option>
@foreach($products as $product)
<option value="{{ $product->id }}">{{ $product->name }} (¥{{ $product->price }})</option>
@endforeach
</select>
<input type="number" name="products[${productIndex}][quantity]" placeholder="数量" min="1" value="1">
`;
productList.appendChild(newRow);
productIndex++;
}
</script>
@endsection

10. 受注編集フォーム:orders/edit.blade.php

@extends('layouts.app')

@section('content')
<h1>受注編集</h1>

<form action="{{ route('orders.update', $order->id) }}" method="POST">
@csrf
@method('PUT')
<div>
<label>顧客</label>
<select name="customer_id">
<option value="">選択してください</option>
@foreach($customers as $customer)
<option value="{{ $customer->id }}"
{{ old('customer_id', $order->customer_id) == $customer->id ? 'selected' : '' }}>
{{ $customer->name }}
</option>
@endforeach
</select>
@error('customer_id')
<div style="color:red;">{{ $message }}</div>
@enderror
</div>

<div>
<label>受注日</label>
<input type="date" name="order_date" value="{{ old('order_date', $order->order_date) }}">
@error('order_date')
<div style="color:red;">{{ $message }}</div>
@enderror
</div>

<h3>商品情報</h3>
<div id="product-list">
@php $i = 0; @endphp
@foreach($order->orderDetails as $detail)
<div class="product-row">
<select name="products[{{ $i }}][product_id]">
<option value="">商品を選択</option>
@foreach($products as $product)
<option value="{{ $product->id }}"
{{ $detail->product_id == $product->id ? 'selected' : '' }}>
{{ $product->name }} (¥{{ $product->price }})
</option>
@endforeach
</select>
<input type="number" name="products[{{ $i }}][quantity]" placeholder="数量" min="1" value="{{ $detail->quantity }}">
</div>
@php $i++; @endphp
@endforeach
</div>

<button type="button" > <button type="submit">更新</button>
</form>

<script>
let productIndex = {{ $i }};
function addProductRow() {
const productList = document.getElementById('product-list');
const newRow = document.createElement('div');
newRow.className = 'product-row';
newRow.innerHTML = `
<select name="products[${productIndex}][product_id]">
<option value="">商品を選択</option>
@foreach($products as $product)
<option value="{{ $product->id }}">{{ $product->name }} (¥{{ $product->price }})</option>
@endforeach
</select>
<input type="number" name="products[${productIndex}][quantity]" placeholder="数量" min="1" value="1">
`;
productList.appendChild(newRow);
productIndex++;
}
</script>
@endsection

11. 受注詳細:orders/show.blade.php

@extends('layouts.app')

@section('content')
<h1>受注詳細</h1>

<p><strong>受注番号:</strong> {{ $order->id }}</p>
<p><strong>顧客名:</strong> {{ $order->customer->name }}</p>
<p><strong>受注日:</strong> {{ $order->order_date }}</p>

<h3>商品一覧</h3>
<table border="1">
<tr>
<th>商品名</th>
<th>価格</th>
<th>数量</th>
</tr>
@foreach($order->orderDetails as $detail)
<tr>
<td>{{ $detail->product->name }}</td>
<td>{{ $detail->product->price }}</td>
<td>{{ $detail->quantity }}</td>
</tr>
@endforeach
</table>

<p><a href="{{ route('orders.index') }}">一覧に戻る</a></p>
@endsection

9. 動作確認

  1. ローカルサーバーを起動bashコピーする編集するphp artisan serve
  2. ブラウザから http://127.0.0.1:8000/products を開く
  3. 商品を登録し、登録一覧を確認
  4. 顧客一覧から顧客を登録
  5. 受注登録から顧客・商品を選択し、受注データを登録
  6. 正常に登録できたら、受注一覧や詳細画面で確認

10. まとめ

本記事では、Laravelを使ってシンプルな販売管理システムを構築する流れを紹介しました。Migrationでテーブルを設計し、Modelでリレーションを定義、Controllerでビジネスロジックを実装し、最後にBladeテンプレートで画面を作成するというLaravelの基本的な流れを踏んでいます。
ここでは簡易的な実装例を示していますが、実際の業務ではバリデーションをより厳密に行ったり、在庫管理や決済機能などを追加したりと、さらに多くの機能を検討する必要があります。Laravelは拡張性が高いフレームワークなので、本記事を参考に自分の要件に合わせたカスタマイズを行ってみてください。

以上で、Laravelを用いた販売管理システムの基本実装について解説を終わります。ぜひ学習や業務の参考にしていただければ幸いです。

upandup

Web制作の記事を中心に、暮らし、ビジネスに役立つ情報を発信します。 アフィリエイトにも参加しています。よろしくお願いいたします。