PHP项目Symfony form与抽屉组件

wen PHP项目 1

本文目录导读:

PHP项目Symfony form与抽屉组件

  1. 基础架构设计
  2. 抽屉模板
  3. 表单与抽屉结合示例
  4. 集成示例
  5. JavaScript交互
  6. CSS样式
  7. 高级功能

我来详细讲解Symfony表单与抽屉组件(Drawer/Modal)的结合使用。

基础架构设计

抽屉组件配置

# config/packages/twig.yaml
twig:
    form_themes: ['bootstrap_5_layout.html.twig']
    globals:
        drawer_component: 'App\Components\DrawerComponent'

抽屉组件类

// src/Components/DrawerComponent.php
namespace App\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent('drawer')]
class DrawerComponent
{
    public string $id;
    public string $title;
    public string $size = 'md'; // sm, md, lg, xl
    public string $position = 'right'; // left, right
    public ?string $formAction = null;
    public ?string $submitLabel = '保存';
    // 表单相关
    public ?array $formFields = null;
    public ?string $formTemplate = null;
}

抽屉模板

{# templates/components/drawer.html.twig #}
<div class="drawer drawer-{{ position }}" id="{{ id }}">
    <div class="drawer-overlay" data-action="click->drawer#close"></div>
    <div class="drawer-content drawer-{{ size }}">
        <div class="drawer-header">
            <h5 class="drawer-title">{{ title }}</h5>
            <button type="button" class="btn-close" data-action="drawer#close"></button>
        </div>
        <div class="drawer-body">
            {% if formFields %}
                {{ form_start(form, {'action': formAction, 'attr': {'novalidate': 'novalidate'}}) }}
                {% for field in formFields %}
                    <div class="mb-3">
                        {{ form_label(form[field]) }}
                        {{ form_widget(form[field]) }}
                        {{ form_errors(form[field]) }}
                    </div>
                {% endfor %}
                <div class="drawer-footer">
                    <button type="button" class="btn btn-secondary" data-action="drawer#close">
                        取消
                    </button>
                    <button type="submit" class="btn btn-primary">
                        {{ submitLabel }}
                    </button>
                </div>
                {{ form_end(form) }}
            {% else %}
                {{ block('drawer_content')|raw }}
            {% endif %}
        </div>
    </div>
</div>

表单与抽屉结合示例

创建表单类型

// src/Form/ProductType.php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Validator\Constraints as Assert;
class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class, [
                'label' => '产品名称',
                'constraints' => [
                    new Assert\NotBlank(['message' => '产品名称不能为空']),
                    new Assert\Length(['max' => 100])
                ],
                'attr' => [
                    'class' => 'form-control',
                    'placeholder' => '请输入产品名称'
                ]
            ])
            ->add('price', NumberType::class, [
                'label' => '价格',
                'constraints' => [
                    new Assert\NotBlank(),
                    new Assert\Positive()
                ],
                'attr' => [
                    'class' => 'form-control',
                    'step' => '0.01'
                ]
            ])
            ->add('description', TextareaType::class, [
                'label' => '描述',
                'required' => false,
                'attr' => [
                    'class' => 'form-control',
                    'rows' => 3
                ]
            ]);
    }
}

控制器处理

// src/Controller/ProductController.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;
class ProductController extends AbstractController
{
    #[Route('/products', name: 'product_list')]
    public function index(): Response
    {
        return $this->render('product/index.html.twig', [
            'products' => $this->getProducts()
        ]);
    }
    #[Route('/products/create', name: 'product_create')]
    public function create(Request $request): Response
    {
        $form = $this->createForm(ProductType::class);
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $data = $form->getData();
            // 处理数据保存
            if ($request->isXmlHttpRequest()) {
                return $this->json([
                    'success' => true,
                    'message' => '产品创建成功',
                    'data' => $data
                ]);
            }
            $this->addFlash('success', '产品创建成功');
            return $this->redirectToRoute('product_list');
        }
        if ($request->isXmlHttpRequest()) {
            return $this->json([
                'success' => false,
                'html' => $this->renderView('product/_form.html.twig', [
                    'form' => $form->createView()
                ])
            ]);
        }
        return $this->render('product/create.html.twig', [
            'form' => $form->createView()
        ]);
    }
}

