构建目录模块

目录模块是每个网店应用程序的重要组成部分。在最基本的层面上,它负责种类和产品的管理和展示。它是后面的模块(如 checkout)的基础,这些模块向我们的 web shop 应用程序添加了实际的销售功能。

更强大的目录功能可能包括大量产品倒入、产品导出、多仓库库存管理、私人会员类别等。不过,这些都不在本章的讨论范围之内。

在本章中,我们将讨论以下主题:

  • 要求

  • 依赖

  • 实施

  • 单元测试

  • 功能测试

要求

根据前面的模块化 商店应用的需求规范中定义的高级应用程序需求,我们的模块将实现多个实体和其他特定功能。

以下是所需模块实体清单:

  • 类别

  • 产品

类别实体包括以下属性及其数据类型:

  • id: integer, auto-increment

  • title: string

  • url_key: string, unique

  • description: text

  • image: string

产品实体包括以下属性:

  • id: integer, auto-increment

  • category_id: integer, 引用类别表ID列的外键

  • title: string

  • price: decimal

  • sku: string, unique

  • url_key: string, unique

  • description: text

  • qty: integer

  • image: string

  • onsale: boolean

除了只添加这些实体和它们的 CRUD 页面外,我们还需要覆盖核心模块服务,负责构建分类菜单和在售商品。

依赖

该模块与其他模块之间没有牢固的依赖关系。Symfony 框架服务层使我们能够以这样的方式对模块进行编码,在大多数情况下,它们之间不需要依赖。虽然模块确实覆盖了核心模块中定义的服务,但模块本身并不依赖于它,因为如果缺少了覆盖的服务,也不会发生任何中断。

实施

我们首先创建一个名为 Foggyline\CatalogBundle 的新模块。我们在控制台的帮助下,通过运行以下命令来实现:

这个命令触发了一个交互过程,在这个过程中会问我们几个问题,如下面的截图所示:

一旦完成,将为我们生成以下结构:

如果我们现在看一下 app/AppKernel.php 文件,我们会看到 registerBundles 方法下面的行:

类似地,app/config/routing.yml 添加了以下路由定义:

这里我们需要将 prefix: / 更改为 prefix: /catalog/,这样我们就不会与核心模块路由发生冲突。如果不改变prefix,那么会覆盖 AppBundle 路由,从而导致输出 helloworld!来自 src/foggyline/catalogbundle/resources/views/default/index.html。我们希望保持事物的美好和分离。这意味着模块不为自己定义根路由。

创建实体

让我们继续创建一个 Category 实体,我们使用控制台来实现,如下所示:

这将在 src/Foggyline/CatalogBundle/ 目录中创建 Entity/Category.phpRepository/CategoryRepository.php 文件。在这之后,我们需要更新数据库,所以它拉入了 Category 实体,如下面的命令行实例所示:

这样就得到了一个和下面截图类似的屏幕:

有了实体,我们就可以生成它的 CRUD 了:

这个结果与交互式输出如下所示:

这将创建 src/Foggyline/CatalogBundle/Controller/CategoryController.php。还为我们的app/config/routing.yml文件添加了一个条目,如下所示。

此外,视图文件被创建在 app/Resources/views/category/ 目录下,这不是我们所期望的。我们希望它们在我们的模块 src/Foggyline/CatalogBundle/Resources/views/Default/category/ 目录下,所以我们需要把它们复制过来。此外,我们需要修改 CategoryController 中的所有 $this->render 调用,将 FoggylineCatalogBundle:default: string 附加到每个模板路径。

接下来,我们继续使用前面讨论过的交互式生成器创建 Product 实体:

我们遵循交互式生成器,遵循以下属性的最小值: title、 price、 sku、 url_key、 description、 qty、 category 和 image。除了十进制和整数类型的 priceqty 之外,所有其他属性都是 string 类型的。此外,skuurl_key 被标记为唯一的。这将在 src/Foggyline/CatalogBundle/目录中创建Entity/Product.phpRepository/ProductRepository.php 文件。

与我们对 Category 视图模板所做的类似,我们需要对 Product 视图模板进行修改。也就是说,将它们从 app/Resources/views/product/ 目录下复制到src/Foggyline/CatalogBundle/Resources/views/Default/product/,然后更新 ProductController 中的所有 $this->render调用,将FoggylineCatalogBundle:default:string 附加到每个模板路径。

