In Part I we created a fresh bleeding edge symfony2 project and briefly went over the directory structure of the project. Now we are going to configure our doctrine2 ORM database connection and create our data model. We are going to use doctrine2′s doc-block annotations to transform our plain old PHP objects into managed entities. If you are coming from a symfony1 background like me then the more you work with symfony2 and doctrine2 the more you will realize how much of the “magic” has been removed. Things don’t really “just work” anymore, at least not yet. This means that you will most likely have to write more code, but this new approach will allow you to have much more flexibility. I’m sure in the coming months, more tools will be available to bring back some of the magic, but it is best to actually understand what is happening behind the curtain.
第1部分,我们创建了一个前沿的Symfony2新项目,并简要地遍历了项目的目录结构。现在我们打算配置Doctrine2的ORM数据库连接并创建数据模型。我们打算使用Doctrine2的文档区块注释,将POPO转换成可管理的实体。如果您象我一样有着Symfony1背景,那么当您使用Symfony2和Doctrine2的时间越长,您就越能发现删除了多少“魔法”。它不是真的“只是工作”,至少目前不是。这就意味着您可能要写更多的代码,但新的方法也使您拥有更多的灵活性。我相信在未来的几个月内,有更多的工具可用,并会带回一些“魔法”功能,但其实最好还是要明白后台发生了什么。

So, lets get to some code. First we are going to configure our doctrine connection so we can connect to our database. In symfony2, there are three different formats for configuration: YAML, PHP and XML. I am going to use YAML for my configuration, routing, etc. You can use whichever one you prefer (refer to the symfony2 project page docs for details). To set the type of configuration you are using, you have to edit the AppKernel.php file in your app folder. YAML is the default format.
好了,让我们开始写一些代码。首先配置doctrine连接。在symfony2里,有三种不同的配置格式:YAML、PHP和XML。我将使用YAML格式来做配置和路由等。当然,您也可以使用任何一种你喜欢的格式。要设置您要用的配置格式,您需要在您的app文件夹里编辑Appkenel.php文件。YAML是缺省格式。

  1. // in AppKernel.php...  
  2.    
  3. public function registerContainerConfiguration(LoaderInterface $loader)  
  4. {  
  5.     // use YAML for configuration  
  6.     // comment to use another configuration format  
  7.     $loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');  
  8.    
  9.     // uncomment to use XML for configuration  
  10.     //$loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.xml');  
  11.    
  12.     // uncomment to use PHP for configuration  
  13.     //$loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.php');  
  14. }  

Now that we have our configuration format setup, we need to configure our doctrine connection so that we can connect to the application’s database. In the app/config folder open up the config.yml (.php or .xml depending on your format). By default the doctrine section of the config is commented out. Lets uncomment it and add our database connection info, credentials and setup some defaults.

现在,我们已经完成配置格式的设置。我们还需要配置我们的doctrine连接,以便连接应用程序数据库。在app/config文件夹中打开config.yml(.php或.xml有赖于您所设置的配置格式)。缺省情况下,配置的doctrine部分是被注释的。让我们去除它并添加我们数据库的连接信息、证书和一些默认设置。

  1. ## Doctrine Configuration 
  2. doctrine: 
  3.     dbal: 
  4.         driver:   %database_driver% 
  5.         host:     %database_host% 
  6.         dbname:   %database_name% 
  7.         user:     %database_user% 
  8.         password: %database_password% 
  9.   
  10.     orm: 
  11.         auto_generate_proxy_classes: %kernel.debug% 
  12.         auto_mapping: true 

