You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Symfony LiveComponent表单测试出现There is currently no session available错误

测试Symfony LiveComponent表单时出现SessionNotFoundException的问题

问题概述

测试带表单的Symfony LiveComponent时触发报错:There is currently no session available,错误发生在Symfony为表单生成CSRF令牌的环节。该组件在网页端运行正常,但测试环境下无法正常初始化Session导致失败。

相关代码

测试文件(/tests/SomeTest.php)

class SomeTest extends WebTestCase
{
    use InteractsWithLiveComponents;

    public function testSomething(): void
    {
        $client = static::createClient();
        $productRepository = static::getContainer()->get(ProductRepository::class);
        $userRepository = static::getContainer()->get(UserRepository::class);

        $product = $productRepository->findOneBy(['name' => 'Chino Beige']);
        $user = $userRepository->findOneBy(['username' => 'admin']);

        $component = $this->createLiveComponent(AddToCart::class, [
            'product' => $product,
        ])->actingAs($user);

        $this->assertInstanceOf(TestLiveComponent::class, $component);
        $this->assertEquals(200, $component->response()->getStatusCode());
    }
}

LiveComponent表单实例化方法(/src/Twig/Components/Form/AddToCart.php)

protected function instantiateForm(): FormInterface
{
    $cartItem = new CartItem();

    return $this->createForm(AddToCartType::class, $cartItem, [
        'product' => $this->product,
        'cart' => $this->cartRepository->findOneBy(['customer' => $this->getUser(), 'catalog' => $this->product->getCatalog()]),
        'view_mode' => $this->product->getCatalog()->getViewMode(),
    ]);
}

AddToCartType表单代码

<?php

namespace App\Form;

// use [...]

class AddToCartType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('product', EntityType::class, [
                'class' => Product::class,
                'data' => $options['product'],
                'required' => true,
                'constraints' => [
                    new NotBlank(),
                ],
            ])
            ->add('cart', EntityType::class, [
                'class' => Cart::class,
                'data' => $options['cart'],
                'choice_label' => 'id',
                'required' => true,
                'constraints' => [
                    new NotBlank(),
                ],
            ])
        ;

        if ($options['view_mode'] === 'grid') {
            foreach ($options['product']->getMeasures() as $measure) {
                $builder->add('quantity_' . $measure->getId(), IntegerType::class, [
                    'mapped' => false,
                    'label' => $measure->getName(),
                    'data' => 0,
                    'required' => false,
                    'attr' => [
                        'min' => 0,
                    ],
                    'constraints' => [
                        new PositiveOrZero(),
                    ],
                ]);
            }
            $builder->add('total_quantity', HiddenType::class, [
                'mapped' => false,
                'label' => false,
                'required' => true,
                'data' => 0,
                'constraints' => [
                    new TotalQuantity(),
                ],
            ]);
        } else {
            $builder
                ->add('measure', EntityType::class, [
                    'label' => 'app.add_to_cart.measures',
                    'class' => Measure::class,
                    'query_builder' => function (MeasureRepository $measureRepository) use ($options): QueryBuilder {
                        $qb = $measureRepository->createQueryBuilder('m');

                        return $qb->andWhere($qb->expr()->in('m.id', ':ids'))
                            ->setParameter('ids', $options['product']->getMeasures()->map(fn($measure) => $measure->getId()));
                    },
                    'expanded' => true,
                    'required' => true,
                    'placeholder' => false,
                    'constraints' => [
                        new NotBlank(),
                    ],
                ])
                ->add('quantity', IntegerType::class, [
                    'label' => 'app.add_to_cart.quantity',
                    'data' => 0,
                    'attr' => [
                        'min' => 0,
                    ],
                    'required' => true,
                    'constraints' => [
                        new Positive(),
                    ],
                ])
                ->add('receiver', TextType::class, [
                    'label' => 'app.add_to_cart.receiver',
                    'attr' => [
                        'placeholder' => 'app.add_to_cart.receiver',
                    ],
                    'required' => true,
                    'constraints' => [
                        new NotBlank(),
                    ],
                ])
            ;
        }
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => CartItem::class,
            'product' => null,
            'cart' => null,
            'view_mode' => 'classic',
        ]);

        $resolver->setAllowedTypes('product', Product::class);
        $resolver->setAllowedTypes('cart', Cart::class);
        $resolver->setAllowedTypes('view_mode', 'string');
    }
}

报错堆栈跟踪

