Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ jobs:
strategy:
matrix:
php: [7.4, 7.3]
laravel: [8.*]
laravel: [6.*, 8.*]
include:
- laravel: 6.*
testbench: 4.*
- laravel: 8.*
testbench: 6.*

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
.idea
.php_cs.cache
.phpunit.result.cache
composer.lock
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Introduction
# Laravel SNS Handler
This package provides an easy way of adding AWS SNS message handling to your Laravel application as a REST endpoint. The package can automatically confirm subscription requests and dispatches events when a message is received.

## How to create an SNS topic
Expand Down Expand Up @@ -75,3 +75,14 @@ This package adds a route to your application for incoming SNS requests. Note th

**Note: You will only be able to subscribe your endpoint if it can be reached from the AWS SNS service**

## Development
This package is expected to work with supported versions of Laravel, including LTS releases. During development, you should be sure to run tests and validate expected behaviors under different releases. Since we use the `orchestra/testbench` package, you can easily switch between installed Laravel framework releases using `composer`:

```bash
# Laravel 6
composer require --dev orchestra/testbench:^4.0 -W
# Laravel 8
composer require --dev orchestra/testbench:^6.0 -W
```

New releases of Laravel should be added to the GitHub workflow matrix in `.github/workflows/run-tests.yml`.
10 changes: 7 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
"require": {
"php": ">=7.3",
"ext-json": "*",
"aws/aws-php-sns-message-validator": "^1.6"
"aws/aws-php-sns-message-validator": "^1.6",
"php-http/discovery": "^1.14",
"psr/http-factory": "^1.0",
"psr/http-client": "^1.0"
},
"require-dev" : {
"roave/security-advisories": "dev-latest",
"guzzlehttp/guzzle": "^7.2",
"orchestra/testbench": "^6.0"
"orchestra/testbench": "^6.0",
"http-interop/http-factory-guzzle": "^1.2",
"php-http/guzzle6-adapter": "^2.0"
},
"autoload": {
"psr-4": {
Expand Down
7 changes: 6 additions & 1 deletion config/sns-handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@

return [
'validate-sns-messages' => env('VALIDATE_SNS_MESSAGES', true),
'sns-class-map' => []
'confirmation-events' => [
Nipwaayoni\SnsHandler\Events\SnsConfirmationRequestReceived::class => ['*']
],
'message-events' => [
Nipwaayoni\SnsHandler\Events\SnsMessageReceived::class => ['*']
],
];
62 changes: 52 additions & 10 deletions src/Listeners/SnsConfirmationRequestListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,67 @@

namespace Nipwaayoni\SnsHandler\Listeners;

use Illuminate\Support\Facades\Http;
use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\Psr17FactoryDiscovery;
use Illuminate\Support\Facades\Log;
use Nipwaayoni\SnsHandler\Events\SnsConfirmationRequestReceived;
use Nipwaayoni\SnsHandler\SnsConfirmSubscriptionException;
use Nipwaayoni\SnsHandler\SnsException;
use Nipwaayoni\SnsHandler\SnsMessage;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseInterface;

class SnsConfirmationRequestListener
{
/**
* @var ClientInterface
*/
private $client;

/**
* @var RequestFactoryInterface
*/
private $requestFactory;

/**
* @param ClientInterface|null $client
* @param RequestFactoryInterface|null $requestFactory
*/
public function __construct(
ClientInterface $client = null,
RequestFactoryInterface $requestFactory = null
) {
$this->client = $client ?? HttpClientDiscovery::find();
$this->requestFactory = $requestFactory ?? Psr17FactoryDiscovery::findRequestFactory();
}

public function handle(SnsConfirmationRequestReceived $event)
{
$message = $event->message();
//TODO Make this work with Laravel 6, as the Http facade was introduced in Laravel 7
$response = Http::get($message->subscribeUrl());
if ($response->successful()) {
$info = sprintf('Subscription confirmation for %s succeeded with status %s', $message->topicArn(), $response->status());
Log::info($info);
return;

$response = $this->getResponse($message);

Log::info(sprintf('Subscription confirmation for %s succeeded with status %s', $message->topicArn(), $response->getStatusCode()));
}

/**
* @param SnsMessage $message
* @return ResponseInterface
* @throws SnsConfirmSubscriptionException
* @throws SnsException
*/
private function getResponse(SnsMessage $message): ResponseInterface
{
try {
return $this->client->sendRequest(
$this->requestFactory->createRequest('GET', $message->subscribeUrl())
);
} catch (ClientExceptionInterface $e) {
throw new SnsConfirmSubscriptionException(
sprintf('Subscription confirmation for %s failed with status %s', $message->topicArn(), $e->getCode())
);
}
$error = sprintf('Subscription confirmation for %s failed with status %s', $message->topicArn(), $response->status());
Log::error($error);
throw new SnsConfirmSubscriptionException($error);
}
}
42 changes: 34 additions & 8 deletions src/SnsBroker.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@

use Aws\Sns\Message;
use Aws\Sns\MessageValidator;
use Illuminate\Config\Repository as Config;
use Illuminate\Support\Facades\Log;
use Nipwaayoni\SnsHandler\Events\SnsConfirmationRequestReceived;
use Nipwaayoni\SnsHandler\Events\SnsMessageReceived;

class SnsBroker
{
Expand All @@ -19,10 +18,15 @@ class SnsBroker
* @var Log
*/
private $log;
/**
* @var Config
*/
private $config;

public function __construct(MessageValidator $validator)
public function __construct(MessageValidator $validator, Config $config)
{
$this->validator = $validator;
$this->config = $config;
}

/**
Expand Down Expand Up @@ -66,19 +70,35 @@ public function handleRequest(SnsHttpRequest $request): void
throw new SnsException(sprintf('Unknown message type: %s', $message->type()));
}

private function getSubscriptionEvent(string $arn)
/**
* @param string $arn
* @return string
* @throws SnsUnknownTopicArnException
*/
private function getSubscriptionEvent(string $arn): string
{
$map = [SnsConfirmationRequestReceived::class => ['*']];
$map = $this->config->get('sns-handler.confirmation-events', []);
return $this->arnMap($arn, $map);
}

private function getNotificationEvent(string $arn)
/**
* @param string $arn
* @return string
* @throws SnsUnknownTopicArnException
*/
private function getNotificationEvent(string $arn): string
{
$map = [SnsMessageReceived::class => ['*']];
$map = $this->config->get('sns-handler.message-events', []);
return $this->arnMap($arn, $map);
}

private function arnMap(string $arn, array $map)
/**
* @param string $arn
* @param array $map
* @return string
* @throws SnsUnknownTopicArnException
*/
private function arnMap(string $arn, array $map): string
{
$default = null;
foreach ($map as $className => $arnList) {
Expand All @@ -90,6 +110,12 @@ private function arnMap(string $arn, array $map)
}
}

if (null === $default) {
throw new SnsUnknownTopicArnException(sprintf('Unmappable TopicArn: %s', $arn));
}

// TODO ensure class is dispatchable

return $default;
}
}
6 changes: 0 additions & 6 deletions src/SnsUnknownTopicArnException.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,6 @@

namespace Nipwaayoni\SnsHandler;

use Throwable;

class SnsUnknownTopicArnException extends SnsException
{
public function __construct(string $topicArn = '', $code = 0, Throwable $previous = null)
{
parent::__construct(sprintf('No handler registered for TopicArn %s', $topicArn), $code, $previous);
}
}
25 changes: 25 additions & 0 deletions tests/Events/SnsConfirmationRequestAlphaReceived.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php


namespace Nipwaayoni\Tests\SnsHandler\Events;

use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Nipwaayoni\SnsHandler\SnsMessage;

class SnsConfirmationRequestAlphaReceived
{
use Dispatchable, InteractsWithSockets;

private $message;

public function __construct(SnsMessage $message)
{
$this->message = $message;
}

public function message(): SnsMessage
{
return $this->message;
}
}
25 changes: 25 additions & 0 deletions tests/Events/SnsConfirmationRequestBetaReceived.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php


namespace Nipwaayoni\Tests\SnsHandler\Events;

use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Nipwaayoni\SnsHandler\SnsMessage;

class SnsConfirmationRequestBetaReceived
{
use Dispatchable, InteractsWithSockets;

private $message;

public function __construct(SnsMessage $message)
{
$this->message = $message;
}

public function message(): SnsMessage
{
return $this->message;
}
}
25 changes: 25 additions & 0 deletions tests/Events/SnsMessageAlphaReceived.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php


namespace Nipwaayoni\Tests\SnsHandler\Events;

use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Nipwaayoni\SnsHandler\SnsMessage;

class SnsMessageAlphaReceived
{
use Dispatchable, InteractsWithSockets;

private $message;

public function __construct(SnsMessage $message)
{
$this->message = $message;
}

public function message(): SnsMessage
{
return $this->message;
}
}
25 changes: 25 additions & 0 deletions tests/Events/SnsMessageBetaReceived.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php


namespace Nipwaayoni\Tests\SnsHandler\Events;

use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Nipwaayoni\SnsHandler\SnsMessage;

class SnsMessageBetaReceived
{
use Dispatchable, InteractsWithSockets;

private $message;

public function __construct(SnsMessage $message)
{
$this->message = $message;
}

public function message(): SnsMessage
{
return $this->message;
}
}
Loading