All we have done here is tell doctrine how to connect to our database using values from our parameters.ini file. We have specified that we are using mysql (obviously change this if you are using something else), that the database name is blogdb and supplied the necessary credentials to login. We have also specified the default connection and the default entity manager. We will talk about entity managers in more detail later, but basically an entity manager is what you use to persist objects to the database and retrieve them from the database. One other thing to note is the mappings setting in the orm section. Under this section we tell doctrine what bundles to look for entities in.
 在这里,我们所做的就是告诉doctrine如何使用parameters.ini文件中的参数来进行数据库连接。我们已经指定使用mysql(显然,如果你使用其它的数据库,您需要改变它),数据库名是blogdb并提供了必要的登录证书。我们还指定了缺省连接和缺省的实体管理器。稍后我们还将讨论实体管理器的有关细节,但实体管理器基本上就是用来将对象持久化到数据库,并在数据库中进行检索的。另一个值得注意的是orm部分的映射设置。在这个部分我们将告诉doctrine如何在Bundle中查找实体。

Now that our doctrine connection is setup, we can move on to writing our entity objects. We are going to do things the doctrine2 way and turn plain old PHP objects into entities using annotations. Basically, we will be adding annotations to doc-block comments on our object fields. If this sounds confusing to you right now, don’t worry it is very simple as you will see shortly.
现在,我们的Doctrine连接已经设置完成,我们接下来可以去写我们的实体对象了。我们打算在doctrine2里使用注释(annotations),将POPO转换成实体。基本上,我们会在我们对象范围的文档区域注解(comment)中添加注释(annotations)。如果现在这个听起来使你困扰,别担心,您将很快发现它其实非常简单。

It’s almost time to start writing code, but first lets discuss our database schema. Our blog application will be very simple. We will have posts, users, categories and tags. A User will create a Post. A Post will have one Category and can optionally have many Tags. The following diagrams lays out our schema.
几乎可以开始写代码了,但首先还是让我们讨论一下我们的数据库方案(schema,译者注:这里采用了Oricle的译法)。我们的博客应用程序非常简单。我们有博文(posts)、用户、类别和标签。一个用户可以创建一篇博文(Post)。一篇博文拥有类别和可选的标签。下图就是我们的方案:

As I mentioned before, this is a very oversimplified blog setup. Even though this is a simple schema, it will allow us to use many of the different types of annotations available in doctrine2. As you can see from the above diagram, User has a one-to-many relationship with Post, Category has a one-to-many relationship with Post and Post has a many-to-many relationship with Tag. Now its finally time to write some code. Fire up your favorite IDE and lets get going.
正如我先前所说,这是个相当简化的博客设定。尽管它是个简单的方案,但它也允许我们使用doctrine2中的许多不同类型的注释(annotations)。如上图所示,User到Post的关系是一到多的,Category到Post的关系也是一到多,而Post与Tag之间的关系则是多到多的。现在到了最终写代码的时间了。选一个你喜欢的IDE,让我们开始吧。

