PHP项目Symfony form与导出模板

wen PHP项目 2

本文目录导读:

PHP项目Symfony form与导出模板

  1. Symfony Form 创建
  2. 导出服务层
  3. 模板系统实现
  4. 前端模板 (Twig)
  5. 高级导出功能

我来为你详细介绍 Symfony Form 和导出模板的实现方案。

Symfony Form 创建

创建表单类

<?php
// src/Form/ExportType.php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ExportType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('exportFormat', ChoiceType::class, [
                'label' => '导出格式',
                'choices' => [
                    'CSV' => 'csv',
                    'Excel (XLSX)' => 'xlsx',
                    'PDF' => 'pdf',
                    'JSON' => 'json',
                    'XML' => 'xml',
                ],
                'expanded' => true,  // 显示为单选按钮
                'multiple' => false,
            ])
            ->add('dateRange', ChoiceType::class, [
                'label' => '日期范围',
                'choices' => [
                    '全部' => 'all',
                    '#39; => 'today',
                    '本周' => 'this_week',
                    '本月' => 'this_month',
                    '自定义范围' => 'custom',
                ],
            ])
            ->add('startDate', DateType::class, [
                'label' => '开始日期',
                'widget' => 'single_text',
                'required' => false,
                'attr' => ['class' => 'datepicker'],
            ])
            ->add('endDate', DateType::class, [
                'label' => '结束日期',
                'widget' => 'single_text',
                'required' => false,
                'attr' => ['class' => 'datepicker'],
            ])
            ->add('includeHeaders', CheckboxType::class, [
                'label' => '包含表头',
                'required' => false,
                'data' => true,
            ])
            ->add('columns', ChoiceType::class, [
                'label' => '选择导出列',
                'choices' => $options['available_columns'],
                'multiple' => true,
                'expanded' => true,
                'attr' => ['class' => 'column-selector'],
            ])
            ->add('encoding', ChoiceType::class, [
                'label' => '编码方式',
                'choices' => [
                    'UTF-8' => 'UTF-8',
                    'GBK' => 'GBK',
                    'ISO-8859-1' => 'ISO-8859-1',
                ],
                'data' => 'UTF-8',
            ])
            ->add('template', FileType::class, [
                'label' => '上传导出模板',
                'mapped' => false,
                'required' => false,
                'attr' => [
                    'accept' => '.xlsx,.xls,.csv,.xml',
                ],
            ])
            ->add('export', SubmitType::class, [
                'label' => '导出数据',
                'attr' => ['class' => 'btn-primary'],
            ]);
    }
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'available_columns' => [],
        ]);
    }
}

控制器实现

<?php
// src/Controller/ExportController.php
namespace App\Controller;
use App\Form\ExportType;
use App\Service\ExportService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ExportController extends AbstractController
{
    #[Route('/export', name: 'app_export')]
    public function export(Request $request, ExportService $exportService): Response
    {
        // 获取可用的列选项
        $availableColumns = [
            'ID' => 'id',
            '用户名' => 'username',
            '邮箱' => 'email',
            '创建时间' => 'created_at',
            '状态' => 'status',
            // ... 更多列
        ];
        $form = $this->createForm(ExportType::class, null, [
            'available_columns' => $availableColumns,
        ]);
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $data = $form->getData();
            $templateFile = $form->get('template')->getData();
            try {
                // 获取要导出的数据
                $exportData = $this->getExportData($data);
                // 调用导出服务
                $response = $exportService->export(
                    $exportData,
                    $data['exportFormat'],
                    $data['includeHeaders'],
                    $data['columns'],
                    $templateFile
                );
                return $response;
            } catch (\Exception $e) {
                $this->addFlash('error', '导出失败: ' . $e->getMessage());
            }
        }
        return $this->render('export/index.html.twig', [
            'form' => $form->createView(),
            'exportHistory' => $this->getExportHistory(),
        ]);
    }
    private function getExportData(array $filters): array
    {
        // 根据筛选条件查询数据
        $queryBuilder = $this->getDoctrine()
            ->getRepository(User::class)
            ->createQueryBuilder('u');
        if ($filters['dateRange'] !== 'all') {
            // 处理日期范围
            $dateFilter = $this->buildDateFilter($filters);
            $queryBuilder->andWhere('u.createdAt BETWEEN :start AND :end')
                ->setParameter('start', $dateFilter['start'])
                ->setParameter('end', $dateFilter['end']);
        }
        return $queryBuilder->getQuery()->getResult();
    }
    private function buildDateFilter(array $filters): array
    {
        $now = new \DateTime();
        $start = clone $now;
        $end = clone $now;
        switch ($filters['dateRange']) {
            case 'today':
                $start->setTime(0, 0, 0);
                break;
            case 'this_week':
                $start->modify('monday this week')->setTime(0, 0, 0);
                break;
            case 'this_month':
                $start->modify('first day of this month')->setTime(0, 0, 0);
                break;
            case 'custom':
                $start = $filters['startDate'];
                $end = $filters['endDate'];
                break;
        }
        return ['start' => $start, 'end' => $end];
    }
    private function getExportHistory(): array
    {
        // 获取最近的导出历史
        return $this->getDoctrine()
            ->getRepository(ExportHistory::class)
            ->findBy(['user' => $this->getUser()], ['createdAt' => 'DESC'], 10);
    }
}

