This commit is contained in:
yansongda 2021-05-31 23:20:15 +08:00
parent b49fafb976
commit 592ddaab78
20 changed files with 416 additions and 225 deletions

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Contract;
use Psr\Http\Message\ResponseInterface;
interface PackerInterface
{
/**
* @return mixed
*/
public function unpack(ResponseInterface $response);
}

View File

@ -9,10 +9,5 @@ use Yansongda\Pay\Rocket;
interface PluginInterface
{
/**
* @author yansongda <me@yansongda.cn>
*
* @return \Yansongda\Supports\Collection|\Symfony\Component\HttpFoundation\Response
*/
public function assembly(Rocket $rocket, Closure $next);
public function assembly(Rocket $rocket, Closure $next): Rocket;
}

View File

@ -40,6 +40,15 @@ class Exception extends \Exception
public const SHORTCUT_NOT_FOUND = 4001;
/**
* 关于api.
*/
public const RESPONSE_ERROR = 5000;
public const REQUEST_RESPONSE_ERROR = 5001;
public const UNPACK_RESPONSE_ERROR = 5002;
/**
* raw.
*

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Exception;
use Throwable;
class InvalidResponseException extends Exception
{
public function __construct(int $code = self::RESPONSE_ERROR, string $message = 'Provider response Error', array $extra = [], Throwable $previous = null)
{
parent::__construct($message, $code, $extra, $previous);
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Packer;
use Psr\Http\Message\ResponseInterface;
use Yansongda\Pay\Contract\PackerInterface;
use Yansongda\Pay\Exception\InvalidResponseException;
class ArrayPacker implements PackerInterface
{
/**
* @throws \Yansongda\Pay\Exception\InvalidResponseException
*/
public function unpack(ResponseInterface $response): array
{
$contents = $response->getBody()->getContents();
$result = json_decode($contents, true);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new InvalidResponseException(InvalidResponseException::UNPACK_RESPONSE_ERROR);
}
return $result;
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Packer;
use Psr\Http\Message\ResponseInterface;
use Yansongda\Pay\Contract\PackerInterface;
use Yansongda\Pay\Pay;
use Yansongda\Supports\Collection;
class CollectionPacker implements PackerInterface
{
/**
* @throws \Yansongda\Pay\Exception\ContainerDependencyException
* @throws \Yansongda\Pay\Exception\ContainerException
* @throws \Yansongda\Pay\Exception\ServiceNotFoundException
*/
public function unpack(ResponseInterface $response): Collection
{
return new Collection(Pay::get(JsonPacker::class)->unpack($response));
}
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Packer;
class JsonPacker extends ArrayPacker
{
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Packer;
use Psr\Http\Message\ResponseInterface;
use Yansongda\Pay\Contract\PackerInterface;
class ResponsePacker implements PackerInterface
{
public function unpack(ResponseInterface $response): ResponseInterface
{
return $response;
}
}

View File

@ -76,22 +76,6 @@ class Pay
$this->registerServices($config);
}
/**
* __call.
*
* @author yansongda <me@yansongda.cn>
*
* @throws \Yansongda\Pay\Exception\ContainerDependencyException
* @throws \Yansongda\Pay\Exception\ContainerException
* @throws \Yansongda\Pay\Exception\ServiceNotFoundException
*
* @return mixed
*/
public function __call(string $service)
{
return self::get($service);
}
/**
* __callStatic.
*

View File

@ -14,7 +14,7 @@ class FilterPlugin implements PluginInterface
/**
* @return \Yansongda\Supports\Collection|\Symfony\Component\HttpFoundation\Response
*/
public function assembly(Rocket $rocket, Closure $next)
public function assembly(Rocket $rocket, Closure $next): Rocket
{
$payload = $rocket->getPayload()->filter(function ($v, $k) {
return '' !== $v && !is_null($v) && 'sign' != $k && Str::startsWith($k, '_');

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay;
use Closure;
use Yansongda\Pay\Contract\PackerInterface;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Pay;
use Yansongda\Pay\Rocket;
class LaunchPlugin implements PluginInterface
{
/**
* @throws \Yansongda\Pay\Exception\ServiceNotFoundException
* @throws \Yansongda\Pay\Exception\ContainerException
* @throws \Yansongda\Pay\Exception\ContainerDependencyException
*/
public function assembly(Rocket $rocket, Closure $next): Rocket
{
/* @var Rocket $rocket */
$rocket = $next($rocket);
/* @var PackerInterface $packer */
$packer = Pay::get(PackerInterface::class);
return $rocket->setDestination($packer->unpack($rocket->getDestination()));
}
}

View File

@ -15,7 +15,7 @@ class PreparePlugin implements PluginInterface
* @throws \Yansongda\Pay\Exception\ContainerException
* @throws \Yansongda\Pay\Exception\ServiceNotFoundException
*/
public function assembly(Rocket $rocket, Closure $next)
public function assembly(Rocket $rocket, Closure $next): Rocket
{
return $next(
$rocket->mergePayload($this->getPayload($rocket->getParams()))

View File

@ -17,7 +17,7 @@ class RadarPlugin implements PluginInterface
* @throws \Yansongda\Pay\Exception\ContainerException
* @throws \Yansongda\Pay\Exception\ContainerDependencyException
*/
public function assembly(Rocket $rocket, Closure $next)
public function assembly(Rocket $rocket, Closure $next): Rocket
{
$config = get_alipay_config($rocket->getParams());

View File

@ -1,17 +0,0 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Shortcut;
use Closure;
use Yansongda\Pay\Plugin\Alipay\Trade\PagePayPlugin;
use Yansongda\Pay\Rocket;
class WebPlugin extends PagePayPlugin
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
return parent::assembly(...func_get_args());
}
}

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Shortcut;
use Closure;
use const ENT_QUOTES;
use GuzzleHttp\Psr7\Response;
use Yansongda\Pay\Contract\PackerInterface;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Contract\ShortcutInterface;
use Yansongda\Pay\Packer\ResponsePacker;
use Yansongda\Pay\Pay;
use Yansongda\Pay\Plugin\Alipay\Trade\PagePayPlugin;
use Yansongda\Pay\Rocket;
use Yansongda\Supports\Collection;
class WebShortcut implements ShortcutInterface
{
/**
* @throws \Yansongda\Pay\Exception\ContainerDependencyException
* @throws \Yansongda\Pay\Exception\ContainerException
* @throws \Yansongda\Pay\Exception\ServiceNotFoundException
*/
public function getPlugins(): array
{
Pay::set(PackerInterface::class, Pay::get(ResponsePacker::class));
return [
PagePayPlugin::class,
$this->buildHtmlResponse(),
];
}
protected function buildHtmlResponse(): PluginInterface
{
return new class() implements PluginInterface {
public function assembly(Rocket $rocket, Closure $next): Rocket
{
/* @var Rocket $rocket */
$rocket = $next($rocket->setDestination(new Response()));
$radar = $rocket->getRadar();
$response = 'GET' === $radar->getMethod() ?
$this->buildRedirect($radar->getUri()->__toString(), $rocket->getPayload()) :
$this->buildHtml($radar->getUri()->__toString(), $rocket->getPayload());
return $rocket->setDestination($response);
}
protected function buildRedirect(string $endpoint, Collection $payload): Response
{
$url = $endpoint.'&'.http_build_query($payload->all());
$content = sprintf('<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="refresh" content="0;url=\'%1$s\'" />
<title>Redirecting to %1$s</title>
</head>
<body>
Redirecting to %1$s.
</body>
</html>', htmlspecialchars($url, ENT_QUOTES)
);
return new Response(302, ['Location' => $url], $content);
}
protected function buildHtml(string $endpoint, Collection $payload): Response
{
$sHtml = "<form id='alipay_submit' name='alipay_submit' action='".$endpoint."' method='POST'>";
foreach ($payload->all() as $key => $val) {
$val = str_replace("'", '&apos;', $val);
$sHtml .= "<input type='hidden' name='".$key."' value='".$val."'/>";
}
$sHtml .= "<input type='submit' value='ok' style='display:none;'></form>";
$sHtml .= "<script>document.forms['alipay_submit'].submit();</script>";
return new Response(200, [], $sHtml);
}
};
}
}

View File

@ -23,7 +23,7 @@ class SignPlugin implements PluginInterface
* @throws \Yansongda\Pay\Exception\InvalidConfigException
* @throws \Yansongda\Pay\Exception\ServiceNotFoundException
*/
public function assembly(Rocket $rocket, Closure $next)
public function assembly(Rocket $rocket, Closure $next): Rocket
{
$payload = $rocket->getPayload();
$privateKey = $this->getPrivateKey();

View File

@ -13,8 +13,7 @@ class PagePayPlugin implements PluginInterface
public function assembly(Rocket $rocket, Closure $next): Rocket
{
return $next(
$rocket->setType(Response::class)
->mergePayload([
$rocket->mergePayload([
'method' => 'alipay.trade.page.pay',
'biz_content' => array_merge(
['product_code' => 'FAST_INSTANT_TRADE_PAY'],

View File

@ -4,12 +4,14 @@ declare(strict_types=1);
namespace Yansongda\Pay\Provider;
use Psr\Http\Message\RequestInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Throwable;
use Yansongda\Pay\Contract\HttpClientInterface;
use Yansongda\Pay\Contract\ShortcutInterface;
use Yansongda\Pay\Exception\InvalidParamsException;
use Yansongda\Pay\Exception\InvalidResponseException;
use Yansongda\Pay\Pay;
use Yansongda\Pay\Plugin\Alipay\FilterPlugin;
use Yansongda\Pay\Plugin\Alipay\LaunchPlugin;
use Yansongda\Pay\Plugin\Alipay\PreparePlugin;
use Yansongda\Pay\Plugin\Alipay\RadarPlugin;
use Yansongda\Pay\Plugin\Alipay\SignPlugin;
@ -32,7 +34,7 @@ class Alipay
* @throws \Yansongda\Pay\Exception\InvalidParamsException
* @throws \Yansongda\Pay\Exception\ServiceNotFoundException
*
* @return \Yansongda\Supports\Collection
* @return \Yansongda\Supports\Collection|\Psr\Http\Message\ResponseInterface
*/
public function __call(string $shortcut, array $params)
{
@ -46,7 +48,14 @@ class Alipay
/* @var ShortcutInterface $money */
$money = Pay::get($plugin);
return $this->pay($money->getPlugins(), ...$params);
$plugins = array_merge(
[PreparePlugin::class],
$money->getPlugins(),
[FilterPlugin::class, SignPlugin::class, RadarPlugin::class],
[LaunchPlugin::class],
);
return $this->pay($plugins, ...$params);
}
/**
@ -54,49 +63,46 @@ class Alipay
* @throws \Yansongda\Pay\Exception\ContainerException
* @throws \Yansongda\Pay\Exception\ServiceNotFoundException
*
* @return \Yansongda\Supports\Collection|\Symfony\Component\HttpFoundation\Response
* @return \Yansongda\Supports\Collection|\Psr\Http\Message\ResponseInterface
*/
public function pay(array $plugins, array $params)
{
$plugins = array_merge(
[PreparePlugin::class],
$plugins,
[FilterPlugin::class, SignPlugin::class, RadarPlugin::class]
);
/* @var Pipeline $pipeline */
$pipeline = Pay::get(Pipeline::class);
return $pipeline
/* @var Rocket $rocket */
$rocket = $pipeline
->send((new Rocket())->setParams($params)->setPayload(new Collection()))
->through($plugins)
->via('assembly')
->then(function ($rocket) {
return $this->ignite($rocket);
});
return $rocket->getDestination();
}
public function ignite(Rocket $rocket)
/**
* @throws \Yansongda\Pay\Exception\ContainerDependencyException
* @throws \Yansongda\Pay\Exception\ContainerException
* @throws \Yansongda\Pay\Exception\ServiceNotFoundException
* @throws \Yansongda\Pay\Exception\InvalidResponseException
*/
public function ignite(Rocket $rocket): Rocket
{
}
protected function launchResponse(RequestInterface $radar, Collection $payload): Response
{
$method = $radar->getMethod();
$endpoint = $radar->getUri()->getScheme().'://'.$radar->getUri()->getHost();
if ('GET' === $method) {
return new RedirectResponse($radar->getUri()->__toString());
if (!empty($rocket->getDestination())) {
return $rocket;
}
$sHtml = "<form id='alipay_submit' name='alipay_submit' action='".$endpoint."' method='".$method."'>";
foreach ($payload->all() as $key => $val) {
$val = str_replace("'", '&apos;', $val);
$sHtml .= "<input type='hidden' name='".$key."' value='".$val."'/>";
}
$sHtml .= "<input type='submit' value='ok' style='display:none;'></form>";
$sHtml .= "<script>document.forms['alipay_submit'].submit();</script>";
/* @var HttpClientInterface $http */
$http = Pay::get(HttpClientInterface::class);
return new Response($sHtml);
try {
$response = $http->sendRequest($rocket->getRadar());
} catch (Throwable $e) {
throw new InvalidResponseException(InvalidResponseException::REQUEST_RESPONSE_ERROR);
}
return $rocket->setDestination($response);
}
}

View File

@ -7,7 +7,6 @@ namespace Yansongda\Pay;
use ArrayAccess;
use JsonSerializable as JsonSerializableInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Serializable as SerializableInterface;
use Yansongda\Supports\Collection;
use Yansongda\Supports\Traits\Accessable;
@ -36,7 +35,7 @@ class Rocket implements JsonSerializableInterface, SerializableInterface, ArrayA
private $payload;
/**
* @var \Psr\Http\Message\ResponseInterface
* @var \Yansongda\Supports\Collection|\Psr\Http\Message\ResponseInterface
*/
private $destination;
@ -83,12 +82,18 @@ class Rocket implements JsonSerializableInterface, SerializableInterface, ArrayA
return $this;
}
public function getDestination(): ResponseInterface
/**
* @return \Psr\Http\Message\ResponseInterface|\Yansongda\Supports\Collection
*/
public function getDestination()
{
return $this->destination;
}
public function setDestination(ResponseInterface $destination): Rocket
/**
* @param \Psr\Http\Message\ResponseInterface|\Yansongda\Supports\Collection $destination
*/
public function setDestination($destination): Rocket
{
$this->destination = $destination;

View File

@ -2,152 +2,138 @@
namespace Yansongda\Pay\Tests;
use DI\Container;
use GuzzleHttp\Client;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Yansongda\Pay\Contract\ConfigInterface;
use Yansongda\Pay\Contract\ContainerInterface;
use Yansongda\Pay\Contract\EventDispatcherInterface;
use Yansongda\Pay\Contract\HttpClientInterface;
use Yansongda\Pay\Exception\ContainerNotFoundException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Pay;
use Yansongda\Supports\Config;
use Yansongda\Supports\Logger;
class PayTest extends TestCase
{
public function testGetContainerNullConfig()
{
$this->expectException(ContainerNotFoundException::class);
$this->expectExceptionCode(ContainerNotFoundException::CONTAINER_NOT_FOUND);
$this->expectExceptionMessage('You must init the container first with config');
Pay::getContainer();
}
public function testGetContainer()
{
self::assertInstanceOf(Container::class, Pay::getContainer([]));
}
public function testHasContainer()
{
self::assertFalse(Pay::hasContainer());
Pay::getContainer([]);
self::assertTrue(Pay::hasContainer());
}
public function testMagicCallNotFoundService()
{
$this->expectException(ServiceNotFoundException::class);
Pay::foo([]);
}
public function testMagicCallSetAndGet()
{
$data = [
'name' => 'yansongda',
'age' => 26
];
Pay::getContainer([]);
Pay::set('profile', $data);
self::assertEquals($data, Pay::get('profile'));
}
public function testCoreServiceBase()
{
$container = Pay::getContainer([]);
self::assertInstanceOf(Container::class, $container);
self::assertInstanceOf(Pay::class, $container->get(ContainerInterface::class));
self::assertInstanceOf(Pay::class, Pay::get(Pay::class));
}
public function testCoreServiceConfig()
{
$config = [
'name' => 'yansongda',
];
$container = Pay::getContainer($config);
self::assertInstanceOf(Config::class, $container->get(ConfigInterface::class));
self::assertEquals($config['name'], Pay::get(ConfigInterface::class)->get('name'));
// 修改 config 的情况
$config2 = [
'name' => 'yansongda2',
];
Pay::set(ConfigInterface::class, new Config($config2));
self::assertEquals($config2['name'], Pay::get(ConfigInterface::class)->get('name'));
}
public function testCoreServiceLogger()
{
$container = Pay::getContainer([]);
$otherLogger = new \Monolog\Logger('test');
self::assertInstanceOf(Logger::class, $container->get(\Yansongda\Pay\Contract\LoggerInterface::class));
self::assertInstanceOf(\Monolog\Logger::class, $container->get(\Yansongda\Pay\Contract\LoggerInterface::class)->getLogger());
self::assertInstanceOf(LoggerInterface::class, $container->get(\Yansongda\Pay\Contract\LoggerInterface::class)->getLogger());
self::assertNotEquals($otherLogger, $container->get(\Yansongda\Pay\Contract\LoggerInterface::class));
$container->get(\Yansongda\Pay\Contract\LoggerInterface::class)->setLogger($otherLogger);
self::assertEquals($otherLogger, $container->get(\Yansongda\Pay\Contract\LoggerInterface::class)->getLogger());
}
public function testCoreServiceEvent()
{
$container = Pay::getContainer([]);
self::assertInstanceOf(EventDispatcher::class, $container->get(EventDispatcherInterface::class));
}
public function testCoreServiceHttpClient()
{
$container = Pay::getContainer([]);
self::assertInstanceOf(Client::class, $container->get(HttpClientInterface::class));
}
public function testCoreServiceExternalHttpClient()
{
Pay::getContainer([]);
$oldClient = Pay::get(HttpClientInterface::class);
$client = new Client(['timeout' => 3.0]);
Pay::set(HttpClientInterface::class, $client);
self::assertEquals($client, Pay::get(HttpClientInterface::class));
self::assertNotEquals($oldClient, Pay::get(HttpClientInterface::class));
}
public function testSingletonContainer()
{
$config1 = ['name' => 'yansongda'];
$config2 = ['age' => 26];
$container1 = Pay::getContainer($config1);
$container2 = Pay::getContainer($config2);
self::assertEquals($container1, $container2);
}
public function testGetNotFoundContainer()
{
$this->expectExceptionMessage('You must init the container first with config');
$this->expectException(ContainerNotFoundException::class);
Pay::getContainer();
}
// public function testGetContainerNullConfig()
// {
// $this->expectException(ContainerNotFoundException::class);
// $this->expectExceptionCode(ContainerNotFoundException::CONTAINER_NOT_FOUND);
// $this->expectExceptionMessage('You must init the container first with config');
//
// Pay::getContainer();
// }
//
// public function testGetContainer()
// {
// self::assertInstanceOf(Container::class, Pay::getContainer([]));
// }
//
// public function testHasContainer()
// {
// self::assertFalse(Pay::hasContainer());
//
// Pay::getContainer([]);
//
// self::assertTrue(Pay::hasContainer());
// }
//
// public function testMagicCallNotFoundService()
// {
// $this->expectException(ServiceNotFoundException::class);
//
// Pay::foo([]);
// }
//
// public function testMagicCallSetAndGet()
// {
// $data = [
// 'name' => 'yansongda',
// 'age' => 26
// ];
//
// Pay::getContainer([]);
//
// Pay::set('profile', $data);
//
// self::assertEquals($data, Pay::get('profile'));
// }
//
// public function testCoreServiceBase()
// {
// $container = Pay::getContainer([]);
//
// self::assertInstanceOf(Container::class, $container);
// self::assertInstanceOf(Pay::class, $container->get(ContainerInterface::class));
// self::assertInstanceOf(Pay::class, Pay::get(Pay::class));
// }
//
// public function testCoreServiceConfig()
// {
// $config = [
// 'name' => 'yansongda',
// ];
//
// $container = Pay::getContainer($config);
//
// self::assertInstanceOf(Config::class, $container->get(ConfigInterface::class));
// self::assertEquals($config['name'], Pay::get(ConfigInterface::class)->get('name'));
//
// // 修改 config 的情况
// $config2 = [
// 'name' => 'yansongda2',
// ];
// Pay::set(ConfigInterface::class, new Config($config2));
//
// self::assertEquals($config2['name'], Pay::get(ConfigInterface::class)->get('name'));
// }
//
// public function testCoreServiceLogger()
// {
// $container = Pay::getContainer([]);
// $otherLogger = new \Monolog\Logger('test');
//
// self::assertInstanceOf(Logger::class, $container->get(\Yansongda\Pay\Contract\LoggerInterface::class));
// self::assertInstanceOf(\Monolog\Logger::class, $container->get(\Yansongda\Pay\Contract\LoggerInterface::class)->getLogger());
// self::assertInstanceOf(LoggerInterface::class, $container->get(\Yansongda\Pay\Contract\LoggerInterface::class)->getLogger());
// self::assertNotEquals($otherLogger, $container->get(\Yansongda\Pay\Contract\LoggerInterface::class));
//
// $container->get(\Yansongda\Pay\Contract\LoggerInterface::class)->setLogger($otherLogger);
// self::assertEquals($otherLogger, $container->get(\Yansongda\Pay\Contract\LoggerInterface::class)->getLogger());
// }
//
// public function testCoreServiceEvent()
// {
// $container = Pay::getContainer([]);
//
// self::assertInstanceOf(EventDispatcher::class, $container->get(EventDispatcherInterface::class));
// }
//
// public function testCoreServiceHttpClient()
// {
// $container = Pay::getContainer([]);
//
// self::assertInstanceOf(Client::class, $container->get(HttpClientInterface::class));
// }
//
// public function testCoreServiceExternalHttpClient()
// {
// Pay::getContainer([]);
//
// $oldClient = Pay::get(HttpClientInterface::class);
//
// $client = new Client(['timeout' => 3.0]);
// Pay::set(HttpClientInterface::class, $client);
//
// self::assertEquals($client, Pay::get(HttpClientInterface::class));
// self::assertNotEquals($oldClient, Pay::get(HttpClientInterface::class));
// }
//
// public function testSingletonContainer()
// {
// $config1 = ['name' => 'yansongda'];
// $config2 = ['age' => 26];
//
// $container1 = Pay::getContainer($config1);
// $container2 = Pay::getContainer($config2);
//
// self::assertEquals($container1, $container2);
// }
//
// public function testGetNotFoundContainer()
// {
// $this->expectExceptionMessage('You must init the container first with config');
// $this->expectException(ContainerNotFoundException::class);
//
// Pay::getContainer();
// }
}