构建目录模块

目录模块是每个网店应用程序的重要组成部分。在最基本的层面上,它负责种类和产品的管理和展示。它是后面的模块(如 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 的新模块。我们在控制台的帮助下,通过运行以下命令来实现:

  1. php bin/console generate:bundle --namespace=Foggyline/CatalogBundle

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

构建目录模块 - 图1

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

构建目录模块 - 图2

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

  1. new Foggyline\CatalogBundle\FoggylineCatalogBundle()

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

  1. foggyline_catalog:
  2. resource: "@FoggylineCatalogBundle/Resources/config/routing.xml"
  3. prefix: /

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

创建实体

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

  1. php bin/console generate:doctrine:entity

构建目录模块 - 图3

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

  1. php bin/console doctrine:schema:update --force

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

构建目录模块 - 图4

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

  1. php bin/console generate:doctrine:crud

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

构建目录模块 - 图5

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

  1. foggyline_catalog_category:
  2. resource: "@FoggylineCatalogBundle/Controller/CategoryController.php"
  3. type: annotation

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

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

  1. php bin/console generate:doctrine:entity

我们遵循交互式生成器,遵循以下属性的最小值: 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,如下所示:

  1. // src/Foggyline/CatalogBundle/Entity/Category.php
  2. /**
  3. * @ORM\OneToMany(targetEntity="Product", mappedBy="category")
  4. */
  5. private $products;
  6. public function __construct()
  7. {
  8. $this->products = new \Doctrine\Common\Collections\ArrayCollection();
  9. }
  10. // src/Foggyline/CatalogBundle/Entity/Product.php
  11. /**
  12. * @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
  13. * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
  14. */
  15. private $category;

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

  1. public function __toString()
  2. {
  3. return $this->getTitle();
  4. }

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

  1. Catchable Fatal Error: Object of class Foggyline\CatalogBundle\Entity\Category could not be converted to string

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

  1. php bin/console doctrine:schema:update --force

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

  1. CREATE TABLE `product` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT,
  3. `category_id` int(11) DEFAULT NULL,
  4. `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  5. `price` decimal(10,2) NOT NULL,
  6. `sku` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  7. `url_key` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  8. `description` longtext COLLATE utf8_unicode_ci,
  9. `qty` int(11) NOT NULL,
  10. `image` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  11. PRIMARY KEY (`id`),
  12. UNIQUE KEY `UNIQ_D34A04ADF9038C4` (`sku`),
  13. UNIQUE KEY `UNIQ_D34A04ADDFAB7B3B` (`url_key`),
  14. KEY `IDX_D34A04AD12469DE2` (`category_id`),
  15. CONSTRAINT `FK_D34A04AD12469DE2` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`)
  16. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

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

构建目录模块 - 图6

管理图像上传

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

  1. //…
  2. use Symfony\Component\Validator\Constraints as Assert;
  3. //…
  4. class [Category|Product]
  5. {
  6. //…
  7. /**
  8. * @var string
  9. *
  10. * @ORM\Column(name="image", type="string", length=255, nullable=true)
  11. * @Assert\File(mimeTypes={ "image/png", "image/jpeg" }, mimeTypesMessage="Please upload the PNG or JPEG image file.")
  12. */
  13. private $image;
  14. //…
  15. }

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

