本文目录导读:

我来详细介绍 Symfony 中处理表单和统计报表的实现方法。
Symfony Form 基础
创建表单类
<?php
// src/Form/ReportFilterType.php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ReportFilterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('startDate', DateType::class, [
'widget' => 'single_text',
'label' => '开始日期',
'required' => true
])
->add('endDate', DateType::class, [
'widget' => 'single_text',
'label' => '结束日期',
'required' => true
])
->add('category', ChoiceType::class, [
'label' => '分类',
'choices' => [
'全部' => '',
'产品A' => 'product_a',
'产品B' => 'product_b',
'产品C' => 'product_c'
],
'required' => false
])
->add('submit', SubmitType::class, [
'label' => '生成报表'
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => null,
'csrf_protection' => true
]);
}
}
Controller 中使用表单
<?php
// src/Controller/ReportController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Form\ReportFilterType;
use App\Service\ReportService;
class ReportController extends AbstractController
{
#[Route('/reports', name: 'reports_index')]
public function index(Request $request, ReportService $reportService): Response
{
$form = $this->createForm(ReportFilterType::class);
$form->handleRequest($request);
$reportData = [];
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$reportData = $reportService->generateReport(
$data['startDate'],
$data['endDate'],
$data['category']
);
}
return $this->render('report/index.html.twig', [
'form' => $form->createView(),
'reportData' => $reportData
]);
}
}
统计报表实现
报表服务层
<?php
// src/Service/ReportService.php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use App\Repository\OrderRepository;
class ReportService
{
private EntityManagerInterface $entityManager;
private OrderRepository $orderRepository;
public function __construct(
EntityManagerInterface $entityManager,
OrderRepository $orderRepository
) {
$this->entityManager = $entityManager;
$this->orderRepository = $orderRepository;
}
/**
* 生成统计报表
*/
public function generateReport(\DateTime $startDate, \DateTime $endDate, ?string $category = null): array
{
// 基础统计
$basicStats = $this->getBasicStats($startDate, $endDate);
// 按分类统计
$categoryStats = $this->getCategoryStats($startDate, $endDate, $category);
// 按日期统计(用于图表)
$dailyStats = $this->getDailyStats($startDate, $endDate);
return [
'basic' => $basicStats,
'category' => $categoryStats,
'daily' => $dailyStats,
'period' => [
'start' => $startDate,
'end' => $endDate
]
];
}
private function getBasicStats(\DateTime $start, \DateTime $end): array
{
$conn = $this->entityManager->getConnection();
$sql = "
SELECT
COUNT(*) as total_orders,
SUM(total_amount) as total_revenue,
AVG(total_amount) as avg_order_value,
COUNT(DISTINCT user_id) as unique_customers
FROM orders
WHERE created_at BETWEEN :start AND :end
AND status NOT IN ('cancelled', 'refunded')
";
$stmt = $conn->executeQuery($sql, [
'start' => $start->format('Y-m-d H:i:s'),
'end' => $end->format('Y-m-d H:i:s')
]);
return $stmt->fetchAssociative();
}
private function getCategoryStats(\DateTime $start, \DateTime $end, ?string $category = null): array
{
$conn = $this->entityManager->getConnection();
$sql = "
SELECT
p.category,
COUNT(*) as order_count,
SUM(oi.quantity) as total_quantity,
SUM(oi.subtotal) as total_revenue
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.created_at BETWEEN :start AND :end
AND o.status NOT IN ('cancelled', 'refunded')
";
$params = [
'start' => $start->format('Y-m-d H:i:s'),
'end' => $end->format('Y-m-d H:i:s')
];
if ($category) {
$sql .= " AND p.category = :category";
$params['category'] = $category;
}
$sql .= " GROUP BY p.category ORDER BY total_revenue DESC";
$stmt = $conn->executeQuery($sql, $params);
return $stmt->fetchAllAssociative();
}
private function getDailyStats(\DateTime $start, \DateTime $end): array
{
$conn = $this->entityManager->getConnection();
$sql = "
SELECT
DATE(created_at) as date,
COUNT(*) as order_count,
SUM(total_amount) as revenue
FROM orders
WHERE created_at BETWEEN :start AND :end
AND status NOT IN ('cancelled', 'refunded')
GROUP BY DATE(created_at)
ORDER BY date ASC
";
$stmt = $conn->executeQuery($sql, [
'start' => $start->format('Y-m-d H:i:s'),
'end' => $end->format('Y-m-d H:i:s')
]);
return $stmt->fetchAllAssociative();
}
}
Twig 模板展示
{# templates/report/index.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}统计报表{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/chart.js@3/dist/chart.min.css">
<style>
.stat-card {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #333;
}
.stat-label {
color: #666;
font-size: 14px;
}
</style>
{% endblock %}
{% block body %}
<div class="container-fluid">
<h1 class="mb-4">统计报表</h1>
<!-- 筛选表单 -->
<div class="card mb-4">
<div class="card-body">
{{ form_start(form)}}
<div class="row">
<div class="col-md-3">{{ form_row(form.startDate) }}</div>
<div class="col-md-3">{{ form_row(form.endDate) }}</div>
<div class="col-md-3">{{ form_row(form.category) }}</div>
<div class="col-md-3 d-flex align-items-end">
{{ form_row(form.submit) }}
</div>
</div>
{{ form_end(form) }}
</div>
</div>
{% if reportData is not empty %}
<!-- 基础统计卡片 -->
<div class="row mb-4">
<div class="col-md-3">
<div class="stat-card">
<div class="stat-label">总订单数</div>
<div class="stat-value">{{ reportData.basic.total_orders|default(0) }}</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-label">总收入</div>
<div class="stat-value">¥{{ reportData.basic.total_revenue|default(0)|number_format(2) }}</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-label">平均订单金额</div>
<div class="stat-value">¥{{ reportData.basic.avg_order_value|default(0)|number_format(2) }}</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-label">独立客户数</div>
<div class="stat-value">{{ reportData.basic.unique_customers|default(0) }}</div>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5>每日收入趋势</h5>
</div>
<div class="card-body">
<canvas id="revenueChart" height="300"></canvas>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5>分类占比</h5>
</div>
<div class="card-body">
<canvas id="categoryChart" height="300"></canvas>
</div>
</div>
</div>
</div>
<!-- 分类统计表格 -->
<div class="card mt-4">
<div class="card-header">
<h5>分类统计详情</h5>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>分类</th>
<th>订单数</th>
<th>销售数量</th>
<th>销售额</th>
</tr>
</thead>
<tbody>
{% for stat in reportData.category %}
<tr>
<td>{{ stat.category }}</td>
<td>{{ stat.order_count }}</td>
<td>{{ stat.total_quantity }}</td>
<td>¥{{ stat.total_revenue|number_format(2) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src="https://cdn.jsdelivr.net/npm/chart.js@3/dist/chart.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
{% if reportData.daily is not empty %}
// 收入趋势图
const dailyData = {{ reportData.daily|json_encode|raw }};
const ctx1 = document.getElementById('revenueChart').getContext('2d');
new Chart(ctx1, {
type: 'line',
data: {
labels: dailyData.map(item => item.date),
datasets: [{
label: '收入',
data: dailyData.map(item => item.revenue),
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false
}
});
{% endif %}
{% if reportData.category is not empty %}
// 分类占比图
const categoryData = {{ reportData.category|json_encode|raw }};
const ctx2 = document.getElementById('categoryChart').getContext('2d');
new Chart(ctx2, {
type: 'pie',
data: {
labels: categoryData.map(item => item.category),
datasets: [{
data: categoryData.map(item => item.total_revenue),
backgroundColor: [
'rgb(255, 99, 132)',
'rgb(54, 162, 235)',
'rgb(255, 205, 86)',
'rgb(75, 192, 192)'
]
}]
}
});
{% endif %}
});
</script>
{% endblock %}
高级报表功能
导出 PDF 报表
<?php
// src/Service/PdfReportService.php
namespace App\Service;
use Dompdf\Dompdf;
use Twig\Environment;
class PdfReportService
{
private Environment $twig;
public function __construct(Environment $twig)
{
$this->twig = $twig;
}
public function generatePdf(array $data): string
{
$html = $this->twig->render('report/pdf_template.html.twig', [
'reportData' => $data,
'generatedAt' => new \DateTime()
]);
$dompdf = new Dompdf();
$dompdf->loadHtml($html);
$dompdf->setPaper('A4', 'landscape');
$dompdf->render();
return $dompdf->output();
}
}
视图构建器模式
<?php
// src/Report/ReportBuilder.php
namespace App\Report;
class ReportBuilder
{
private array $data = [];
private array $charts = [];
private array $tables = [];
public function addMetric(string $label, $value, string $format = null): self
{
$this->data['metrics'][] = [
'label' => $label,
'value' => $value,
'format' => $format
];
return $this;
}
public function addChart(string $type, array $data, array $options = []): self
{
$this->charts[] = [
'type' => $type,
'data' => $data,
'options' => $options
];
return $this;
}
public function addTable(string $title, array $headers, array $rows): self
{
$this->tables[] = [
'title' => $title,
'headers' => $headers,
'rows' => $rows
];
return $this;
}
public function build(): array
{
return [
'data' => $this->data,
'charts' => $this->charts,
'tables' => $this->tables
];
}
}
这些代码提供了 Symfony 项目中实现表单过滤和统计报表的完整解决方案,包括:
- 表单创建和验证
- 数据查询和聚合
- 图表可视化
- PDF 导出功能
- 可扩展的报表构建器模式
你可以根据具体需求调整查询逻辑和展示方式。