集成示例

页面模板

{# templates/product/index.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<div class="container mt-4">
    <div class="d-flex justify-content-between align-items-center mb-3">
        <h1>产品列表</h1>
        <button class="btn btn-primary" 
                data-action="click->drawer#open" 
                data-drawer-id="createProductDrawer">
            <i class="bi bi-plus"></i> 添加产品
        </button>
    </div>
    <!-- 产品列表 -->
    <div id="productList">
        {% for product in products %}
            <div class="card mb-2">
                <div class="card-body">
                    {{ product.name }} - ¥{{ product.price }}
                </div>
            </div>
        {% endfor %}
    </div>
    <!-- 创建产品抽屉 -->
    <twig:Drawer 
        id="createProductDrawer" 
        title="添加产品" 
        size="lg"
        formAction="{{ path('product_create') }}"
        submitLabel="创建产品">
        {% block drawer_content %}
            {{ form_start(form, {'action': path('product_create'), 'attr': {'id': 'productForm', 'novalidate': 'novalidate'}}) }}
            <div class="mb-3">
                {{ form_label(form.name) }}
                {{ form_widget(form.name) }}
                {{ form_errors(form.name) }}
            </div>
            <div class="mb-3">
                {{ form_label(form.price) }}
                {{ form_widget(form.price) }}
                {{ form_errors(form.price) }}
            </div>
            <div class="mb-3">
                {{ form_label(form.description) }}
                {{ form_widget(form.description) }}
                {{ form_errors(form.description) }}
            </div>
            {{ form_end(form) }}
        {% endblock %}
    </twig:Drawer>
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
    const productForm = document.getElementById('productForm');
    if (productForm) {
        productForm.addEventListener('submit', function(e) {
            e.preventDefault();
            const formData = new FormData(this);
            fetch(this.action, {
                method: 'POST',
                body: formData,
                headers: {
                    'X-Requested-With': 'XMLHttpRequest'
                }
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    // 关闭抽屉
                    const drawer = document.getElementById('createProductDrawer');
                    if (drawer) {
                        drawer.dispatchEvent(new Event('close'));
                    }
                    // 刷新列表
                    location.reload();
                } else {
                    // 更新表单显示错误
                    const formContainer = document.querySelector('#createProductDrawer .drawer-body');
                    if (formContainer && data.html) {
                        formContainer.innerHTML = data.html;
                    }
                }
            })
            .catch(error => {
                console.error('Error:', error);
            });
        });
    }
});
</script>
{% endblock %}

JavaScript交互

// assets/js/drawer.js
class DrawerManager {
    constructor() {
        this.drawers = new Map();
        this.init();
    }
    init() {
        document.querySelectorAll('[data-drawer-id]').forEach(trigger => {
            const drawerId = trigger.dataset.drawerId;
            const drawer = document.getElementById(drawerId);
            if (drawer) {
                this.drawers.set(drawerId, drawer);
                trigger.addEventListener('click', () => this.open(drawerId));
            }
        });
        // 监听自定义事件
        document.addEventListener('drawer:open', (e) => {
            this.open(e.detail.id);
        });
        document.addEventListener('drawer:close', (e) => {
            this.close(e.detail.id);
        });
    }
    open(id) {
        const drawer = this.drawers.get(id);
        if (drawer) {
            drawer.classList.add('show');
            document.body.classList.add('drawer-open');
            // 触发AJAX加载表单
            this.loadForm(drawer);
        }
    }
    close(id) {
        const drawer = this.drawers.get(id);
        if (drawer) {
            drawer.classList.remove('show');
            document.body.classList.remove('drawer-open');
        }
    }
    loadForm(drawer) {
        const formAction = drawer.dataset.formAction;
        if (formAction && !drawer.dataset.loaded) {
            drawer.dataset.loaded = 'true';
            fetch(formAction, {
                headers: {
                    'X-Requested-With': 'XMLHttpRequest'
                }
            })
            .then(response => response.json())
            .then(data => {
                if (data.html) {
                    const body = drawer.querySelector('.drawer-body');
                    if (body) {
                        body.innerHTML = data.html;
                        this.attachFormHandler(body.querySelector('form'));
                    }
                }
            });
        }
    }
    attachFormHandler(form) {
        if (form) {
            form.addEventListener('submit', (e) => {
                e.preventDefault();
                this.submitForm(form);
            });
        }
    }
    submitForm(form) {
        const formData = new FormData(form);
        formData.append('_drawer_submit', '1');
        fetch(form.action, {
            method: form.method || 'POST',
            body: formData,
            headers: {
                'X-Requested-With': 'XMLHttpRequest'
            }
        })
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                this.close(form.closest('[data-drawer-id]')?.id || 
                          form.closest('.drawer')?.id);
                // 触发成功事件
                document.dispatchEvent(new CustomEvent('drawer:form-success', {
                    detail: data
                }));
            } else {
                // 显示错误
                const body = form.closest('.drawer-body');
                if (body && data.html) {
                    body.innerHTML = data.html;
                    this.attachFormHandler(body.querySelector('form'));
                }
            }
        });
    }
}
// 初始化
const drawerManager = new DrawerManager();
export default drawerManager;