构建目录模块 - 图7

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

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

  1. <service id="foggyline_catalog.image_uploader" class="Foggyline\CatalogBundle\Service\ImageUploader">
  2. <argument>%foggyline_catalog_images_directory%</argument>
  3. </service>

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

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

  1. namespace Foggyline\CatalogBundle\Service;
  2. use Symfony\Component\HttpFoundation\File\UploadedFile;
  3. class ImageUploader
  4. {
  5. private $targetDir;
  6. public function __construct($targetDir)
  7. {
  8. $this->targetDir = $targetDir;
  9. }
  10. public function upload(UploadedFile $file)
  11. {
  12. $fileName = md5(uniqid()) . '.' . $file->guessExtension();
  13. $file->move($this->targetDir, $fileName);
  14. return $fileName;
  15. }
  16. }

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

  1. parameters:
  2. foggyline_catalog_images_directory: "%kernel.root_dir%/../web/uploads/foggyline_catalog_images"

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

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

  1. $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
  2. $loader->load('parameters.yml');

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

  1. public function newAction(Request $request) {
  2. // ...
  3. if ($form->isSubmitted() && $form->isValid()) {
  4. /* @var $image \Symfony\Component\HttpFoundation\File\UploadedFile */
  5. if ($image = $category->getImage()) {
  6. $name = $this->get('foggyline_catalog.image_uploader')->upload($image);
  7. $category->setImage($name);
  8. }
  9. $em = $this->getDoctrine()->getManager();
  10. // ...
  11. }
  12. // ...
  13. }
  14. public function editAction(Request $request, Category $category) {
  15. $existingImage = $category->getImage();
  16. if ($existingImage) {
  17. $category->setImage(
  18. new File($this->getParameter('foggyline_catalog_images_directory') . '/' . $existingImage)
  19. );
  20. }
  21. $deleteForm = $this->createDeleteForm($category);
  22. // ...
  23. if ($editForm->isSubmitted() && $editForm->isValid()) {
  24. /* @var $image \Symfony\Component\HttpFoundation\File\UploadedFile */
  25. if ($image = $category->getImage()) {
  26. $name = $this->get('foggyline_catalog.image_uploader')->upload($image);
  27. $category->setImage($name);
  28. } elseif ($existingImage) {
  29. $category->setImage($existingImage);
  30. }
  31. $em = $this->getDoctrine()->getManager();
  32. // ...
  33. }
  34. // ...
  35. }

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

覆盖核心模块服务

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

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

  1. <service id="foggyline_catalog.category_menu" class="Foggyline\CatalogBundle\Service\Menu\Category">
  2. <argument type="service" id="doctrine.orm.entity_manager" />
  3. <argument type="service" id="router" />
  4. </service>
  5. <service id="foggyline_catalog.onsale" class="Foggyline\CatalogBundle\Service\Menu\OnSale">
  6. <argument type="service" id="doctrine.orm.entity_manager" />
  7. <argument type="service" id="router" />
  8. </service>

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

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

  1. //Category.php
  2. namespace Foggyline\CatalogBundle\Service\Menu;
  3. class Category
  4. {
  5. private $em;
  6. private $router;
  7. public function __construct(
  8. \Doctrine\ORM\EntityManager $entityManager,
  9. \Symfony\Bundle\FrameworkBundle\Routing\Router $router
  10. )
  11. {
  12. $this->em = $entityManager;
  13. $this->router = $router;
  14. }
  15. public function getItems()
  16. {
  17. $categories = array();
  18. $_categories = $this->em->getRepository('FoggylineCatalogBundle:Category')->findAll();
  19. foreach ($_categories as $_category) {
  20. /* @var $_category \Foggyline\CatalogBundle\Entity\Category */
  21. $categories[] = array(
  22. 'path' => $this->router->generate('category_show', array('id' => $_category->getId())),
  23. 'label' => $_category->getTitle(),
  24. );
  25. }
  26. return $categories;
  27. }
  28. }
  29. //OnSale.php
  30. namespace Foggyline\CatalogBundle\Service\Menu;
  31. class OnSale
  32. {
  33. private $em;
  34. private $router;
  35. public function __construct(\Doctrine\ORM\EntityManager $entityManager, $router)
  36. {
  37. $this->em = $entityManager;
  38. $this->router = $router;
  39. }
  40. public function getItems()
  41. {
  42. $products = array();
  43. $_products = $this->em->getRepository('FoggylineCatalogBundle:Product')->findBy(
  44. array('onsale' => true),
  45. null,
  46. 5
  47. );
  48. foreach ($_products as $_product) {
  49. /* @var $_product \Foggyline\CatalogBundle\Entity\Product */
  50. $products[] = array(
  51. 'path' => $this->router->generate('product_show', array('id' => $_product->getId())),
  52. 'name' => $_product->getTitle(),
  53. 'image' => $_product->getImage(),
  54. 'price' => $_product->getPrice(),
  55. 'id' => $_product->getId(),
  56. );
  57. }
  58. return $products;
  59. }
  60. }

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

  1. namespace Foggyline\CatalogBundle\DependencyInjection\Compiler;
  2. use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
  3. use Symfony\Component\DependencyInjection\ContainerBuilder;
  4. class OverrideServiceCompilerPass implements CompilerPassInterface
  5. {
  6. public function process(ContainerBuilder $container)
  7. {
  8. // Override the core module 'category_menu' service
  9. $container->removeDefinition('category_menu');
  10. $container->setDefinition('category_menu', $container->getDefinition('foggyline_catalog.category_menu'));
  11. // Override the core module 'onsale' service
  12. $container->removeDefinition('onsale');
  13. $container->setDefinition('onsale', $container->getDefinition('foggyline_catalog.onsale'));
  14. }
  15. }

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

  1. public function build(ContainerBuilder $container)
  2. {
  3. parent::build($container);
  4. $container->addCompilerPass(new \Foggyline\CatalogBundle\DependencyInjection\Compiler\OverrideServiceCompilerPass());
  5. }

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

设置分类页面

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

构建目录模块 - 图8

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

  1. <div class="row">
  2. <div class="small-12 large-12 columns text-center">
  3. <h1>{{ category.title }}</h1>
  4. <p>{{ category.description }}</p>
  5. </div>
  6. </div>
  7. <div class="row">
  8. <img src="{{ asset('uploads/foggyline_catalog_images/' ~ category.image) }}"/>
  9. </div>
  10. {% set products = category.getProducts() %}
  11. {% if products %}
  12. <div class="row products_onsale text-center small-up-1 medium-up-3 large-up-5" data-equalizer data-equalize-by-row="true">
  13. {% for product in products %}
  14. <div class="column product">
  15. <img src="{{ asset('uploads/foggyline_catalog_images/' ~ product.image) }}"
  16. alt="missing image"/>
  17. <a href="{{ path('product_show', {'id': product.id}) }}">{{ product.title }}</a>
  18. <div>${{ product.price }}</div>
  19. <div><a class="small button" href="{{ path('product_show', {'id': product.id}) }}">View</a></div>
  20. </div>
  21. {% endfor %}
  22. </div>
  23. {% else %}
  24. <div class="row">
  25. <p>There are no products assigned to this category.</p>
  26. </div>
  27. {% endif %}
  28. {% if is_granted('ROLE_ADMIN') %}
  29. <ul>
  30. <li>
  31. <a href="{{ path('category_edit', { 'id': category.id }) }}">Edit</a>
  32. </li>
  33. <li>
  34. {{ form_start(delete_form) }}
  35. <input type="submit" value="Delete">
  36. form_end(delete_form) }}
  37. </li>
  38. </ul>
  39. {% endif %}

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

设置产品页面

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

构建目录模块 - 图9

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

  1. <div class="row">
  2. <div class="small-12 large-6 columns">
  3. <img class="thumbnail" src="{{ asset('uploads/foggyline_catalog_images/' ~ product.image) }}"/>
  4. </div>
  5. <div class="small-12 large-6 columns">
  6. <h1>{{ product.title }}</h1>
  7. <div>SKU: {{ product.sku }}</div>
  8. {% if product.qty %}
  9. <div>IN STOCK</div>
  10. {% else %}
  11. <div>OUT OF STOCK</div>
  12. {% endif %}
  13. <div>$ {{ product.price }}</div>
  14. <form action="{{ add_to_cart_url.getAddToCartUrl
  15. (product.id) }}" method="get">
  16. <div class="input-group">
  17. <span class="input-group-label">Qty</span>
  18. <input class="input-group-field" type="number">
  19. <div class="input-group-button">
  20. <input type="submit" class="button" value="Add to Cart">
  21. </div>
  22. </div>
  23. </form>
  24. </div>
  25. </div>
  26. <div class="row">
  27. <p>{{ product.description }}</p>
  28. </div>
  29. {% if is_granted('ROLE_ADMIN') %}
  30. <ul>
  31. <li>
  32. <a href="{{ path('product_edit', { 'id': product.id }) }}">Edit</a>
  33. </li>
  34. <li>
  35. {{ form_start(delete_form) }}
  36. <input type="submit" value="Delete">
  37. {{ form_end(delete_form) }}
  38. </li>
  39. </ul>
  40. {% endif %}

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

单元测试

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

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

  1. <directory>src/Foggyline/CatalogBundle/Tests</directory>

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

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

  1. namespace Foggyline\CatalogBundle\Tests\Service\Menu;
  2. use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
  3. use Foggyline\CatalogBundle\Service\Menu\Category;
  4. class CategoryTest extends KernelTestCase
  5. {
  6. private $container;
  7. private $em;
  8. private $router;
  9. public function setUp()
  10. {
  11. static::bootKernel();
  12. $this->container = static::$kernel->getContainer();
  13. $this->em = $this->container->get('doctrine.orm.entity_manager');
  14. $this->router = $this->container->get('router');
  15. }
  16. public function testGetItems()
  17. {
  18. $service = new Category($this->em, $this->router);
  19. $this->assertNotEmpty($service->getItems());
  20. }
  21. protected function tearDown()
  22. {
  23. $this->em->close();
  24. unset($this->em, $this->router);
  25. }
  26. }

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

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

  1. phpunit src/Foggyline/CatalogBundle/Tests/Service/Menu/CategoryTest.php

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

功能测试

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

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

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

  1. // Create a new client to browse the application
  2. $client = static::createClient(
  3. array(), array(
  4. 'PHP_AUTH_USER' => 'john',
  5. 'PHP_AUTH_PW' => '1L6lllW9zXg0',
  6. )
  7. );
  8. // Create a new entry in the database
  9. $crawler = $client->request('GET', '/category/');
  10. $this->assertEquals(200, $client->getResponse()->getStatusCode(), "Unexpected HTTP status code for GET /product/");
  11. $crawler = $client->click($crawler->selectLink('Create a new entry')->link());
  12. // Fill in the form and submit it
  13. $form = $crawler->selectButton('Create')->form(array(
  14. 'category[title]' => 'Test',
  15. 'category[urlKey]' => 'Test urlKey',
  16. 'category[description]' => 'Test description',
  17. ));
  18. $client->submit($form);
  19. $crawler = $client->followRedirect();
  20. // Check data in the show view
  21. $this->assertGreaterThan(0, $crawler->filter('h1:contains("Test")')->count(), 'Missing element h1:contains("Test")');
  22. // Edit the entity
  23. $crawler = $client->click($crawler->selectLink('Edit')->link());
  24. $form = $crawler->selectButton('Edit')->form(array(
  25. 'category[title]' => 'Foo',
  26. 'category[urlKey]' => 'Foo urlKey',
  27. 'category[description]' => 'Foo description',
  28. ));
  29. $client->submit($form);
  30. $crawler = $client->followRedirect();
  31. // Check the element contains an attribute with value equals "Foo"
  32. $this->assertGreaterThan(0, $crawler->filter('[value="Foo"]')->count(), 'Missing element [value="Foo"]');
  33. // Delete the entity
  34. $client->submit($crawler->selectButton('Delete')->form());
  35. $crawler = $client->followRedirect();
  36. // Check the entity has been delete on the list
  37. $this->assertNotRegExp('/Foo title/', $client->getResponse()->getContent());

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

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

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

小结

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

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