本文目录导读:

我来为你详细介绍 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;
}
}
这个完整的方案包含了:
- 完整的表单系统 - 支持各种导出选项
- 多种格式导出 - CSV、Excel、PDF、JSON、XML
- 模板系统 - 支持自定义导出模板
- 前端优化 - 友好的用户界面和交互
- 高级功能 - 大数据导出、进度显示等