首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

Symfony 表單與分頁實戰:登入系統、權限控制、Propel 分頁器與路由優化完整教程

     編輯:PHP 綜合知識

項目背景:askeet 問答社區

本教程基於經典 Symfony 1.x 教程「askeet」改編,展示 Symfony 表單處理、用戶認證、權限控制、Propel ORM 分頁、路由優化、代碼重構等核心技術。雖然 Symfony 版本已迭代至 6/7,但核心概念(表單組件、安全組件、分頁器、路由、MVC 分層)完全通用。

第一部分:用戶登入系統

1. 路由配置

# config/routes.yaml
login:
    path: /login
    controller: App\Controller\UserController::login
logout:
    path: /logout
    controller: App\Controller\UserController::logout

2. 登入表單類型

// src/Form/LoginType.php
namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;

class LoginType extends AbstractType {
    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder
            ->add('nickname', TextType::class, [
                'label' => '暱稱',
                'constraints' => [new Assert\NotBlank(), new Assert\Length(max: 50)],
                'attr' => ['autocomplete' => 'username'],
            ])
            ->add('password', PasswordType::class, [
                'label' => '密碼',
                'constraints' => [new Assert\NotBlank()],
                'attr' => ['autocomplete' => 'current-password'],
            ])
            ->add('remember_me', CheckboxType::class, [
                'label' => '記住我',
                'required' => false,
            ])
            ->add('submit', SubmitType::class, ['label' => '登入']);
    }
    
    public function configureOptions(OptionsResolver $resolver) {
        $resolver->setDefaults([
            'csrf_protection' => true,
            'csrf_field_name' => '_token',
        ]);
    }
}

3. 控制器處理

// src/Controller/UserController.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 Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use App\Form\LoginType;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;

class UserController extends AbstractController {
    public function login(Request $request, AuthenticationUtils $authUtils): Response {
        // 獲取登入錯誤、上次用戶名
        $error = $authUtils->getLastAuthenticationError();
        $lastUsername = $authUtils->getLastUsername();
        
        $form = $this->createForm(LoginType::class, [
            'nickname' => $lastUsername,
        ]);
        
        $form->handleRequest($request);
        
        if ($form->isSubmitted() && $form->isValid()) {
            // 實際認證由 Security 組件攔截器處理(form_login)
            // 這裡只處理表單驗證通過後的邏輯
            // 真實項目中配置 security.yaml 的 form_login 自動處理認證
        }
        
        return $this->render('user/login.html.twig', [
            'form' => $form->createView(),
            'error' => $error,
        ]);
    }
    
    public function logout(): void {
        // 由 security.yaml 的 logout 配置處理
        throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
    }
}

4. 安全配置

# config/packages/security.yaml
security:
    password_hashers:
        App\Entity\User: 'auto'
    
    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: nickname
    
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: app_user_provider
            form_login:
                login_path: login
                check_path: login
                enable_csrf: true
                username_parameter: nickname
                password_parameter: password
            logout:
                path: logout
                target: homepage
            remember_me:
                secret: '%kernel.secret%'
                lifetime: 2592000 # 30 days
                path: /
    
    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/profile, roles: ROLE_USER }

第二部分:佈局與權限顯示

