From c3c17f35d0499fbfcc020627ba26e687ecdfd99f Mon Sep 17 00:00:00 2001 From: Serge Beresnev | Keriat Date: Sun, 19 Jul 2020 01:02:56 +0300 Subject: [PATCH] =?UTF-8?q?*=20All=20config-related=20functional=20fully?= =?UTF-8?q?=20moved=20to=20Config=20*=20=E2=80=9Ccustomfields=E2=80=9D=20e?= =?UTF-8?q?ncoding=20moved=20to=20the=20config=20as=20well=20*=20Config=20?= =?UTF-8?q?refactored=20*=20Connector=20now=20store=20the=20config=20insid?= =?UTF-8?q?e=20after=20the=20initialization=20*=20Connector=20simplified?= =?UTF-8?q?=20dramatically,=20become=20slick=20and=20readable=20*=20All=20?= =?UTF-8?q?code=20was=20heavily=20tested,=20linted,=20formatted=20*=20demo?= =?UTF-8?q?=20added=20*=20facade=20now=20cover=20three=20basic=20functions?= =?UTF-8?q?:=20=20=20=20=20=E2=80=94=20to=20run=20it=20(initiate=20entity)?= =?UTF-8?q?=20and=20connect,=20allowing=20to=20avoid=20any=20internal=20cl?= =?UTF-8?q?ass=20usage=20outside=20of=20the=20module=20=20=20=20=20?= =?UTF-8?q?=E2=80=94=20to=20make=20calls=20directly=20through=20the=20Faca?= =?UTF-8?q?de=20=20=20=20=20=E2=80=94=20to=20create=20an=20instance=20of?= =?UTF-8?q?=20the=20Connector=20and=20use=20it=20directly=20(so-called=20?= =?UTF-8?q?=E2=80=9Cexpert=20mode=E2=80=9D)=20*=20Exceptions=20triggered?= =?UTF-8?q?=20by=20the=20package=20limited=20to=202:=20=20=20=20=20?= =?UTF-8?q?=E2=80=94=20GuzzleHttp\Exception\ClientException=20(to=20assure?= =?UTF-8?q?=20any=20debugging=20can=20be=20done=20=20=20=20=20=E2=80=94=20?= =?UTF-8?q?Fruitware\WhmcsWrapper\Exception\RuntimeException=20(main=20exc?= =?UTF-8?q?eption=20handling=20any=20fatal=20errors)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The package follows principles described in “The Pragmatic Programmer” by Andy Hunt. P.S. It can be considered a beta-release --- demo/index.php | 20 +++++ src/Config/ConfigInterface.php | 100 +++++++++------------- src/Config/DefaultConfig.php | 149 ++++++++++----------------------- src/Connect/Connector.php | 134 +++++------------------------ src/Facade.php | 110 ++++++++++++++++-------- 5 files changed, 199 insertions(+), 314 deletions(-) create mode 100644 demo/index.php diff --git a/demo/index.php b/demo/index.php new file mode 100644 index 0000000..37b2897 --- /dev/null +++ b/demo/index.php @@ -0,0 +1,20 @@ +connect( + $apiUrl, + $apiIdentifier, + $apiSecret + ); + var_dump($client->call('GetClients')); +} catch (Exception $exception) { + var_dump('Error: ', $exception->getMessage(), $exception->getTraceAsString()); +} diff --git a/src/Config/ConfigInterface.php b/src/Config/ConfigInterface.php index a2e01dd..2b8538c 100644 --- a/src/Config/ConfigInterface.php +++ b/src/Config/ConfigInterface.php @@ -3,107 +3,83 @@ namespace Fruitware\WhmcsWrapper\Config; /** - * @todo End uo with the configuration architecture * @package Fruitware\WhmcsWrapper + * @author Fruits Foundation */ interface ConfigInterface { /** - * Constructor expected to be protected but omitted to comply - * * ConfigInterface constructor. + * Protected to disable instancing but static i() method + * + * * @param string $apiBaseUri - * @param string $apiIdentifier - * @param string $apiSecret + * @param string $key + * @param string $secret * @param array $postDefaultParams - * @param array $requestOptions + * @param array $clientOptions */ public function __construct( string $apiBaseUri, - string $apiIdentifier, - string $apiSecret, + string $key, + string $secret, array $postDefaultParams, - array $requestOptions + array $clientOptions ); /** - * Singleton config initialization - * - * @link https://developers.whmcs.com/api/authentication/ - * * @param string $baseUri path to your WHMCS installation (HTTP_ROOT) - * * @param string $identifier WHMCS Identifier * @param string $secret WHMCS Identifier's Secret key - * - * Members are used as `form_fields` for each API-call as POST-request * @param array|string[] $defaultConfig default: array['responsetype' => 'json'] + * @param array $clientOptions Request options to apply. See \GuzzleHttp\RequestOptions * - * @param array $requestOptions Request options to apply. See \GuzzleHttp\RequestOptions * @return ConfigInterface */ public static function i( string $baseUri, string $identifier, string $secret, - array $defaultConfig = ['responsetype' => 'json'], - array $requestOptions = ['timeout' => 3.0] + array $defaultConfig = [], + array $clientOptions = [] ): ConfigInterface; + public function getRequestUrl(): string; + /** - * Transmitting securely default form_field POST-params to the Connector - * + * GuzzleHttp\ClientInterface client's config (used during initialization) * @return array */ - public function getDefaultParams(): array; + public function prepareConfig(): array; /** - * Transmitting securely API POST request url + * Base64 encoded serialized array of custom field values + * @link https://developers.whmcs.com/api-reference/addorder/ + * @link http://mahmudulruet.blogspot.com/2011/10/adding-and-posting-custom-field-values.html * - * @return string - */ - public function getRequestUrl(): string; - - /** - * Transmitting given authorization `identifier` to configure http-client + * 1. Login to your WHMCS admin panel. + * 2. Navigate Setup->Client Custom Fields + * 3. If there is no custom fields yet create a new one; say named "VAT". + * 4. After creating the field, see the HTML source from the page by right clicking on page and click view source (in Firefox). + * 5. Find the text "VAT". + * 6. You will find something like this line of HTML code () + * 7. This fieldname[] with id may vary by users admin panel. So track the id from fieldname[11] and yes it's 11. If you find something like then 'xx' will be the id you have to use in your $customfields array. The array now should look like: * - * @return string - */ - public function getAuthIdentifier(): string; - - /** - * Transmitting `base_uri` value to configure http-client + * $customfields = [ + * "11" => "123456", + * "12" => "678945" + * ]; + * $postfields["customfields"] = base64_encode(serialize($customfields)); * - * @return string - */ - public function getBaseUri(): string; - - /** - * Transmitting given authorization `secret` to configure http-client + * Preprocessing the request params. + * Main purpose of the method is to encode `customfields` param. * - * @return string - */ - public function getAuthSecret(): string; - - /** - * Setting default http-client options + * @param string $action method name + * @param array $params query params + * @param bool $skipValidation cancel validation of the required fields * * @return array */ - public function getRequestOptions(): array; - - - /** - * @param array $params - * @return ConfigInterface - */ - public function updateDefaultParams(array $params = []): ConfigInterface; - - - /** - * @param array $options - * @return ConfigInterface - */ - public function updateRequestOptions(array $options = []): ConfigInterface; + public function prepareRequestOptions(string $action, array $params, bool $skipValidation): array; } \ No newline at end of file diff --git a/src/Config/DefaultConfig.php b/src/Config/DefaultConfig.php index a686568..403cd2d 100644 --- a/src/Config/DefaultConfig.php +++ b/src/Config/DefaultConfig.php @@ -2,64 +2,49 @@ namespace Fruitware\WhmcsWrapper\Config; +use function filter_var; use function rtrim; /** * Class DefaultConfig * - * @package Fruitware\WhmcsWrapper\Config + * @package Fruitware\WhmcsWrapper + * @author Fruits Foundation */ final class DefaultConfig implements ConfigInterface { - /** - * @var string - */ protected string $apiRequestUrl = '/includes/api.php'; + protected string $key; + protected string $secret; - /** - * @var string - */ - protected string $apiBaseUri; - - /** - * @var string - */ - protected string $apiIdentifier; - - /** - * @var string - */ - protected string $apiSecret; - - /** - * @var array - */ - protected array $postDefaultParams; + protected array $postDefaultParams = [ + 'responsetype' => 'json' + ]; - /** - * @var array - */ - protected array $requestOptions; + protected array $clientOptions = [ + 'base_uri' => '', + ]; /** * @inheritDoc */ - protected function __construct( + public function __construct( string $apiBaseUri, - string $apiIdentifier, - string $apiSecret, + string $key, + string $secret, array $postDefaultParams, - array $requestOptions + array $clientOptions ) { - /** - * Filtering trailing slash - */ - $this->apiBaseUri = rtrim($apiBaseUri, '/'); + $this->key = $key; + $this->secret = $secret; - $this->apiIdentifier = $apiIdentifier; - $this->apiSecret = $apiSecret; - $this->postDefaultParams = $postDefaultParams; - $this->requestOptions = $requestOptions; + if ($postDefaultParams) { + $this->postDefaultParams = $postDefaultParams; + } + if ($clientOptions) { + $this->clientOptions = $clientOptions; + } + $this->clientOptions['base_uri'] = filter_var(rtrim($apiBaseUri, "/"), FILTER_SANITIZE_URL); } /** @@ -69,19 +54,18 @@ public static function i( string $baseUri, string $identifier, string $secret, - array $defaultConfig = ['responsetype' => 'json'], - array $requestOptions = ['timeout' => 3.0] + array $defaultConfig = [], + array $clientOptions = [] ): ConfigInterface { return new self( $baseUri, $identifier, $secret, $defaultConfig, - $requestOptions + $clientOptions ); } - /** * Transmitting securely API POST request url * @@ -93,77 +77,34 @@ public function getRequestUrl(): string } /** - * Transmitting given authorization `identifier` to configure http-client - * - * @return string - */ - public function getAuthIdentifier(): string - { - return $this->apiIdentifier; - } - - - /** - * Transmitting `base_uri` value to configure http-client - * - * @return string - */ - public function getBaseUri(): string - { - return $this->apiBaseUri; - } - - /** - * @return string `secret` to configure http-client - */ - public function getAuthSecret(): string - { - return $this->apiSecret; - } - - /** - * @return array default http-client options + * @inheritDoc */ - public function getRequestOptions(): array + public function prepareConfig(): array { - return $this->requestOptions; + return $this->clientOptions; } /** - * @param array $options - * @return ConfigInterface + * @inheritDoc */ - public function updateRequestOptions(array $options = []): ConfigInterface + public function prepareRequestOptions(string $action, array $params, bool $skipValidation): array { - if ($options) { - $this->requestOptions = array_merge($this->requestOptions, $options); + if ($skipValidation) { + $params['skipvalidation'] = true; } - return $this; - } + $params['action'] = $action; + $result = array_merge( + $this->postDefaultParams, [ + 'identifier' => $this->key, + 'secret' => $this->secret, + ], $params); - /** - * @param array $params - * @return ConfigInterface - */ - public function updateDefaultParams(array $params = []): ConfigInterface - { - if ($params) { - $this->postDefaultParams = array_merge($this->postDefaultParams, $params); + if (!empty($result['customfields'])) { + $result['customfields'] = base64_encode(serialize($result['customfields'])); } - return $this; - } - - /** - * Transmitting securely default form_field POST-params to the Connector - * - * @return array - */ - public function getDefaultParams(): array - { - return $this->postDefaultParams + [ - 'identifier' => $this->apiIdentifier, - 'secret' => $this->apiSecret, - ]; + return [ + 'form_params' => $result + ]; } } \ No newline at end of file diff --git a/src/Connect/Connector.php b/src/Connect/Connector.php index 7608c94..71e19e4 100644 --- a/src/Connect/Connector.php +++ b/src/Connect/Connector.php @@ -3,131 +3,41 @@ namespace Fruitware\WhmcsWrapper\Connect; use Exception; -use Fruitware\WhmcsWrapper\Config\ConfigInterface; -use Fruitware\WhmcsWrapper\Exception\RuntimeException; -use GuzzleHttp\Client; -use GuzzleHttp\Promise\AggregateException; +use Fruitware\WhmcsWrapper\{Config\ConfigInterface, Exception\RuntimeException}; +use GuzzleHttp\{Client, ClientInterface, Promise\AggregateException}; +use function json_decode; /** - * @todo: morph connector into the - * * Class Connector * * @package Fruitware\WhmcsWrapper + * @author Fruits Foundation */ final class Connector { /** * WHMCS API wrapper - * - * @example https://github.com/PeteBishwhip/WHMCSAPI + * @var ClientInterface|null */ - private ?Client $client; - - protected string $whmcsUri; - protected string $whmcsIdentifier; - protected string $whmcsSecret; - - /** @var array http-client options */ - protected array $requestOptions; + protected ?ClientInterface $client = null; /** - * @var array params automatically used for each api-call + * @var ConfigInterface|null */ - protected array $defaultParams; - - /** @var string Request url (path) for any api call */ - protected string $requestUrl; + protected ?ConfigInterface $config; - /** - * Protected constructor - * Doesn't supposed to be called directly - * - * @param ConfigInterface $config - */ protected function __construct(ConfigInterface $config) { - $this->whmcsUri = $config->getBaseUri(); - $this->whmcsIdentifier = $config->getAuthIdentifier(); - $this->whmcsSecret = $config->getAuthSecret(); - $this->defaultParams = $config->getDefaultParams(); - $this->requestUrl = $config->getRequestUrl(); - $this->requestOptions = $config->getRequestOptions(); + $this->config = $config; } - /** - * To avoid direct calls to the client are - * @return Client - */ - protected function getClient(): Client + protected function getClient(): ClientInterface { if (!$this->client) { - $this->client = new Client( - array_merge( - $this->defaultParams, - $this->requestOptions - ) - ); - } - return $this->client; - } - - /** - * Base64 encoded serialized array of custom field values - * @link https://developers.whmcs.com/api-reference/addorder/ - * @link http://mahmudulruet.blogspot.com/2011/10/adding-and-posting-custom-field-values.html - * - * 1. Login to your WHMCS admin panel. - * 2. Navigate Setup->Client Custom Fields - * 3. If there is no custom fields yet create a new one; say named "VAT". - * 4. After creating the field, see the HTML source from the page by right clicking on page and click view source (in Firefox). - * 5. Find the text "VAT". - * 6. You will find something like this line of HTML code () - * 7. This fieldname[] with id may vary by users admin panel. So track the id from fieldname[11] and yes it's 11. If you find something like then 'xx' will be the id you have to use in your $customfields array. The array now should look like: - * - * $customfields = [ - * "11" => "123456", - * "12" => "678945" - * ]; - * $postfields["customfields"] = base64_encode(serialize($customfields)); - * - * Preprocessing the request params. - * Main purpose of the method is to encode `customfields` param. - * - * @param string $action method name - * @param array $params query params - * @param bool $skipValidation cancel validation of the required fields - * - * @return array - */ - protected function filterParams(string $action, array $params, bool $skipValidation): array - { - if ($skipValidation) { - $params['skipvalidation'] = true; + $this->client = new Client($this->config->prepareConfig()); } - $params['action'] = $action; - $result = array_merge($this->defaultParams, $params); - if (!empty($result['customfields'])) { - $result['customfields'] = base64_encode(serialize($result['customfields'])); - } - return [ - 'form_params' => $result - ]; - } - - /** - * Get servers. In addition tries to fetch status. Void - * - * @throws RuntimeException - */ - public function validate(): void - { - $response = $this->call('GetServers', ['fetchStatus' => true]); - - if (!$response || $response['result'] !== 'success') { - throw new RuntimeException('Server is done!', 500); - } + return $this->client; } /** @@ -146,16 +56,14 @@ public function call(string $action, array $params = [], bool $skipValidation = { try { $response = $this->getClient()->post( - $this->whmcsUri, - $this->filterParams( - $action, - $params, - $skipValidation - ) + $this->config->getRequestUrl(), + $this->config->prepareRequestOptions($action, $params, $skipValidation) ); - if ($response->getStatusCode() !== 200) { - throw new RuntimeException('Code not 200: '.$response->getReasonPhrase(), $response->getStatusCode()); + throw new RuntimeException( + "Error: Status {$response->getStatusCode()}, reason: {$response->getReasonPhrase()}", + $response->getStatusCode() + ); } return json_decode( @@ -181,7 +89,11 @@ public function call(string $action, array $params = [], bool $skipValidation = public static function connect(ConfigInterface $config): Connector { $self = new self($config); - $self->validate(); + + /** + * Validate the connection + */ + $self->call('GetClientGroups'); return $self; } diff --git a/src/Facade.php b/src/Facade.php index 7f8bd65..d19dede 100644 --- a/src/Facade.php +++ b/src/Facade.php @@ -5,19 +5,19 @@ use Fruitware\WhmcsWrapper\Config\DefaultConfig; use Fruitware\WhmcsWrapper\Connect\Connector; use Fruitware\WhmcsWrapper\Exception\RuntimeException; +use GuzzleHttp\Exception\ClientException; /** - * @property Connector|null connector + * @package Fruitware\WhmcsWrapper + * @author Fruits Foundation * - * @todo split Facade from the Connector and encapsulate it to avoid external call (expect the special “expert” flow) - * @todo apply pattern Facede for real + * @link https://developers.whmcs.com/api/ * * Class Facade - * @package Fruitware\WhmcsWrapper + * @property Connector|null connector */ class Facade { - private static ?Facade $obj; protected ?Connector $connector; /** @@ -25,7 +25,7 @@ class Facade * * @throws RuntimeException */ - protected function getConnector(): Connector + protected function connector(): Connector { if (!$this->connector) { throw new RuntimeException('You should call `connect` method first and pass the connection data'); @@ -35,34 +35,69 @@ protected function getConnector(): Connector /** * Damn simple connect method: - * ```Facade::i()->call - * This method works with static cached version of the Connector * - * @param string $uri - * @param string $apiId - * @param string $apiSecret + * $whmscClient = Facade::run()->connect(, , ); + * $whmscClient->call('GetHealthStatus'); * - * @param array|null $params - * @param array|null $options - * @return Connector + * OR since method is chained to Facade is can be a one-liner:, + * + * Facade::run()->connect(, , )->call('GetHealthStatus'); + * + * “Authenticating With API Credentials” method is the only available right now + * @link https://developers.whmcs.com/api/authentication/ * - * @throws Exception\RuntimeException + * @param string $uri path to your WHMCS installation (HTTP_ROOT) + * @param string $key WHMCS Identifier + * @param string $secret WHMCS Identifier's Secret key + * @param array|string[] $params `form_fields` that will be used for each API-call e.g. 'responsetype' => 'json' + * @param array $config Request options to apply. See \GuzzleHttp\RequestOptions + * @return Facade + * + * @throws RuntimeException */ public function connect( string $uri, - string $apiId, - string $apiSecret, - array $params = null, - array $options = null + string $key, + string $secret, + array $params = [], + array $config = [] + ): Facade { + $this->connector = self::getConnector($uri, $key, $secret, $params, $config); + + return $this; + } + + + /** + * Direct access to the connector object + * + * “Authenticating With API Credentials” method is the only available right now + * @link https://developers.whmcs.com/api/authentication/ + * + * @param string $uri path to your WHMCS installation (HTTP_ROOT) + * @param string $key WHMCS Identifier + * @param string $secret WHMCS Identifier's Secret key + * @param array|string[] $params `form_fields` that will be used for each API-call e.g. 'responsetype' => 'json' + * @param array $config Request options to apply. See \GuzzleHttp\RequestOptions + * @return Connector + * + * @throws RuntimeException + */ + public static function getConnector( + string $uri, + string $key, + string $secret, + array $params = [], + array $config = [] ): ?Connector { - $config = DefaultConfig::i( + return Connector::connect(DefaultConfig::i( $uri, - $apiId, - $apiSecret - )->updateDefaultParams($params)->updateRequestOptions($options); - - return $this->connector = Connector::connect($config); + $key, + $secret, + $params, + $config + )); } @@ -72,27 +107,28 @@ public function connect( */ final public static function run(): Facade { - if (!self::$obj) { - self::$obj = new self(); + static $self; + if (!$self) { + $self = new self(); } - return self::$obj; + return $self; } /** + * Suitable for any API-call + * + * @link https://developers.whmcs.com/api/api-index/ + * * @param string $action - * @param array $attributes + * @param array $params * @param bool $skipValidation * * @return array|null * - * @throws Exception\RuntimeException + * @throws RuntimeException|ClientException */ - final public function call( - string $action, - array $attributes = [], - bool $skipValidation = false - ): ?array { - return self::run()->getConnector()->call($action, $attributes, $skipValidation); - + final public function call(string $action, array $params = [], bool $skipValidation = false): ?array + { + return self::run()->connector()->call($action, $params, $skipValidation); } }