The first thing we need to do is to navigate to the src/Company/BlogBundle folder. Here we can already see the Controller, Resources and Tests folders have been created as well as the BlogBundle.php class file which defines our bundle. The folder names should be self-explanatory so we will skip a long explanation of them right now and come back to that later. Placing your entity classes in the Entity namespace of your bundle is the standard convention for symfony2 bundles. So lets do that now and create a new folder named Entity in this directory. Now we can create our first entity. Create a file named User.php. Here are the most interesting parts of the code for the User class:
我们需要做的第一件事就是去浏览一下src/Company/BlogBundle文件夹。我们看到在该文件夹下已经创建了Controller,Resources和Tests文件夹,以及定义了我们Bundle的BlogBundle.php类文件。文件夹名应该是不言而明的,以便我们现在或今后查看时可以省下大量的解释。将实体类放入Bundle的实体命名空间里是Symfony2中Bundles的一种标准约定。让我们现在就开始做,在该目录下创建一个新的名为Entity的文件夹。这样我们就可以创建我们的第一个实体了。创建一个名为User.php的文件。以下是User类,这里代码中最有趣的部分:

  1. namespace Company\BlogBundle\Entity; 
  2.   
  3. use Doctrine\Common\Collections\ArrayCollection; 
  4. use Doctrine\ORM\Mapping as ORM; 
  5.   
  6. /**
  7.   * @ORM\Entity
  8.   * @ORM\Table(name="user")
  9.   */ 
  10. class User 
  11.     /**
  12.       * @ORM\Id
  13.       * @ORM\Column(type="integer")
  14.       * @ORM\GeneratedValue(strategy="AUTO")
  15.       *
  16.       * @var integer $id
  17.       */ 
  18.     protected $id
  19.   
  20.     /**
  21.       * @orm:Column(type="string", length="255", name="first_name")
  22.       *
  23.       * @var string $firstName
  24.       */ 
  25.     protected $firstName
  26.   
  27.     /**
  28.       * @ORM\Column(type="string", length="255", name="last_name")
  29.       *
  30.       * @var string $lastName
  31.       */ 
  32.     protected $lastName
  33.   
  34.     /**
  35.       * @ORM\Column(type="string", length="255")
  36.       *
  37.       * @var string $email
  38.       */ 
  39.     protected $email
  40.   
  41.     /**
  42.       * @ORM\Column(type="datetime", name="created_at")
  43.       *
  44.       * @var DateTime $createdAt
  45.       */ 
  46.     protected $createdAt
  47.   
  48.     /**
  49.       * @ORM\OneToMany(targetEntity="Post", mappedBy="user")
  50.       * @ORM\OrderBy({"createdAt" = "DESC"})
  51.       *
  52.       * @var ArrayCollection $posts
  53.       */ 
  54.     protected $posts
  55.   
  56.     //... 
  57.   
  58.     /**
  59.       * Constructs a new instance of User
  60.       */ 
  61.     public function __construct() 
  62.     { 
  63.         $this->posts = new ArrayCollection(); 
  64.         $this->createdAt = new \DateTime(); 
  65.     } 
  66.   
  67.     //... 

Lets go over this class slowly and cover all of the annotations used. First thing we do is declare the namespace of our entity. Next we are going to declare to the parser that we are using the ArrayCollection class provided by doctrine. You will see why we are using this class in just a bit. Now we can declare our class, all of its fields and annotate them. First look at the annotation on the User class declaration. Here we have two annotations; Entity and Table. The Entity annotation tells doctrine that this is an entity class. The Table annotation lets us set options for the table that doctrine will create for the entity. Easy so far right? Lets move on to the id member variable. Here we see three new annotations; Id, Column and GeneratedValue. The Id annotation tells doctrine that this field will be a identifier/primary key for the table representing the entity. The Column annotation is much like the Table annotation except it allows us to set options on the column instead of the table. For the id field we have set the type of the column to integer. You can view all of the different types here. The GeneratedValue annotation tells doctrine to automatically generate the value for this identifier. The strategy parameter of this annotation tells doctrine how to generate the value. You can read the doctrine documentation for more information on that. The annotations for the other fields should be easy to understand now except for the posts field, which details an association annotation.
让我们慢慢过一遍这个类,然后看一下所用到的注释(annotations)。首先是声明我们实体的命名空间。然后声明我们将Dctrine提供的ArrayCollection类作解析器。稍后您将会明白我们为什么使用这个类。现在我们声明了我们的类,所有的字段和注释。首先我们关注一下User类声明中的注释(annotation)。这里有两个注释(annotations):Entity和Table。Entity注释告诉Doctrine这是一个实体类。Table 注释让我们为Doctrine将要创建的表设置选项。到目前为止,容易吗?让我们看看id成员变量。这儿,我们看到三个新的注释(annotation):Id、Column和GeneratedValue。Id注释(annotation)告诉Doctrine,该字段是实体表中的一个标标识/主健。Column注释(annotation)与Table注释(annotation)类似,只是它允许我们为字段而非表设置选项。Id字段里我们设置字段类型为整型。您可以在这里查到所有的类型。GeneratedValue注释(annotation)告诉Doctrine自动生成该字段的值。该注释的Strategy这个参数告诉Doctrine如何生成值。更多详情您可以查阅doctrine的文档。现在除了posts字段,其他字段的注释(annotations)应该容易理解,它有一个关联注释 。
 

