diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..cd8eb86e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d07f69a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/vendor +build +composer.phar +composer.lock +.phpunit.result.cache diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 00000000..0285f179 --- /dev/null +++ b/.styleci.yml @@ -0,0 +1 @@ +preset: laravel diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100755 index 00000000..fb1b006a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +All notable changes to `unifonic` will be documented in this file + +## 1.0.0 - 201X-XX-XX + +- initial release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100755 index 00000000..4da74e3f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +Please read and understand the contribution guide before creating an issue or pull request. + +## Etiquette + +This project is open source, and as such, the maintainers give their free time to build and maintain the source code +held within. They make the code freely available in the hope that it will be of use to other developers. It would be +extremely unfair for them to suffer abuse or anger for their hard work. + +Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the +world that developers are civilized and selfless people. + +It's the duty of the maintainer to ensure that all submissions to the project are of sufficient +quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. + +## Viability + +When requesting or submitting new features, first consider whether it might be useful to others. Open +source projects are used by many developers, who may have entirely different needs to your own. Think about +whether or not your feature is likely to be used by other users of the project. + +## Procedure + +Before filing an issue: + +- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. +- Check to make sure your feature suggestion isn't already present within the project. +- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. +- Check the pull requests tab to ensure that the feature isn't already in progress. + +Before submitting a pull request: + +- Check the codebase to ensure that your feature doesn't already exist. +- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. + +## Requirements + +If the project maintainer has any additional requirements, you will find them listed here. + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + +**Happy coding**! diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..9d86a80e --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright (c) imad + +> 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. diff --git a/README.md b/README.md index fa8362b7..964ecb89 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,113 @@ -# New Notification Channels +# UNIFONIC notification channel for Laravel -### Suggesting a new channel -Have a suggestion or working on a new channel? Please create a new issue for that service. +[![Latest Version on Packagist](https://img.shields.io/packagist/v/laravel-notification-channels/unifonic.svg?style=flat-square)](https://packagist.org/packages/laravel-notification-channels/unifonic) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) +[![Build Status](https://img.shields.io/travis/laravel-notification-channels/unifonic/master.svg?style=flat-square)](https://travis-ci.org/laravel-notification-channels/unifonic) +[![StyleCI](https://styleci.io/repos/:style_ci_id/shield)](https://styleci.io/repos/:style_ci_id) +[![SensioLabsInsight](https://img.shields.io/sensiolabs/i/:sensio_labs_id.svg?style=flat-square)](https://insight.sensiolabs.com/projects/:sensio_labs_id) +[![Quality Score](https://img.shields.io/scrutinizer/g/laravel-notification-channels/unifonic.svg?style=flat-square)](https://scrutinizer-ci.com/g/laravel-notification-channels/unifonic) +[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/laravel-notification-channels/unifonic/master.svg?style=flat-square)](https://scrutinizer-ci.com/g/laravel-notification-channels/unifonic/?branch=master) +[![Total Downloads](https://img.shields.io/packagist/dt/laravel-notification-channels/unifonic.svg?style=flat-square)](https://packagist.org/packages/laravel-notification-channels/unifonic) -### I'm working on a new channel -Please create an issue for it if it does not already exist, then PR you code for review. +This package makes it easy to send notifications using [Unifonic Api](https://unifonic.docs.apiary.io/#reference/messages/send) with Laravel 5.5+, 6.x, 7.x and 8.x -## Workflow for new channels +## Contents -1) Head over to the [skeleton repo](https://github.com/laravel-notification-channels/skeleton) download a ZIP copy. This is important, to ensure you start from a fresh commit history. -2) Use find/replace to replace all of the placeholders with the correct values (package name, author name, email, etc). -3) Implement to logic for the channel & add tests. -4) Fork this repo, add it as a remote and push your new channel to a branch. -5) Submit a new PR against this repo for review. +- [Requierements](#requirements) +- [Installation](#installation) + - [Setting up the Unifonic service](#setting-up-the-Unifonic-service) +- [Usage](#usage) + - [Available Message methods](#available-message-methods) +- [Changelog](#changelog) +- [Testing](#testing) +- [Security](#security) +- [Contributing](#contributing) +- [Credits](#credits) +- [License](#license) -Take a look at our [FAQ](http://laravel-notification-channels.com/) to see our small list of rules, to provide top-notch notification channels. +## Requierements + +- Before start you have to create an account on [Unifonic](https://unifonic.com). + +## Installation + +1. You can install the package via composer: + +```bash +composer require laravel-notification-channels/unifonic +``` + +### Setting up the Unifonic service + +Add your Unifonic AppsId to your `config/services.php`: + +```php +// config/services.php +... +'unifonic' => [ + 'appsId' => env('UNIFONIC_APPS_ID'), +], +... +``` + +## Usage + +Now you can use the channel in your `via()` method inside the notification: + +```php +use NotificationChannels\Unifonic\UnifonicChannel; +use NotificationChannels\Unifonic\UnifonicMessage; +use Illuminate\Notifications\Notification; + +class InvoicePaid extends Notification +{ + public function via($notifiable) + { + return [UnifonicChannel::class]; + } + + public function toUnifonic($notifiable) + { + return UnifonicMessage::create("Your invoice {$notifiable->order->id} was paid!"); + } +} +``` + +In order to let your Notification know which phone numer you are targeting, add the `routeNotificationForUnifonic` method to your Notifiable model. + +**Important note**: Unifonic requires the recipients phone number to be [E.164](https://developers.omnisend.com/guides/e164-phone-number-formatting) + +```php +// app/Models/User.php +public function routeNotificationForUnifonic() +{ + return '+21267064497'; +} +``` + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. + +## Testing + +```bash +$ composer test +``` + +## Security + +If you discover any security related issues, please email imad@devinweb.com instead of using the issue tracker. + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +## Credits + +- [imad](https://github.com/darbaoui) +- [All Contributors](../../contributors) + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..0d99c7f7 --- /dev/null +++ b/composer.json @@ -0,0 +1,49 @@ +{ + "name": "laravel-notification-channels/unifonic", + "description": "Unifonic SMS notification channel for Laravel 5 and up", + "homepage": "https://github.com/laravel-notification-channels/unifonic", + "license": "MIT", + "authors": [ + { + "name": "imad", + "email": "imad@devinweb.com", + "homepage": "https://devinweb.com", + "role": "Developer" + } + ], + "require": { + "php": ">=7.2", + "guzzlehttp/guzzle": "^6.2|^7.0", + "illuminate/notifications": "~6.0 || ^7.0 || ^8.0", + "illuminate/support": "~6.0 || ^7.0 || ^8.0", + "orchestra/testbench": "~3.8.0 || ^4.0 || ^5.0 || ^6.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^9.0" + }, + "extra": { + "laravel": { + "providers": [ + "NotificationChannels\\Unifonic\\UnifonicServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "NotificationChannels\\Unifonic\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "NotificationChannels\\Unifonic\\Test\\": "tests" + } + }, + "scripts": { + "test": "phpunit", + "test:coverage": "phpunit --coverage-text --coverage-clover=coverage.clover" + }, + "config": { + "sort-packages": true + } +} \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..5f1112bc --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,22 @@ + + + + + tests + + + + + ./app + + + \ No newline at end of file diff --git a/src/Exceptions/CouldNotSendNotification.php b/src/Exceptions/CouldNotSendNotification.php new file mode 100644 index 00000000..355f8189 --- /dev/null +++ b/src/Exceptions/CouldNotSendNotification.php @@ -0,0 +1,11 @@ +client = $client; + } + + /** + * Send the given notification. + * + * @param mixed $notifiable + * @param \Illuminate\Notifications\Notification $notification + * + * @throws \NotificationChannels\Unifonic\Exceptions\CouldNotSendNotification + */ + public function send($notifiable, Notification $notification) + { + if (! $recipient = $notifiable->routeNotificationFor('Unifonic')) { + return; + } + + $message = $notification->toUnifonic($notifiable); + + if (is_string($message)) { + $message = UnifonicMessage::create($message); + } + + $recipient = $this->unifonicRecipientNumberValidation($recipient); + $this->client->send($message, $recipient); + } + + /** + * Remove + from the internation phone number. + * @param string $recipient + * + * @return string + */ + private function unifonicRecipientNumberValidation($recipient) + { + return preg_replace('/^\+?1|\|1|\D/', '', $recipient); + } +} diff --git a/src/UnifonicClient.php b/src/UnifonicClient.php new file mode 100644 index 00000000..59bfb4b1 --- /dev/null +++ b/src/UnifonicClient.php @@ -0,0 +1,67 @@ +client = $client; + $this->appsId = $appsId; + } + + /** + * @param UnifonicMessage $message + * @param string $recipient + * + * @throws CouldNotSendNotification + */ + public function send(UnifonicMessage $message, string $recipient) + { + $response = $this->client->request('POST', static::GATEWAY_URL, [ + 'form_params' => $this->buildMessageParameters($message, $recipient), + ]); + + $body = $response->getBody()->getContents(); + $array_body = json_decode($body, true); + if (Arr::get($array_body, 'success') === 'false') { + throw CouldNotSendNotification::serviceRespondedWithAnError($body); + } + } + + /** + * @param UnifonicMessage $message + * @param string $recipient + * + * @return array $parameters + */ + private function buildMessageParameters(UnifonicMessage $message, string $recipient): array + { + $mesage_body = $message->getContent(); + $parameters = array_merge(array_filter(['Body' => $mesage_body, 'Recipient' => $recipient]), [ + 'AppSid' => $this->appsId, + ]); + + return $parameters; + } +} diff --git a/src/UnifonicMessage.php b/src/UnifonicMessage.php new file mode 100644 index 00000000..7bdfb5e2 --- /dev/null +++ b/src/UnifonicMessage.php @@ -0,0 +1,53 @@ +body($body); + } + + /** + * @param string $body + * + * @return $this + */ + public function body(string $body) + { + $this->body = trim($body); + + return $this; + } + + /** + * get the content message. + * + * @return string $body + */ + public function getContent(): string + { + return $this->body; + } + + /** + * @param string $body + * + * @return static + */ + public static function create($body = ''): self + { + return new static($body); + } +} diff --git a/src/UnifonicServiceProvider.php b/src/UnifonicServiceProvider.php new file mode 100644 index 00000000..91f1cd40 --- /dev/null +++ b/src/UnifonicServiceProvider.php @@ -0,0 +1,28 @@ +app->when(UnifonicChannel::class) + ->needs(UnifonicClient::class) + ->give(function () { + if (is_null($appsId = config('services.unifonic.appsId'))) { + throw InvalidConfiguration::configurationNotSet(); + } + + return new UnifonicClient(new GuzzleClient(), $appsId); + }); + } +} diff --git a/tests/UnifonicChannelTest.php b/tests/UnifonicChannelTest.php new file mode 100644 index 00000000..a47b782b --- /dev/null +++ b/tests/UnifonicChannelTest.php @@ -0,0 +1,71 @@ +notification = new TestNotification(); + $this->notifiable = new TestNotifiable(); + $this->guzzle = Mockery::mock(new Client()); + $this->appsId = '1335b3f7-ba23-4316-b2dd-202448bd8904'; + // Config::set('services.unifonic.appsId', $this->appsId); + $this->client = Mockery::mock(new UnifonicClient($this->guzzle, $this->appsId)); + $this->channel = new UnifonicChannel($this->client); + } + + public function tearDown(): void + { + Mockery::close(); + parent::tearDown(); + } + + /** @test */ + public function it_can_be_instantiated() + { + $this->assertInstanceOf(UnifonicClient::class, $this->client); + $this->assertInstanceOf(UnifonicChannel::class, $this->channel); + } + + /** + * @test + * @doesNotPerformAssertions + */ + public function it_send_message() + { + $this->client->shouldReceive('send')->once(); + $this->channel->send($this->notifiable, $this->notification); + } +} + +class TestNotifiable +{ + use Notifiable; + + public function routeNotificationForUnifonic() + { + return '212679064497'; + } +} + +class TestNotification extends Notification +{ + public function toUnifonic($notifiable) + { + return UnifonicMessage::create('Message content'); + } +} diff --git a/tests/UnifonicClientTest.php b/tests/UnifonicClientTest.php new file mode 100644 index 00000000..64e54f57 --- /dev/null +++ b/tests/UnifonicClientTest.php @@ -0,0 +1,60 @@ +appsId = '1335b3f7-ba23-4316-b2dd-202448bd8904'; + $this->guzzle = Mockery::mock(new Client()); + $this->client = Mockery::mock(new UnifonicClient($this->guzzle, $this->appsId)); + $this->message = UnifonicMessage::create('Message body'); + } + + public function tearDown(): void + { + Mockery::close(); + parent::tearDown(); + } + + /** @test */ + public function it_can_be_instantiated() + { + $this->assertInstanceOf(UnifonicClient::class, $this->client); + $this->assertInstanceOf(UnifonicMessage::class, $this->message); + } + + /** + * @test + * @doesNotPerformAssertions + */ + public function it_can_send_message() + { + $this->guzzle + ->shouldReceive('request') + ->once() + ->andReturn(new Response(200, [], '')); + $this->client->send($this->message, '212679'); + } + + /** @test */ + public function it_throws_exception_on_error_response() + { + $this->expectException(CouldNotSendNotification::class); + $this->guzzle + ->shouldReceive('request') + ->once() + ->andReturn(new Response(200, [], '{"success": "false"}')); + $this->client->send($this->message, '212679'); + } +} diff --git a/tests/UnifonicMessageTest.php b/tests/UnifonicMessageTest.php new file mode 100644 index 00000000..8a14c6b9 --- /dev/null +++ b/tests/UnifonicMessageTest.php @@ -0,0 +1,26 @@ +assertInstanceOf(UnifonicMessage::class, $message); + } + + /** @test */ + public function it_supports_create_method() + { + $message = UnifonicMessage::create('Foo'); + + $this->assertInstanceOf(UnifonicMessage::class, $message); + $this->assertEquals('Foo', $message->getContent()); + } +}