PHP项目Symfony form与乐观锁

wen PHP项目 1

本文目录导读:

PHP项目Symfony form与乐观锁

  1. 乐观锁基础配置
  2. 创建Form类型
  3. 控制器处理乐观锁
  4. 使用自定义乐观锁管理器
  5. 前端实现乐观锁显示
  6. 处理乐观锁冲突的控制器扩展
  7. 测试乐观锁功能
  8. 最佳实践建议
  9. 完整工作流程总结

我来详细讲解一下在Symfony项目中使用乐观锁与表单(Form)的结合方案。

乐观锁基础配置

Doctrine ORM实体配置

// src/Entity/Product.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private ?int $id = null;
    #[ORM\Column(type: 'string', length: 255)]
    #[Assert\NotBlank]
    private ?string $name = null;
    #[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
    #[Assert\NotBlank]
    #[Assert\Positive]
    private ?string $price = null;
    #[ORM\Column(type: 'integer', version: true)]
    private ?int $version = 1;
    // Getters and Setters...
    public function getId(): ?int
    {
        return $this->id;
    }
    public function getName(): ?string
    {
        return $this->name;
    }
    public function setName(string $name): self
    {
        $this->name = $name;
        return $this;
    }
    public function getPrice(): ?string
    {
        return $this->price;
    }
    public function setPrice(string $price): self
    {
        $this->price = $price;
        return $this;
    }
    public function getVersion(): ?int
    {
        return $this->version;
    }
    public function setVersion(int $version): self
    {
        $this->version = $version;
        return $this;
    }
}

创建Form类型

// src/Form/ProductType.php
namespace App\Form;
use App\Entity\Product;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class, [
                'label' => '产品名称',
            ])
            ->add('price', NumberType::class, [
                'label' => '价格',
                'scale' => 2,
            ])
            ->add('version', HiddenType::class, [
                'mapped' => true,
                'required' => false,
            ]);
    }
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Product::class,
            'csrf_protection' => true,
            'version_field' => 'version',
        ]);
    }
}

控制器处理乐观锁

// src/Controller/ProductController.php
namespace App\Controller;
use App\Entity\Product;
use App\Form\ProductType;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\OptimisticLockException;
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
{
    private EntityManagerInterface $entityManager;
    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }
    #[Route('/product/edit/{id}', name: 'product_edit')]
    public function edit(Request $request, Product $product): Response
    {
        $originalVersion = $product->getVersion();
        $form = $this->createForm(ProductType::class, $product);
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            try {
                // 开始事务
                $this->entityManager->beginTransaction();
                // 检查版本冲突
                if ($this->checkVersionConflict($product, $originalVersion)) {
                    throw new OptimisticLockException(
                        '该产品已被其他用户修改,请刷新后重试',
                        $product
                    );
                }
                // 增加版本号
                $product->setVersion($product->getVersion() + 1);
                $this->entityManager->flush();
                $this->entityManager->commit();
                $this->addFlash('success', '产品更新成功');
                return $this->redirectToRoute('product_list');
            } catch (OptimisticLockException $e) {
                $this->entityManager->rollback();
                $this->addFlash('error', $e->getMessage());
                // 重新加载最新数据
                return $this->redirectToRoute('product_edit', ['id' => $product->getId()]);
            } catch (\Exception $e) {
                $this->entityManager->rollback();
                $this->addFlash('error', '更新失败:' . $e->getMessage());
            }
        }
        return $this->render('product/edit.html.twig', [
            'form' => $form->createView(),
            'product' => $product,
        ]);
    }
    private function checkVersionConflict(Product $product, int $originalVersion): bool
    {
        // 重新从数据库获取最新版本
        $freshProduct = $this->entityManager
            ->getRepository(Product::class)
            ->find($product->getId());
        return $freshProduct->getVersion() !== $originalVersion;
    }
}

使用自定义乐观锁管理器