To understand the annotation used on the posts field of the User class we need to take a quick look at the Post entity object code. Here is the relevant code that needs explanation from the Post class:
为了了解User类中posts字段所有的注释,我们需要看一下Post实体对象代码。以下是来自Post类中,需要说明的相关代码:

  1. namespace Company\BlogBundle\Entity; 
  2.   
  3. use Doctrine\Common\Collections\ArrayCollection; 
  4.   
  5. /**
  6.   * @ORM\Entity
  7.   * @ORM\Table(name="post")
  8.   * @ORM\HasLifecycleCallbacks
  9.   */ 
  10. class Post 
  11.     /**
  12.       * @ORM\Id
  13.       * @ORM\Column(type="integer")
  14.       * @ORM\GeneratedValue(strategy="AUTO")
  15.       *
  16.       * @var integer $id
  17.       */ 
  18.     protected $id
  19.   
  20.     /**
  21.       * @ORM\Column(type="string", length="255")
  22.       *
  23.       * @var string $title
  24.       */ 
  25.     protected $title
  26.   
  27.     /**
  28.       * @ORM\Column(type="string", length="255")
  29.       *
  30.       * @var string $slug
  31.       */ 
  32.     protected $slug
  33.   
  34.     /**
  35.       * @ORM\Column(type="text")
  36.       *
  37.       * @var string $content
  38.       */ 
  39.     protected $content
  40.   
  41.     /**
  42.       * @ORM\Column(type="datetime", name="created_at")
  43.       *
  44.       * @var DateTime $createdAt
  45.       */ 
  46.     protected $createdAt
  47.   
  48.     /**
  49.       * @ORM\Column(type="datetime", name="updated_at", nullable="true")
  50.       *
  51.       * @var DateTime $updatedAt
  52.       */ 
  53.     protected $updatedAt
  54.   
  55.     /**
  56.       * @ORM\ManyToOne(targetEntity="Category")
  57.       * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
  58.       *
  59.       * @var Category $category
  60.       */ 
  61.     protected $category
  62.   
  63.     /**
  64.       * @ORM\ManyToOne(targetEntity="User", inversedBy="posts")
  65.       * @ORM\JoinColumn(name="user_id", referencedColumnName="id")
  66.       *
  67.       * @var User $user
  68.       */ 
  69.     protected $user
  70.   
  71.     /**
  72.       * @ORM\ManyToMany(targetEntity="Tag")
  73.       * @ORM\JoinTable(name="post_tag",
  74.       *     joinColumns={@ORM\JoinColumn(name="post_id", referencedColumnName="id")},
  75.       *     inverseJoinColumns={@ORM\JoinColumn(name="tag_id", referencedColumnName="id")}
  76.       * )
  77.       *
  78.       * @var ArrayCollection $tags
  79.       */ 
  80.     protected $tags
  81.   
  82.     //... 
  83.   
  84.     /**
  85.       * Constructs a new instance of Post. 
  86.       */ 
  87.     public function __construct() 
  88.     { 
  89.         $this->createdAt = new \DateTime(); 
  90.         $this->tags = new ArrayCollection(); 
  91.     } 
  92.   
  93.     /**
  94.       * Invoked before the entity is updated.
  95.       *
  96.       * @ORM\PreUpdate
  97.       */ 
  98.     public function preUpdate() 
  99.     { 
  100.         $this->updatedAt = new \DateTime(); 
  101.     } 

