├── .gitignore ├── .php_cs ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Commands │ ├── ExtensionsCommand.php │ └── Provider.php ├── Contracts │ └── Encrypter.php ├── Delegation │ ├── DelegationOptions.php │ ├── DelegationTo.php │ └── Hydrate.php ├── EasyWeChat.php ├── Encryption │ └── DefaultEncrypter.php ├── Exceptions │ ├── DecryptException.php │ ├── DelegationException.php │ └── EncryptException.php ├── Extension.php ├── Http │ ├── DelegationResponse.php │ └── Response.php ├── Laravel │ ├── Http │ │ └── Controllers │ │ │ └── DelegatesController.php │ ├── ServiceProvider.php │ ├── config.php │ └── routes.php ├── ManifestManager.php ├── Plugin.php └── Traits │ ├── MakesHttpRequests.php │ └── WithAggregator.php └── tests └── ManifestManagerTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /vendor 3 | composer.lock 4 | extensions.php 5 | .php_cs.cache 6 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | 7 | 8 | This source file is subject to the MIT license that is bundled 9 | with this source code in the file LICENSE. 10 | EOF; 11 | 12 | return PhpCsFixer\Config::create() 13 | ->setRiskyAllowed(true) 14 | ->setRules([ 15 | '@Symfony' => true, 16 | 'header_comment' => ['header' => $header], 17 | 'declare_strict_types' => true, 18 | 'ordered_imports' => true, 19 | 'strict_comparison' => true, 20 | 'no_empty_comment' => false, 21 | 'yoda_style' => false, 22 | ]) 23 | ->setFinder( 24 | PhpCsFixer\Finder::create() 25 | ->exclude('vendor') 26 | ->notPath('src/Laravel/config.php', 'src/Laravel/routes.php') 27 | ->in(__DIR__) 28 | ) 29 | ; 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.0 5 | - 7.1 6 | - 7.2 7 | - 7.3 8 | 9 | install: 10 | - travis_retry composer install --no-interaction --no-suggest 11 | 12 | script: ./vendor/bin/phpunit 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 张铭阳 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

EasyWeChat Composer Plugin

3 |

4 | 5 |

6 | Build Status 7 | Scrutinizer Code Quality 8 | Latest Stable Version 9 | Total Downloads 10 | License 11 |

