T-CREATOR

PHP で社内業務自動化:CSV→DB 取込・定期バッチ・Slack 通知の実例

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

この図からわかるように、手作業では確認と修正のループが発生しやすく、処理時間が予測できません。また、各工程で人が介在するため、ミスの混入ポイントが多数存在します。

自動化で解決すべき要件

これらの課題を解決するために、以下の要件を満たす自動化システムが必要です。

#要件期待効果
1CSV ファイルの自動読み込みとバリデーションデータ形式エラーの事前検出
2データベースへの自動登録(重複チェック付き)登録作業の効率化とミス防止
3定期的な自動実行(毎朝 9 時など)担当者の作業負荷軽減
4処理結果の Slack 自動通知リアルタイムな状況共有
5エラー時の詳細ログ出力トラブルシューティングの効率化

解決策

システム構成の概要

PHP を中心とした自動化システムを構築することで、前述の課題を解決できます。システムの主要コンポーネントは以下の通りです。

#コンポーネント役割
1PHP スクリプトCSV 読み込み、DB 登録、Slack 通知の制御
2MySQL データベースデータの永続化と管理
3cron定期実行のスケジューリング
4Slack 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 の設定内容を表で整理すると以下のようになります。

#フィールド意味
100 分(毎時 0 分)
299 時
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)

この図から、各コンポーネントの責任範囲が明確に分離されていることがわかりますね。エラーが発生した場合でも、どの段階で問題が起きたのかを特定しやすい構造になっています。

エラーハンドリングとログ出力

実際の運用では、さまざまなエラーが発生する可能性があります。以下は、主要なエラーとその対処方法をまとめた表です。

#エラー種別エラーコード例対処方法
1CSV ファイルが見つからないfile_not_foundファイルパスとパーミッションを確認
2データベース接続エラーPDOException: SQLSTATE[HY000] [2002]DB サーバーの稼働状況と接続情報を確認
3CSV フォーマット不正validation_errorCSV ファイルの形式を確認(列数、データ型)
4Slack 通知失敗HTTP 404​/​500Webhook 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 通知の二重化トラブルシューティングの効率化
5cron による定期実行人手を介さない完全自動化

導入効果

このシステムを導入することで、以下の効果が期待できます。

作業時間の削減: 毎日 30 分かかっていた作業がゼロになります。月間で約 10 時間、年間では 120 時間もの時間を創出できるのです。

ミスの防止: 手入力によるヒューマンエラーを完全に排除できます。データの整合性が保たれ、品質が向上しますね。

可視化の向上: Slack 通知により、チーム全体で処理状況をリアルタイムに把握できます。担当者が不在でも状況を確認できるため、業務の属人化を防げるでしょう。

応用と展開

この仕組みは、CSV インポート以外にもさまざまな業務に応用できます。

  • レポート自動生成: データベースから集計して PDF や Excel を生成
  • API 連携: 外部サービスからデータを取得して加工・保存
  • 定期メンテナンス: 古いログの削除やバックアップの自動実行
  • 監視とアラート: サーバーやアプリケーションの状態を定期チェック

PHP は Web 開発だけでなく、バッチ処理や自動化にも強力なツールです。今回の実装を参考に、皆さんの業務でも自動化を進めてみてはいかがでしょうか。最初は小さな処理から始めて、徐々に拡張していくことをおすすめします。

自動化により生まれた時間を、より創造的な業務に充てられるようになることを願っています。

関連リンク