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

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

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

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

在bundle外部存储类?

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

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

Tip

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

服务:命名和格式

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

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

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

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

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

Note

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

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

Best Practice

Best Practice

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

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

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

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

Best Practice

Best Practice

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

服务的格式:YAML

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

Best Practice

Best Practice

使用YAML定义你的服务。

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

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

服务:禁止使用类名参数

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

  1. # app/config/services.yml
  2.  
  3. # service definition with class namespace as parameter
  4. # 在服务定义中把类的命名空间当作一个参数
  5. parameters:
  6. slugger.class: AppBundle\Utils\Slugger
  7. services:
  8. app.slugger:
  9. 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. symfony2-project/
  2. ├─ ...
  3. └─ src/
  4. └─ AppBundle/
  5. └─ Entity/
  6. ├─ Comment.php
  7. ├─ Post.php
  8. └─ 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. namespace AppBundle\Entity;
  2.  
  3. use Doctrine\ORM\Mapping as ORM;
  4. use Doctrine\Common\Collections\ArrayCollection;
  5.  
  6. /**
  7. * @ORM\Entity
  8. */
  9. class Post
  10. {
  11. const NUM_ITEMS = 10;
  12.  
  13. /**
  14. * @ORM\Id
  15. * @ORM\GeneratedValue
  16. * @ORM\Column(type="integer")
  17. */
  18. private $id;
  19.  
  20. /**
  21. * @ORM\Column(type="string")
  22. */
  23. private $title;
  24.  
  25. /**
  26. * @ORM\Column(type="string")
  27. */
  28. private $slug;
  29.  
  30. /**
  31. * @ORM\Column(type="text")
  32. */
  33. private $content;
  34.  
  35. /**
  36. * @ORM\Column(type="string")
  37. */
  38. private $authorEmail;
  39.  
  40. /**
  41. * @ORM\Column(type="datetime")
  42. */
  43. private $publishedAt;
  44.  
  45. /**
  46. * @ORM\OneToMany(
  47. * targetEntity="Comment",
  48. * mappedBy="post",
  49. * orphanRemoval=true
  50. * )
  51. * @ORM\OrderBy({"publishedAt" = "ASC"})
  52. */
  53. private $comments;
  54.  
  55. public function __construct()
  56. {
  57. $this->publishedAt = new \DateTime();
  58. $this->comments = new ArrayCollection();
  59. }
  60.  
  61. // getters and setters ...

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

Data Fixtures

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

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

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

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

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

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

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

编码标准

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