// src/OptimisticLock/OptimisticLockManager.php
namespace App\OptimisticLock;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\OptimisticLockException;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class OptimisticLockManager
{
    private EntityManagerInterface $entityManager;
    private SessionInterface $session;
    public function __construct(
        EntityManagerInterface $entityManager,
        SessionInterface $session
    ) {
        $this->entityManager = $entityManager;
        $this->session = $session;
    }
    public function lock(string $entityClass, $entityId): void
    {
        $lockKey = $this->getLockKey($entityClass, $entityId);
        $this->session->set($lockKey, time());
    }
    public function unlock(string $entityClass, $entityId): void
    {
        $lockKey = $this->getLockKey($entityClass, $entityId);
        $this->session->remove($lockKey);
    }
    public function isLocked(string $entityClass, $entityId): bool
    {
        $lockKey = $this->getLockKey($entityClass, $entityId);
        $lockTime = $this->session->get($lockKey);
        if ($lockTime === null) {
            return false;
        }
        // 5分钟超时
        if (time() - $lockTime > 300) {
            $this->unlock($entityClass, $entityId);
            return false;
        }
        return true;
    }
    private function getLockKey(string $entityClass, $entityId): string
    {
        return sprintf('optimistic_lock_%s_%s', str_replace('\\', '_', $entityClass), $entityId);
    }
}

前端实现乐观锁显示

