使用Symfony框架开发一个简易博客项目——(一)登录和注册
文章目录
- 前言
- 文章参考
- 一些关于Symfony的概念
- 安装配置Symfony
- 安装Symfony并创建一个项目
- 运行Symfony
- 安装组件和包
- 利用Doctrine ORM与数据库互动
- 配置数据库
- 配置数据库连接信息
- 创建一个数据库
- 创建Entity类
- 创建一个Author Entity
- 创建一个BlogPost Entity
- 生成数据表
- 构建填写作者信息页面
- 创建Author表单类
- 创建Admin控制器
- 创建模板
- 引入Bootstrap和样式文件
- 身份管理
- 事件监听
- 构建主页面
- 最终成果
前言
一直以来使用混编方式开发小项目,这次尝试使用Symfony框架开发一个简易的博客项目。Symfony是一个基于MVC设计模式的PHP框架。博客项目代码可以在GitHub找到。
文章参考
https://auth0.com/blog/symfony-tutorial-building-a-blog-part-1
一些关于Symfony的概念
- 组件 Component :组件是构成Symfony框架的基石,来实现相应的功能。通过组合使用不同的组件,可以缩放Symfony以适用于任何需求。使用composer命令来安装组件,例如安装asset组件:
composer require symfony/asset
- 包 Bundles :Bundles包含一组经常一起使用的组件,安装Bundles的示例如下:
composer require sensio/generator-bundle
查看component和bundles以及不同版本:Symfony Recipes Server
安装配置Symfony
安装Symfony并创建一个项目
可以安装Symfony Installer或者通过Composer安装,具体步骤官网写的很清楚:
安装完成后,进入到你想要创建项目的目录,使用命令new来创建一个名为myblog的Symfony程序:
symfony new myblog
运行Symfony
Symfony可以利用PHP内置的web服务器,也可以配置其他的web服务器。这里我配置了Apache为Symfony程序的服务器。
最简单的方法是,首先执行以下命令来安装对应的pack:
composer require symfony/apache-pack
这条命令会在myblog/public/目录中安装一个.htaccess文件(分布式配置文件),包含配置web服务器所需的重写规则。
找到apache的主要配置文件httpd.conf(通常在usr/local/apache/conf目录下),找到DocumentRoot,更改路径为myblog的public文件夹。将AllowOverride None改为AllowOverride All:
DocumentRoot "/usr/local/apache2/htdocs/myblog/public"
<Directory "/usr/local/apache2/htdocs/myblog/public">
AllowOverride All
</Directory>
重启apache:
systemctl restart httpd
在浏览器中打开你的公网IP地址:
安装组件和包
为了实现后续功能,运行以下命令来安装所需的组件和包:
composer require annotations //Configure your controllers with annotations
composer require template //A Twig pack for Symfony projects
composer require security-bundle //Symfony SecurityBundle
composer require --dev maker-bundle //Helps you create empty commands, controllers, form classes, tests and more
composer require symfony/orm-pack //A pack for the Doctrine ORM
composer require validator //Symfony Validator Component
利用Doctrine ORM与数据库互动
对于任何应用程序来说,与数据库互动都是一个重要任务。Symfony框架紧密集成了一个名为Doctrine的第三方类库。它提供一个强有力的工具,令与数据库的互动变得更加轻松灵活。
Doctrine允许从数据库中取出整个对象,以及持久化整个对象到数据库中,而不是只把数据的行取到数组中。
本文使用Doctrine ORM(Object Relational Mapping),也就是把对象映射到关系型数据库(文中是MySQL)中。但如果你倾向于使用数据库的原始查询,可以参考:
http://www.symfonychina.com/doc/current/doctrine/dbal.html
配置数据库
配置数据库连接信息
安装了symfony/orm-pack后,会看到提示:
在根目录中找到.env文件,可以看到类似于以下的内容:
###> doctrine/doctrine-bundle ###
DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7
###< doctrine/doctrine-bundle ###
用你的MySQL数据库的设置替换其中的值。
创建一个数据库
配置完成后,运行以下命令:
php bin/console doctrine:database:create
它将使用你在上条配置中指定的数据库名称创建一个新数据库。
创建Entity类
Entity类定义着数据,除此以外,Entity类中还有一些其他的内容:
- 数据表到Entity类的映射关系:数据表的列(columns)映射为Entity类的特定属性。
- Getters和Setters:public的get方法和set方法,以便在程序其他地方来访问属性。
创建一个Author Entity
通过以下命令来创建一个名为Author的entity:
php bin/console make:entity
这条命令会创建两个新文件:
AuthorRepository.php中定义着一些查询方法。
打开Author.php,写入:
<?php
namespace App\Entity;
use App\Repository\AuthorRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="author")
* @ORM\Entity(repositoryClass=AuthorRepository::class)
*/
class Author
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(name="id",type="integer")
*/
private $id;
/**
* @ORM\Column(name="username", type="string", length=255, unique=true)
*/
private $username;
/**
* @ORM\Column(name="name", type="string", length=255, unique=true)
*/
private $name;
/**
* @ORM\Column(name="short_bio", type="string", length=500)
*/
private $shortBio;
/**
* @ORM\Column(name="email", type="string", length=255, nullable=true)
*/
private $email;
public function getId()
{
return $this->id;
}
public function getUsername()
{
return $this->username;
}
public function setUsername($username)
{
$this->username = $username;
return $this;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getShortBio()
{
return $this->shortBio;
}
public function setShortBio($shortBio)
{
$this->shortBio = $shortBio;
return $this;
}
public function getEmail()
{
return $this->email;
}
public function setEmail($email)
{
$this->email = $email;
return $this;
}
}
创建一个BlogPost Entity
同样的方法再创建一个名为BlogPost的entity,写入:
<?php
namespace App\Entity;
use App\Repository\BlogPostRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="blog_post")
* @ORM\Entity(repositoryClass=BlogPostRepository::class)
* @ORM\HasLifecycleCallbacks
*/
class BlogPost
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(name="id", type="integer")
*/
private $id;
/**
* @ORM\Column(name="title", type="string", length=255)
*/
private $title;
/**
* @ORM\Column(name="slug", type="string", length=255, unique=true)
*/
private $slug;
/**
* @ORM\Column(name="body", type="text")
*/
private $body;
/**
* @ORM\ManyToOne(targetEntity="Author")
* @ORM\JoinColumn(name="author_id", referencedColumnName="id")
*/
private $author;
/**
* @ORM\Column(name="subtime", type="datetime")
*/
private $subtime;
/**
* @ORM\Column(name="category", type="string", length=255)
*/
private $category;
public function getId()
{
return $this->id;
}
public function getTitle()
{
return $this->title;
}
public function setTitle($title)
{
$this->title = $title;
return $this;
}
public function getSlug()
{
return $this->slug;
}
public function setSlug($slug)
{
$this->slug = $slug;
return $this;
}
public function getBody()
{
return $this->body;
}
public function setBody($body)
{
$this->body = $body;
return $this;
}
public function getAuthor()
{
return $this->author;
}
public function setAuthor(Author $author)
{
$this->author = $author;
return $this;
}
public function getSubtime()
{
return $this->subtime;
}
public function setSubtime($subtime)
{
$this->subtime = $subtime;
return $this;
}
public function getCategory()
{
return $this->category;
}
public function setCategory($category)
{
$this->category = $category;
return $this;
}
/**
* @ORM\PrePersist
*/
public function prePersist()
{
if (!$this->getSubtime()) {
$this->setSubtime(new \DateTime());
}
}
}
这其中有两点需要特别注意:
- 在数据库中,数据表之间的关系由外键表示。但通过使用Doctrine,你不再需要(也不应该)直接操作外键,而是通过连接的实体来间接使用。
- 有时候你需要在一个实体被创建、更新或者删除的前后执行操作。这些操作处在一个实体的不同生命周期阶段,所以被称为“生命周期回调”。在注释中我们用 @ORM\HasLifecycleCallback() 开启了一个生命周期回调,然后使用 @ORM\PrePersist 在新的BlogPost实体第一次被创建时设置subtime列为当前日期。
生成数据表
根据你之前创建的两个实体,Doctrine可以自动创建所有数据表。运行:
php bin/console doctrine:schema:update --force
注意:这一步容易出错,如果字符集没有设置正确的话。
构建填写作者信息页面
初次登录成功后,用户需要填写一些个人信息来成为作者才能继续使用网站其他功能。
创建Author表单类
Symfony整合了一个Form组件,让处理表单变得容易。表单可以直接在控制器中被创建和使用。但是在一个单独的类中包含表单逻辑能让你在程序的任何地方复用表单。一旦有了表单类,就可以在控制器中快速创建表单。
在src/路径下新建一个目录Form,并在里面新建一个文件AuthorFormType.php,添加下列代码:
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AuthorFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class)
->add('shortBio', TextareaType::class)
->add('email', TextType::class, ['required' => false])
->add('submit', SubmitType::class, ['label' => 'Become an author!']);
}
//每个表单都需要知道“持有底层数据的类”的名称
//也可以通过表单创建时传入createForm方法的第二个参数来指定
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'App\Entity\Author'
]);
}
}
创建Admin控制器
在控制器中来实现程序真正的业务逻辑。
通过 php bin/console make:controller 命令来新建一个AdminController。在这个控制器里我们需要实现:
- 用户通过页面提交数据到表单
- 提交到表单的数据传入Author对象的属性中
- 持久化Author对象到数据库
首先导入一些需要用到的类和库:
//为了能够使用AbstractController类中的方法,需要导入并继承它
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
//收集数据
use Symfony\Component\HttpFoundation\Request;
//导入我们之前创建的实体类和表单类
use App\Entity\Author;
use App\Form\AuthorFormType;
//在控制器中,可以借助服务的类或接口以请求容器中的服务
use Doctrine\ORM\EntityManagerInterface;
在控制器里我们需要一个Entity Manager对象来负责数据库的持久化(写入),还需要Repository用于从数据库检索数据:
private $entityManager;
private $authorRepository;
private $blogPostRepository;
//当你type-hint一个接口参数时,Symfony会提供相应的服务
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
$this->blogPostRepository = $entityManager->getRepository('App:BlogPost');
$this->authorRepository = $entityManager->getRepository('App:Author');
}
将默认生成的index方法修改为以下:
/**
* @Route("/admin/author/create", name="author_create")
*/
public function createAuthorAction(Request $request)
{
//如果检索到作者已存在,提示错误并重定向到主页面
//getUser、getUserName、addFlash和redirectToRoute都是继承来的方法
if ($this->authorRepository->findOneByUsername($this->getUser()->getUserName())) {
$this->addFlash('error', 'Unable to create author, author already exists!');
return $this->redirectToRoute('homepage');
}
//实例化,并像常规PHP对象一样去使用$author对象
$author = new Author();
$author->setUsername($this->getUser()->getUserName());
//利用表单类AuthorFormType来快速创建一个表单对象,让用户提交到表单的数据可以传入$author对象的属性中
$form = $this->createForm(AuthorFormType::class, $author);
//识别用户提交,将用户的提交写入表单对象
$form->handleRequest($request);
//当用户以合法数据提交表单时
if ($form->isSubmitted() && $form->isValid()) {
//告诉Doctrine去管理$author对象,但没有引发对数据库的请求
$this->entityManager->persist($author);
//持久化$author对象到数据库
$this->entityManager->flush($author);
//设置session
$request->getSession()->set('user_is_author', true);
$this->addFlash('success', 'Congratulations! You are now an author.');
return $this->redirectToRoute('homepage');
}
//将表单对象渲染到模板
return $this->render('admin/create_author.html.twig', [
'form' => $form->createView()
]);
}
创建模板
我们希望将表单传递到用户将看到的模板中,所以在templates/admin/下创建一个新的模板文件create_author.html.twig:
{% extends 'base.html.twig' %}
{% block title %}{% endblock %}
{% block body %}
<div class="container">
<div class="blog-header">
<h2 class="blog-title">Creating your author</h2>
</div>
<div class="row">
<div class="col-md-12 col-lg-12 col-xl-12">
{% for label, messages in app.flashes %}
{% for message in messages %}
<div class="alert alert-{{ label }}" role="alert">
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
{{ message }}
</div>
{% endfor %}
{% endfor %}
</div>
<div class="col-sm-12 blog-main">
{{ form_start(form) }}
<div class="col-md-12">
<div class="form-group col-md-4">
{{ form_row(form.name) }}
</div>
<div class="form-group col-md-12">
{{ form_row(form.shortBio) }}
</div>
<div class="form-group col-md-4">
{{ form_row(form.email) }}
</div>
<div class="form-group col-md-4">
{{ form_row(form.shortBio) }}
</div>
</div>
<div class="form-group col-md-4 pull-right">
{{ form_row(form.submit) }}
</div>
{{ form_end(form) }}
</div>
</div>
</div>
{% endblock %}
引入Bootstrap和样式文件
创建一个CSS文件public/css/style.css,将这个文件和Bootstrap的CSS和JS文件引入templates/base.html.twig:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}{% endblock %}
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<!-- jQuery library -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<!-- Latest compiled JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<!-- Custom CSS File -->
<script src="css/style.css"></script>
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
</html>
身份管理
本文中借助了HWIOAuthBundle来验证和授权用户身份:
https://auth0.com/blog/symfony-tutorial-building-a-blog-part-1/
你也可以在Symfony官方文档中找到更多身份管理的方式:
http://www.symfonychina.com/doc/current/security.html
事件监听
通过Auth0进行了身份验证后,我们要求用户填写作者信息并成为作者,在这之前他们不被允许访问关于作者的其他部分。
在src/EventListener/Author目录下新建一个事件监听文件CheckIsAuthorListener.php:
<?php
namespace App\EventListener\Author;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class CheckIsAuthorListener
{
protected $router;
protected $session;
private $tokenStorage;
private $entityManager;
public function __construct(
RouterInterface $router,
SessionInterface $session,
TokenStorageInterface $tokenStorage,
EntityManagerInterface $entityManager
) {
$this->router = $router;
$this->session = $session;
$this->tokenStorage = $tokenStorage;
$this->entityManager = $entityManager;
}
public function onKernelController(FilterControllerEvent $event)
{
// Don't add to the flasher if the current path does not begin with /admin
if (!preg_match('/^\/admin/i', $event->getRequest()->getPathInfo())) {
return;
}
if (null === $user = $this->tokenStorage->getToken()->getUser()) {
return;
}
// Use the session to exit this listener early, if the relevant checks have already been made
if (true === $this->session->get('user_is_author')) {
return;
}
$route = $this->router->generate('author_create');
// Check we are not already attempting to create an author!
if (0 === strpos($event->getRequest()->getPathInfo(), $route)) {
return;
}
// Check if authenticated user has an author associated with them.
if ($author = $this->entityManager
->getRepository('App:Author')
->findOneByUsername($user->getUsername())
) {
$this->session->set('user_is_author', true);
}
if (!$author && $this->session->get('pending_user_is_author')) {
$this->session->getFlashBag()->add(
'warning',
'Your author access is being set up, this may take up to 30 seconds. Please try again shortly.'
);
$route = $this->router->generate('homepage');
} else {
$this->session->getFlashBag()->add(
'warning',
'You cannot access the author section until you become an author. Please complete the form below to proceed.'
);
}
$event->setController(function () use ($route) {
return new RedirectResponse($route);
});
}
}
添加这个类为服务,以便以后在控制器上请求它。在config/services.yaml最后添加以下内容:
App\EventListener\Author\CheckIsAuthorListener:
tags:
- { name: kernel.event_listener, event: kernel.controller }
通过tag告诉Symfony这是一个针对kernel.controller事件的监听。
构建主页面
通过 php bin/console make:controller 命令来新建一个BlogController:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class BlogController extends AbstractController
{
/**
* @Route("/", name="homepage")
*/
public function indexAction()
{
return $this->render('blog/index.html.twig', [
'controller_name' => 'BlogController',
]);
}
}
在模板./templates/blog/index.html.twig中包含其他部分的入口:
{% extends 'base.html.twig' %}
{% block body %}
<nav class="navbar navbar-default navbar-static-top">
<div id="navbar" class="collapse navbar-collapse pull-right">
<ul class="nav navbar-nav">
{% if app.user %}
<li><a href="{{ path("author_create") }}">Admin</a></li>
<li><a href="{{ logout_url("secured_area") }}">Logout</a></li>
{% else %}
<li class="active"><a href="/connect/auth0">Login</a></li>
{% endif %}
</ul>
</div>
</nav>
{% endblock %}
最终成果
打开页面提示登录:
点击登录到达Auth0提供的身份认证:
登录或者注册成功后,进入页面:
点击Admin,进入作者创建页面:
也可以点击Logout退出登录。