导出服务层

<?php
// src/Service/ExportService.php
namespace App\Service;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
class ExportService
{
    public function export(
        array $data,
        string $format,
        bool $includeHeaders,
        array $selectedColumns,
        $templateFile = null
    ): Response {
        switch ($format) {
            case 'csv':
                return $this->exportCsv($data, $includeHeaders, $selectedColumns);
            case 'xlsx':
                return $this->exportExcel($data, $includeHeaders, $selectedColumns, $templateFile);
            case 'pdf':
                return $this->exportPdf($data, $includeHeaders, $selectedColumns);
            case 'json':
                return $this->exportJson($data, $selectedColumns);
            case 'xml':
                return $this->exportXml($data, $selectedColumns);
            default:
                throw new \InvalidArgumentException('不支持的导出格式');
        }
    }
    private function processData(array $data, array $selectedColumns): array
    {
        $processed = [];
        foreach ($data as $item) {
            $row = [];
            foreach ($selectedColumns as $column) {
                $row[$column] = $this->getColumnValue($item, $column);
            }
            $processed[] = $row;
        }
        return $processed;
    }
    private function getColumnValue($item, string $column): mixed
    {
        // 处理对象或数组
        if (is_object($item)) {
            $getter = 'get' . ucfirst($column);
            return method_exists($item, $getter) ? $item->$getter() : null;
        }
        return $item[$column] ?? null;
    }
    private function exportCsv(array $data, bool $includeHeaders, array $selectedColumns): Response
    {
        $processedData = $this->processData($data, $selectedColumns);
        $handle = fopen('php://temp', 'r+');
        if ($includeHeaders) {
            fputcsv($handle, array_keys($selectedColumns));
        }
        foreach ($processedData as $row) {
            fputcsv($handle, $row);
        }
        rewind($handle);
        $content = stream_get_contents($handle);
        fclose($handle);
        $response = new Response($content);
        $response->headers->set('Content-Type', 'text/csv');
        $response->headers->set('Content-Disposition', 'attachment; filename="export.csv"');
        return $response;
    }
    private function exportExcel(
        array $data,
        bool $includeHeaders,
        array $selectedColumns,
        $templateFile = null
    ): Response {
        if ($templateFile) {
            // 使用模板文件
            $spreadsheet = IOFactory::load($templateFile->getPathname());
            $worksheet = $spreadsheet->getActiveSheet();
            // 找到数据开始的行
            $startRow = $this->findDataStartRow($worksheet);
        } else {
            $spreadsheet = new Spreadsheet();
            $worksheet = $spreadsheet->getActiveSheet();
            $startRow = 1;
            if ($includeHeaders) {
                $col = 'A';
                foreach ($selectedColumns as $header => $field) {
                    $worksheet->setCellValue($col . $startRow, $header);
                    $col++;
                }
                $startRow++;
            }
        }
        // 填充数据
        $processedData = $this->processData($data, $selectedColumns);
        foreach ($processedData as $rowIndex => $rowData) {
            $col = 'A';
            foreach ($rowData as $value) {
                $worksheet->setCellValue($col . ($startRow + $rowIndex), $value);
                $col++;
            }
        }
        // 自动调整列宽
        foreach (range('A', $worksheet->getHighestColumn()) as $col) {
            $worksheet->getColumnDimension($col)->setAutoSize(true);
        }
        // 生成临时文件
        $writer = new Xlsx($spreadsheet);
        $tempFile = tempnam(sys_get_temp_dir(), 'export');
        $writer->save($tempFile);
        $response = new BinaryFileResponse($tempFile);
        $response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
        $response->setContentDisposition(
            ResponseHeaderBag::DISPOSITION_ATTACHMENT,
            'export.xlsx'
        );
        return $response;
    }
    private function findDataStartRow($worksheet): int
    {
        // 查找模板中的数据起始行
        foreach ($worksheet->getRowIterator() as $row) {
            $cellValue = $worksheet->getCell('A' . $row->getRowIndex())->getValue();
            if (strpos($cellValue, '{data_start}') !== false) {
                return $row->getRowIndex() + 1;
            }
        }
        return 1;
    }
}

