PHP で社内業務自動化:CSV→DB 取込・定期バッチ・Slack 通知の実例
社内の定型業務を自動化したいと考えたことはありませんか。毎日同じファイルを開いて、データを確認して、手作業で入力する作業は時間もかかりますし、ミスも起こりやすいですよね。
この記事では、PHP を使って実際の業務でよくあるシーンを自動化する方法をご紹介します。具体的には、CSV ファイルからデータベースへの取り込み、定期的なバッチ処理の実行、そして処理結果を Slack に通知する仕組みを、実践的なコード例とともに解説していきますね。
背景
業務自動化が求められる理由
多くの企業では、日々の業務でさまざまなデータを扱っています。営業データ、在庫情報、顧客リスト、売上レポートなど、これらのデータは Excel や CSV ファイルとして提供されることが多いでしょう。
従来の手作業での処理には以下のような課題があります。
| # | 課題 | 具体例 |
|---|---|---|
| 1 | 時間のロス | 毎朝 30 分かけて CSV を開き、データを確認・入力 |
| 2 | ヒューマンエラー | 数値の入力ミス、コピー&ペーストの漏れ |
| 3 | 属人化 | 特定の担当者しか処理方法を知らない |
| 4 | 通知の遅れ | 処理完了を口頭やメールで共有するため遅延が発生 |
PHP を選ぶ理由
PHP は Web 開発だけでなく、バッチ処理や自動化にも優れた特性を持っています。
- 導入の容易さ: 多くのサーバーで標準的に利用可能
- データベース連携: MySQL、PostgreSQL などとの相性が抜群
- 豊富なライブラリ: CSV 操作、HTTP 通信など標準で対応
- cron との親和性: Linux サーバーでの定期実行が簡単
以下の図は、PHP を使った業務自動化の基本的な流れを示したものです。
mermaidflowchart LR
csv["CSV ファイル"] -->|読み込み| php["PHP バッチ処理"]
php -->|データ挿入| db[("MySQL DB")]
php -->|処理結果| slack["Slack 通知"]
cron["cron(定期実行)"] -->|トリガー| php
この図からわかるように、CSV ファイルを起点として、PHP がデータベースへの登録と Slack への通知を一括で処理します。cron による定期実行により、人手を介さずに自動化が実現できるのです。
課題
手作業による業務の問題点
実際の業務現場では、以下のような状況に直面することが多いのではないでしょうか。
データ取り込みの煩雑さ
毎朝届く売上データの CSV ファイルを、担当者が手動でデータベースに登録する作業があるとします。この作業には以下の手順が必要になります。
| # | 作業内容 | 所要時間 |
|---|---|---|
| 1 | メールから CSV ファイルをダウンロード | 2 分 |
| 2 | ファイルを開いてデータの妥当性を確認 | 5 分 |
| 3 | 管理画面にログインしてデータを入力 | 15 分 |
| 4 | 入力内容の確認と修正 | 5 分 |
| 5 | 関係者への完了報告メール作成・送信 | 3 分 |
合計で約 30 分の作業が毎日発生し、月間では 10 時間以上のコストになってしまいます。
エラーハンドリングの困難さ
手作業では以下のようなエラーが発生しやすくなります。
- データ形式の不一致: CSV の列順が変わった場合の対応漏れ
- 重複データの登録: 同じファイルを誤って 2 回処理してしまう
- 必須項目の欠落: 空欄のままデータベースに登録してしまう
処理状況の可視化不足
誰が、いつ、どのファイルを処理したのかが不明確になり、以下の問題が生じます。
- 処理の重複や漏れが発生
- トラブル時の原因特定が困難
- チーム内での情報共有が遅れる
以下の図は、手作業による処理フローの問題点を示しています。
mermaidflowchart TD
start["CSV ファイル受信"] --> check["担当者が手動確認"]
check --> input["管理画面に手入力"]
input --> verify["目視で再確認"]
verify --> error{エラー発見?}
error -->|はい| fix["手動修正"]
fix --> verify
error -->|いいえ| mail["完了メール送信"]
mail --> done["処理完了"]
style error fill:#ffcccc
style fix fill:#ffcccc
この図からわかるように、手作業では確認と修正のループが発生しやすく、処理時間が予測できません。また、各工程で人が介在するため、ミスの混入ポイントが多数存在します。
自動化で解決すべき要件
これらの課題を解決するために、以下の要件を満たす自動化システムが必要です。
| # | 要件 | 期待効果 |
|---|---|---|
| 1 | CSV ファイルの自動読み込みとバリデーション | データ形式エラーの事前検出 |
| 2 | データベースへの自動登録(重複チェック付き) | 登録作業の効率化とミス防止 |
| 3 | 定期的な自動実行(毎朝 9 時など) | 担当者の作業負荷軽減 |
| 4 | 処理結果の Slack 自動通知 | リアルタイムな状況共有 |
| 5 | エラー時の詳細ログ出力 | トラブルシューティングの効率化 |
解決策
システム構成の概要
PHP を中心とした自動化システムを構築することで、前述の課題を解決できます。システムの主要コンポーネントは以下の通りです。
| # | コンポーネント | 役割 |
|---|---|---|
| 1 | PHP スクリプト | CSV 読み込み、DB 登録、Slack 通知の制御 |
| 2 | MySQL データベース | データの永続化と管理 |
| 3 | cron | 定期実行のスケジューリング |
| 4 | Slack Incoming Webhook | 処理結果の通知先 |
| 5 | ログファイル | エラーや処理履歴の記録 |
以下の図は、これらのコンポーネントがどのように連携するかを示しています。
mermaidflowchart TB
subgraph schedule["スケジュール管理"]
cron["cron<br/>(定期実行設定)"]
end
subgraph processing["処理エンジン"]
php["PHP バッチスクリプト"]
validator["データバリデーター"]
importer["DB インポーター"]
notifier["通知サービス"]
end
subgraph storage["データ保存"]
csv["CSV ファイル"]
db[("MySQL DB")]
log["ログファイル"]
end
subgraph notification["通知先"]
slack["Slack チャンネル"]
end
cron -->|実行トリガー| php
csv -->|読み込み| validator
validator -->|検証済みデータ| importer
importer -->|INSERT/UPDATE| db
importer -->|処理結果| notifier
importer -->|エラー/成功ログ| log
notifier -->|POST リクエスト| slack
このアーキテクチャにより、各処理が独立しており、エラー発生時の切り分けが容易になります。また、ログを確実に記録することで、後からトラブルシューティングが可能です。
技術的なアプローチ
自動化を実現するための技術的なポイントは以下の通りです。
CSV パース処理
PHP の標準関数 fgetcsv() を使うことで、メモリ効率的に大容量ファイルを処理できます。1 行ずつ読み込むため、数万行のデータでもメモリ不足になりません。
トランザクション管理
データベース操作には PDO(PHP Data Objects)を使用し、トランザクションで一括処理を保証します。途中でエラーが発生した場合は全てロールバックされるため、データの整合性が保たれるのです。
エラーハンドリング
try-catch 文で例外をキャッチし、エラー内容をログファイルと Slack の両方に出力します。これにより、担当者が不在でもチーム全体でエラーを認識できますね。
具体例
それでは、実際のコードを使って具体的な実装方法を見ていきましょう。ここでは、売上データの CSV を毎日自動で取り込むシステムを構築します。
ディレクトリ構成
まず、プロジェクトの構成を整理します。
text/var/www/batch/
├── config/
│ └── database.php # DB 接続設定
├── data/
│ └── sales.csv # 取り込み対象 CSV
├── logs/
│ └── import.log # 処理ログ
├── src/
│ ├── CsvImporter.php # CSV 取り込みクラス
│ ├── SlackNotifier.php # Slack 通知クラス
│ └── import.php # メイン処理
└── .env # 環境変数(Slack Webhook URL など)
環境変数の設定
まず、機密情報を .env ファイルで管理します。このファイルにデータベース接続情報や Slack の Webhook URL を記載します。
text# データベース接続情報
DB_HOST=localhost
DB_NAME=company_db
DB_USER=batch_user
DB_PASS=secure_password
# Slack Webhook URL
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
データベース接続設定
次に、データベースへの接続を管理するクラスを作成します。PDO を使用することで、SQL インジェクション対策とエラーハンドリングが容易になります。
php<?php
// config/database.php
/**
* データベース接続クラス
* PDO を使用した MySQL への安全な接続を提供
*/
class Database
{
private $host;
private $db_name;
private $username;
private $password;
private $conn;
public function __construct()
{
// .env ファイルから環境変数を読み込み
$this->loadEnv();
$this->host = getenv('DB_HOST');
$this->db_name = getenv('DB_NAME');
$this->username = getenv('DB_USER');
$this->password = getenv('DB_PASS');
}
環境変数を読み込むメソッドを実装します。
php /**
* .env ファイルを読み込んで環境変数にセット
*/
private function loadEnv()
{
$envFile = __DIR__ . '/../.env';
if (!file_exists($envFile)) {
throw new Exception('.env ファイルが見つかりません');
}
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
// コメント行はスキップ
if (strpos(trim($line), '#') === 0) {
continue;
}
list($key, $value) = explode('=', $line, 2);
putenv(trim($key) . '=' . trim($value));
}
}
データベース接続を確立するメソッドです。エラー時には例外をスローして、呼び出し元で適切に処理できるようにします。
php /**
* データベース接続を取得
* @return PDO データベース接続オブジェクト
* @throws PDOException 接続失敗時
*/
public function getConnection()
{
$this->conn = null;
try {
$dsn = "mysql:host={$this->host};dbname={$this->db_name};charset=utf8mb4";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
$this->conn = new PDO($dsn, $this->username, $this->password, $options);
} catch (PDOException $e) {
throw new Exception('データベース接続エラー: ' . $e->getMessage());
}
return $this->conn;
}
}
CSV インポータークラス
CSV ファイルを読み込んで、データベースに登録する処理を行うクラスを作成します。このクラスはバリデーション、重複チェック、データ登録の機能を持ちます。
php<?php
// src/CsvImporter.php
require_once __DIR__ . '/../config/database.php';
/**
* CSV インポータークラス
* CSV ファイルの読み込みとデータベース登録を担当
*/
class CsvImporter
{
private $db;
private $logFile;
private $stats = [
'total' => 0, // 処理件数
'inserted' => 0, // 新規登録件数
'updated' => 0, // 更新件数
'skipped' => 0, // スキップ件数
'errors' => [] // エラー情報
];
public function __construct($logFile = null)
{
$database = new Database();
$this->db = $database->getConnection();
$this->logFile = $logFile ?? __DIR__ . '/../logs/import.log';
}
ログ出力用のメソッドを実装します。ファイルとコンソールの両方に出力することで、cron 実行時とコマンドライン実行時の両方に対応できます。
php /**
* ログメッセージを出力
* @param string $message ログメッセージ
* @param string $level ログレベル(INFO, ERROR, WARNING)
*/
private function log($message, $level = 'INFO')
{
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[{$timestamp}] [{$level}] {$message}" . PHP_EOL;
// ファイルに出力
file_put_contents($this->logFile, $logMessage, FILE_APPEND);
// コンソールにも出力
echo $logMessage;
}
CSV ファイルを読み込んでバリデーションを行うメソッドです。
php /**
* CSV ファイルのバリデーション
* @param string $filePath CSV ファイルパス
* @return bool バリデーション結果
*/
private function validateCsv($filePath)
{
// ファイル存在チェック
if (!file_exists($filePath)) {
$this->log("CSV ファイルが見つかりません: {$filePath}", 'ERROR');
return false;
}
// ファイルサイズチェック(100MB 以上はエラー)
$fileSize = filesize($filePath);
if ($fileSize > 100 * 1024 * 1024) {
$this->log("CSV ファイルサイズが大きすぎます: " . round($fileSize / 1024 / 1024, 2) . "MB", 'ERROR');
return false;
}
// ファイルが読み込み可能かチェック
if (!is_readable($filePath)) {
$this->log("CSV ファイルの読み込み権限がありません: {$filePath}", 'ERROR');
return false;
}
return true;
}
CSV の 1 行分のデータをバリデーションするメソッドです。必須項目チェックやデータ型の検証を行います。
php /**
* CSV 行データのバリデーション
* @param array $row CSV 行データ
* @param int $lineNumber 行番号
* @return bool バリデーション結果
*/
private function validateRow($row, $lineNumber)
{
// 列数チェック(想定: 日付, 商品コード, 商品名, 数量, 単価)
if (count($row) !== 5) {
$error = "行 {$lineNumber}: 列数が不正です(期待: 5列, 実際: " . count($row) . "列)";
$this->log($error, 'WARNING');
$this->stats['errors'][] = $error;
return false;
}
// 必須項目チェック
if (empty($row[0]) || empty($row[1]) || empty($row[2])) {
$error = "行 {$lineNumber}: 必須項目(日付/商品コード/商品名)が空です";
$this->log($error, 'WARNING');
$this->stats['errors'][] = $error;
return false;
}
// 日付形式チェック(YYYY-MM-DD)
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $row[0])) {
$error = "行 {$lineNumber}: 日付形式が不正です(期待: YYYY-MM-DD, 実際: {$row[0]})";
$this->log($error, 'WARNING');
$this->stats['errors'][] = $error;
return false;
}
// 数量と単価の数値チェック
if (!is_numeric($row[3]) || !is_numeric($row[4])) {
$error = "行 {$lineNumber}: 数量または単価が数値ではありません";
$this->log($error, 'WARNING');
$this->stats['errors'][] = $error;
return false;
}
return true;
}
データベースに既にレコードが存在するかチェックするメソッドです。
php /**
* レコードの重複チェック
* @param string $date 日付
* @param string $productCode 商品コード
* @return int|null 既存レコードの ID、存在しない場合は null
*/
private function checkDuplicate($date, $productCode)
{
$sql = "SELECT id FROM sales WHERE sale_date = :date AND product_code = :code";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':date' => $date,
':code' => $productCode
]);
$result = $stmt->fetch();
return $result ? $result['id'] : null;
}
メインのインポート処理を行うメソッドです。トランザクションを使用して、エラー時には全てロールバックします。
php /**
* CSV ファイルをインポート
* @param string $filePath CSV ファイルパス
* @return array 処理統計情報
*/
public function import($filePath)
{
$this->log("=== CSV インポート処理開始 ===");
$this->log("対象ファイル: {$filePath}");
// CSV バリデーション
if (!$this->validateCsv($filePath)) {
return $this->stats;
}
try {
// トランザクション開始
$this->db->beginTransaction();
// CSV ファイルを開く
$handle = fopen($filePath, 'r');
if ($handle === false) {
throw new Exception('CSV ファイルのオープンに失敗しました');
}
// ヘッダー行をスキップ
$header = fgetcsv($handle);
$this->log("ヘッダー: " . implode(', ', $header));
$lineNumber = 1;
CSV ファイルを 1 行ずつ読み込んで処理するループ部分です。
php // データ行を処理
while (($row = fgetcsv($handle)) !== false) {
$lineNumber++;
$this->stats['total']++;
// 行のバリデーション
if (!$this->validateRow($row, $lineNumber)) {
$this->stats['skipped']++;
continue;
}
// データを変数に展開
list($date, $productCode, $productName, $quantity, $price) = $row;
// 重複チェック
$existingId = $this->checkDuplicate($date, $productCode);
if ($existingId) {
// 既存レコードを更新
$this->updateRecord($existingId, $productName, $quantity, $price);
$this->stats['updated']++;
$this->log("行 {$lineNumber}: 更新 (ID: {$existingId})", 'INFO');
} else {
// 新規レコードを挿入
$this->insertRecord($date, $productCode, $productName, $quantity, $price);
$this->stats['inserted']++;
$this->log("行 {$lineNumber}: 新規登録", 'INFO');
}
}
fclose($handle);
// トランザクションコミット
$this->db->commit();
$this->log("=== CSV インポート処理完了 ===");
} catch (Exception $e) {
// エラー時はロールバック
$this->db->rollBack();
$error = "インポート処理中にエラーが発生しました: " . $e->getMessage();
$this->log($error, 'ERROR');
$this->stats['errors'][] = $error;
}
return $this->stats;
}
新規レコードを挿入するメソッドです。プリペアドステートメントを使用して SQL インジェクションを防ぎます。
php /**
* 新規レコードを挿入
*/
private function insertRecord($date, $productCode, $productName, $quantity, $price)
{
$sql = "INSERT INTO sales (sale_date, product_code, product_name, quantity, price, created_at)
VALUES (:date, :code, :name, :quantity, :price, NOW())";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':date' => $date,
':code' => $productCode,
':name' => $productName,
':quantity' => $quantity,
':price' => $price
]);
}
/**
* 既存レコードを更新
*/
private function updateRecord($id, $productName, $quantity, $price)
{
$sql = "UPDATE sales
SET product_name = :name, quantity = :quantity, price = :price, updated_at = NOW()
WHERE id = :id";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':id' => $id,
':name' => $productName,
':quantity' => $quantity,
':price' => $price
]);
}
/**
* 処理統計を取得
*/
public function getStats()
{
return $this->stats;
}
}
Slack 通知クラス
処理結果を Slack に通知するクラスを作成します。Incoming Webhook を使用して、処理統計やエラー情報を見やすい形式で投稿します。
php<?php
// src/SlackNotifier.php
/**
* Slack 通知クラス
* Incoming Webhook を使用した通知機能を提供
*/
class SlackNotifier
{
private $webhookUrl;
public function __construct($webhookUrl = null)
{
// .env から Webhook URL を取得
if ($webhookUrl === null) {
$webhookUrl = getenv('SLACK_WEBHOOK_URL');
}
if (empty($webhookUrl)) {
throw new Exception('Slack Webhook URL が設定されていません');
}
$this->webhookUrl = $webhookUrl;
}
処理結果のサマリーを整形して Slack に送信するメソッドです。
php /**
* インポート結果を Slack に通知
* @param array $stats 処理統計情報
* @param string $fileName 処理したファイル名
*/
public function notifyImportResult($stats, $fileName)
{
$hasErrors = !empty($stats['errors']);
$status = $hasErrors ? ':warning: 警告あり' : ':white_check_mark: 成功';
// メッセージの色を設定(エラーあり: 警告色、なし: 成功色)
$color = $hasErrors ? '#FFA500' : '#36A64F';
// 基本メッセージ
$message = [
'text' => "CSV インポート処理が完了しました {$status}",
'attachments' => [
[
'color' => $color,
'title' => 'インポート結果',
'fields' => [
[
'title' => 'ファイル名',
'value' => $fileName,
'short' => false
],
[
'title' => '処理件数',
'value' => $stats['total'] . ' 件',
'short' => true
],
[
'title' => '新規登録',
'value' => $stats['inserted'] . ' 件',
'short' => true
],
[
'title' => '更新',
'value' => $stats['updated'] . ' 件',
'short' => true
],
[
'title' => 'スキップ',
'value' => $stats['skipped'] . ' 件',
'short' => true
]
],
'footer' => '業務自動化バッチ',
'ts' => time()
]
]
];
エラー情報がある場合は、詳細を追加します。
php // エラーがある場合は詳細を追加
if ($hasErrors) {
$errorList = implode("\n", array_slice($stats['errors'], 0, 5));
if (count($stats['errors']) > 5) {
$errorList .= "\n... 他 " . (count($stats['errors']) - 5) . " 件のエラー";
}
$message['attachments'][] = [
'color' => '#FF0000',
'title' => 'エラー詳細',
'text' => "```\n{$errorList}\n```",
'mrkdwn_in' => ['text']
];
}
$this->send($message);
}
実際に Slack へ POST リクエストを送信するメソッドです。cURL を使用して、エラーハンドリングも行います。
php /**
* Slack へメッセージを送信
* @param array $message メッセージ内容
*/
private function send($message)
{
$payload = json_encode($message);
$ch = curl_init($this->webhookUrl);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($payload)
]);
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode !== 200) {
error_log("Slack 通知エラー: HTTP {$httpCode}, Response: {$result}");
throw new Exception("Slack への通知に失敗しました(HTTP {$httpCode})");
}
curl_close($ch);
}
/**
* エラー通知を送信
* @param string $errorMessage エラーメッセージ
*/
public function notifyError($errorMessage)
{
$message = [
'text' => ':x: CSV インポート処理でエラーが発生しました',
'attachments' => [
[
'color' => '#FF0000',
'title' => 'エラー内容',
'text' => "```\n{$errorMessage}\n```",
'mrkdwn_in' => ['text'],
'footer' => '業務自動化バッチ',
'ts' => time()
]
]
];
$this->send($message);
}
}
メイン処理スクリプト
全ての機能を統合したメインスクリプトを作成します。このファイルが cron から実行されます。
php<?php
// src/import.php
require_once __DIR__ . '/CsvImporter.php';
require_once __DIR__ . '/SlackNotifier.php';
/**
* メイン処理
* CSV インポートと Slack 通知を実行
*/
function main()
{
$startTime = microtime(true);
// 処理対象の CSV ファイルパス
$csvFile = __DIR__ . '/../data/sales.csv';
try {
echo "=== 売上データ インポートバッチ ===" . PHP_EOL;
echo "開始時刻: " . date('Y-m-d H:i:s') . PHP_EOL . PHP_EOL;
// CSV インポート実行
$importer = new CsvImporter();
$stats = $importer->import($csvFile);
処理結果を Slack に通知する部分です。
php // Slack 通知
$notifier = new SlackNotifier();
$notifier->notifyImportResult($stats, basename($csvFile));
echo PHP_EOL . "=== 処理完了 ===" . PHP_EOL;
echo "終了時刻: " . date('Y-m-d H:i:s') . PHP_EOL;
$execTime = round(microtime(true) - $startTime, 2);
echo "実行時間: {$execTime} 秒" . PHP_EOL;
// エラーがあった場合は終了コード 1 を返す
if (!empty($stats['errors'])) {
exit(1);
}
exit(0);
} catch (Exception $e) {
// 致命的なエラーが発生した場合
$errorMessage = "致命的エラー: " . $e->getMessage() . "\n";
$errorMessage .= "ファイル: " . $e->getFile() . "\n";
$errorMessage .= "行番号: " . $e->getLine();
echo $errorMessage . PHP_EOL;
// Slack にエラー通知
try {
$notifier = new SlackNotifier();
$notifier->notifyError($errorMessage);
} catch (Exception $notifyError) {
echo "Slack 通知失敗: " . $notifyError->getMessage() . PHP_EOL;
}
exit(1);
}
}
// スクリプト実行
main();
データベーステーブル定義
売上データを格納するテーブルの SQL 定義です。インデックスを適切に設定することで、重複チェックのパフォーマンスを向上させます。
sql-- sales テーブル作成
CREATE TABLE IF NOT EXISTS sales (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'ID',
sale_date DATE NOT NULL COMMENT '売上日',
product_code VARCHAR(50) NOT NULL COMMENT '商品コード',
product_name VARCHAR(255) NOT NULL COMMENT '商品名',
quantity INT NOT NULL DEFAULT 0 COMMENT '数量',
price DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '単価',
created_at DATETIME NOT NULL COMMENT '登録日時',
updated_at DATETIME NULL COMMENT '更新日時',
-- 重複チェック用のユニークインデックス
UNIQUE KEY unique_sale (sale_date, product_code),
-- 検索用のインデックス
INDEX idx_sale_date (sale_date),
INDEX idx_product_code (product_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='売上データ';
サンプル CSV ファイル
実際の CSV ファイルのサンプルです。このフォーマットでデータを用意します。
csv日付,商品コード,商品名,数量,単価
2025-01-15,P001,ノートPC,5,120000
2025-01-15,P002,マウス,20,1500
2025-01-15,P003,キーボード,15,5000
2025-01-16,P001,ノートPC,3,120000
2025-01-16,P004,モニター,8,35000
cron 設定
最後に、このスクリプトを毎日自動実行するための cron 設定を行います。以下は毎日午前 9 時に実行する例です。
bash# crontab -e で編集
# 毎日 9:00 に実行
0 9 * * * /usr/bin/php /var/www/batch/src/import.php >> /var/www/batch/logs/cron.log 2>&1
cron の設定内容を表で整理すると以下のようになります。
| # | フィールド | 値 | 意味 |
|---|---|---|---|
| 1 | 分 | 0 | 0 分(毎時 0 分) |
| 2 | 時 | 9 | 9 時 |
| 3 | 日 | * | 毎日 |
| 4 | 月 | * | 毎月 |
| 5 | 曜日 | * | 全曜日 |
実行フローの全体像
ここまでに作成したコンポーネントが、どのように連携して動作するのかを図で確認しましょう。
mermaidsequenceDiagram
participant cron as cron
participant main as import.php<br/>(メイン処理)
participant importer as CsvImporter<br/>(インポーター)
participant db as MySQL DB
participant notifier as SlackNotifier<br/>(通知)
participant slack as Slack
cron->>main: 毎日 9:00 に実行
main->>importer: CSV インポート開始
importer->>importer: ファイル存在確認
importer->>importer: CSV バリデーション
loop 各行を処理
importer->>importer: 行データバリデーション
importer->>db: 重複チェック
db-->>importer: 結果返却
alt 既存データあり
importer->>db: UPDATE 実行
else 新規データ
importer->>db: INSERT 実行
end
end
importer-->>main: 処理統計を返却
main->>notifier: 通知リクエスト
notifier->>slack: POST リクエスト
slack-->>notifier: 200 OK
notifier-->>main: 通知完了
main-->>cron: 終了(exit 0)
この図から、各コンポーネントの責任範囲が明確に分離されていることがわかりますね。エラーが発生した場合でも、どの段階で問題が起きたのかを特定しやすい構造になっています。
エラーハンドリングとログ出力
実際の運用では、さまざまなエラーが発生する可能性があります。以下は、主要なエラーとその対処方法をまとめた表です。
| # | エラー種別 | エラーコード例 | 対処方法 |
|---|---|---|---|
| 1 | CSV ファイルが見つからない | file_not_found | ファイルパスとパーミッションを確認 |
| 2 | データベース接続エラー | PDOException: SQLSTATE[HY000] [2002] | DB サーバーの稼働状況と接続情報を確認 |
| 3 | CSV フォーマット不正 | validation_error | CSV ファイルの形式を確認(列数、データ型) |
| 4 | Slack 通知失敗 | HTTP 404/500 | Webhook URL の有効性を確認 |
| 5 | トランザクションエラー | PDOException: SQLSTATE[40001] | ロールバック後に再試行 |
ログファイルには以下のような情報が記録されます。
text[2025-01-15 09:00:01] [INFO] === CSV インポート処理開始 ===
[2025-01-15 09:00:01] [INFO] 対象ファイル: /var/www/batch/data/sales.csv
[2025-01-15 09:00:01] [INFO] ヘッダー: 日付, 商品コード, 商品名, 数量, 単価
[2025-01-15 09:00:02] [INFO] 行 2: 新規登録
[2025-01-15 09:00:02] [INFO] 行 3: 新規登録
[2025-01-15 09:00:02] [WARNING] 行 4: 数量または単価が数値ではありません
[2025-01-15 09:00:03] [INFO] 行 5: 更新 (ID: 123)
[2025-01-15 09:00:05] [INFO] === CSV インポート処理完了 ===
まとめ
今回は、PHP を使った社内業務自動化の具体例として、CSV ファイルのデータベース取り込みから Slack 通知までを実装しました。
実装のポイント
本記事で紹介した自動化システムのポイントを振り返りましょう。
| # | ポイント | 効果 |
|---|---|---|
| 1 | クラス分割による責務の明確化 | 保守性と拡張性の向上 |
| 2 | トランザクションによるデータ整合性の保証 | ロールバックにより部分的な更新を防止 |
| 3 | 詳細なバリデーションとエラーハンドリング | 不正データの流入を防止 |
| 4 | ログと Slack 通知の二重化 | トラブルシューティングの効率化 |
| 5 | cron による定期実行 | 人手を介さない完全自動化 |
導入効果
このシステムを導入することで、以下の効果が期待できます。
作業時間の削減: 毎日 30 分かかっていた作業がゼロになります。月間で約 10 時間、年間では 120 時間もの時間を創出できるのです。
ミスの防止: 手入力によるヒューマンエラーを完全に排除できます。データの整合性が保たれ、品質が向上しますね。
可視化の向上: Slack 通知により、チーム全体で処理状況をリアルタイムに把握できます。担当者が不在でも状況を確認できるため、業務の属人化を防げるでしょう。
応用と展開
この仕組みは、CSV インポート以外にもさまざまな業務に応用できます。
- レポート自動生成: データベースから集計して PDF や Excel を生成
- API 連携: 外部サービスからデータを取得して加工・保存
- 定期メンテナンス: 古いログの削除やバックアップの自動実行
- 監視とアラート: サーバーやアプリケーションの状態を定期チェック
PHP は Web 開発だけでなく、バッチ処理や自動化にも強力なツールです。今回の実装を参考に、皆さんの業務でも自動化を進めてみてはいかがでしょうか。最初は小さな処理から始めて、徐々に拡張していくことをおすすめします。
自動化により生まれた時間を、より創造的な業務に充てられるようになることを願っています。
関連リンク
articlePHP で社内業務自動化:CSV→DB 取込・定期バッチ・Slack 通知の実例
articlePHP 基本文法を 90 分で速習:型宣言・null 合体・スプレッド構文の実践
articlePHP アーキテクチャ設計入門:レイヤリング・依存逆転・ユースケース分離
articlePHP 構文チートシート:配列・クロージャ・型宣言・match を一枚で把握
articlePHP 開発環境の作り方【完全ガイド】:macOS/Windows/Linux 別最適解
articlePHP とは?2025 年版の特徴・強み・できることを徹底解説【保存版】
articleDeno とは?Node.js との違い・強み・ユースケースを最新整理
articlegpt-oss アーキテクチャを分解図で理解する:推論ランタイム・トークナイザ・サービング層の役割
articlePHP で社内業務自動化:CSV→DB 取込・定期バッチ・Slack 通知の実例
articleGPT-5 × Cloudflare Workers/Edge:低遅延サーバーレスのスターターガイド
articleNotebookLM と Notion AI/ChatGPT の比較:根拠提示とソース管理の違い
articleFlutter で ToDo アプリを 90 分で作る:状態管理・永続化・ダークモード対応
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来