Symfony\Component\HttpFoundation\Exception\SessionNotFoundException : There is currently no session available.
 .../vendor/symfony/http-foundation/RequestStack.php:105
 ...:vendor/symfony/security-csrf/TokenStorage/SessionTokenStorage.php:104
 .../vendor/symfony/security-csrf/TokenStorage/SessionTokenStorage.php:71
 .../vendor/symfony/security-csrf/CsrfTokenManager.php:69
 .../vendor/symfony/form/Extension/Csrf/Type/FormTypeCsrfExtension.php:78
 .../vendor/symfony/form/ResolvedFormType.php:125
 .../vendor/symfony/form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php:86
 .../vendor/symfony/form/ResolvedFormType.php:119
 .../vendor/symfony/form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php:86
 .../vendor/symfony/form/Form.php:904
 .../vendor/symfony/ux-live-component/src/ComponentWithFormTrait.php:118
 .../vendor/symfony/ux-live-component/src/ComponentWithFormTrait.php:93
 .../vendor/symfony/ux-twig-component/src/ComponentFactory.php:205
 .../vendor/symfony/ux-twig-component/src/ComponentFactory.php:91
 .../vendor/symfony/ux-twig-component/src/ComponentFactory.php:65
 .../vendor/symfony/ux-live-component/src/Test/TestLiveComponent.php:174
 .../vendor/symfony/ux-live-component/src/Test/TestLiveComponent.php:123
 .../vendor/symfony/ux-live-component/src/Test/TestLiveComponent.php:51
 .../tests/SomeTest.php:31

环境信息

  • Symfony 7.0.6
  • PHP 8.3
  • PHPUnit 9.6.19
  • Symfony UX Live Component 2.17

原因分析与解决方法

核心原因

测试中通过createLiveComponent直接创建组件时,上下文未初始化Session;而表单生成CSRF令牌依赖Session存储令牌数据。虽然调用了actingAs($user),但该方法仅设置认证用户,不会自动创建Session。

解决方案

方案1:手动在测试中启动Session

修改测试代码,先通过客户端触发请求初始化Session:

public function testSomething(): void
{
    $client = static::createClient();
    // 发起请求以初始化Session
    $client->request('GET', '/');
    $session = $client->getContainer()->get('session');
    $session->start();

    $productRepository = static::getContainer()->get(ProductRepository::class);
    $userRepository = static::getContainer()->get(UserRepository::class);

    $product = $productRepository->findOneBy(['name' => 'Chino Beige']);
    $user = $userRepository->findOneBy(['username' => 'admin']);

    $component = $this->createLiveComponent(AddToCart::class, [
        'product' => $product,
    ])->actingAs($user);

    $this->assertInstanceOf(TestLiveComponent::class, $component);
    $this->assertEquals(200, $component->response()->getStatusCode());
}

方案2:测试环境下禁用表单CSRF保护

在LiveComponent的表单实例化逻辑中,判断环境并临时关闭CSRF:

protected function instantiateForm(): FormInterface
{
    $cartItem = new CartItem();
    $formOptions = [
        'product' => $this->product,
        'cart' => $this->cartRepository->findOneBy(['customer' => $this->getUser(), 'catalog' => $this->product->getCatalog()]),
        'view_mode' => $this->product->getCatalog()->getViewMode(),
    ];

    // 测试环境禁用CSRF
    if ($this->container->get('kernel')->getEnvironment() === 'test') {
        $formOptions['csrf_protection'] = false;
    }

    return $this->createForm(AddToCartType::class, $cartItem, $formOptions);
}

方案3:通过客户端请求渲染组件

放弃直接创建组件,改为通过客户端请求包含组件的页面,自动初始化Session:

public function testSomething(): void
{
    $client = static::createClient();
    $productRepository = static::getContainer()->get(ProductRepository::class);
    $userRepository = static::getContainer()->get(UserRepository::class);

    $product = $productRepository->findOneBy(['name' => 'Chino Beige']);
    $user = $userRepository->findOneBy(['username' => 'admin']);

    // 模拟用户登录
    $client->loginUser($user);
    // 请求包含AddToCart组件的页面(替换为实际路由)
    $client->request('GET', '/product/' . $product->getId());

    $this->assertEquals(200, $client->getResponse()->getStatusCode());
    // 可选:断言组件存在
    $this->assertSelectorExists('[data-controller="live-component"]');
}

内容的提问来源于stack exchange,提问作者Dylan Adam

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 02:08:09