模板系统实现

<?php
// src/Service/ExportTemplateService.php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Twig\Environment;
class ExportTemplateService
{
    private $twig;
    private $entityManager;
    public function __construct(Environment $twig, EntityManagerInterface $entityManager)
    {
        $this->twig = $twig;
        $this->entityManager = $entityManager;
    }
    public function createTemplateFromExcel(UploadedFile $file, string $name): ExportTemplate
    {
        $template = new ExportTemplate();
        $template->setName($name);
        $template->setFileName($this->uploadTemplate($file));
        $template->setCreatedAt(new \DateTime());
        // 解析模板结构
        $this->parseTemplateStructure($template, $file);
        $this->entityManager->persist($template);
        $this->entityManager->flush();
        return $template;
    }
    public function renderTemplate(ExportTemplate $template, array $data): string
    {
        $templateContent = file_get_contents($template->getFilePath());
        return $this->twig->createTemplate($templateContent)->render([
            'data' => $data,
            'export_date' => new \DateTime(),
            'app_name' => $_ENV['APP_NAME'],
        ]);
    }
    private function uploadTemplate(UploadedFile $file): string
    {
        $filename = uniqid() . '.' . $file->getClientOriginalExtension();
        $file->move($this->getTemplateDirectory(), $filename);
        return $filename;
    }
    private function getTemplateDirectory(): string
    {
        $dir = __DIR__ . '/../../var/export_templates';
        if (!is_dir($dir)) {
            mkdir($dir, 0777, true);
        }
        return $dir;
    }
}

前端模板 (Twig)

