优化你的业务逻辑

3.3 version
维护中的版本

在计算机软件领域,业务逻辑(business logic)或域逻辑(domain logic)指的是“程序中用于处理现实世界中决定数据被创建、显示、存储和改变的业务规则那部分内容”。(参考完整定义

Symfony程序中,业务逻辑是指你为自己的不与框架本身重合(比如路由或控制器)的程序所写的全部定制代码。Domain classes,Doctrine entities以及被当作服务来使用的常规PHP类,都是业务逻辑的好样板。

对于多数项目来说,你应该把所有东西存放到AppBundle中。在这里,你可以创建任意目录,用来组织内容:

1
2
3
4
5
6
7
8
9
10
symfony2-project/
├─ app/
├─ src/
│  └─ AppBundle/
│     └─ Utils/
│        └─ MyClass.php
├─ tests/
├─ var/
├─ vendor/
└─ web/

在bundle外部存储类? 

不过,并没有技术上的理由要求把业务逻辑放在bundle之内。如果你喜欢,你可以在src/目录下创建你自己的命名空间,然后把代码放进去:

1
2
3
4
5
6
7
8
9
10
11
symfony2-project/
├─ app/
├─ src/
│  ├─ Acme/
│  │   └─ Utils/
│  │      └─ MyClass.php
│  └─ AppBundle/
├─ tests/
├─ var/
├─ vendor/
└─ web/

Tip

推荐的方案是使用AppBundle/目录以简化操作。如果你的技术足够先进,了解哪些东西必须在bundle之内,而哪些可以移到bundle外面,请随意。

服务:命名和格式 

博客程序需要一个工具,用来把文章标题(如“Hello World”)转换成slug(“hello-world”)。此处的slug将被用于文章URL的一部分。

我们先在src/AppBundle/Utils/创建一个新的Slugger类并添加下面的slugify()方法:

1
2
3
4
5
6
7
8
9
10
11
12
// src/AppBundle/Utils/Slugger.php
namespace AppBundle\Utils;
 
class Slugger
{
    public function slugify($string)
    {
        return preg_replace(
            '/[^a-z0-9]/', '-', strtolower(trim(strip_tags($string)))
        );
    }
}

然后,为这个类定义一个服务:

1
2
3
4
5
# app/config/services.yml
services:
    # keep your service names short
    app.slugger:
        class: AppBundle\Utils\Slugger

Note

如果使用的是 默认的services.yml配置,类会被自动注册为服务。

传统上,一个服务的命名约定,会受到该类名称及其所在位置的影响,以避免命名冲突。因此,上面这个服务将被命名为 app.utils.slugger。但是通过使用更短的服务名,你的代码可以被更容易理解和使用。

Best Practice

Best Practice

程序级别的服务之名称,宜尽可能的短,但必须唯一,以便在所需之时你可随时从项目中找到它。

现在你可以在任何controller中使用这个自定义slugger了,比如AdminController:

1
2
3
4
5
6
7
8
9
10
11
public function createAction(Request $request)
{
    // ...
 
    if ($form->isSubmitted() && $form->isValid()) {
        $slug = $this->get('app.slugger')->slugify($post->getTitle());
        $post->setSlug($slug);
 
        // ...
    }
}

服务也可以是 public 或 private 的。如果你使用了 默认的services.yml配置,所有的服务默认都是私有的。

Best Practice

Best Practice

Services应尽可能的 private。这可以防止通过 $container->get() 来访问服务。取而代之的是你必须使用依赖注入。

服务的格式:YAML 

前面小节中,YAML被用于定义服务。

Best Practice

Best Practice

使用YAML定义你的服务。

这是有争议的,而且在我们的实验中,YAML和XML的使用即便在开发者中亦存在争议,YAML略微占先。两种格式拥有相同的性能,所以使用谁最终取决于个人口味。

我们推荐YAML是因为它对初学者友好且简洁。你当然可以选择你喜欢的格式。

服务:禁止使用类名参数 

你可能已经注意到,配置服务定义时我们已经不再使用参数来替代类的命名空间:

1
2
3
4
5
6
7
8
9
10
# app/config/services.yml
 
# service definition with class namespace as parameter
# 在服务定义中把类的命名空间当作一个参数
parameters:
    slugger.class: AppBundle\Utils\Slugger

services:
    app.slugger:
        class: '%slugger.class%'

这种方式对于你自己的服务来说,完全是累赘和不必要的:

Best Practice

Best Practice

决不在自己的服务定义中把类名搞成参数。

这个实践一度被第三方bundle错误地采纳。当Symfony引入服务容器时,一些开发者使用了使用了这种技巧以便能够轻松覆写服务。然而实际上,仅仅是通过改变类的名字这样去覆写一个服务,在实战中实在是太罕见了,更多的情况是,新服务的构造器完全不同于旧服务。

使用持久化分离层 

Symfony是一个HTTP框架,它只关心为每一个HTTP请求生成一个HTTP响应。这就是为何Symfony不提供用于持久化的层(即database,外部API)的原因。对此,你可以选择自己的类库或策略来达到目的。

实践中,很多Symfony程序依赖于独立的Doctrine project,利用entity和repository来定义其数据模型。就像在业务逻辑中建议的那样,我们推荐把Doctrine entities存放在AppBundle目录下。

以下三个entities,是我们的博客样板程序要用的,可谓一个良好范例:

1
2
3
4
5
6
7
8
symfony2-project/
├─ ...
└─ src/
   └─ AppBundle/
      └─ Entity/
         ├─ Comment.php
         ├─ Post.php
         └─ User.php

Tip

如果你是高级程度,当然也可以把它们存放在src/下你自己的命名空间内。

Doctrine映射信息 

Doctrine entity,是一个原生PHP对象,你要把它存入“数据库”。Doctrine只能通过你配置在model类中的metadata(元数据)来获知这个entity。Doctrine支持四种格式的metadata:YAML、XML、PHP和annotations。

Best Practice

Best Practice

使用annotation来定义Doctrine实体的映射信息。

annotations是目前设置和查找metadata时最方便也是最灵活的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
namespace AppBundle\Entity;
 
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
 
/**
 * @ORM\Entity
 */
class Post
{
    const NUM_ITEMS = 10;
 
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;
 
    /**
     * @ORM\Column(type="string")
     */
    private $title;
 
    /**
     * @ORM\Column(type="string")
     */
    private $slug;
 
    /**
     * @ORM\Column(type="text")
     */
    private $content;
 
    /**
     * @ORM\Column(type="string")
     */
    private $authorEmail;
 
    /**
     * @ORM\Column(type="datetime")
     */
    private $publishedAt;
 
    /**
     * @ORM\OneToMany(
     *      targetEntity="Comment",
     *      mappedBy="post",
     *      orphanRemoval=true
     * )
     * @ORM\OrderBy({"publishedAt" = "ASC"})
     */
    private $comments;
 
    public function __construct()
    {
        $this->publishedAt = new \DateTime();
        $this->comments = new ArrayCollection();
    }
 
    // getters and setters ...

所有格式的性能相同,因此再次提醒选择哪种完全依个人口味而定。

Data Fixtures 

由于fixtures功能并未在Symfony中默认开启,你应该执行下述命令来安装Doctrine fixtures bundle:

1
$  composer require "doctrine/doctrine-fixtures-bundle"

然后在AppKernel.php中开启这bundle,但是仅在devtest环境中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use Symfony\Component\HttpKernel\Kernel;
 
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            // ...
        );
 
        if (in_array($this->getEnvironment(), array('dev', 'test'))) {
            // ...
            $bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle();
        }
 
        return $bundles;
    }
 
    // ...
}

为了简化操作,我们推荐仅创建一个 fixture class,但如果类中的内容过长的话你也可以创建更多类。

假设你至少有一个fixtures类,而且数据库连接信息已被正确配置,通过以下命令即可加载你的fixtures:

1
2
3
4
5
$  php bin/console doctrine:fixtures:load
 
Careful, database will be purged. Do you want to continue Y/N ? Y
  > purging database
  > loading AppBundle\DataFixtures\ORM\LoadFixtures

编码标准 

Symfony源代码遵循PSR-1PSR-2代码书写规范,它们是由PHP社区制定的。你可以从the Symfony Coding standards 了解更多,甚至使用PHP-CS-Fixer,这是一个命令行工具,它可以“秒完成”地修复整个代码库的编码标准。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)