diff --git a/.github/workflows/code-checks.yml b/.github/workflows/code-checks.yml new file mode 100644 index 0000000..c17e30b --- /dev/null +++ b/.github/workflows/code-checks.yml @@ -0,0 +1,48 @@ +name: CodeChecks + +on: + push: + pull_request: + +jobs: + tests: + runs-on: ubuntu-latest + + strategy: + matrix: + php: + - '7.3' + - '7.4' + - '8.0' + + name: PHP ${{ matrix.php }} tests + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: xdebug + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v2 + with: + path: vendor + key: ${{ runner.os }}-php${{ matrix.php }}-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php${{ matrix.php }}- + + - name: Composer + run: composer install --no-progress + + - name: PHPUnit + run: vendor/bin/phpunit --coverage-clover=coverage.xml + + - name: "Upload coverage to Codecov" + uses: "codecov/codecov-action@v2" + with: + fail_ci_if_error: true diff --git a/composer.json b/composer.json index 153c76c..c5f4020 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ ], "require": { "php": "^7.0", - "beberlei/assert": "^2.7", + "beberlei/assert": "^2.9", "guzzlehttp/guzzle": "~6.2", "psr/http-message": "~1.0", "symfony/options-resolver": "^3.0|^4.0", diff --git a/src/Client.php b/src/Client.php index 4e2095b..502d73a 100644 --- a/src/Client.php +++ b/src/Client.php @@ -15,7 +15,10 @@ namespace D3\LinkmobilityClient; +use D3\LinkmobilityClient\Exceptions\ApiException; use D3\LinkmobilityClient\Request\RequestInterface; +use GuzzleHttp\RequestOptions; +use Psr\Http\Message\ResponseInterface; class Client { @@ -34,10 +37,11 @@ class Client $this->requestClient = $client ?: new \GuzzleHttp\Client( [ 'base_uri' => $this->apiUrl->getBaseUri() ] ); } - public function request(RequestInterface $request) : ResponseInterface + public function request(RequestInterface $request) : \D3\LinkmobilityClient\Response\ResponseInterface { $request->validate(); $responseClass = $request->getResponseClass(); + return $request->getResponseInstance( $this->rawRequest($request->getUri(), $request->getMethod(), $request->getOptions()) ); @@ -48,22 +52,14 @@ class Client * @param string $method * @param array $postArgs * - * @return string + * @return ResponseInterface * @throws ApiException * @throws GuzzleException */ - protected function rawRequest( $url, string $method = RequestInterface::METHOD_GET, array $options = []): string + protected function rawRequest( $url, string $method = RequestInterface::METHOD_GET, array $options = []): ResponseInterface { - $options['headers']['Authorization'] = 'access_token '.$this->accessToken; - - if (!empty($body)) { - $options['json'] = $body; - } - dumpvar(__METHOD__.__LINE__.PHP_EOL); -dumpvar($options); -dumpVar($method); -dumpvar($url); -die(); + $options['headers']['Authorization'] = 'Bearer '.$this->accessToken; + $response = $this->requestClient->request( $method, $url, diff --git a/src/Exceptions/ApiException.php b/src/Exceptions/ApiException.php new file mode 100644 index 0000000..e1e5d40 --- /dev/null +++ b/src/Exceptions/ApiException.php @@ -0,0 +1,21 @@ + + * @link http://www.oxidmodule.com + */ + +namespace D3\LinkmobilityClient\Exceptions; + +class ApiException extends LinkmobilityException +{ + +} \ No newline at end of file diff --git a/src/Exceptions/LinkmobilityException.php b/src/Exceptions/LinkmobilityException.php new file mode 100644 index 0000000..7b056da --- /dev/null +++ b/src/Exceptions/LinkmobilityException.php @@ -0,0 +1,23 @@ + + * @link http://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\LinkmobilityClient\Exceptions; + +class LinkmobilityException extends \Exception +{ + +} \ No newline at end of file diff --git a/src/Exceptions/RecipientException.php b/src/Exceptions/RecipientException.php new file mode 100644 index 0000000..9e82f70 --- /dev/null +++ b/src/Exceptions/RecipientException.php @@ -0,0 +1,23 @@ + + * @link http://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\LinkmobilityClient\Exceptions; + +class RecipientException extends LinkmobilityException +{ + +} \ No newline at end of file diff --git a/src/RecipientsList/RecipientsList.php b/src/RecipientsList/RecipientsList.php index 87ef2be..89bf13e 100644 --- a/src/RecipientsList/RecipientsList.php +++ b/src/RecipientsList/RecipientsList.php @@ -18,6 +18,7 @@ declare(strict_types=1); namespace D3\LinkmobilityClient\RecipientsList; use D3\LinkmobilityClient\ValueObject\Recipient; +use libphonenumber\PhoneNumberType; class RecipientsList implements RecipientsListInterface, \Iterator { @@ -26,14 +27,33 @@ class RecipientsList implements RecipientsListInterface, \Iterator */ private $recipients = []; - public function add(Recipient $recipient) + public function add(Recipient $recipient) : RecipientsListInterface { - $this->recipients[md5(serialize($recipient))] = $recipient; + $phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance(); + try { + $phoneNumber = $phoneUtil->parse($recipient->get(), $recipient->getCountryCode()); + + if (false === $phoneUtil->isValidNumber($phoneNumber)) { + throw new \D3\LinkmobilityClient\Exceptions\RecipientException('invalid recipient phone number'); + } elseif (false === in_array($phoneUtil->getNumberType($phoneNumber), [PhoneNumberType::MOBILE, PhoneNumberType::FIXED_LINE_OR_MOBILE])) { + throw new \D3\LinkmobilityClient\Exceptions\RecipientException('not a mobile number'); + } + + $this->recipients[md5(serialize($recipient))] = $recipient; + } catch (\libphonenumber\NumberParseException $e) { +// var_dump($e); + } catch (\D3\LinkmobilityClient\Exceptions\RecipientException $e) { +// var_dump($e); + } + + return $this; } - public function clearRecipents() + public function clearRecipents() : RecipientsListInterface { $this->recipients = []; + + return $this; } public function getRecipients() : array diff --git a/src/RecipientsList/RecipientsListInterface.php b/src/RecipientsList/RecipientsListInterface.php index 0303328..5e10673 100644 --- a/src/RecipientsList/RecipientsListInterface.php +++ b/src/RecipientsList/RecipientsListInterface.php @@ -21,9 +21,9 @@ use D3\LinkmobilityClient\ValueObject\Recipient; interface RecipientsListInterface { - public function add(Recipient $recipient); + public function add(Recipient $recipient) : RecipientsListInterface; - public function clearRecipents(); + public function clearRecipents() : RecipientsListInterface; public function getRecipients() : array; } \ No newline at end of file diff --git a/src/Request/Request.php b/src/Request/Request.php index 3dad0dc..9ad4f6a 100644 --- a/src/Request/Request.php +++ b/src/Request/Request.php @@ -25,6 +25,7 @@ use D3\LinkmobilityClient\ValueObject\Recipient; use D3\LinkmobilityClient\ValueObject\Sender; use D3\LinkmobilityClient\ValueObject\SmsMessage; use D3\LinkmobilityClient\ValueObject\StringValueObject; +use GuzzleHttp\RequestOptions; use OxidEsales\Eshop\Core\Registry; abstract class Request implements RequestInterface @@ -125,7 +126,8 @@ abstract class Request implements RequestInterface Assert::that($this->getOptions())->isArray(); Assert::that( $this->getRecipientsList() )->isInstanceOf(RecipientsList::class)->notEmpty(); - Assert::thatAll( $this->getRecipientsList() )->isInstanceOf( Recipient::class ); + Assert::that( $this->getRecipientsList()->getRecipients())->notEmpty(); + Assert::thatAll( $this->getRecipientsList() )->isInstanceOf( Recipient::class )->notEmpty(); // optional properties Assert::thatNullOr( $this->getClientMessageId() )->string(); @@ -151,10 +153,10 @@ abstract class Request implements RequestInterface 'priority' => $this->getPriority(), 'recipientAddressList' => $this->getRecipientsList()->getRecipients(), 'sendAsFlashSms' => $this->getSendAsFlashSms(), - 'senderAddress' => $this->getSenderAddress()->get(), + 'senderAddress' => $this->getSenderAddress() ? $this->getSenderAddress()->get() : null, 'senderAddressType' => $this->getSenderAddressType(), 'test' => $this->isTest(), - 'validityPeriode' => $this->getValidityPeriode() + 'validityPeriode' => $this->getValidityPeriode() ]; } @@ -166,7 +168,7 @@ abstract class Request implements RequestInterface switch ($this->getContentType()) { case RequestInterface::CONTENTTYPE_JSON: - return ['json' => json_encode($body)]; + return [RequestOptions::JSON => $body]; default: return $body; } @@ -180,7 +182,6 @@ abstract class Request implements RequestInterface 'Accept' => $this->contentType, 'Content-Type' => $this->contentType, ] - ], $this->getBody() ); @@ -382,9 +383,9 @@ abstract class Request implements RequestInterface } /** - * @return string|null + * @return Sender|null */ - public function getSenderAddress() : Sender + public function getSenderAddress() { return $this->senderAddress; } @@ -444,6 +445,7 @@ abstract class Request implements RequestInterface */ public function getResponseInstance(\Psr\Http\Message\ResponseInterface $rawResponse): ResponseInterface { - return new $this->getResponseClass($rawResponse); + $FQClassName = $this->getResponseClass(); + return new $FQClassName($rawResponse); } } \ No newline at end of file diff --git a/src/Response/Response.php b/src/Response/Response.php index 1373620..fbff257 100644 --- a/src/Response/Response.php +++ b/src/Response/Response.php @@ -8,26 +8,78 @@ use Assert\Assert; abstract class Response implements ResponseInterface { + const STATUSCODE = 'statusCode'; + const STATUSMESSAGE = 'statusMessage'; + const CLIENTMESSAGEID = 'clientMessageId'; + const TRANSFERID = 'transferId'; + const SMSCOUNT = 'smsCount'; + /** - * @var array + * @var \Psr\Http\Message\ResponseInterface */ - protected $data; + protected $rawResponse; + + protected $content; /** * @var int */ protected $status; - public function __construct(array $data) + public function __construct(\Psr\Http\Message\ResponseInterface $rawResponse) { - Assert::that($data)->keyExists('status'); + $this->rawResponse = $rawResponse; + $this->content = json_decode($this->rawResponse->getBody()->getContents(), true); + } + + public function getRawResponse() : \Psr\Http\Message\ResponseInterface + { + return $this->rawResponse; + } - $this->data = $data; - $this->status = (int)$this->data['status']; + public function getContent() + { + return $this->content; + } - if ($this->isSuccessful()) { - $this->init(); - } + /** + * @return int + */ + public function getInternalStatus() : int + { + return $this->getContent()[self::STATUSCODE]; + } + + /** + * @return string + */ + public function getStatusMessage() : string + { + return $this->getContent()[self::STATUSMESSAGE]; + } + + /** + * @return string|null + */ + public function getClientMessageId() + { + return $this->getContent()[self::CLIENTMESSAGEID]; + } + + /** + * @return string|null + */ + public function getTransferId() + { + return $this->getContent()[self::TRANSFERID]; + } + + /** + * @return string|null + */ + public function getSmsCount() : int + { + return $this->getContent()[self::SMSCOUNT]; } /** @@ -35,11 +87,13 @@ abstract class Response implements ResponseInterface */ public function isSuccessful(): bool { - return $this->status >= 200 && $this->status < 300; + $status = $this->getInternalStatus(); + + return $status >= 2000 && $status <= 2999; } - public function getError(): string + public function getErrorMessage(): string { - return $this->isSuccessful() ? '' : $this->data['message']; + return $this->isSuccessful() ? '' : $this->getStatusMessage(); } } diff --git a/src/Response/ResponseInterface.php b/src/Response/ResponseInterface.php index 351d9fb..66cae6d 100644 --- a/src/Response/ResponseInterface.php +++ b/src/Response/ResponseInterface.php @@ -6,24 +6,21 @@ namespace D3\LinkmobilityClient\Response; interface ResponseInterface { - /** - * Should instantiate the object from the data given - */ - public function init() : void; + public function __construct(\Psr\Http\Message\ResponseInterface $rawResponse); - public function __construct(array $data); + public function getRawResponse() : \Psr\Http\Message\ResponseInterface; - /** - * Must return true if the request was successful - * - * @return bool - */ - public function isSuccessful() : bool; + public function getInternalStatus() : int; - /** - * This must return the error, if any occurred, else it must return '' - * - * @return string - */ - public function getError() : string; + public function getStatusMessage() : string; + + public function getClientMessageId(); + + public function getTransferId(); + + public function getSmsCount() : int; + + public function isSuccessful(): bool; + + public function getErrorMessage(): string; } diff --git a/src/SMS/TextRequest.php b/src/SMS/TextRequest.php index 0582b00..f595863 100644 --- a/src/SMS/TextRequest.php +++ b/src/SMS/TextRequest.php @@ -133,7 +133,7 @@ class TextRequest extends \D3\LinkmobilityClient\Request\Request public function getUri(): string { - return '/smsmessaging/text/'; + return '/rest/smsmessaging/text'; } public function validate(): void diff --git a/src/ValueObject/Recipient.php b/src/ValueObject/Recipient.php index 77e7b28..4960e00 100644 --- a/src/ValueObject/Recipient.php +++ b/src/ValueObject/Recipient.php @@ -5,21 +5,36 @@ declare(strict_types=1); namespace D3\LinkmobilityClient\ValueObject; use Assert\Assert; +use libphonenumber\PhoneNumberType; class Recipient extends StringValueObject { - public function __construct(string $value) + /** + * @var string + */ + private $countryCode; + + public function __construct(string $number, string $iso2CountryCode) { - // ohne +, dafür mit Ländervorwahl - // eine führende 0 scheint lokale Version - // zwei führende Nullen einfach weggeschnitten + Assert::that($iso2CountryCode)->string()->length(2); - //https://github.com/matmar10/msisdn-format-bundle/blob/master/Matmar10/Bundle/MsisdnFormatBundle/Resources/config/msisdn-country-formats.xml + $phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance(); + try { + $phoneNumber = $phoneUtil->parse($number, strtoupper($iso2CountryCode)); + $number = ltrim($phoneUtil->format($phoneNumber, \libphonenumber\PhoneNumberFormat::E164), '+'); + } catch (\libphonenumber\NumberParseException $e) { + var_dump($e); + } + parent::__construct($number); + $this->countryCode = $iso2CountryCode; + } - // valid formats can be found here: https://linkmobility.atlassian.net/wiki/spaces/COOL/pages/26017807/08.+Messages#id-08.Messages-recipients - Assert::that($value)->regex('/^(\+|c)?[0-9]+$/i', 'Recipient does not match valid phone number.'); - - parent::__construct($value); + /** + * @return string + */ + public function getCountryCode() :string + { + return $this->countryCode; } } diff --git a/src/ValueObject/Sender.php b/src/ValueObject/Sender.php index 7d3a3ac..e82de54 100644 --- a/src/ValueObject/Sender.php +++ b/src/ValueObject/Sender.php @@ -8,13 +8,22 @@ use Assert\Assert; class Sender extends StringValueObject { - public function __construct(string $value) + public function __construct(string $number, string $iso2CountryCode) { - // if the sender is alphanumeric, test the length - if (!preg_match('/^\+[0-9]+$/i', $value)) { - Assert::that($value)->maxLength(11); + Assert::that($iso2CountryCode)->string()->length(2); + + $phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance(); + try { + $phoneNumber = $phoneUtil->parse( $number, strtoupper($iso2CountryCode) ); + $number = $phoneUtil->format( $phoneNumber, \libphonenumber\PhoneNumberFormat::E164 ); + + if (false === $phoneUtil->isValidNumber($phoneNumber)) { + throw new \D3\LinkmobilityClient\Exceptions\RecipientException( 'invalid sender phone number' ); + } + } catch (\libphonenumber\NumberParseException $e) { + var_dump($e); } - parent::__construct($value); + parent::__construct( $number); } } diff --git a/src/ValueObject/SmsMessage.php b/src/ValueObject/SmsMessage.php index a8a9b2b..88ab561 100644 --- a/src/ValueObject/SmsMessage.php +++ b/src/ValueObject/SmsMessage.php @@ -12,9 +12,9 @@ class SmsMessage extends StringValueObject const GSM_7BIT = '7-bit'; const GSM_UCS2 = 'ucs-2'; - public function __construct(string $value) + public function __construct(string $number) { - parent::__construct($value); + parent::__construct( $number); $smsLength = new SmsLength($this->value); $smsLength->validate(); diff --git a/src/ValueObject/StringValueObject.php b/src/ValueObject/StringValueObject.php index 0e90e3f..0dc9611 100644 --- a/src/ValueObject/StringValueObject.php +++ b/src/ValueObject/StringValueObject.php @@ -8,11 +8,11 @@ use Assert\Assert; abstract class StringValueObject extends ValueObject { - public function __construct(string $value) + public function __construct(string $number) { - Assert::that($value)->notEmpty(); + Assert::that( $number)->notEmpty(); - $this->value = $value; + $this->value = $number; } public function __toString() diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000..9914677 --- /dev/null +++ b/todo.txt @@ -0,0 +1,2 @@ +RecipientsList als eigene Klasse +Debug mit none, error, all log