PHP 7: 真实世界的应用开发
  • 前言
  • 模块一
    • 第一章、建立基础
      • PHP 7 安装注意事项
      • 使用内置的 PHP web 服务器
      • 创建一个 MySQL 测试数据库
      • 安装 PHPUnit
      • 实现类的自动加载
      • 抓取一个网站
      • 建立一个深度网络扫描器
      • 创建一个 PHP 5 到 PHP 7 代码转换器
    • 第二章、使用 PHP 7 高性能特性
      • 了解抽象语法树
      • 理解句法分析中的差异
      • 理解 foreach() 处理中的差异
      • 使用 PHP 7 增强功能提高性能
      • 遍历海量文件
      • 将电子表格上传到数据库
      • 递归目录迭代器
    • 第三章、使用 PHP 函数
      • 函数开发
      • 数据类型提示
      • 使用返回值数据类型
      • 使用迭代器
      • 使用生成器编写自己的迭代器
    • 第四章、使用 PHP 面向对象程序设计
      • 类的开发
      • 类的扩展
      • 使用静态属性和方法
      • 使用命名空间
      • 定义可见性
      • 使用接口
      • 使用特性
      • 实现匿名类
    • 第五章、与数据库的交互
      • 使用PDO连接数据库
      • 构建一个 OOP SQL 查询生成器
      • 处理分页
      • 定义实体以匹配数据库表
      • 将实体类与RDBMS查询绑定
      • 将二次查找嵌入到查询结果中
      • 实现jQuery DataTables的PHP查找
    • 第六章、建立可扩展的网站
      • 创建通用表单元素生成器
      • 创建一个HTML单选元素生成器
      • 创建一个HTML选择元素生成器
      • 实现表单工厂
      • 链式 $_POST 过滤器
      • 链式 $_POST 验证器
      • 将验证绑定到表单
    • 第七章、访问Web服务
      • 在PHP和XML之间转换
      • 创建一个简单的REST客户端
      • 创建一个简单的REST服务器
      • 创建一个简单的SOAP客户端
      • 创建一个简单的SOAP服务器
    • 第八章、处理日期/时间和国际化方面
      • 在视图脚本中使用 emoji
      • 转换复杂字符
      • 从浏览器数据获取语言环境
      • 按地区设置数字格式
      • 按地区处理货币
      • 按地区设置日期/时间格式
      • 创建一个HTML国际日历生成器
      • 构建一个周期性事件生成器
      • 不使用gettext处理翻译
    • 第九章、开发中间件
      • 使用中间件进行认证
      • 使用中间件实现访问控制
      • 使用高速缓存提高性能
      • 实施路由选择
      • 进行框架间的系统调用
      • 使用中间件来跨语言
    • 第十章、高级算法
      • 使用 getter 和 setter
      • 实现一个链表
      • 建立冒泡排序
      • 实现一个堆栈
      • 构建一个二分法查找类
      • 实现一个搜索引擎
      • 显示多维数组并累计总数
    • 第十一章、软件设计模式的实现
      • 创建数组到对象的转化器
      • 构建对象到数组到转化器
      • 实施策略模式
      • 定义一个映射器
      • 实现对象关系映射
      • 实施发布/订阅设计模式
    • 第十二章、提高网站安全
      • 过滤$_POST数据
      • 验证$_POST数据
      • 保护PHP session
      • 用令牌保护表格的安全
      • 建立一个安全的密码生成器
      • 带有验证码的安全保护表格
      • 不使用mcrypt进行加密/解密
    • 第十三章、最佳实践、测试和调试
      • 使用特征和接口
      • 通用异常处理程序
      • 通用错误处理程序
      • 编写一个简单的测试
      • 编写测试套件
      • 生成虚假的测试数据
      • 使用session_start参数自定义会话
    • PSR-7
  • 模块二
  • 模块三
    • GoF 设计模式
      • 结构型
      • 行为型
      • 小结
    • SOLID 设计原则
      • 开闭原则
      • 里氏替换原则
      • 接口隔离原则
      • 依赖反转原则
      • 小结
    • 模块化网店应用的需求规范
      • 线框设计
      • 定义技术栈
      • 小结
    • Symfony 概述
      • 创建一个空白项目
      • 使用 Symfony 控制台
      • 控制器
      • 路由
      • 模板
      • 表单
      • 配置 Symfony
      • bundle 系统
      • 数据库和 Doctrine
      • 测试
      • 验证
      • 小结
    • 构建核心模块
    • 构建目录模块
    • 构建客户模块
    • 构建支付模块
    • 构建发货模块
    • 构建销售模块
    • 总结
由 GitBook 提供支持
在本页
  • 要求
  • 依赖
  • 实施
  • 创建统一费率的运输服务
  • 创建统一费率装运控制器和路由
  • 创建动态费率支付服务
  • 创建动态费率发货控制器和路由
  • 单元测试
  • 功能测试
  • 小结
  1. 模块三

构建发货模块

发货模块与支付模块一起,为我们网店的进一步销售功能提供了基础。当我们到达即将到来的销售模块的结账流程时,它将使我们能够选择发货方式。与支付类似,发货方式也可以分为静态和动态两种。静态可能意味着一个固定的价格值,甚至是通过一些简单的条件计算出来的,而动态通常意味着与外部API服务的连接。

在本章中,我们将对这两种类型进行接触,并看看如何为实现发货模块设置一个基本结构。

在本章中,我们将涉及到发货模块的以下主题:

  • 要求

  • 依赖

  • 实施

  • 单元测试

  • 功能测试

要求

应用需求在前面的模块化网店App需求规范中定义的,并没有给我们任何具体的说明,我们需要实现什么样的快递方式。因此,在本章中,我们将开发两种发货方式:动态费率发货和定额发货。动态费率装运是作为一种将发货方法与真正的快递商(如UPS、FedEx等)连接起来的方式。然而,它不会实际连接到任何外部API。

理想情况下,我们希望通过类似于下面的接口来实现。

namespace Foggyline\SalesBundle\Interface;

interface Shipment
{
  function getInfo($street, $city, $country, $postcode, $amount, $qty);
  function process($street, $city, $country, $postcode, $amount, $qty);
}

然后,getInfo方法可以用来获取给定订单信息的可用送货选项,而 process方法则会处理选定的送货选项。例如,我们可以让 API 返回 "当日送达($9.99)"和 "标准送达($4.99) "作为动态费率装运方法下的送货选项。

有了这样的发货接口,就会强加一个SalesBundle模块的要求,而我们还没有开发这个模块。因此,我们将继续我们的发货方法,使用 Symfony 控制器来处理流程方法,使用服务来处理getInfo方法。

与上一章的支付方法类似,我们将通过标记的 Symfony 服务来公开 getInfo 方法。我们将在发货方法中使用的标签是shipment_method。稍后,在结账过程中,SalesBundle模块将获取所有被标记为shipment_method的服务,并在内部使用它们作为可用的发货方法列表。

依赖

我们正在以相反的方式构建模块。也就是说,我们在对SalesBundle模块有任何了解之前就开始构建该模块,因为SalesBundle是唯一会使用该模块的模块。考虑到这一点,发货模块与其他模块没有牢固的依赖关系。然而,如果先构建SalesBundle模块,然后再暴露一些shipment模块可能使用的接口,可能会更方便。

实施

我们将从创建一个新的模块Foggyline\ShipmentBundle开始。我们将在控制台的帮助下运行以下命令:

php bin/console generate:bundle --namespace=Foggyline/ShipmentBundle

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

完成后,文件app/AppKernel.php和app/config/routing.yml会自动修改。AppKernel类的registerBundles方法被添加到$bundles数组下的下面一行:

new Foggyline\PaymentBundle\FoggylineShipmentBundle(),

routing.yml文件已经更新,加入了以下条目:

foggyline_payment:
  resource: "@FoggylineShipmentBundle/Resources/config/routing.xml"
  prefix:   /

为了避免与核心应用代码发生冲突,我们需要更改 prefix: /改成prefix: /shipment/。

创建统一费率的运输服务

统一费率发货服务要提供固定的发货方式,我们的销售模块要在其结账过程中使用。它的作用是提供装运方式标签、代码、交付选项和处理 URL。

我们先在 src/Foggyline/ShipmentBundle/Resources/config/services.xml 文件的 services 元素下定义以下服务:

<service id="foggyline_shipment.dynamicrate_shipment"class="Foggyline\ShipmentBundle\Service\DynamicRateShipment">
  <argument type="service" id="router"/>
  <tag name="shipment_method"/>
</service>

这个服务只接受一个参数:router。tagname的值被设置为shipment_method,因为我们的SalesBundle模块将根据分配给服务的shipment_method标签来寻找发货方式。

现在我们将在 src/Foggyline/ShipmentBundle/Service/FlatRateShipment.php 文件中创建实际的服务类,如下所示:

namespace Foggyline\ShipmentBundle\Service;
class FlatRateShipment
{
  private $router;

  public function __construct(
    \Symfony\Bundle\FrameworkBundle\Routing\Router $router
  )
  {
    $this->router = $router;
  }

  public function getInfo($street, $city, $country, $postcode, $amount, $qty)
  {
    return array(
      'shipment' => array(
        'title' =>'Foggyline FlatRate Shipment',
        'code' =>'flat_rate',
        'delivery_options' => array(
        'title' =>'Fixed',
        'code' =>'fixed',
        'price' => 9.99
      ),
      'url_process' => $this->router->generate('foggyline_shipment_flat_rate_process'),
    )
  ;
  }
}

getInfo方法将为我们未来的SalesBundle模块提供必要的信息,以便它能构建结账过程中的发货步骤。它接受一系列参数:$street、$city、$country、$postcode、$amount和$qty。url_process是我们将插入所选发货方式的URL。然后,我们未来的SalesBundle模块将仅仅是对这个URL做一个AJAX POST,期望得到一个成功或错误的JSON响应,这与我们想象中对支付方式做的事情很相似。

创建统一费率装运控制器和路由

我们编辑 src/Foggyline/ShipmentBundle/Resources/config/routing.xml 文件,在其中添加以下路由定义:

<route id="foggyline_shipment_flat_rate_process"path="/flat_rate/process">
  <default key="_controller">FoggylineShipmentBundle:FlatRate:process
  </default>
</route>

然后我们创建一个src/Foggyline/ShipmentBundle/Controller/FlatRateController.php文件,内容如下:

namespace Foggyline\ShipmentBundle\Controller;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class FlatRateController extends Controller
{
  public function processAction(Request $request)
  {
    // Simulating some transaction id, if any
    $transaction = md5(time() . uniqid());

    return new JsonResponse(array(
      'success' => $transaction
    ));
  }
}

现在我们应该能够访问一个URL,比如/app_dev.php/shipment/flat_rate/process,并看到processAction的输出。这里给出的实现是虚拟的。对我们来说,重要的是销售模块在结账过程中,将通过shipment_method标签服务的getInfo方法推送任何可能的交付选项。意思是,结账过程应该显示平价运费作为一个选项。结账的行为将被编码为,如果没有选择发货方式,它将阻止结账过程的进一步发展。当我们进入到SalesBundle模块时,我们将进一步触及这个问题。

创建动态费率支付服务

除了统一费率的发货方式,我们再来定义一个动态的发货方式,叫做动态费率。

我们先在 src/Foggyline/ShipmentBundle/Resources/config/services.xml 文件的 services 元素下定义以下服务:

<service id="foggyline_shipment.dynamicrate_shipment"class="Foggyline\ShipmentBundle\Service\DynamicRateShipment">
  <argument type="service" id="router"/>
  <tag name="shipment_method"/>
</service>

这里定义的服务只接受一个路由器参数。tag name与统一费率装运服务相同,我们将创建src/Foggyline/ShipmentBundle/Service/DynamicRateShipment.php文件。

然后我们将创建src/Foggyline/ShipmentBundle/Service/DynamicRateShipment.php文件,内容如下:

namespace Foggyline\ShipmentBundle\Service;

class DynamicRateShipment
{
  private $router;

  public function __construct(
    \Symfony\Bundle\FrameworkBundle\Routing\Router $router
  )
  {
    $this->router = $router;
  }

  public function getInfo($street, $city, $country, $postcode, $amount, $qty)
  {
    return array(
      'shipment' => array(
        'title' =>'Foggyline DynamicRate Shipment',
        'code' =>'dynamic_rate_shipment',
        'delivery_options' => $this->getDeliveryOptions($street, $city, $country, $postcode, $amount, $qty),
        'url_process' => $this->router->generate('foggyline_shipment_dynamic_rate_process'),
      )
    );
  }

  public function getDeliveryOptions($street, $city, $country, $postcode, $amount, $qty)
  {
    // Imagine we are hitting the API with: $street, $city, $country, $postcode, $amount, $qty
    return array(
      array(
        'title' =>'Same day delivery',
        'code' =>'dynamic_rate_sdd',
        'price' => 9.99
      ),
      array(
        'title' =>'Standard delivery',
        'code' =>'dynamic_rate_sd',
        'price' => 4.99
      ),
    );
  }
}

与统一费率发货不同,这里getInfo方法的delivery_options键是用getDeliveryOptions方法的响应来构造的。该方法是服务的内部方法,并没有被想象成是暴露的,也没有被看成是接口的一部分。我们可以很容易地想象在它内部做一些API调用,以便为我们的动态发货方法获取计算的费率。

创建动态费率发货控制器和路由

一旦动态费率发货服务到位,我们就可以继续为它创建必要的路由。我们将首先在 src/Foggyline/ShipmentBundle/Resources/config/routing.xml 文件中添加以下路径定义:

<route id="foggyline_shipment_dynamic_rate_process" path="/dynamic_rate/process">
  <default key="_controller">FoggylineShipmentBundle:DynamicRate:process
  </default>
</route>

然后我们将创建src/Foggyline/ShipmentBundle/Controller/DynamicRateController.php文件,内容如下所示:

namespace Foggyline\ShipmentBundle\Controller;

use Foggyline\ShipmentBundle\Entity\DynamicRate;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;

class DynamicRateController extends Controller
{
  public function processAction(Request $request)
  {
    // Just a dummy string, simulating some transaction id
    $transaction = md5(time() . uniqid());

    if ($transaction) {
      return new JsonResponse(array(
'success' => $transaction
      ));
    }

    return new JsonResponse(array(
      'error' =>'Error occurred while processing DynamicRate shipment.'
    ));
  }
}

与统一费率发货类似,这里我们添加了一个简单的虚体实现过程和方法。传入的$request应该包含与服务getInfo方法相同的信息,也就是说,它应该有以下参数可用:$street、$city、$country、$postcode、$amount和$qty。方法的响应将在后面反馈到SalesBundle模块中。我们可以很容易地从这些方法中实现更强大的功能,但这不在本章的范围内。

单元测试

FoggylineShipmentBundle模块非常简单。通过只提供两个简单的服务和两个简单的控制器,它很容易测试。

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

<directory>src/Foggyline/ShipmentBundle/Tests</directory>

有了这些,从商店的根目录下运行phpunit命令,就可以在src/Foggyline/ShipmentBundle/Tests/目录下找到我们定义的测试。

现在,让我们继续为FlatRateShipment服务创建一个测试。我们将创建一个 src/Foggyline/ShipmentBundle/Tests/Service/FlatRateShipmentTest.php 文件,内容如下:

namespace Foggyline\ShipmentBundle\Tests\Service;

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class FlatRateShipmentTest extends KernelTestCase
{
  private $container;
  private $router;

  private $street = 'Masonic Hill Road';
  private $city = 'Little Rock';
  private $country = 'US';
  private $postcode = 'AR 72201';
  private $amount = 199.99;
  private $qty = 7;

  public function setUp()
  {
    static::bootKernel();
    $this->container = static::$kernel->getContainer();
    $this->router = $this->container->get('router');
  }

  public function testGetInfoViaService()
  {
    $shipment = $this->container->get('foggyline_shipment.flat_rate');

    $info = $shipment->getInfo(
      $this->street, $this->city, $this->country, $this->postcode, $this->amount, $this->qty
    );

    $this->validateGetInfoResponse($info);
  }

  public function testGetInfoViaClass()
  {
    $shipment = new \Foggyline\ShipmentBundle\Service\FlatRateShipment($this->router);

    $info = $shipment->getInfo(
      $this->street, $this->city, $this->country, $this->postcode, $this->amount, $this->qty
    );

    $this->validateGetInfoResponse($info);
  }

  public function validateGetInfoResponse($info)
  {
    $this->assertNotEmpty($info);
    $this->assertNotEmpty($info['shipment']['title']);
    $this->assertNotEmpty($info['shipment']['code']);
    $this->assertNotEmpty($info['shipment']['delivery_options']);
    $this->assertNotEmpty($info['shipment']['url_process']);
  }
}

这里正在运行两个简单的测试。一个检查我们是否可以通过容器实例化一个服务,另一个检查我们是否可以直接实例化。一旦被实例化,我们只需调用服务的getInfo方法,向它传递一个虚拟地址和订单信息。虽然我们实际上并没有在getInfo方法中使用这些数据,但我们需要传递一些东西,否则测试会失败。该方法预计将返回一个响应,其中包含shipment键下的几个键,最主要的是 title, code, delivery_options和url_process。

现在,让我们继续为我们的DynamicRateShipment服务创建一个测试。我们将创建一个 src/Foggyline/ShipmentBundle/Tests/Service/DynamicRateShipmentTest.php 文件,内容如下:

namespace Foggyline\ShipmentBundle\Tests\Service;

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class DynamicRateShipmentTest extends KernelTestCase
{
  private $container;
  private $router;

  private $street = 'Masonic Hill Road';
  private $city = 'Little Rock';
  private $country = 'US';
  private $postcode = 'AR 72201';
  private $amount = 199.99;
  private $qty = 7;

  public function setUp()
  {
    static::bootKernel();
    $this->container = static::$kernel->getContainer();
    $this->router = $this->container->get('router');
  }

  public function testGetInfoViaService()
  {
    $shipment = $this->container->get('foggyline_shipment.dynamicrate_shipment');
    $info = $shipment->getInfo(
      $this->street, $this->city, $this->country, $this->postcode, $this->amount, $this->qty
    );
    $this->validateGetInfoResponse($info);
  }

  public function testGetInfoViaClass()
  {
    $shipment = new \Foggyline\ShipmentBundle\Service\DynamicRateShipment($this->router);
    $info = $shipment->getInfo(
      $this->street, $this->city, $this->country, $this->postcode, $this->amount, $this->qty
    );

    $this->validateGetInfoResponse($info);
  }

  public function validateGetInfoResponse($info)
  {
    $this->assertNotEmpty($info);
    $this->assertNotEmpty($info['shipment']['title']);
    $this->assertNotEmpty($info['shipment']['code']);

    // Could happen that dynamic rate has none?!
    //$this->assertNotEmpty($info['shipment']['delivery_options']);

    $this->assertNotEmpty($info['shipment']['url_process']);
  }
}

这个测试与FlatRateShipment服务的测试几乎相同。在这里,我们也有两个简单的测试:一个是通过容器获取支付方法,另一个是直接通过类获取。不同的是,我们不再断言delivery_options不为空的存在。这是因为根据给定的地址和订单信息,真正的API请求可能不会返回任何交付选项。

功能测试

我们整个模块有两个控制器类,我们要测试响应。首先要确保FlatRateController和DynamicRateController类的process方法可以访问和工作,我们将首先创建src/Foggyline/ShipmentBundle/Tests/Controller/FlatRateControllerTest.php文件。

我们先创建一个src/Foggyline/ShipmentBundle/Tests/Controller/FlatRateControllerTest.php文件,内容如下:

namespace Foggyline\ShipmentBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class FlatRateControllerTest extends WebTestCase
{
  private $client;
  private $router;

  public function setUp()
  {
    $this->client = static::createClient();
    $this->router = $this->client->getContainer()->get('router');
  }

  public function testProcessAction()
  {
    $this->client->request('GET', $this->router->generate('foggyline_shipment_flat_rate_process'));
    $this->assertSame(200, $this->client->getResponse()->getStatusCode());
    $this->assertSame('application/json', $this->client->getResponse()->headers->get('Content-Type'));
    $this->assertContains('success', $this->client->getResponse()->getContent());
    $this->assertNotEmpty($this->client->getResponse()->getContent());
  }
}

然后我们将创建一个src/Foggyline/ShipmentBundle/Tests/Controller/DynamicRateControllerTest.php文件,内容如下:

namespace Foggyline\ShipmentBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class DynamicRateControllerTest extends WebTestCase
{
  private $client;
  private $router;

  public function setUp()
  {
    $this->client = static::createClient();
    $this->router = $this->client->getContainer()->get('router');
  }

  public function testProcessAction()
  {
    $this->client->request('GET', $this->router->generate('foggyline_shipment_dynamic_rate_process'));
    $this->assertSame(200, $this->client->getResponse()->getStatusCode());
    $this->assertSame('application/json', $this->client->getResponse()->headers->get('Content-Type'));
    $this->assertContains('success', $this->client->getResponse()->getContent());
    $this->assertNotEmpty($this->client->getResponse()->getContent());
  }
}

这两个测试几乎完全相同。它们包含了一个单一的过程动作方法的测试。按照现在的编码,控制器过程动作只是返回一个固定的成功JSON响应。我们可以很容易地扩展它,使其返回的不仅仅是一个固定的响应,并且可以用一个更强大的功能测试来配合这种变化。

小结

在本章中,我们建立了一个有两种发货方式的发货模块。每个发货方法都提供了可用的交付选项。统一费率的发货方法在其交付选项下只有一个固定的值,而动态费率方法则从getDeliveryOptions方法中获取其值。我们可以很容易地嵌入一个真正的运费API作为getDeliveryOptions的一部分,以便提供真正的动态运费选项。

显然,我们在这里缺乏官方的接口,就像我们对支付方法所做的那样。然而,当我们最终确定最终模块时,我们可以随时回来重构我们的应用程序。

与支付方式类似,这里的想法是创建一个最小的结构,展示如何开发一个简单的运输模块,以便进一步定制。使用shipment_methodservice标签,我们有效地暴露了未来销售模块的发货方法。

在下一章中,我们将建立一个销售模块,它将最终利用我们的支付和发货模块。

上一页构建支付模块下一页构建销售模块

最后更新于4年前