As you can see most of the annotations for the Post class are similar to that of the User class. If you take a look at the annotations for the user field you will see that we have used a ManyToOne annotation. This tells doctrine that Posts has a many-to-one relationship with User. The targetEntity attribute points to the entity to which the association is with, in our case User. The inversedBy attribute tells doctrine that the User object has a property named posts which represents the relationship in the inverse direction. This makes the association bi-directional. If you look at the posts field annotation in the User entity class it should now make sense. It simply says that we have a one-to-many relationship with the Post entity. This is spelled out using the targetEntity and the mappedBy attributes. The mappedBy attribute simply designates the field in the entity that is the owner of the relationship. If you look at the category field of the Post entity, you will see a very similar association mapping as the user field except that it is a unidirectional mapping. As an exercise you can change it into a bidirectional mapping if you wish.
正如您所看到的,Post类中的大部分注释与User类中的相似。如果您注意到user字段的注释,您将发现我们使用了ManyToOne注释。它告诉Doctrine,Posts类与User类有着多对一的关系。targetEntity属性指出与哪个实体关联,在这里是User。inverseBy属性告诉doctrine,User对象有一个名为posts的属性,表示的是反向关系。这形成了双向关联。如果你在User实体类中看到posts字段,那么现在您将明白是什么意思了。简单来说该类与Post实体有着一对多的关系。它是由targetEntity和 mappedBy属性组成。mappedBy属性指定拥有这个关系的实体中的字段。如果你注意到Post实体中的category字段,你会发现一个与user字段中非常相似的关联映射,除了它是一个单向映射。如果您希望,您可以做将它转变为双向映射的练习。

Next, take a look at the annotations for the tags field in the Post object. These annotations define a unidirectional many-to-many relationship. This annotation is fairly straightforward. One of the cool things about doctrine2 annotations is that we don’t need to create join tables for our many-to-many relationships by hand. We can just specify them with a JoinTable annotation and doctrine will take care of the rest for us.
接下来,看一下Post对象中tags字段的注释。这些注释定义了一个单向的多到多的关系。该注释相当简单,Doctrine2注释中很酷的地方在于我们不需要手工创建多对多的表连接。我们只需使用JoinTable注释指定它们,然后Doctrine就会帮我们完成剩余的事情。

You may have also noticed the HasLifecycleCallbacks annotation on the Post class. Lifecycle callbacks are basically functions that will be called during the lifecycle of a managed entity object. For a full list of the callbacks see this page. In the Post class we are using the PreUpdate callback to update the updatedAt field.
也许您还注意到了Post类中的HasLifecycleCallbacks注释。生命周期回调函数是在受管实体对象生命周期中调用的基础函数。在这里可以看到所有的回调函数列表。在Post类中,我们使用PreUpdate回调函数更新updateAt 字段。

The last thing you may have noticed in these entity classes is the constructor. In the constructor of the classes we have set our createdAt date. This is because doctrine will never directly invoke the constructor of the entity. This means that it is safe to set the createdAt field there. Also in the constructor, we have set the fields which are collections to the doctrine ArrayCollection class. The type of the many-valued fields must implement the doctrine Collection interface, which ArrayCollection does. This is a doctrine requirement and you can read more about it here.
最后您也许还注意到了这些实体类的构造函数。在类的构造函数中我们设置了createAt的日期。这是因为Doctrine从不直接执行实体的构造函数。这意味着在这里设置createdAt字段是安全的。同时在构造函数里, 我们已经设置了Doctrine的ArrayCollection类集合的字段。多值类型的字段必须实现Doctrine的Collection接口,正如ArrayCollection所做的那样。这是doctrine的要求,更多详情请查阅这里

The full entity classes can be downloaded here. After you have all of your entities set up you can launch a terminal and run the following command from your base project directory to have doctrine create your database tables based on your annotations:
完整的实体类可以从这里下载。当您设置好您所有实体之后,您可以运行一个终端,并在您的项目根目录中直接运行下列命令,从而使得Doctrine基于您的注释创建您的数据表。

  1. php app/console doctrine:schema:create 

That is it for this part. We have configured our doctrine connection, created and annotated the entity objects, and let doctrine automatically create our database tables based on those annotations. Next time we will start to work with routing, controllers and templates.
这就是本部分所讲内容。我们配置了我们的Doctrine连接,创建和注释了实体对象,并让Doctrine根据那些注释自动创建我们的数据表。接下来的时间,我们将开始路由、控制器和模板方面的工作。