{# templates/product/edit.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
    <div class="container">
        <h1>编辑产品</h1>
        {% if app.session.get('optimistic_lock_' ~ product.id) is defined %}
            <div class="alert alert-warning">
                <i class="fas fa-lock"></i> 
                该产品当前被锁定,其他用户正在编辑
            </div>
        {% endif %}
        {{ form_start(form, {'attr': {'class': 'product-form', 'novalidate': 'novalidate'}}) }}
            {{ form_row(form.name) }}
            {{ form_row(form.price) }}
            {# 隐藏的版本字段 #}
            <div class="d-none">
                {{ form_row(form.version) }}
            </div>
            <button type="submit" class="btn btn-primary">
                <i class="fas fa-save"></i> 保存
            </button>
            <a href="{{ path('product_list') }}" class="btn btn-secondary">
                取消
            </a>
        {{ form_end(form) }}
    </div>
{% endblock %}
{% block javascripts %}
    {{ parent() }}
    <script>
        // 添加版本冲突检测
        document.querySelector('.product-form').addEventListener('submit', function(e) {
            const versionField = document.querySelector('#product_version');
            // 如果有多个用户编辑冲突,前端也可以做检查
            if (versionField) {
                const currentVersion = '{{ product.version }}';
                const formVersion = versionField.value;
                if (currentVersion !== formVersion) {
                    if (!confirm('数据已被修改,是否继续保存?')) {
                        e.preventDefault();
                        return false;
                    }
                }
            }
        });
        // 自动更新版本号
        setInterval(function() {
            const versionField = document.querySelector('#product_version');
            if (versionField) {
                // 这里可以添加Ajax请求来获取最新版本号
                fetch('/product/' + {{ product.id }} + '/version')
                    .then(response => response.json())
                    .then(data => {
                        if (data.version !== versionField.value) {
                            versionField.value = data.version;
                            // 显示版本变更提示
                        }
                    });
            }
        }, 30000); // 30秒检查一次
    </script>
{% endblock %}

处理乐观锁冲突的控制器扩展

// src/Controller/ProductController.php (添加新方法)
#[Route('/product/{id}/version', name: 'product_version', methods: ['GET'])]
public function getVersion(Product $product): JsonResponse
{
    return $this->json([
        'version' => $product->getVersion(),
        'lastModified' => $product->getUpdatedAt()?->format('Y-m-d H:i:s')
    ]);
}
// 批量更新处理乐观锁
#[Route('/products/batch-update', name: 'products_batch_update', methods: ['POST'])]
public function batchUpdate(Request $request): Response
{
    $data = json_decode($request->getContent(), true);
    $results = [];
    $this->entityManager->beginTransaction();
    try {
        foreach ($data['products'] as $productData) {
            $product = $this->entityManager
                ->getRepository(Product::class)
                ->find($productData['id']);
            if (!$product) {
                throw new \Exception("Product {$productData['id']} not found");
            }
            // 验证版本
            if ($productData['version'] !== $product->getVersion()) {
                $results[] = [
                    'id' => $productData['id'],
                    'success' => false,
                    'error' => '版本冲突'
                ];
                continue;
            }
            // 更新字段
            if (isset($productData['name'])) {
                $product->setName($productData['name']);
            }
            if (isset($productData['price'])) {
                $product->setPrice($productData['price']);
            }
            $product->setVersion($product->getVersion() + 1);
            $results[] = [
                'id' => $productData['id'],
                'success' => true,
                'newVersion' => $product->getVersion()
            ];
        }
        $this->entityManager->flush();
        $this->entityManager->commit();
        return $this->json([
            'success' => true,
            'results' => $results
        ]);
    } catch (\Exception $e) {
        $this->entityManager->rollback();
        return $this->json([
            'success' => false,
            'error' => $e->getMessage()
        ], 500);
    }
}

测试乐观锁功能

// tests/Functional/OptimisticLockTest.php
namespace App\Tests\Functional;
use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class OptimisticLockTest extends WebTestCase
{
    private EntityManagerInterface $entityManager;
    protected function setUp(): void
    {
        self::bootKernel();
        $this->entityManager = self::$kernel->getContainer()
            ->get('doctrine')
            ->getManager();
    }
    public function testOptimisticLockConflict(): void
    {
        $product = $this->createTestProduct();
        // 模拟两个用户同时编辑
        $product1 = $this->entityManager
            ->getRepository(Product::class)
            ->find($product->getId());
        $product2 = $this->entityManager
            ->getRepository(Product::class)
            ->find($product->getId());
        // 用户1更新
        $product1->setName('Updated by User 1');
        $product1->setVersion($product1->getVersion() + 1);
        $this->entityManager->flush();
        // 用户2尝试更新(应该触发乐观锁异常)
        $this->expectException(\Doctrine\ORM\OptimisticLockException::class);
        $product2->setName('Updated by User 2');
        $this->entityManager->flush();
        // 清理
        $this->cleanupTestProduct($product);
    }
    private function createTestProduct(): Product
    {
        $product = new Product();
        $product->setName('Test Product');
        $product->setPrice('99.99');
        $this->entityManager->persist($product);
        $this->entityManager->flush();
        return $product;
    }
    private function cleanupTestProduct(Product $product): void
    {
        $this->entityManager->remove($product);
        $this->entityManager->flush();
    }
}

最佳实践建议

配置文件优化

# config/packages/doctrine.yaml
doctrine:
    orm:
        auto_generate_proxy_classes: true
        optimistic_locking: true
        filters:
            optimistic_lock:
                class: App\Doctrine\Filter\OptimisticLockFilter
                enabled: true

使用事件监听器

// src/EventListener/OptimisticLockListener.php
namespace App\EventListener;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: Events::onFlush)]
class OptimisticLockListener
{
    public function onFlush(OnFlushEventArgs $args): void
    {
        $em = $args->getObjectManager();
        $uow = $em->getUnitOfWork();
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
            if (method_exists($entity, 'getVersion')) {
                // 自定义版本检查逻辑
                if ($entity->getVersion() === null) {
                    $entity->setVersion(1);
                }
            }
        }
    }
}

完整工作流程总结

  1. 读取数据:用户打开编辑页面时,加载产品数据和当前版本号
  2. 编辑过程:使用隐藏字段保存版本号,前端定时检查版本变化
  3. 提交验证:后端验证提交的版本号与数据库版本号一致
  4. 成功更新:版本号+1,确保其他用户无法覆盖此版本
  5. 冲突处理:版本冲突时回滚事务,显示错误信息并重定向到最新版本

这样就实现了完整的Symfony表单与乐观锁集成方案!

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