{# templates/export/index.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}数据导出{% endblock %}
{% block stylesheets %}
    <style>
        .export-form {
            max-width: 800px;
            margin: 20px auto;
        }
        .export-options {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 5px;
            margin-bottom: 20px;
        }
        .column-selector {
            columns: 3;
            -webkit-columns: 3;
            -moz-columns: 3;
        }
        .export-history {
            margin-top: 30px;
        }
        .loading-overlay {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(255,255,255,0.8);
            z-index: 1000;
        }
        .loading-spinner {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
        }
    </style>
{% endblock %}
{% block body %}
    <div class="container export-form">
        <h1>数据导出</h1>
        <div class="alert alert-info">
            <i class="fas fa-info-circle"></i>
            选择导出格式和列,点击"导出数据"按钮开始导出
        </div>
        {{ form_start(form, {'attr': {'class': 'export-form', 'id': 'exportForm'}}) }}
        <div class="export-options">
            <h4>导出格式</h4>
            <div class="row">
                {% for child in form.exportFormat %}
                    <div class="col-md-3">
                        <div class="form-check">
                            {{ form_widget(child) }}
                            {{ form_label(child) }}
                        </div>
                    </div>
                {% endfor %}
            </div>
        </div>
        <div class="export-options">
            <h4>日期范围</h4>
            <div class="row">
                <div class="col-md-6">
                    {{ form_row(form.dateRange) }}
                </div>
            </div>
            <div class="row date-range-custom" style="display: none;">
                <div class="col-md-6">
                    {{ form_row(form.startDate) }}
                </div>
                <div class="col-md-6">
                    {{ form_row(form.endDate) }}
                </div>
            </div>
        </div>
        <div class="export-options">
            <h4>导出设置</h4>
            <div class="row">
                <div class="col-md-6">
                    {{ form_row(form.includeHeaders) }}
                </div>
                <div class="col-md-6">
                    {{ form_row(form.encoding) }}
                </div>
            </div>
        </div>
        <div class="export-options">
            <h4>选择导出列</h4>
            {{ form_widget(form.columns, {'attr': {'class': 'column-selector'}}) }}
        </div>
        <div class="export-options">
            <h4>使用模板(可选)</h4>
            {{ form_row(form.template) }}
            <small class="form-text text-muted">
                支持的格式: XLSX, XLS, CSV, XML
            </small>
        </div>
        <div class="text-center">
            {{ form_widget(form.export, {'attr': {'class': 'btn btn-lg btn-primary'}}) }}
        </div>
        {{ form_end(form) }}
        {% if exportHistory is not empty %}
            <div class="export-history">
                <h3>最近的导出记录</h3>
                <table class="table">
                    <thead>
                        <tr>
                            <th>时间</th>
                            <th>格式</th>
                            <th>记录数</th>
                            <th>状态</th>
                            <th>操作</th>
                        </tr>
                    </thead>
                    <tbody>
                        {% for history in exportHistory %}
                            <tr>
                                <td>{{ history.createdAt|date('Y-m-d H:i:s') }}</td>
                                <td>{{ history.format }}</td>
                                <td>{{ history.recordCount }}</td>
                                <td>
                                    <span class="badge badge-{{ history.status == 'success' ? 'success' : 'danger' }}">
                                        {{ history.status == 'success' ? '成功' : '失败' }}
                                    </span>
                                </td>
                                <td>
                                    <a href="{{ path('app_export_download', {'id': history.id}) }}"
                                       class="btn btn-sm btn-secondary">
                                        下载
                                    </a>
                                </td>
                            </tr>
                        {% endfor %}
                    </tbody>
                </table>
            </div>
        {% endif %}
        <div id="loadingOverlay" class="loading-overlay">
            <div class="loading-spinner text-center">
                <div class="spinner-border text-primary" role="status">
                    <span class="sr-only">正在导出...</span>
                </div>
                <p class="mt-2">正在生成导出文件,请稍候...</p>
            </div>
        </div>
    </div>
{% endblock %}
{% block javascripts %}
    <script>
        $(document).ready(function() {
            // 日期范围选择切换
            $('input[name="export_form[dateRange]"]').change(function() {
                if ($(this).val() === 'custom') {
                    $('.date-range-custom').show();
                } else {
                    $('.date-range-custom').hide();
                }
            });
            // 全选/取消全选
            $('#selectAllColumns').change(function() {
                $('.column-selector input[type="checkbox"]').prop('checked', $(this).is(':checked'));
            });
            // 导出按钮点击
            $('#exportForm').submit(function(e) {
                e.preventDefault();
                // 验证至少选择了一列
                if ($('.column-selector input[type="checkbox"]:checked').length === 0) {
                    alert('请至少选择一列进行导出');
                    return;
                }
                // 显示加载动画
                $('#loadingOverlay').show();
                // 提交表单
                this.submit();
            });
            // 导出格式预览
            $('input[name="export_form[exportFormat]"]').change(function() {
                var format = $(this).val();
                var previewText = '';
                switch(format) {
                    case 'csv':
                        previewText = '导出为CSV格式,可用Excel打开';
                        break;
                    case 'xlsx':
                        previewText = '导出为Excel 2007+格式';
                        break;
                    case 'pdf':
                        previewText = '导出为PDF文档格式';
                        break;
                    case 'json':
                        previewText = '导出为JSON数据格式';
                        break;
                }
                $('#formatPreview').text(previewText);
            });
        });
        // 显示导出进度
        function showExportProgress() {
            var progress = 0;
            var interval = setInterval(function() {
                progress += Math.random() * 10;
                if (progress > 90) {
                    clearInterval(interval);
                }
                $('#exportProgress').css('width', progress + '%');
            }, 1000);
        }
    </script>
{% endblock %}

高级导出功能

支持大数据导出

<?php
// src/Service/LargeDataExportService.php
namespace App\Service;
use Symfony\Component\HttpFoundation\StreamedResponse;
class LargeDataExportService
{
    public function streamCsvExport(callable $dataProvider, array $headers, string $filename): StreamedResponse
    {
        $response = new StreamedResponse(function() use ($dataProvider, $headers) {
            $handle = fopen('php://output', 'w');
            // 写入表头
            fputcsv($handle, $headers);
            // 逐块处理数据
            $page = 1;
            while ($data = $dataProvider($page)) {
                foreach ($data as $row) {
                    fputcsv($handle, $row);
                }
                $page++;
                flush();
            }
            fclose($handle);
        });
        $response->headers->set('Content-Type', 'text/csv');
        $response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"');
        return $response;
    }
}

这个完整的方案包含了:

  1. 完整的表单系统 - 支持各种导出选项
  2. 多种格式导出 - CSV、Excel、PDF、JSON、XML
  3. 模板系统 - 支持自定义导出模板
  4. 前端优化 - 友好的用户界面和交互
  5. 高级功能 - 大数据导出、进度显示等

抱歉,评论功能暂时关闭!