12 | 13 | Usage 14 | --- 15 | 16 | Set the `type` to be `easywechat-extension` in your package composer.json file: 17 | 18 | ```json 19 | { 20 | "name": "your/package", 21 | "type": "easywechat-extension" 22 | } 23 | ``` 24 | 25 | Specify server observer classes in the extra section: 26 | 27 | ```json 28 | { 29 | "name": "your/package", 30 | "type": "easywechat-extension", 31 | "extra": { 32 | "observers": [ 33 | "Acme\\Observers\\Handler" 34 | ] 35 | } 36 | } 37 | ``` 38 | 39 | Examples 40 | --- 41 | * [easywechat-composer/open-platform-testcase](https://github.com/mingyoung/open-platform-testcase) 42 | 43 | Server Delegation 44 | --- 45 | 46 | > 目前仅支持 Laravel 47 | 48 | 1. 在 `config/app.php` 中添加 `EasyWeChatComposer\Laravel\ServiceProvider::class` 49 | 50 | 2. 在**本地项目**的 `.env` 文件中添加如下配置: 51 | 52 | ``` 53 | EASYWECHAT_DELEGATION=true # false 则不启用 54 | EASYWECHAT_DELEGATION_HOST=https://example.com # 线上域名 55 | ``` 56 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easywechat-composer/easywechat-composer", 3 | "description": "The composer plugin for EasyWeChat", 4 | "type": "composer-plugin", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "张铭阳", 9 | "email": "mingyoungcheung@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.0", 14 | "composer-plugin-api": "^1.0 || ^2.0" 15 | }, 16 | "require-dev": { 17 | "composer/composer": "^1.0 || ^2.0", 18 | "phpunit/phpunit": "^6.5 || ^7.0" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "EasyWeChatComposer\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "EasyWeChatComposer\\Tests\\": "tests/" 28 | } 29 | }, 30 | "extra": { 31 | "class": "EasyWeChatComposer\\Plugin" 32 | }, 33 | "minimum-stability": "dev", 34 | "prefer-stable": true 35 | } 36 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | tests 13 | 14 | 15 | 16 | 17 | src 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Commands/ExtensionsCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Commands; 15 | 16 | use Composer\Command\BaseCommand; 17 | use Symfony\Component\Console\Helper\Table; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Output\OutputInterface; 20 | 21 | class ExtensionsCommand extends BaseCommand 22 | { 23 | /** 24 | * Configures the current command. 25 | */ 26 | protected function configure() 27 | { 28 | $this->setName('easywechat:extensions') 29 | ->setDescription('Lists all installed extensions.'); 30 | } 31 | 32 | /** 33 | * Executes the current command. 34 | * 35 | * @param InputInterface $input 36 | * @param OutputInterface $output 37 | */ 38 | protected function execute(InputInterface $input, OutputInterface $output) 39 | { 40 | $extensions = require __DIR__.'/../../extensions.php'; 41 | 42 | if (empty($extensions) || !is_array($extensions)) { 43 | return $output->writeln('No extension installed.'); 44 | } 45 | 46 | $table = new Table($output); 47 | $table->setHeaders(['Name', 'Observers']) 48 | ->setRows( 49 | array_map([$this, 'getRows'], array_keys($extensions), $extensions) 50 | )->render(); 51 | } 52 | 53 | /** 54 | * @param string $name 55 | * @param array $extension 56 | * 57 | * @return array 58 | */ 59 | protected function getRows($name, $extension) 60 | { 61 | return [$name, implode("\n", $extension['observers'] ?? [])]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Commands/Provider.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Commands; 15 | 16 | use Composer\Plugin\Capability\CommandProvider; 17 | 18 | class Provider implements CommandProvider 19 | { 20 | /** 21 | * Retrieves an array of commands. 22 | * 23 | * @return \Composer\Command\BaseCommand[] 24 | */ 25 | public function getCommands() 26 | { 27 | return [ 28 | new ExtensionsCommand(), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Contracts/Encrypter.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Contracts; 15 | 16 | interface Encrypter 17 | { 18 | /** 19 | * Encrypt the given value. 20 | * 21 | * @param string $value 22 | * 23 | * @return string 24 | */ 25 | public function encrypt($value); 26 | 27 | /** 28 | * Decrypt the given value. 29 | * 30 | * @param string $payload 31 | * 32 | * @return string 33 | */ 34 | public function decrypt($payload); 35 | } 36 | -------------------------------------------------------------------------------- /src/Delegation/DelegationOptions.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Delegation; 15 | 16 | use EasyWeChatComposer\EasyWeChat; 17 | 18 | class DelegationOptions 19 | { 20 | /** 21 | * @var array 22 | */ 23 | protected $config = [ 24 | 'enabled' => false, 25 | ]; 26 | 27 | /** 28 | * @return $this 29 | */ 30 | public function enable() 31 | { 32 | $this->config['enabled'] = true; 33 | 34 | return $this; 35 | } 36 | 37 | /** 38 | * @return $this 39 | */ 40 | public function disable() 41 | { 42 | $this->config['enabled'] = false; 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * @param bool $ability 49 | * 50 | * @return $this 51 | */ 52 | public function ability($ability) 53 | { 54 | $this->config['enabled'] = (bool) $ability; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * @param string $host 61 | * 62 | * @return $this 63 | */ 64 | public function toHost($host) 65 | { 66 | $this->config['host'] = $host; 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * Destructor. 73 | */ 74 | public function __destruct() 75 | { 76 | EasyWeChat::mergeConfig([ 77 | 'delegation' => $this->config, 78 | ]); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Delegation/DelegationTo.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Delegation; 15 | 16 | use EasyWeChatComposer\Traits\MakesHttpRequests; 17 | 18 | class DelegationTo 19 | { 20 | use MakesHttpRequests; 21 | 22 | /** 23 | * @var \EasyWeChat\Kernel\ServiceContainer 24 | */ 25 | protected $app; 26 | 27 | /** 28 | * @var array 29 | */ 30 | protected $identifiers = []; 31 | 32 | /** 33 | * @param \EasyWeChat\Kernel\ServiceContainer $app 34 | * @param string $identifier 35 | */ 36 | public function __construct($app, $identifier) 37 | { 38 | $this->app = $app; 39 | 40 | $this->push($identifier); 41 | } 42 | 43 | /** 44 | * @param string $identifier 45 | */ 46 | public function push($identifier) 47 | { 48 | $this->identifiers[] = $identifier; 49 | } 50 | 51 | /** 52 | * @param string $identifier 53 | * 54 | * @return $this 55 | */ 56 | public function __get($identifier) 57 | { 58 | $this->push($identifier); 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * @param string $method 65 | * @param array $arguments 66 | * 67 | * @return mixed 68 | */ 69 | public function __call($method, $arguments) 70 | { 71 | $config = array_intersect_key($this->app->getConfig(), array_flip(['app_id', 'secret', 'token', 'aes_key', 'response_type', 'component_app_id', 'refresh_token'])); 72 | 73 | $data = [ 74 | 'config' => $config, 75 | 'application' => get_class($this->app), 76 | 'identifiers' => $this->identifiers, 77 | 'method' => $method, 78 | 'arguments' => $arguments, 79 | ]; 80 | 81 | return $this->request('easywechat-composer/delegate', $data); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Delegation/Hydrate.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Delegation; 15 | 16 | use EasyWeChat; 17 | use EasyWeChatComposer\Http\DelegationResponse; 18 | 19 | class Hydrate 20 | { 21 | /** 22 | * @var array 23 | */ 24 | protected $attributes; 25 | 26 | /** 27 | * @param array $attributes 28 | */ 29 | public function __construct(array $attributes) 30 | { 31 | $this->attributes = $attributes; 32 | } 33 | 34 | /** 35 | * @return array 36 | */ 37 | public function handle() 38 | { 39 | $app = $this->createsApplication()->shouldntDelegate(); 40 | 41 | foreach ($this->attributes['identifiers'] as $identifier) { 42 | $app = $app->$identifier; 43 | } 44 | 45 | return call_user_func_array([$app, $this->attributes['method']], $this->attributes['arguments']); 46 | } 47 | 48 | /** 49 | * @return \EasyWeChat\Kernel\ServiceContainer 50 | */ 51 | protected function createsApplication() 52 | { 53 | $application = $this->attributes['application']; 54 | 55 | if ($application === EasyWeChat\OpenPlatform\Authorizer\OfficialAccount\Application::class) { 56 | return $this->createsOpenPlatformApplication('officialAccount'); 57 | } 58 | 59 | if ($application === EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Application::class) { 60 | return $this->createsOpenPlatformApplication('miniProgram'); 61 | } 62 | 63 | return new $application($this->buildConfig($this->attributes['config'])); 64 | } 65 | 66 | protected function createsOpenPlatformApplication($type) 67 | { 68 | $config = $this->attributes['config']; 69 | 70 | $authorizerAppId = $config['app_id']; 71 | 72 | $config['app_id'] = $config['component_app_id']; 73 | 74 | return EasyWeChat\Factory::openPlatform($this->buildConfig($config))->$type($authorizerAppId, $config['refresh_token']); 75 | } 76 | 77 | protected function buildConfig(array $config) 78 | { 79 | $config['response_type'] = DelegationResponse::class; 80 | 81 | return $config; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/EasyWeChat.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer; 15 | 16 | use EasyWeChatComposer\Delegation\DelegationOptions; 17 | 18 | class EasyWeChat 19 | { 20 | /** 21 | * @var array 22 | */ 23 | protected static $config = []; 24 | 25 | /** 26 | * Encryption key. 27 | * 28 | * @var string 29 | */ 30 | protected static $encryptionKey; 31 | 32 | /** 33 | * @param array $config 34 | */ 35 | public static function mergeConfig(array $config) 36 | { 37 | static::$config = array_merge(static::$config, $config); 38 | } 39 | 40 | /** 41 | * @return array 42 | */ 43 | public static function config() 44 | { 45 | return static::$config; 46 | } 47 | 48 | /** 49 | * Set encryption key. 50 | * 51 | * @param string $key 52 | * 53 | * @return static 54 | */ 55 | public static function setEncryptionKey(string $key) 56 | { 57 | static::$encryptionKey = $key; 58 | 59 | return new static(); 60 | } 61 | 62 | /** 63 | * Get encryption key. 64 | * 65 | * @return string 66 | */ 67 | public static function getEncryptionKey(): string 68 | { 69 | return static::$encryptionKey; 70 | } 71 | 72 | /** 73 | * @return \EasyWeChatComposer\Delegation\DelegationOptions 74 | */ 75 | public static function withDelegation() 76 | { 77 | return new DelegationOptions(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Encryption/DefaultEncrypter.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Encryption; 15 | 16 | use EasyWeChatComposer\Contracts\Encrypter; 17 | use EasyWeChatComposer\Exceptions\DecryptException; 18 | use EasyWeChatComposer\Exceptions\EncryptException; 19 | 20 | class DefaultEncrypter implements Encrypter 21 | { 22 | /** 23 | * @var string 24 | */ 25 | protected $key; 26 | 27 | /** 28 | * @var string 29 | */ 30 | protected $cipher; 31 | 32 | /** 33 | * @param string $key 34 | * @param string $cipher 35 | */ 36 | public function __construct($key, $cipher = 'AES-256-CBC') 37 | { 38 | $this->key = $key; 39 | $this->cipher = $cipher; 40 | } 41 | 42 | /** 43 | * Encrypt the given value. 44 | * 45 | * @param string $value 46 | * 47 | * @return string 48 | * 49 | * @throws \EasyWeChatComposer\Exceptions\EncryptException 50 | */ 51 | public function encrypt($value) 52 | { 53 | $iv = random_bytes(openssl_cipher_iv_length($this->cipher)); 54 | 55 | $value = openssl_encrypt($value, $this->cipher, $this->key, 0, $iv); 56 | 57 | if ($value === false) { 58 | throw new EncryptException('Could not encrypt the data.'); 59 | } 60 | 61 | $iv = base64_encode($iv); 62 | 63 | return base64_encode(json_encode(compact('iv', 'value'))); 64 | } 65 | 66 | /** 67 | * Decrypt the given value. 68 | * 69 | * @param string $payload 70 | * 71 | * @return string 72 | * 73 | * @throws \EasyWeChatComposer\Exceptions\DecryptException 74 | */ 75 | public function decrypt($payload) 76 | { 77 | $payload = json_decode(base64_decode($payload), true); 78 | 79 | $iv = base64_decode($payload['iv']); 80 | 81 | $decrypted = openssl_decrypt($payload['value'], $this->cipher, $this->key, 0, $iv); 82 | 83 | if ($decrypted === false) { 84 | throw new DecryptException('Could not decrypt the data.'); 85 | } 86 | 87 | return $decrypted; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Exceptions/DecryptException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Exceptions; 15 | 16 | use Exception; 17 | 18 | class DecryptException extends Exception 19 | { 20 | // 21 | } 22 | -------------------------------------------------------------------------------- /src/Exceptions/DelegationException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Exceptions; 15 | 16 | use Exception; 17 | 18 | class DelegationException extends Exception 19 | { 20 | /** 21 | * @var string 22 | */ 23 | protected $exception; 24 | 25 | /** 26 | * @param string $exception 27 | */ 28 | public function setException($exception) 29 | { 30 | $this->exception = $exception; 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getException() 39 | { 40 | return $this->exception; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Exceptions/EncryptException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Exceptions; 15 | 16 | use Exception; 17 | 18 | class EncryptException extends Exception 19 | { 20 | // 21 | } 22 | -------------------------------------------------------------------------------- /src/Extension.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer; 15 | 16 | use EasyWeChat\Kernel\Contracts\EventHandlerInterface; 17 | use Pimple\Container; 18 | use ReflectionClass; 19 | 20 | class Extension 21 | { 22 | /** 23 | * @var \Pimple\Container 24 | */ 25 | protected $app; 26 | 27 | /** 28 | * @var string 29 | */ 30 | protected $manifestPath; 31 | 32 | /** 33 | * @var array|null 34 | */ 35 | protected $manifest; 36 | 37 | /** 38 | * @param \Pimple\Container $app 39 | */ 40 | public function __construct(Container $app) 41 | { 42 | $this->app = $app; 43 | $this->manifestPath = __DIR__.'/../extensions.php'; 44 | } 45 | 46 | /** 47 | * Get observers. 48 | * 49 | * @return array 50 | */ 51 | public function observers(): array 52 | { 53 | if ($this->shouldIgnore()) { 54 | return []; 55 | } 56 | 57 | $observers = []; 58 | 59 | foreach ($this->getManifest() as $name => $extra) { 60 | $observers = array_merge($observers, $extra['observers'] ?? []); 61 | } 62 | 63 | return array_map([$this, 'listObserver'], array_filter($observers, [$this, 'validateObserver'])); 64 | } 65 | 66 | /** 67 | * @param mixed $observer 68 | * 69 | * @return bool 70 | */ 71 | protected function isDisable($observer): bool 72 | { 73 | return in_array($observer, $this->app->config->get('disable_observers', [])); 74 | } 75 | 76 | /** 77 | * Get the observers should be ignore. 78 | * 79 | * @return bool 80 | */ 81 | protected function shouldIgnore(): bool 82 | { 83 | return !file_exists($this->manifestPath) || $this->isDisable('*'); 84 | } 85 | 86 | /** 87 | * Validate the given observer. 88 | * 89 | * @param mixed $observer 90 | * 91 | * @return bool 92 | * 93 | * @throws \ReflectionException 94 | */ 95 | protected function validateObserver($observer): bool 96 | { 97 | return !$this->isDisable($observer) 98 | && (new ReflectionClass($observer))->implementsInterface(EventHandlerInterface::class) 99 | && $this->accessible($observer); 100 | } 101 | 102 | /** 103 | * Determine whether the given observer is accessible. 104 | * 105 | * @param string $observer 106 | * 107 | * @return bool 108 | */ 109 | protected function accessible($observer): bool 110 | { 111 | if (!method_exists($observer, 'getAccessor')) { 112 | return true; 113 | } 114 | 115 | return in_array(get_class($this->app), (array) $observer::getAccessor()); 116 | } 117 | 118 | /** 119 | * @param mixed $observer 120 | * 121 | * @return array 122 | */ 123 | protected function listObserver($observer): array 124 | { 125 | $condition = method_exists($observer, 'onCondition') ? $observer::onCondition() : '*'; 126 | 127 | return [$observer, $condition]; 128 | } 129 | 130 | /** 131 | * Get the easywechat manifest. 132 | * 133 | * @return array 134 | */ 135 | protected function getManifest(): array 136 | { 137 | if (!is_null($this->manifest)) { 138 | return $this->manifest; 139 | } 140 | 141 | return $this->manifest = file_exists($this->manifestPath) ? require $this->manifestPath : []; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Http/DelegationResponse.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Http; 15 | 16 | class DelegationResponse extends Response 17 | { 18 | /** 19 | * @return string 20 | */ 21 | public function getBodyContents() 22 | { 23 | return $this->response->getBodyContents(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Http/Response.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Http; 15 | 16 | use EasyWeChat\Kernel\Contracts\Arrayable; 17 | use EasyWeChat\Kernel\Http\Response as HttpResponse; 18 | use JsonSerializable; 19 | 20 | class Response implements Arrayable, JsonSerializable 21 | { 22 | /** 23 | * @var \EasyWeChat\Kernel\Http\Response 24 | */ 25 | protected $response; 26 | 27 | /** 28 | * @var array 29 | */ 30 | protected $array; 31 | 32 | /** 33 | * @param \EasyWeChat\Kernel\Http\Response $response 34 | */ 35 | public function __construct(HttpResponse $response) 36 | { 37 | $this->response = $response; 38 | } 39 | 40 | /** 41 | * @see \ArrayAccess::offsetExists 42 | * 43 | * @param string $offset 44 | * 45 | * @return bool 46 | */ 47 | public function offsetExists($offset) 48 | { 49 | return isset($this->toArray()[$offset]); 50 | } 51 | 52 | /** 53 | * @see \ArrayAccess::offsetGet 54 | * 55 | * @param string $offset 56 | * 57 | * @return mixed 58 | */ 59 | public function offsetGet($offset) 60 | { 61 | return $this->toArray()[$offset] ?? null; 62 | } 63 | 64 | /** 65 | * @see \ArrayAccess::offsetSet 66 | * 67 | * @param string $offset 68 | * @param mixed $value 69 | */ 70 | public function offsetSet($offset, $value) 71 | { 72 | // 73 | } 74 | 75 | /** 76 | * @see \ArrayAccess::offsetUnset 77 | * 78 | * @param string $offset 79 | */ 80 | public function offsetUnset($offset) 81 | { 82 | // 83 | } 84 | 85 | /** 86 | * Get the instance as an array. 87 | * 88 | * @return array 89 | */ 90 | public function toArray() 91 | { 92 | return $this->array ?: $this->array = $this->response->toArray(); 93 | } 94 | 95 | /** 96 | * Convert the object into something JSON serializable. 97 | * 98 | * @return array 99 | */ 100 | public function jsonSerialize() 101 | { 102 | return $this->toArray(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Laravel/Http/Controllers/DelegatesController.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Laravel\Http\Controllers; 15 | 16 | use EasyWeChatComposer\Delegation\Hydrate; 17 | use EasyWeChatComposer\Encryption\DefaultEncrypter; 18 | use Illuminate\Http\Request; 19 | use Throwable; 20 | 21 | class DelegatesController 22 | { 23 | /** 24 | * @param \Illuminate\Http\Request $request 25 | * @param \EasyWeChatComposer\Encryption\DefaultEncrypter $encrypter 26 | * 27 | * @return \Illuminate\Http\Response 28 | */ 29 | public function __invoke(Request $request, DefaultEncrypter $encrypter) 30 | { 31 | try { 32 | $data = json_decode($encrypter->decrypt($request->get('encrypted')), true); 33 | 34 | $hydrate = new Hydrate($data); 35 | 36 | $response = $hydrate->handle(); 37 | 38 | return response()->json([ 39 | 'response_type' => get_class($response), 40 | 'response' => $encrypter->encrypt($response->getBodyContents()), 41 | ]); 42 | } catch (Throwable $t) { 43 | return [ 44 | 'exception' => get_class($t), 45 | 'message' => $t->getMessage(), 46 | ]; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Laravel/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Laravel; 15 | 16 | use EasyWeChatComposer\EasyWeChat; 17 | use EasyWeChatComposer\Encryption\DefaultEncrypter; 18 | use Illuminate\Foundation\Application; 19 | use Illuminate\Support\Arr; 20 | use Illuminate\Support\Facades\Cache; 21 | use Illuminate\Support\Facades\Route; 22 | use Illuminate\Support\ServiceProvider as LaravelServiceProvider; 23 | use RuntimeException; 24 | 25 | class ServiceProvider extends LaravelServiceProvider 26 | { 27 | /** 28 | * Bootstrap any application services. 29 | */ 30 | public function boot() 31 | { 32 | $this->registerRoutes(); 33 | $this->publishes([ 34 | __DIR__.'/config.php' => config_path('easywechat-composer.php'), 35 | ]); 36 | 37 | EasyWeChat::setEncryptionKey( 38 | $defaultKey = $this->getKey() 39 | ); 40 | 41 | EasyWeChat::withDelegation() 42 | ->toHost($this->config('delegation.host')) 43 | ->ability($this->config('delegation.enabled')); 44 | 45 | $this->app->when(DefaultEncrypter::class)->needs('$key')->give($defaultKey); 46 | } 47 | 48 | /** 49 | * Register routes. 50 | */ 51 | protected function registerRoutes() 52 | { 53 | Route::prefix('easywechat-composer')->namespace('EasyWeChatComposer\Laravel\Http\Controllers')->group(function () { 54 | $this->loadRoutesFrom(__DIR__.'/routes.php'); 55 | }); 56 | } 57 | 58 | /** 59 | * Register any application services. 60 | */ 61 | public function register() 62 | { 63 | $this->configure(); 64 | } 65 | 66 | /** 67 | * Register config. 68 | */ 69 | protected function configure() 70 | { 71 | $this->mergeConfigFrom( 72 | __DIR__.'/config.php', 'easywechat-composer' 73 | ); 74 | } 75 | 76 | /** 77 | * Get the specified configuration value. 78 | * 79 | * @param string|null $key 80 | * @param mixed $default 81 | * 82 | * @return mixed 83 | */ 84 | protected function config($key = null, $default = null) 85 | { 86 | $config = $this->app['config']->get('easywechat-composer'); 87 | 88 | if (is_null($key)) { 89 | return $config; 90 | } 91 | 92 | return Arr::get($config, $key, $default); 93 | } 94 | 95 | /** 96 | * @return string 97 | */ 98 | protected function getKey() 99 | { 100 | return $this->config('encryption.key') ?: $this->getMd5Key(); 101 | } 102 | 103 | /** 104 | * @return string 105 | */ 106 | protected function getMd5Key() 107 | { 108 | $ttl = (version_compare(Application::VERSION, '5.8') === -1) ? 30 : 1800; 109 | 110 | return Cache::remember('easywechat-composer.encryption_key', $ttl, function () { 111 | throw_unless(file_exists($path = base_path('composer.lock')), RuntimeException::class, 'No encryption key provided.'); 112 | 113 | return md5_file($path); 114 | }); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Laravel/config.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | return [ 15 | 16 | 'encryption' => [ 17 | 18 | 'key' => env('EASYWECHAT_KEY'), 19 | 20 | ], 21 | 22 | 'delegation' => [ 23 | 24 | 'enabled' => env('EASYWECHAT_DELEGATION', false), 25 | 26 | 'host' => env('EASYWECHAT_DELEGATION_HOST'), 27 | ], 28 | 29 | ]; 30 | -------------------------------------------------------------------------------- /src/Laravel/routes.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | use Illuminate\Support\Facades\Route; 15 | 16 | Route::post('delegate', 'DelegatesController'); 17 | -------------------------------------------------------------------------------- /src/ManifestManager.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer; 15 | 16 | use Composer\Plugin\PluginInterface; 17 | 18 | class ManifestManager 19 | { 20 | const PACKAGE_TYPE = 'easywechat-extension'; 21 | 22 | const EXTRA_OBSERVER = 'observers'; 23 | 24 | /** 25 | * The vendor path. 26 | * 27 | * @var string 28 | */ 29 | protected $vendorPath; 30 | 31 | /** 32 | * The manifest path. 33 | * 34 | * @var string 35 | */ 36 | protected $manifestPath; 37 | 38 | /** 39 | * @param string $vendorPath 40 | * @param string|null $manifestPath 41 | */ 42 | public function __construct(string $vendorPath, string $manifestPath = null) 43 | { 44 | $this->vendorPath = $vendorPath; 45 | $this->manifestPath = $manifestPath ?: $vendorPath.'/easywechat-composer/easywechat-composer/extensions.php'; 46 | } 47 | 48 | /** 49 | * Remove manifest file. 50 | * 51 | * @return $this 52 | */ 53 | public function unlink() 54 | { 55 | if (file_exists($this->manifestPath)) { 56 | @unlink($this->manifestPath); 57 | } 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * Build the manifest file. 64 | */ 65 | public function build() 66 | { 67 | $packages = []; 68 | 69 | if (file_exists($installed = $this->vendorPath.'/composer/installed.json')) { 70 | $packages = json_decode(file_get_contents($installed), true); 71 | if (version_compare(PluginInterface::PLUGIN_API_VERSION, '2.0.0', 'ge')) { 72 | $packages = $packages['packages']; 73 | } 74 | } 75 | 76 | $this->write($this->map($packages)); 77 | } 78 | 79 | /** 80 | * @param array $packages 81 | * 82 | * @return array 83 | */ 84 | protected function map(array $packages): array 85 | { 86 | $manifest = []; 87 | 88 | $packages = array_filter($packages, function ($package) { 89 | if(isset($package['type'])){ 90 | return $package['type'] === self::PACKAGE_TYPE; 91 | } 92 | }); 93 | 94 | foreach ($packages as $package) { 95 | $manifest[$package['name']] = [self::EXTRA_OBSERVER => $package['extra'][self::EXTRA_OBSERVER] ?? []]; 96 | } 97 | 98 | return $manifest; 99 | } 100 | 101 | /** 102 | * Write the manifest array to a file. 103 | * 104 | * @param array $manifest 105 | */ 106 | protected function write(array $manifest) 107 | { 108 | file_put_contents( 109 | $this->manifestPath, 110 | 'invalidate($this->manifestPath); 114 | } 115 | 116 | /** 117 | * Invalidate the given file. 118 | * 119 | * @param string $file 120 | */ 121 | protected function invalidate($file) 122 | { 123 | if (function_exists('opcache_invalidate')) { 124 | @opcache_invalidate($file, true); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer; 15 | 16 | use Composer\Composer; 17 | use Composer\EventDispatcher\EventSubscriberInterface; 18 | use Composer\Installer\PackageEvent; 19 | use Composer\Installer\PackageEvents; 20 | use Composer\IO\IOInterface; 21 | use Composer\Plugin\Capable; 22 | use Composer\Plugin\PluginInterface; 23 | use Composer\Script\Event; 24 | use Composer\Script\ScriptEvents; 25 | 26 | class Plugin implements PluginInterface, EventSubscriberInterface, Capable 27 | { 28 | /** 29 | * @var bool 30 | */ 31 | protected $activated = true; 32 | 33 | /** 34 | * Apply plugin modifications to Composer. 35 | */ 36 | public function activate(Composer $composer, IOInterface $io) 37 | { 38 | // 39 | } 40 | 41 | /** 42 | * Remove any hooks from Composer. 43 | * 44 | * This will be called when a plugin is deactivated before being 45 | * uninstalled, but also before it gets upgraded to a new version 46 | * so the old one can be deactivated and the new one activated. 47 | */ 48 | public function deactivate(Composer $composer, IOInterface $io) 49 | { 50 | // 51 | } 52 | 53 | /** 54 | * Prepare the plugin to be uninstalled. 55 | * 56 | * This will be called after deactivate. 57 | */ 58 | public function uninstall(Composer $composer, IOInterface $io) 59 | { 60 | } 61 | 62 | /** 63 | * @return array 64 | */ 65 | public function getCapabilities() 66 | { 67 | return [ 68 | 'Composer\Plugin\Capability\CommandProvider' => 'EasyWeChatComposer\Commands\Provider', 69 | ]; 70 | } 71 | 72 | /** 73 | * Listen events. 74 | * 75 | * @return array 76 | */ 77 | public static function getSubscribedEvents() 78 | { 79 | return [ 80 | PackageEvents::PRE_PACKAGE_UNINSTALL => 'prePackageUninstall', 81 | ScriptEvents::POST_AUTOLOAD_DUMP => 'postAutoloadDump', 82 | ]; 83 | } 84 | 85 | /** 86 | * @param \Composer\Installer\PackageEvent 87 | */ 88 | public function prePackageUninstall(PackageEvent $event) 89 | { 90 | if ($event->getOperation()->getPackage()->getName() === 'overtrue/wechat') { 91 | $this->activated = false; 92 | } 93 | } 94 | 95 | public function postAutoloadDump(Event $event) 96 | { 97 | if (!$this->activated) { 98 | return; 99 | } 100 | 101 | $manifest = new ManifestManager( 102 | rtrim($event->getComposer()->getConfig()->get('vendor-dir'), '/') 103 | ); 104 | 105 | $manifest->unlink()->build(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Traits/MakesHttpRequests.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Traits; 15 | 16 | use EasyWeChat\Kernel\Http\StreamResponse; 17 | use EasyWeChat\Kernel\Traits\ResponseCastable; 18 | use EasyWeChatComposer\Contracts\Encrypter; 19 | use EasyWeChatComposer\EasyWeChat; 20 | use EasyWeChatComposer\Encryption\DefaultEncrypter; 21 | use EasyWeChatComposer\Exceptions\DelegationException; 22 | use GuzzleHttp\Client; 23 | use GuzzleHttp\ClientInterface; 24 | 25 | trait MakesHttpRequests 26 | { 27 | use ResponseCastable; 28 | 29 | /** 30 | * @var \GuzzleHttp\ClientInterface 31 | */ 32 | protected $httpClient; 33 | 34 | /** 35 | * @var \EasyWeChatComposer\Contracts\Encrypter 36 | */ 37 | protected $encrypter; 38 | 39 | /** 40 | * @param string $endpoint 41 | * @param array $payload 42 | * 43 | * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string 44 | * 45 | * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException 46 | * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException 47 | * @throws \GuzzleHttp\Exception\GuzzleException 48 | */ 49 | protected function request($endpoint, array $payload) 50 | { 51 | $response = $this->getHttpClient()->request('POST', $endpoint, [ 52 | 'form_params' => $this->buildFormParams($payload), 53 | ]); 54 | 55 | $parsed = $this->parseResponse($response); 56 | 57 | return $this->detectAndCastResponseToType( 58 | $this->getEncrypter()->decrypt($parsed['response']), 59 | ($parsed['response_type'] === StreamResponse::class) ? 'raw' : $this->app['config']['response_type'] 60 | ); 61 | } 62 | 63 | /** 64 | * @param array $payload 65 | * 66 | * @return array 67 | */ 68 | protected function buildFormParams($payload) 69 | { 70 | return [ 71 | 'encrypted' => $this->getEncrypter()->encrypt(json_encode($payload)), 72 | ]; 73 | } 74 | 75 | /** 76 | * @param \Psr\Http\Message\ResponseInterface $response 77 | * 78 | * @return array 79 | */ 80 | protected function parseResponse($response) 81 | { 82 | $result = json_decode((string) $response->getBody(), true); 83 | 84 | if (isset($result['exception'])) { 85 | throw (new DelegationException($result['message']))->setException($result['exception']); 86 | } 87 | 88 | return $result; 89 | } 90 | 91 | /** 92 | * @return \GuzzleHttp\ClientInterface 93 | */ 94 | protected function getHttpClient(): ClientInterface 95 | { 96 | return $this->httpClient ?: $this->httpClient = new Client([ 97 | 'base_uri' => $this->app['config']['delegation']['host'], 98 | ]); 99 | } 100 | 101 | /** 102 | * @return \EasyWeChatComposer\Contracts\Encrypter 103 | */ 104 | protected function getEncrypter(): Encrypter 105 | { 106 | return $this->encrypter ?: $this->encrypter = new DefaultEncrypter( 107 | EasyWeChat::getEncryptionKey() 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Traits/WithAggregator.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Traits; 15 | 16 | use EasyWeChat\Kernel\BaseClient; 17 | use EasyWeChatComposer\Delegation\DelegationTo; 18 | use EasyWeChatComposer\EasyWeChat; 19 | 20 | trait WithAggregator 21 | { 22 | /** 23 | * Aggregate. 24 | */ 25 | protected function aggregate() 26 | { 27 | foreach (EasyWeChat::config() as $key => $value) { 28 | $this['config']->set($key, $value); 29 | } 30 | } 31 | 32 | /** 33 | * @return bool 34 | */ 35 | public function shouldDelegate($id) 36 | { 37 | return $this['config']->get('delegation.enabled') 38 | && $this->offsetGet($id) instanceof BaseClient; 39 | } 40 | 41 | /** 42 | * @return $this 43 | */ 44 | public function shouldntDelegate() 45 | { 46 | $this['config']->set('delegation.enabled', false); 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * @param string $id 53 | * 54 | * @return \EasyWeChatComposer\Delegation 55 | */ 56 | public function delegateTo($id) 57 | { 58 | return new DelegationTo($this, $id); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/ManifestManagerTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This source file is subject to the MIT license that is bundled 11 | * with this source code in the file LICENSE. 12 | */ 13 | 14 | namespace EasyWeChatComposer\Tests; 15 | 16 | use EasyWeChatComposer\ManifestManager; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class ManifestManagerTest extends TestCase 20 | { 21 | private $vendorPath; 22 | private $manifestPath; 23 | 24 | protected function getManifestManager() 25 | { 26 | return new ManifestManager( 27 | $this->vendorPath = __DIR__.'/__fixtures__/vendor/', 28 | $this->manifestPath = __DIR__.'/__fixtures__/extensions.php' 29 | ); 30 | } 31 | 32 | public function testUnlink() 33 | { 34 | $this->assertInstanceOf(ManifestManager::class, $this->getManifestManager()->unlink()); 35 | $this->assertFalse(file_exists($this->manifestPath)); 36 | } 37 | } 38 | --------------------------------------------------------------------------------