This commit is contained in:
2025-10-20 10:02:41 +08:00
parent a4858d47fc
commit dc0a271adf
2805 changed files with 451240 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
.idea/
/vendor
composer.lock
extensions.php
.php_cs.cache

View File

@@ -0,0 +1,29 @@
<?php
$header = <<<EOF
This file is part of the EasyWeChatComposer.
(c) 张铭阳 <mingyoungcheung@gmail.com>
This source file is subject to the MIT license that is bundled
with this source code in the file LICENSE.
EOF;
return PhpCsFixer\Config::create()
->setRiskyAllowed(true)
->setRules([
'@Symfony' => true,
'header_comment' => ['header' => $header],
'declare_strict_types' => true,
'ordered_imports' => true,
'strict_comparison' => true,
'no_empty_comment' => false,
'yoda_style' => false,
])
->setFinder(
PhpCsFixer\Finder::create()
->exclude('vendor')
->notPath('src/Laravel/config.php', 'src/Laravel/routes.php')
->in(__DIR__)
)
;

View File

@@ -0,0 +1,12 @@
language: php
php:
- 7.0
- 7.1
- 7.2
- 7.3
install:
- travis_retry composer install --no-interaction --no-suggest
script: ./vendor/bin/phpunit

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 张铭阳
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,55 @@
<p align="center">
<h1 align="center">EasyWeChat Composer Plugin</h1>
</p>
<p align="center">
<a href="https://travis-ci.org/mingyoung/easywechat-composer"><img src="https://travis-ci.org/mingyoung/easywechat-composer.svg" alt="Build Status"></a>
<a href="https://scrutinizer-ci.com/g/mingyoung/easywechat-composer/?branch=master"><img src="https://scrutinizer-ci.com/g/mingyoung/easywechat-composer/badges/quality-score.png?b=master" alt="Scrutinizer Code Quality"></a>
<a href="https://packagist.org/packages/easywechat-composer/easywechat-composer"><img src="https://poser.pugx.org/easywechat-composer/easywechat-composer/v/stable.svg" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/easywechat-composer/easywechat-composer"><img src="https://poser.pugx.org/easywechat-composer/easywechat-composer/d/total.svg" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/easywechat-composer/easywechat-composer"><img src="https://poser.pugx.org/easywechat-composer/easywechat-composer/license.svg" alt="License"></a>
</p>
Usage
---
Set the `type` to be `easywechat-extension` in your package composer.json file:
```json
{
"name": "your/package",
"type": "easywechat-extension"
}
```
Specify server observer classes in the extra section:
```json
{
"name": "your/package",
"type": "easywechat-extension",
"extra": {
"observers": [
"Acme\\Observers\\Handler"
]
}
}
```
Examples
---
* [easywechat-composer/open-platform-testcase](https://github.com/mingyoung/open-platform-testcase)
Server Delegation
---
> 目前仅支持 Laravel
1.`config/app.php` 中添加 `EasyWeChatComposer\Laravel\ServiceProvider::class`
2. 在**本地项目**的 `.env` 文件中添加如下配置:
```
EASYWECHAT_DELEGATION=true # false 则不启用
EASYWECHAT_DELEGATION_HOST=https://example.com # 线上域名
```

View File

@@ -0,0 +1,35 @@
{
"name": "easywechat-composer/easywechat-composer",
"description": "The composer plugin for EasyWeChat",
"type": "composer-plugin",
"license": "MIT",
"authors": [
{
"name": "张铭阳",
"email": "mingyoungcheung@gmail.com"
}
],
"require": {
"php": ">=7.0",
"composer-plugin-api": "^1.0 || ^2.0"
},
"require-dev": {
"composer/composer": "^1.0 || ^2.0",
"phpunit/phpunit": "^6.5 || ^7.0"
},
"autoload": {
"psr-4": {
"EasyWeChatComposer\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"EasyWeChatComposer\\Tests\\": "tests/"
}
},
"extra": {
"class": "EasyWeChatComposer\\Plugin"
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/7.0/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
forceCoversAnnotation="true"
beStrictAboutCoversAnnotation="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
verbose="true">
<testsuite name="EasyWeChatComposer Test">
<directory suffix="Test.php">tests</directory>
</testsuite>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Commands;
use Composer\Command\BaseCommand;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ExtensionsCommand extends BaseCommand
{
/**
* Configures the current command.
*/
protected function configure()
{
$this->setName('easywechat:extensions')
->setDescription('Lists all installed extensions.');
}
/**
* Executes the current command.
*
* @param InputInterface $input
* @param OutputInterface $output
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$extensions = require __DIR__.'/../../extensions.php';
if (empty($extensions) || !is_array($extensions)) {
return $output->writeln('<info>No extension installed.</info>');
}
$table = new Table($output);
$table->setHeaders(['Name', 'Observers'])
->setRows(
array_map([$this, 'getRows'], array_keys($extensions), $extensions)
)->render();
}
/**
* @param string $name
* @param array $extension
*
* @return array
*/
protected function getRows($name, $extension)
{
return [$name, implode("\n", $extension['observers'] ?? [])];
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Commands;
use Composer\Plugin\Capability\CommandProvider;
class Provider implements CommandProvider
{
/**
* Retrieves an array of commands.
*
* @return \Composer\Command\BaseCommand[]
*/
public function getCommands()
{
return [
new ExtensionsCommand(),
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Contracts;
interface Encrypter
{
/**
* Encrypt the given value.
*
* @param string $value
*
* @return string
*/
public function encrypt($value);
/**
* Decrypt the given value.
*
* @param string $payload
*
* @return string
*/
public function decrypt($payload);
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Delegation;
use EasyWeChatComposer\EasyWeChat;
class DelegationOptions
{
/**
* @var array
*/
protected $config = [
'enabled' => false,
];
/**
* @return $this
*/
public function enable()
{
$this->config['enabled'] = true;
return $this;
}
/**
* @return $this
*/
public function disable()
{
$this->config['enabled'] = false;
return $this;
}
/**
* @param bool $ability
*
* @return $this
*/
public function ability($ability)
{
$this->config['enabled'] = (bool) $ability;
return $this;
}
/**
* @param string $host
*
* @return $this
*/
public function toHost($host)
{
$this->config['host'] = $host;
return $this;
}
/**
* Destructor.
*/
public function __destruct()
{
EasyWeChat::mergeConfig([
'delegation' => $this->config,
]);
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Delegation;
use EasyWeChatComposer\Traits\MakesHttpRequests;
class DelegationTo
{
use MakesHttpRequests;
/**
* @var \EasyWeChat\Kernel\ServiceContainer
*/
protected $app;
/**
* @var array
*/
protected $identifiers = [];
/**
* @param \EasyWeChat\Kernel\ServiceContainer $app
* @param string $identifier
*/
public function __construct($app, $identifier)
{
$this->app = $app;
$this->push($identifier);
}
/**
* @param string $identifier
*/
public function push($identifier)
{
$this->identifiers[] = $identifier;
}
/**
* @param string $identifier
*
* @return $this
*/
public function __get($identifier)
{
$this->push($identifier);
return $this;
}
/**
* @param string $method
* @param array $arguments
*
* @return mixed
*/
public function __call($method, $arguments)
{
$config = array_intersect_key($this->app->getConfig(), array_flip(['app_id', 'secret', 'token', 'aes_key', 'response_type', 'component_app_id', 'refresh_token']));
$data = [
'config' => $config,
'application' => get_class($this->app),
'identifiers' => $this->identifiers,
'method' => $method,
'arguments' => $arguments,
];
return $this->request('easywechat-composer/delegate', $data);
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Delegation;
use EasyWeChat;
use EasyWeChatComposer\Http\DelegationResponse;
class Hydrate
{
/**
* @var array
*/
protected $attributes;
/**
* @param array $attributes
*/
public function __construct(array $attributes)
{
$this->attributes = $attributes;
}
/**
* @return array
*/
public function handle()
{
$app = $this->createsApplication()->shouldntDelegate();
foreach ($this->attributes['identifiers'] as $identifier) {
$app = $app->$identifier;
}
return call_user_func_array([$app, $this->attributes['method']], $this->attributes['arguments']);
}
/**
* @return \EasyWeChat\Kernel\ServiceContainer
*/
protected function createsApplication()
{
$application = $this->attributes['application'];
if ($application === EasyWeChat\OpenPlatform\Authorizer\OfficialAccount\Application::class) {
return $this->createsOpenPlatformApplication('officialAccount');
}
if ($application === EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Application::class) {
return $this->createsOpenPlatformApplication('miniProgram');
}
return new $application($this->buildConfig($this->attributes['config']));
}
protected function createsOpenPlatformApplication($type)
{
$config = $this->attributes['config'];
$authorizerAppId = $config['app_id'];
$config['app_id'] = $config['component_app_id'];
return EasyWeChat\Factory::openPlatform($this->buildConfig($config))->$type($authorizerAppId, $config['refresh_token']);
}
protected function buildConfig(array $config)
{
$config['response_type'] = DelegationResponse::class;
return $config;
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer;
use EasyWeChatComposer\Delegation\DelegationOptions;
class EasyWeChat
{
/**
* @var array
*/
protected static $config = [];
/**
* Encryption key.
*
* @var string
*/
protected static $encryptionKey;
/**
* @param array $config
*/
public static function mergeConfig(array $config)
{
static::$config = array_merge(static::$config, $config);
}
/**
* @return array
*/
public static function config()
{
return static::$config;
}
/**
* Set encryption key.
*
* @param string $key
*
* @return static
*/
public static function setEncryptionKey(string $key)
{
static::$encryptionKey = $key;
return new static();
}
/**
* Get encryption key.
*
* @return string
*/
public static function getEncryptionKey(): string
{
return static::$encryptionKey;
}
/**
* @return \EasyWeChatComposer\Delegation\DelegationOptions
*/
public static function withDelegation()
{
return new DelegationOptions();
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Encryption;
use EasyWeChatComposer\Contracts\Encrypter;
use EasyWeChatComposer\Exceptions\DecryptException;
use EasyWeChatComposer\Exceptions\EncryptException;
class DefaultEncrypter implements Encrypter
{
/**
* @var string
*/
protected $key;
/**
* @var string
*/
protected $cipher;
/**
* @param string $key
* @param string $cipher
*/
public function __construct($key, $cipher = 'AES-256-CBC')
{
$this->key = $key;
$this->cipher = $cipher;
}
/**
* Encrypt the given value.
*
* @param string $value
*
* @return string
*
* @throws \EasyWeChatComposer\Exceptions\EncryptException
*/
public function encrypt($value)
{
$iv = random_bytes(openssl_cipher_iv_length($this->cipher));
$value = openssl_encrypt($value, $this->cipher, $this->key, 0, $iv);
if ($value === false) {
throw new EncryptException('Could not encrypt the data.');
}
$iv = base64_encode($iv);
return base64_encode(json_encode(compact('iv', 'value')));
}
/**
* Decrypt the given value.
*
* @param string $payload
*
* @return string
*
* @throws \EasyWeChatComposer\Exceptions\DecryptException
*/
public function decrypt($payload)
{
$payload = json_decode(base64_decode($payload), true);
$iv = base64_decode($payload['iv']);
$decrypted = openssl_decrypt($payload['value'], $this->cipher, $this->key, 0, $iv);
if ($decrypted === false) {
throw new DecryptException('Could not decrypt the data.');
}
return $decrypted;
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Exceptions;
use Exception;
class DecryptException extends Exception
{
//
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Exceptions;
use Exception;
class DelegationException extends Exception
{
/**
* @var string
*/
protected $exception;
/**
* @param string $exception
*/
public function setException($exception)
{
$this->exception = $exception;
return $this;
}
/**
* @return string
*/
public function getException()
{
return $this->exception;
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Exceptions;
use Exception;
class EncryptException extends Exception
{
//
}

View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer;
use EasyWeChat\Kernel\Contracts\EventHandlerInterface;
use Pimple\Container;
use ReflectionClass;
class Extension
{
/**
* @var \Pimple\Container
*/
protected $app;
/**
* @var string
*/
protected $manifestPath;
/**
* @var array|null
*/
protected $manifest;
/**
* @param \Pimple\Container $app
*/
public function __construct(Container $app)
{
$this->app = $app;
$this->manifestPath = __DIR__.'/../extensions.php';
}
/**
* Get observers.
*
* @return array
*/
public function observers(): array
{
if ($this->shouldIgnore()) {
return [];
}
$observers = [];
foreach ($this->getManifest() as $name => $extra) {
$observers = array_merge($observers, $extra['observers'] ?? []);
}
return array_map([$this, 'listObserver'], array_filter($observers, [$this, 'validateObserver']));
}
/**
* @param mixed $observer
*
* @return bool
*/
protected function isDisable($observer): bool
{
return in_array($observer, $this->app->config->get('disable_observers', []));
}
/**
* Get the observers should be ignore.
*
* @return bool
*/
protected function shouldIgnore(): bool
{
return !file_exists($this->manifestPath) || $this->isDisable('*');
}
/**
* Validate the given observer.
*
* @param mixed $observer
*
* @return bool
*
* @throws \ReflectionException
*/
protected function validateObserver($observer): bool
{
return !$this->isDisable($observer)
&& (new ReflectionClass($observer))->implementsInterface(EventHandlerInterface::class)
&& $this->accessible($observer);
}
/**
* Determine whether the given observer is accessible.
*
* @param string $observer
*
* @return bool
*/
protected function accessible($observer): bool
{
if (!method_exists($observer, 'getAccessor')) {
return true;
}
return in_array(get_class($this->app), (array) $observer::getAccessor());
}
/**
* @param mixed $observer
*
* @return array
*/
protected function listObserver($observer): array
{
$condition = method_exists($observer, 'onCondition') ? $observer::onCondition() : '*';
return [$observer, $condition];
}
/**
* Get the easywechat manifest.
*
* @return array
*/
protected function getManifest(): array
{
if (!is_null($this->manifest)) {
return $this->manifest;
}
return $this->manifest = file_exists($this->manifestPath) ? require $this->manifestPath : [];
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Http;
class DelegationResponse extends Response
{
/**
* @return string
*/
public function getBodyContents()
{
return $this->response->getBodyContents();
}
}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Http;
use EasyWeChat\Kernel\Contracts\Arrayable;
use EasyWeChat\Kernel\Http\Response as HttpResponse;
use JsonSerializable;
class Response implements Arrayable, JsonSerializable
{
/**
* @var \EasyWeChat\Kernel\Http\Response
*/
protected $response;
/**
* @var array
*/
protected $array;
/**
* @param \EasyWeChat\Kernel\Http\Response $response
*/
public function __construct(HttpResponse $response)
{
$this->response = $response;
}
/**
* @see \ArrayAccess::offsetExists
*
* @param string $offset
*
* @return bool
*/
public function offsetExists($offset)
{
return isset($this->toArray()[$offset]);
}
/**
* @see \ArrayAccess::offsetGet
*
* @param string $offset
*
* @return mixed
*/
public function offsetGet($offset)
{
return $this->toArray()[$offset] ?? null;
}
/**
* @see \ArrayAccess::offsetSet
*
* @param string $offset
* @param mixed $value
*/
public function offsetSet($offset, $value)
{
//
}
/**
* @see \ArrayAccess::offsetUnset
*
* @param string $offset
*/
public function offsetUnset($offset)
{
//
}
/**
* Get the instance as an array.
*
* @return array
*/
public function toArray()
{
return $this->array ?: $this->array = $this->response->toArray();
}
/**
* Convert the object into something JSON serializable.
*
* @return array
*/
public function jsonSerialize()
{
return $this->toArray();
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Laravel\Http\Controllers;
use EasyWeChatComposer\Delegation\Hydrate;
use EasyWeChatComposer\Encryption\DefaultEncrypter;
use Illuminate\Http\Request;
use Throwable;
class DelegatesController
{
/**
* @param \Illuminate\Http\Request $request
* @param \EasyWeChatComposer\Encryption\DefaultEncrypter $encrypter
*
* @return \Illuminate\Http\Response
*/
public function __invoke(Request $request, DefaultEncrypter $encrypter)
{
try {
$data = json_decode($encrypter->decrypt($request->get('encrypted')), true);
$hydrate = new Hydrate($data);
$response = $hydrate->handle();
return response()->json([
'response_type' => get_class($response),
'response' => $encrypter->encrypt($response->getBodyContents()),
]);
} catch (Throwable $t) {
return [
'exception' => get_class($t),
'message' => $t->getMessage(),
];
}
}
}

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Laravel;
use EasyWeChatComposer\EasyWeChat;
use EasyWeChatComposer\Encryption\DefaultEncrypter;
use Illuminate\Foundation\Application;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
use RuntimeException;
class ServiceProvider extends LaravelServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot()
{
$this->registerRoutes();
$this->publishes([
__DIR__.'/config.php' => config_path('easywechat-composer.php'),
]);
EasyWeChat::setEncryptionKey(
$defaultKey = $this->getKey()
);
EasyWeChat::withDelegation()
->toHost($this->config('delegation.host'))
->ability($this->config('delegation.enabled'));
$this->app->when(DefaultEncrypter::class)->needs('$key')->give($defaultKey);
}
/**
* Register routes.
*/
protected function registerRoutes()
{
Route::prefix('easywechat-composer')->namespace('EasyWeChatComposer\Laravel\Http\Controllers')->group(function () {
$this->loadRoutesFrom(__DIR__.'/routes.php');
});
}
/**
* Register any application services.
*/
public function register()
{
$this->configure();
}
/**
* Register config.
*/
protected function configure()
{
$this->mergeConfigFrom(
__DIR__.'/config.php', 'easywechat-composer'
);
}
/**
* Get the specified configuration value.
*
* @param string|null $key
* @param mixed $default
*
* @return mixed
*/
protected function config($key = null, $default = null)
{
$config = $this->app['config']->get('easywechat-composer');
if (is_null($key)) {
return $config;
}
return Arr::get($config, $key, $default);
}
/**
* @return string
*/
protected function getKey()
{
return $this->config('encryption.key') ?: $this->getMd5Key();
}
/**
* @return string
*/
protected function getMd5Key()
{
$ttl = (version_compare(Application::VERSION, '5.8') === -1) ? 30 : 1800;
return Cache::remember('easywechat-composer.encryption_key', $ttl, function () {
throw_unless(file_exists($path = base_path('composer.lock')), RuntimeException::class, 'No encryption key provided.');
return md5_file($path);
});
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) mingyoung <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
return [
'encryption' => [
'key' => env('EASYWECHAT_KEY'),
],
'delegation' => [
'enabled' => env('EASYWECHAT_DELEGATION', false),
'host' => env('EASYWECHAT_DELEGATION_HOST'),
],
];

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
use Illuminate\Support\Facades\Route;
Route::post('delegate', 'DelegatesController');

View File

@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer;
use Composer\Plugin\PluginInterface;
class ManifestManager
{
const PACKAGE_TYPE = 'easywechat-extension';
const EXTRA_OBSERVER = 'observers';
/**
* The vendor path.
*
* @var string
*/
protected $vendorPath;
/**
* The manifest path.
*
* @var string
*/
protected $manifestPath;
/**
* @param string $vendorPath
* @param string|null $manifestPath
*/
public function __construct(string $vendorPath, string $manifestPath = null)
{
$this->vendorPath = $vendorPath;
$this->manifestPath = $manifestPath ?: $vendorPath.'/easywechat-composer/easywechat-composer/extensions.php';
}
/**
* Remove manifest file.
*
* @return $this
*/
public function unlink()
{
if (file_exists($this->manifestPath)) {
@unlink($this->manifestPath);
}
return $this;
}
/**
* Build the manifest file.
*/
public function build()
{
$packages = [];
if (file_exists($installed = $this->vendorPath.'/composer/installed.json')) {
$packages = json_decode(file_get_contents($installed), true);
if (version_compare(PluginInterface::PLUGIN_API_VERSION, '2.0.0', 'ge')) {
$packages = $packages['packages'];
}
}
$this->write($this->map($packages));
}
/**
* @param array $packages
*
* @return array
*/
protected function map(array $packages): array
{
$manifest = [];
$packages = array_filter($packages, function ($package) {
if(isset($package['type'])){
return $package['type'] === self::PACKAGE_TYPE;
}
});
foreach ($packages as $package) {
$manifest[$package['name']] = [self::EXTRA_OBSERVER => $package['extra'][self::EXTRA_OBSERVER] ?? []];
}
return $manifest;
}
/**
* Write the manifest array to a file.
*
* @param array $manifest
*/
protected function write(array $manifest)
{
file_put_contents(
$this->manifestPath,
'<?php return '.var_export($manifest, true).';'
);
$this->invalidate($this->manifestPath);
}
/**
* Invalidate the given file.
*
* @param string $file
*/
protected function invalidate($file)
{
if (function_exists('opcache_invalidate')) {
@opcache_invalidate($file, true);
}
}
}

View File

@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer;
use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\Installer\PackageEvent;
use Composer\Installer\PackageEvents;
use Composer\IO\IOInterface;
use Composer\Plugin\Capable;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
class Plugin implements PluginInterface, EventSubscriberInterface, Capable
{
/**
* @var bool
*/
protected $activated = true;
/**
* Apply plugin modifications to Composer.
*/
public function activate(Composer $composer, IOInterface $io)
{
//
}
/**
* Remove any hooks from Composer.
*
* This will be called when a plugin is deactivated before being
* uninstalled, but also before it gets upgraded to a new version
* so the old one can be deactivated and the new one activated.
*/
public function deactivate(Composer $composer, IOInterface $io)
{
//
}
/**
* Prepare the plugin to be uninstalled.
*
* This will be called after deactivate.
*/
public function uninstall(Composer $composer, IOInterface $io)
{
}
/**
* @return array
*/
public function getCapabilities()
{
return [
'Composer\Plugin\Capability\CommandProvider' => 'EasyWeChatComposer\Commands\Provider',
];
}
/**
* Listen events.
*
* @return array
*/
public static function getSubscribedEvents()
{
return [
PackageEvents::PRE_PACKAGE_UNINSTALL => 'prePackageUninstall',
ScriptEvents::POST_AUTOLOAD_DUMP => 'postAutoloadDump',
];
}
/**
* @param \Composer\Installer\PackageEvent
*/
public function prePackageUninstall(PackageEvent $event)
{
if ($event->getOperation()->getPackage()->getName() === 'overtrue/wechat') {
$this->activated = false;
}
}
public function postAutoloadDump(Event $event)
{
if (!$this->activated) {
return;
}
$manifest = new ManifestManager(
rtrim($event->getComposer()->getConfig()->get('vendor-dir'), '/')
);
$manifest->unlink()->build();
}
}

View File

@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Traits;
use EasyWeChat\Kernel\Http\StreamResponse;
use EasyWeChat\Kernel\Traits\ResponseCastable;
use EasyWeChatComposer\Contracts\Encrypter;
use EasyWeChatComposer\EasyWeChat;
use EasyWeChatComposer\Encryption\DefaultEncrypter;
use EasyWeChatComposer\Exceptions\DelegationException;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
trait MakesHttpRequests
{
use ResponseCastable;
/**
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* @var \EasyWeChatComposer\Contracts\Encrypter
*/
protected $encrypter;
/**
* @param string $endpoint
* @param array $payload
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
protected function request($endpoint, array $payload)
{
$response = $this->getHttpClient()->request('POST', $endpoint, [
'form_params' => $this->buildFormParams($payload),
]);
$parsed = $this->parseResponse($response);
return $this->detectAndCastResponseToType(
$this->getEncrypter()->decrypt($parsed['response']),
($parsed['response_type'] === StreamResponse::class) ? 'raw' : $this->app['config']['response_type']
);
}
/**
* @param array $payload
*
* @return array
*/
protected function buildFormParams($payload)
{
return [
'encrypted' => $this->getEncrypter()->encrypt(json_encode($payload)),
];
}
/**
* @param \Psr\Http\Message\ResponseInterface $response
*
* @return array
*/
protected function parseResponse($response)
{
$result = json_decode((string) $response->getBody(), true);
if (isset($result['exception'])) {
throw (new DelegationException($result['message']))->setException($result['exception']);
}
return $result;
}
/**
* @return \GuzzleHttp\ClientInterface
*/
protected function getHttpClient(): ClientInterface
{
return $this->httpClient ?: $this->httpClient = new Client([
'base_uri' => $this->app['config']['delegation']['host'],
]);
}
/**
* @return \EasyWeChatComposer\Contracts\Encrypter
*/
protected function getEncrypter(): Encrypter
{
return $this->encrypter ?: $this->encrypter = new DefaultEncrypter(
EasyWeChat::getEncryptionKey()
);
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Traits;
use EasyWeChat\Kernel\BaseClient;
use EasyWeChatComposer\Delegation\DelegationTo;
use EasyWeChatComposer\EasyWeChat;
trait WithAggregator
{
/**
* Aggregate.
*/
protected function aggregate()
{
foreach (EasyWeChat::config() as $key => $value) {
$this['config']->set($key, $value);
}
}
/**
* @return bool
*/
public function shouldDelegate($id)
{
return $this['config']->get('delegation.enabled')
&& $this->offsetGet($id) instanceof BaseClient;
}
/**
* @return $this
*/
public function shouldntDelegate()
{
$this['config']->set('delegation.enabled', false);
return $this;
}
/**
* @param string $id
*
* @return \EasyWeChatComposer\Delegation
*/
public function delegateTo($id)
{
return new DelegationTo($this, $id);
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* This file is part of the EasyWeChatComposer.
*
* (c) 张铭阳 <mingyoungcheung@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChatComposer\Tests;
use EasyWeChatComposer\ManifestManager;
use PHPUnit\Framework\TestCase;
class ManifestManagerTest extends TestCase
{
private $vendorPath;
private $manifestPath;
protected function getManifestManager()
{
return new ManifestManager(
$this->vendorPath = __DIR__.'/__fixtures__/vendor/',
$this->manifestPath = __DIR__.'/__fixtures__/extensions.php'
);
}
public function testUnlink()
{
$this->assertInstanceOf(ManifestManager::class, $this->getManifestManager()->unlink());
$this->assertFalse(file_exists($this->manifestPath));
}
}