CSS样式

// assets/styles/drawer.scss
.drawer {
    position: fixed;
    top: 0;
    z-index: 1050;
    height: 100vh;
    display: none;
    &.show {
        display: block;
    }
    &-overlay {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background-color: rgba(0, 0, 0, 0.5);
        z-index: 1040;
    }
    &-content {
        position: fixed;
        top: 0;
        background: white;
        z-index: 1050;
        overflow-y: auto;
        transition: transform 0.3s ease-in-out;
        &.drawer-sm { width: 300px; }
        &.drawer-md { width: 400px; }
        &.drawer-lg { width: 600px; }
        &.drawer-xl { width: 800px; }
    }
    &-right &-content {
        right: 0;
        transform: translateX(100%);
        .show & {
            transform: translateX(0);
        }
    }
    &-left &-content {
        left: 0;
        transform: translateX(-100%);
        .show & {
            transform: translateX(0);
        }
    }
    &-header {
        padding: 1rem;
        border-bottom: 1px solid #dee2e6;
        display: flex;
        justify-content: space-between;
        align-items: center;
    }
    &-body {
        padding: 1rem;
        flex: 1;
    }
    &-footer {
        padding: 1rem;
        border-top: 1px solid #dee2e6;
        display: flex;
        justify-content: flex-end;
        gap: 0.5rem;
        position: sticky;
        bottom: 0;
        background: white;
    }
}
// 防止页面滚动
body.drawer-open {
    overflow: hidden;
}

高级功能

表单验证错误处理

// src/Form/Validation/FormValidationTrait.php
namespace App\Form\Validation;
trait FormValidationTrait
{
    private function getFormErrors($form): array
    {
        $errors = [];
        foreach ($form->getErrors(true, true) as $error) {
            $fieldName = $error->getOrigin()->getName();
            $errors[$fieldName][] = $error->getMessage();
        }
        return $errors;
    }
    private function handleAjaxFormSubmission(Request $request, FormInterface $form): JsonResponse
    {
        $form->handleRequest($request);
        if ($form->isSubmitted()) {
            if ($form->isValid()) {
                // 处理表单
                return $this->json([
                    'success' => true,
                    'message' => '操作成功'
                ]);
            } else {
                return $this->json([
                    'success' => false,
                    'errors' => $this->getFormErrors($form),
                    'html' => $this->renderView('product/_form.html.twig', [
                        'form' => $form->createView()
                    ])
                ]);
            }
        }
        // 首次加载表单
        return $this->json([
            'success' => true,
            'html' => $this->renderView('product/_form.html.twig', [
                'form' => $form->createView()
            ])
        ]);
    }
}

这种实现提供了完整的Symfony表单与抽屉组件的集成方案,包括表单验证、AJAX提交、错误处理等功能,根据具体需求可以进一步定制和优化。

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