├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── scorecard.yml ├── .php_cs ├── .scrutinizer.yml ├── CONTRIBUTING.md ├── composer.json └── src ├── Commands ├── TwilioCallCommand.php └── TwilioSmsCommand.php ├── Dummy.php ├── LoggingDecorator.php ├── Manager.php ├── Support └── Laravel │ ├── Facade.php │ └── ServiceProvider.php ├── Twilio.php ├── TwilioInterface.php └── config └── config.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: hannesvdvreken 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "Composer" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '20 6 * * 5' 14 | push: 15 | branches: [ "5.x" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | # Uncomment the permissions below if installing in a private repository. 30 | # contents: read 31 | # actions: read 32 | 33 | steps: 34 | - name: "Checkout code" 35 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 36 | with: 37 | persist-credentials: false 38 | 39 | - name: "Run analysis" 40 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 41 | with: 42 | results_file: results.sarif 43 | results_format: sarif 44 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 45 | # - you want to enable the Branch-Protection check on a *public* repository, or 46 | # - you are installing Scorecard on a *private* repository 47 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. 48 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 49 | 50 | # Public repositories: 51 | # - Publish results to OpenSSF REST API for easy access by consumers 52 | # - Allows the repository to include the Scorecard badge. 53 | # - See https://github.com/ossf/scorecard-action#publishing-results. 54 | # For private repositories: 55 | # - `publish_results` will always be set to `false`, regardless 56 | # of the value entered here. 57 | publish_results: true 58 | 59 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 60 | # format to the repository Actions tab. 61 | - name: "Upload artifact" 62 | uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 63 | with: 64 | name: SARIF file 65 | path: results.sarif 66 | retention-days: 5 67 | 68 | # Upload the results to GitHub's code scanning dashboard (optional). 69 | # Commenting out will disable upload of results to your repo's Code Scanning dashboard 70 | - name: "Upload to code-scanning" 71 | uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 72 | with: 73 | sarif_file: results.sarif 74 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | in(['src', 'tests']); 4 | 5 | return PhpCsFixer\Config::create() 6 | ->setRules([ 7 | '@Symfony' => true, 8 | '@PhpCsFixer' => true, 9 | 'ereg_to_preg' => true, 10 | 'no_php4_constructor' => true, 11 | 'strict_comparison' => true, 12 | 'strict_param' => true, 13 | 'php_unit_internal_class' => false, 14 | 'php_unit_test_class_requires_covers' => false, 15 | 'no_superfluous_phpdoc_tags' => false, 16 | 'function_declaration' => [ 17 | 'closure_function_spacing' => 'none', 18 | ], 19 | 'phpdoc_align' => [ 20 | 'align' => 'left', 21 | ], 22 | 'multiline_whitespace_before_semicolons' => [ 23 | 'strategy' => 'no_multi_line', 24 | ], 25 | ]) 26 | ->setFinder($finder); 27 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: [tests/*] 3 | checks: 4 | php: 5 | code_rating: true 6 | remove_extra_empty_lines: true 7 | remove_php_closing_tag: true 8 | remove_trailing_whitespace: true 9 | fix_use_statements: 10 | remove_unused: true 11 | preserve_multiple: false 12 | preserve_blanklines: true 13 | order_alphabetically: true 14 | fix_php_opening_tag: true 15 | fix_linefeed: true 16 | fix_line_ending: true 17 | fix_identation_4spaces: true 18 | fix_doc_comments: true 19 | tools: 20 | php_analyzer: true 21 | php_code_coverage: false 22 | php_code_sniffer: 23 | config: 24 | standard: PSR2 25 | filter: 26 | paths: ['src'] 27 | php_loc: 28 | enabled: true 29 | excluded_dirs: [vendor, tests] 30 | php_cpd: 31 | enabled: true 32 | excluded_dirs: [vendor, tests] -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/aloha/laravel-twilio). 6 | 7 | ## Pull Requests 8 | 9 | - **[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 run `composer lint`. 10 | 11 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. First write a failing test that describes the bug, then write a fix. 12 | 13 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 14 | 15 | - **Keep it Backwards Compatible** - We try to follow [semver v2.0.0](http://semver.org/). Randomly breaking public APIs is not a good idea. 16 | 17 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 18 | 19 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. 20 | 21 | ## Running Tests 22 | 23 | ``` bash 24 | $ composer tests 25 | ``` 26 | 27 | **Happy coding**! 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aloha/twilio", 3 | "description": "Twilio API for Laravel", 4 | "type": "library", 5 | "keywords": ["sms", "ivr", "laravel", "twilio"], 6 | "authors": [ 7 | { 8 | "name": "Travis J Ryan", 9 | "email": "travisjryan@gmail.com" 10 | }, 11 | { 12 | "name": "Hannes Van De Vreken", 13 | "email": "vandevreken.hannes@gmail.com" 14 | } 15 | ], 16 | "homepage":"https://github.com/aloha/laravel-twilio", 17 | "license": "MIT", 18 | "require": { 19 | "php": ">=7.2.0", 20 | "twilio/sdk": "^6.0" 21 | }, 22 | "require-dev": { 23 | "friendsofphp/php-cs-fixer": "^2.16", 24 | "illuminate/console": "~6||~7||~8||~9", 25 | "illuminate/support": "~6||~7||~8||~9", 26 | "phpunit/phpunit": "^8.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Aloha\\Twilio\\": "src/" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Aloha\\Twilio\\Tests\\": "tests/" 36 | } 37 | }, 38 | "scripts": { 39 | "tests": "phpunit", 40 | "lint": "php-cs-fixer fix --allow-risky=yes" 41 | }, 42 | "funding": [ 43 | { 44 | "type": "github", 45 | "url": "https://github.com/sponsors/hannesvdvreken" 46 | } 47 | ], 48 | "config": { 49 | "sort-packages": true 50 | }, 51 | "extra": { 52 | "laravel": { 53 | "providers": [ 54 | "Aloha\\Twilio\\Support\\Laravel\\ServiceProvider" 55 | ], 56 | "aliases": { 57 | "Twilio": "Aloha\\Twilio\\Support\\Laravel\\Facade" 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Commands/TwilioCallCommand.php: -------------------------------------------------------------------------------- 1 | twilio = $twilio; 41 | } 42 | 43 | /** 44 | * Execute the console command. 45 | */ 46 | public function handle() 47 | { 48 | $this->line('Creating a call via Twilio to: '.$this->argument('phone')); 49 | 50 | // Grab options 51 | $from = $this->option('from'); 52 | $url = $this->option('url'); 53 | 54 | // Set a default URL if we haven't specified one since is mandatory. 55 | if (is_null($url)) { 56 | $url = 'http://demo.twilio.com/docs/voice.xml'; 57 | } 58 | 59 | $this->twilio->call($this->argument('phone'), $url, [], $from); 60 | } 61 | 62 | /** 63 | * Get the console command arguments. 64 | * 65 | * @return array 66 | */ 67 | protected function getArguments(): array 68 | { 69 | return [ 70 | ['phone', InputArgument::REQUIRED, 'The phone number that will receive a test message.'], 71 | ]; 72 | } 73 | 74 | /** 75 | * Get the console command options. 76 | * 77 | * @return array 78 | */ 79 | protected function getOptions(): array 80 | { 81 | return [ 82 | ['url', null, InputOption::VALUE_OPTIONAL, 'Optional url that will be used to fetch xml for call.', null], 83 | ['from', null, InputOption::VALUE_OPTIONAL, 'Optional from number that will be used.', null], 84 | ]; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Commands/TwilioSmsCommand.php: -------------------------------------------------------------------------------- 1 | twilio = $twilio; 41 | } 42 | 43 | /** 44 | * Execute the console command. 45 | */ 46 | public function handle() 47 | { 48 | $this->line('Sending SMS via Twilio to: '.$this->argument('phone')); 49 | 50 | // Grab the text option if specified 51 | $text = $this->option('text'); 52 | 53 | // If we havent specified a message, setup a default one 54 | if (is_null($text)) { 55 | $text = 'This is a test message sent from the artisan console'; 56 | } 57 | 58 | $this->line($text); 59 | 60 | $this->twilio->message($this->argument('phone'), $text); 61 | } 62 | 63 | /** 64 | * Get the console command arguments. 65 | * 66 | * @return array 67 | */ 68 | protected function getArguments(): array 69 | { 70 | return [ 71 | ['phone', InputArgument::REQUIRED, 'The phone number that will receive a test message.'], 72 | ]; 73 | } 74 | 75 | /** 76 | * Get the console command options. 77 | * 78 | * @return array 79 | */ 80 | protected function getOptions(): array 81 | { 82 | return [ 83 | ['text', null, InputOption::VALUE_OPTIONAL, 'Optional message that will be sent.', null], 84 | ]; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Dummy.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 28 | $this->wrapped = $wrapped; 29 | } 30 | 31 | /** 32 | * @param string $to 33 | * @param string $message 34 | * @param array $mediaUrls 35 | * @param array $params 36 | * 37 | * @return MessageInstance 38 | */ 39 | public function message($to, $message, array $mediaUrls = [], array $params = []): MessageInstance 40 | { 41 | $this->logger->info(sprintf('Sending a message ["%s"] to %s', $message, $to)); 42 | 43 | return $this->wrapped->message($to, $message, $mediaUrls, $params); 44 | } 45 | 46 | /** 47 | * @param string $to 48 | * @param callable|string $message 49 | * @param array $params 50 | * 51 | * @return CallInstance 52 | */ 53 | public function call(string $to, $message, array $params = []): CallInstance 54 | { 55 | $this->logger->info(sprintf('Calling %s', $to)); 56 | 57 | return $this->wrapped->call($to, $message, $params); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Manager.php: -------------------------------------------------------------------------------- 1 | default = $default; 29 | $this->settings = $settings; 30 | } 31 | 32 | /** 33 | * @param string $method 34 | * @param array $arguments 35 | * 36 | * @return mixed 37 | */ 38 | public function __call(string $method, array $arguments) 39 | { 40 | return call_user_func_array([$this->defaultConnection(), $method], $arguments); 41 | } 42 | 43 | /** 44 | * @param string $connection 45 | * 46 | * @return TwilioInterface 47 | */ 48 | public function from(string $connection): TwilioInterface 49 | { 50 | if (!isset($this->settings[$connection])) { 51 | throw new InvalidArgumentException("Connection \"{$connection}\" is not configured."); 52 | } 53 | 54 | $settings = $this->settings[$connection]; 55 | 56 | return new Twilio($settings['sid'], $settings['token'], $settings['from']); 57 | } 58 | 59 | /** 60 | * @param string $to 61 | * @param string $message 62 | * @param array $mediaUrls 63 | * @param array $params 64 | * 65 | * @return MessageInstance 66 | */ 67 | public function message(string $to, string $message, array $mediaUrls = [], array $params = []): MessageInstance 68 | { 69 | return $this->defaultConnection()->message($to, $message, $mediaUrls, $params); 70 | } 71 | 72 | /** 73 | * @param string $to 74 | * @param callable|string|TwiML $message 75 | * @param array $params 76 | * 77 | * @return CallInstance 78 | */ 79 | public function call(string $to, $message, array $params = []): CallInstance 80 | { 81 | return $this->defaultConnection()->call($to, $message, $params); 82 | } 83 | 84 | /** 85 | * @return TwilioInterface 86 | */ 87 | public function defaultConnection(): TwilioInterface 88 | { 89 | return $this->from($this->default); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Support/Laravel/Facade.php: -------------------------------------------------------------------------------- 1 | app->singleton('twilio', function() { 21 | $config = $this->app['config']->get('twilio.twilio'); 22 | 23 | return new Manager($config['default'], $config['connections']); 24 | }); 25 | 26 | // Define an alias. 27 | $this->app->alias('twilio', Manager::class); 28 | 29 | // Register Twilio Test SMS Command. 30 | $this->app->singleton('twilio.sms', TwilioSmsCommand::class); 31 | 32 | // Register Twilio Test Call Command. 33 | $this->app->singleton('twilio.call', TwilioCallCommand::class); 34 | 35 | // Register TwilioInterface concretion. 36 | $this->app->singleton(TwilioInterface::class, function() { 37 | return $this->app->make('twilio')->defaultConnection(); 38 | }); 39 | } 40 | 41 | /** 42 | * Boot method. 43 | */ 44 | public function boot() 45 | { 46 | $this->publishes([ 47 | __DIR__.'/../../config/config.php' => config_path('twilio.php'), 48 | ], 'config'); 49 | 50 | $this->mergeConfigFrom(__DIR__.'/../../config/config.php', 'twilio'); 51 | 52 | $this->commands([ 53 | TwilioCallCommand::class, 54 | TwilioSmsCommand::class, 55 | ]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Twilio.php: -------------------------------------------------------------------------------- 1 | sid = $sid; 49 | $this->token = $token; 50 | $this->from = $from; 51 | $this->sslVerify = $sslVerify; 52 | } 53 | 54 | /** 55 | * @param string $to 56 | * @param string $message 57 | * @param array $mediaUrls 58 | * @param array $params 59 | * 60 | * @see https://www.twilio.com/docs/api/messaging/send-messages Documentation 61 | * 62 | * @throws ConfigurationException 63 | * @throws TwilioException 64 | * 65 | * @return MessageInstance 66 | */ 67 | public function message(string $to, string $message, array $mediaUrls = [], array $params = []): MessageInstance 68 | { 69 | $params['body'] = $message; 70 | 71 | if (!isset($params['from'])) { 72 | $params['from'] = $this->from; 73 | } 74 | 75 | if (!empty($mediaUrls)) { 76 | $params['mediaUrl'] = $mediaUrls; 77 | } 78 | 79 | return $this->getTwilio()->messages->create($to, $params); 80 | } 81 | 82 | /** 83 | * @param string $to 84 | * @param callable|string|TwiML $message 85 | * @param array $params 86 | * 87 | * @throws TwilioException 88 | * 89 | * @see https://www.twilio.com/docs/api/voice/making-calls Documentation 90 | * 91 | * @return CallInstance 92 | */ 93 | public function call(string $to, $message, array $params = []): CallInstance 94 | { 95 | if (is_callable($message)) { 96 | $message = $this->twiml($message); 97 | } 98 | 99 | if ($message instanceof TwiML) { 100 | $params['twiml'] = (string) $message; 101 | } else { 102 | $params['url'] = $message; 103 | } 104 | 105 | return $this->getTwilio()->calls->create( 106 | $to, 107 | $this->from, 108 | $params 109 | ); 110 | } 111 | 112 | /** 113 | * @throws ConfigurationException 114 | * 115 | * @return Client 116 | */ 117 | public function getTwilio(): Client 118 | { 119 | if ($this->twilio) { 120 | return $this->twilio; 121 | } 122 | 123 | return $this->twilio = new Client($this->sid, $this->token); 124 | } 125 | 126 | /** 127 | * @param callable $callback 128 | * 129 | * @return TwiML 130 | */ 131 | private function twiml(callable $callback): TwiML 132 | { 133 | $message = new VoiceResponse(); 134 | 135 | call_user_func($callback, $message); 136 | 137 | return $message; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/TwilioInterface.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'default' => 'twilio', 6 | 'connections' => [ 7 | 'twilio' => [ 8 | /* 9 | |-------------------------------------------------------------------------- 10 | | SID 11 | |-------------------------------------------------------------------------- 12 | | 13 | | Your Twilio Account SID # 14 | | 15 | */ 16 | 'sid' => env('TWILIO_SID', ''), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Access Token 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Access token that can be found in your Twilio dashboard 24 | | 25 | */ 26 | 'token' => env('TWILIO_TOKEN', ''), 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | From Number 31 | |-------------------------------------------------------------------------- 32 | | 33 | | The Phone number registered with Twilio that your SMS & Calls will come from 34 | | 35 | */ 36 | 'from' => env('TWILIO_FROM', ''), 37 | ], 38 | ], 39 | ], 40 | ]; 41 | --------------------------------------------------------------------------------