{# templates/base.html.twig #}



    {% block title %}askeet{% endblock %}
    {{ encore_entry_link_tags('app') }}


    
    
    
{% for message in app.flashes('success') %}
{{ message }}
{% endfor %} {% for message in app.flashes('error') %}
{{ message }}
{% endfor %} {% block body %}{% endblock %}

第三部分:問題列表分頁

1. Repository 查詢方法

// src/Repository/QuestionRepository.php
namespace App\Repository;

use App\Entity\Question;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\ORM\QueryBuilder;

class QuestionRepository extends ServiceEntityRepository {
    public function __construct(ManagerRegistry $registry) {
        parent::__construct($registry, Question::class);
    }
    
    public function getHomepagePager(int $page, int $limit = 20): Paginator {
        $qb = $this->createQueryBuilder('q')
            ->leftJoin('q.author', 'u')->addSelect('u')
            ->orderBy('q.interestedUsersCount', 'DESC')
            ->setFirstResult(($page - 1) * $limit)
            ->setMaxResults($limit);
        
        return new Paginator($qb);
    }
    
    public function findByStrippedTitle(string $title): ?Question {
        return $this->createQueryBuilder('q')
            ->where('q.strippedTitle = :title')
            ->setParameter('title', $title)
            ->getQuery()
            ->getOneOrNullResult();
    }
}

2. 控制器分頁處理

// src/Controller/QuestionController.php
public function list(Request $request, QuestionRepository $questionRepo): Response {
    $page = max(1, $request->query->getInt('page', 1));
    $limit = $this->getParameter('app.pager.homepage_max') ?? 20;
    
    $paginator = $questionRepo->getHomepagePager($page, $limit);
    $totalItems = count($paginator);
    $totalPages = (int)ceil($totalItems / $limit);
    
    return $this->render('question/list.html.twig', [
        'paginator' => $paginator,
        'currentPage' => $page,
        'totalPages' => $totalPages,
        'totalItems' => $totalItems,
        'limit' => $limit,
    ]);
}

3. Twig 分頁模板

{# templates/question/list.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}

熱門問題

{% include 'question/_list.html.twig' with {questions: paginator} %} {% endblock %}

4. 局部模板

{# templates/question/_list.html.twig #}

        {% for question in questions %}
        
        {% else %}
        
        {% endfor %}
    
標題 作者 關注數 創建時間
{{ question.title }} {{ question.author.nickname }} {{ question.interestedUsersCount }} {{ question.createdAt|date('Y-m-d H:i') }}
暫無問題

第四部分:路由優化

# config/routes.yaml
question_list:
    path: /questions
    controller: App\Controller\QuestionController::list
    defaults: { page: 1 }
    methods: [GET]

question_list_paginated:
    path: /questions/page/{page}
    controller: App\Controller\QuestionController::list
    requirements:
        page: '\d+'
    methods: [GET]

question_show:
    path: /question/{strippedTitle}
    controller: App\Controller\QuestionController::show
    requirements:
        strippedTitle: '[a-z0-9-]+'
    methods: [GET]

# 為了 SEO 友好,也可以保留 /questions/page/2 格式

第五部分:代碼重構與模型層封裝

1. 服務層封裝業務邏輯

// src/Service/QuestionService.php
namespace App\Service;

use App\Entity\Question;
use App\Repository\QuestionRepository;
use Doctrine\ORM\EntityManagerInterface;
use Knp\Component\Pager\Pagination\PaginationInterface;
use Knp\Component\Pager\PaginatorInterface;

class QuestionService {
    public function __construct(
        private QuestionRepository $questionRepo,
        private EntityManagerInterface $em,
        private PaginatorInterface $paginator
    ) {}
    
    public function getHomepageQuestions(int $page, int $limit = 20): PaginationInterface {
        $query = $this->questionRepo->getHomepageQueryBuilder();
        return $this->paginator->paginate($query, $page, $limit);
    }
    
    public function publishQuestion(string $title, string $content, User $author): Question {
        $question = new Question();
        $question->setTitle($title);
        $question->setContent($content);
        $question->setAuthor($author);
        $question->setStrippedTitle($this->generateStrippedTitle($title));
        
        $this->em->persist($question);
        $this->em->flush();
        
        return $question;
    }
    
    private function generateStrippedTitle(string $title): string {
        // 簡單 slugify,實際用 slugger 服務
        return strtolower(trim(preg_replace('/[^a-z0-9]+/i', '-', $title), '-'));
    }
}

2. 控制器瘦身

public function list(Request $request, QuestionService $questionService): Response {
    $page = max(1, $request->query->getInt('page', 1));
    $pagination = $questionService->getHomepageQuestions($page);
    
    return $this->render('question/list.html.twig', [
        'pagination' => $pagination,
    ]);
}

第六部分:Propel 到 Doctrine 遷移對照

Propel (舊)Doctrine (現代)說明
sfPropelPagerKnpPaginator / Doctrine Paginator分頁器
Peer 類靜態方法Repository 實例方法查詢封裝
Criteria 對象QueryBuilder / DQL查詢構建
doSelectJoinUser()leftJoin + addSelect關聯查詢
配置文件 schema.ymlPHP 實體註解/屬性映射定義

總結

Symfony 表單與分頁實戰涵蓋了登入認證、CSRF 保護、記住我、權限控制、Twig 模板繼承、分頁查詢、路由優化、Repository 封裝、服務層分離、KnpPaginator 集成等企業級開發核心技能。現代 Symfony (6/7) 配合 Doctrine ORM、Security 組件、Form 組件、Twig、Routing、DependencyInjection,提供了類型安全、聲明式配置、自動配線、測試友好的開發體驗。核心架構原則:控制器瘦、模型富、服務封裝業務、表單處理輸入、模板只負責展示、路由語義化、分頁可配置。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved