发货模块与支付模块一起,为我们网店的进一步销售功能提供了基础。当我们到达即将到来的销售模块的结账流程时,它将使我们能够选择发货方式。与支付类似,发货方式也可以分为静态和动态两种。静态可能意味着一个固定的价格值,甚至是通过一些简单的条件计算出来的,而动态通常意味着与外部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
标签,我们有效地暴露了未来销售模块的发货方法。
在下一章中,我们将建立一个销售模块,它将最终利用我们的支付和发货模块。