在这一点上,我们不会急于更新模式,因为我们要在代码中添加适当的关系。每一个产品都应该能够与一个Category 实体建立关系。为了达到这个目的,我们需要在 src/Foggyline/CatalogBundle/Entity/ 目录下编辑 Category.phpProduct.php,如下所示:

我们还需要编辑Category.php文件,在其中添加__toString方法实现,如下所示:

我们这样做的原因是,以后我们的产品编辑表单就会知道在Category选择下要列出哪些标签,否则系统会抛出以下错误。

有了上述变化,我们现在可以运行模式更新,如下:

如果我们现在看一下我们的数据库,产品表的CREATE命令语法如下:

我们可以看到,根据提供给我们的交互式实体生成器的条目,定义了两个唯一键和一个外键约束。现在我们已经准备好为我们的Product实体生成 CRUD。为此,我们运行 generate:doctrine:crud 命令,并按照这里所示的交互式生成器进行操作:

管理图像上传

此时,如果我们访问 /category/new//product/new/ URL,图片字段只是一个简单的输入文本字段,而不是我们想要的实际图片上传。要想把它变成图片上传字段,我们需要编辑Category.phpProduct.php$image属性,如下所示:

只要我们这样做,输入字段就会变成文件上传字段,如图所示:

接下来,我们将继续在表单中实现上传功能。

我们首先定义处理实际上传的服务。通过在 src/Foggyline/CatalogBundle/Resources/config/services.xml 文件的 services 元素下添加以下条目来定义服务:

%foggyline_catalog_images_directory%参数值是我们即将定义的参数名称。

然后我们创建 src/Foggyline/CatalogBundle/service/ImageUploader.php 文件,内容如下:

然后我们在 src/Foggyline/CatalogBundle/Resources/config 目录下创建自己的 parameters.yml 文件,内容如下:

这是我们的服务期望找到的参数。如果需要的话,它可以很容易地在app/config/parameters.yml下被覆盖。

为了让我们的bundle能找到parameters.yml文件,我们仍然需要在src/Foggyline/CatalogBundle/DependencyInjection/目录下编辑FoggylineCatalogExtension.php文件,在load 方法的最后添加以下loader:

在这一点上,我们的 Symfony 模块能够读取它的 parameters.yml,从而使定义的服务能够为它的参数获取合适的值。剩下的就是调整新建表单和编辑表单的代码,为它们附加上传功能。由于两个表单都是一样的,下面是一个Category的例子,同样也适用于Product表单:

现在,新建表单和编辑表单都应该能够处理文件上传。

覆盖核心模块服务

现在我们继续来解决分类菜单和发售商品的问题。早在构建核心模块的时候,我们在app/config/config.yml文件的twig:global部分下定义了全局变量。这些变量是指向app/config/services.yml文件中定义的服务。为了让我们改变分类菜单和在售商品的内容,我们需要覆盖这些服务。

我们首先在 src/Foggyline/CatalogBundle/Resources/config/services.xml 文件下添加以下两个服务定义:

这两个服务都接受 Doctrine ORM 实体管理器和路由器服务参数,因为我们需要在内部使用这些参数。

然后,我们在 src/Foggyline/CatalogBundle/Service/Menu/ 目录中创建实际的 CategoryOnSale 服务类,如下所示:

仅仅这样是不会触发核心模块服务的覆盖的。在src/Foggyline/CatalogBundle/DependencyInjection/Compiler/目录内,我们需要创建一个OverrideServiceCompilerPass类,实现CompilerPassInterface。在它的process方法中,我们就可以更改服务的定义,如下图:

最后,我们需要编辑src/Foggyline/CatalogBundle/FoggylineCatalogBundle.php文件的构建方法,使编译通过,如图所示:

现在,我们的CategoryOnSale服务已经覆盖核心模块中定义的服务,从而为主页的Category菜单和On Sale部分提供正确的值。

设置分类页面

自动生成的CRUD为我们做了一个Category页面,布局如下:

这与我们在模块化网店应用的需求规范中定义的Category页面有很大不同。因此,我们需要修改分类展示页面,修改src/Foggyline/CatalogBundle/Resources/views/default/category/目录下的show.html.twig文件。我们用以下代码替换body block的全部内容:

现在正文分为三个方面。首先,我们要解决类别标题和描述的输出。然后,我们正在获取并循环处理分配给类别的产品列表,呈现每个单独的产品。最后,我们使用is_granted Twig扩展来检查当前用户角色是否为ROLE_ADMIN,在这种情况下,我们将显示类别的编辑和删除链接。

设置产品页面

自动生成的CRUD为我们做了一个产品页面,布局如下:

这与我们在模块化网店应用的需求规范中定义的产品页面不同。为了解决这个问题,我们需要修改产品展示页面,修改src/Foggyline/CatalogBundle/Resources/views/Default/product/目录下的show.html.twig文件。我们用下面的代码替换整个body block的内容:

现在正文主要分为两个方面。首先,我们要解决的是产品图片、标题、库存状态和添加到购物车的输出。添加到购物车表单使用add_to_cart_url服务来提供正确的链接。这个服务是在核心模块下定义的,此时,只提供一个虚链接。稍后,当我们到了结账模块时,我们将为这个服务实现一个覆盖,并注入正确的add to cart链接。然后,我们输出描述部分。最后,我们使用is_granted Twig扩展,就像我们在Category例子上做的那样,确定用户是否可以访问产品的EditDelete链接。

单元测试

我们现在有几个与控制器无关的类文件,这意味着我们可以针对它们运行单元测试。不过,作为本书的一部分,我们还是不会去追求完整的代码覆盖,而是专注于一些小事情,比如在我们的测试类中使用容器。

我们首先在phpunit.xml.dist文件的testuites元素下添加以下一行:

设置好之后,从我们商店的根目录运行 phpunit 命令应该可以获取我们在 src/Foggyline/CatalogBundle/Tests/ 目录下定义的所有测试。

现在,让我们继续为类别服务菜单创建一个测试。 为此,我们创建了一个 src/Foggyline/CatalogBundle/Tests/Service/Menu/CategoryTest.php文件,其内容如下:

前面的例子展示了 setUptearDown 方法调用的用法,它们的行为类似于 PHP 的 __construct__destruct 方法。我们使用 setUp 方法来设置实体管理器和路由器服务,我们可以在类的其余部分中使用。tearDown方法只是一个清理方法。现在如果我们运行phpunit命令,我们应该会看到我们的测试和其他测试一起被接收和执行。

我们甚至可以通过执行phpunit命令来指定这个类的完整路径,如图所示:

与我们对CategoryTest所做的类似,我们可以继续创建OnSaleTest。 两者之间的唯一区别是类名。

功能测试

自动生成 CRUD工具最大的好处是,它甚至可以为我们生成功能测试。更具体地说,在本例中,它在src/Foggyline/CatalogBundle/Tests/Controller/目录下生成了CategoryControllerTest.phpProductControllerTest.php文件。

自动生成的功能测试在类体中有一个被注释掉的方法. 这将在 phpunit 运行时抛出一个错误。我们至少需要在其中定义一个虚拟测试方法来允许phpunit忽略它们.

如果我们查看这两个文件,我们可以看到它们都定义了一个testCompleteScenario方法,这个方法被完全注释掉了。让我们继续修改CategoryControllerTest.php的内容,如下所示:

我们首先将 PHP_AUTH_USERPHP_AUTH_PW 设置为 createClient 方法的参数。这是因为我们的/new/edit路由受到核心模块的安全保护。这些设置允许我们沿着请求传递基本的 HTTP 认证。然后,我们测试了是否可以访问分类列表页面,以及是否可以点击其创建新条目链接。此外,我们还测试了创建和编辑表单,以及它们的结果。

剩下的就是重复刚才 CategoryControllerTest.phpProductControllerTest.php 的方法。我们只需要修改 ProductControllerTest 类文件中的一些标签,使其与产品路线和预期结果相匹配。

现在运行 phpunit命令应该可以成功执行我们的测试。

小结

在本章中,我们已经建立了一个微型的,但功能性的目录模块。它允许我们创建、编辑和删除类别和产品。通过在自动生成的 CRUD 之上添加几行自定义代码,我们能够实现类别和产品的图片上传功能。我们还看到了如何覆盖核心模块服务,只需删除现有的服务定义并提供一个新的服务。在测试方面,我们看到了如何沿着我们的请求传递认证,以测试受保护的路由。

接下来,在下一章,我们将建立一个客户模块。

最后更新于