maxbeckers/amazon-alexa-bundle
Symfony bundle for building Amazon Alexa skills with ease. It handles Alexa requests and responses, intent and event dispatching, session and slot data, and integrates the amazon-alexa-php library into your Symfony app through services and configuration.
Install and register
composer require maxbeckers/amazon-alexa-bundle
Symfony Flex likely has no recipe for this bundle, so register it manually in config/bundles.php:
return [
// ...
MaxBeckers\AmazonAlexaBundle\AmazonAlexaBundle::class => ['all' => true],
];
Configure the skill
The bundle’s extension exposes the Alexa application ID (and possibly signature-validation settings). Confirm the exact root key and options with:
bin/console config:dump-reference amazon_alexa
# or, if that fails:
bin/console config:dump-reference maxbeckers_amazon_alexa
Typical config:
# config/packages/amazon_alexa.yaml
amazon_alexa:
application_id: '%env(ALEXA_SKILL_ID)%'
ALEXA_SKILL_ID=amzn1.ask.skill.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Where to look first
vendor/maxbeckers/amazon-alexa-bundle/src/Resources/config/ — the service definitions and any compiler passes. This tells you which services are public, which tag is used to collect request handlers, and whether a controller is provided.vendor/maxbeckers/amazon-alexa-php/README.md and src/ — the bundle is a thin Symfony wrapper around this library; all the Request, Response, OutputSpeech, and RequestHandlerInterface classes come from there.bin/console debug:container --tag=<handler-tag> and debug:router — verify handler discovery and whether you need to wire your own endpoint.First use case: a Hello World intent handler
Create one handler per intent, implementing the library’s request-handler interface and tagged for auto-collection by the bundle:
namespace App\Alexa;
use MaxBeckers\AmazonAlexa\Request\Request;
use MaxBeckers\AmazonAlexa\RequestHandler\RequestHandlerInterface;
use MaxBeckers\AmazonAlexa\Response\OutputSpeech;
use MaxBeckers\AmazonAlexa\Response\Response;
class HelloWorldHandler implements RequestHandlerInterface
{
public function supportsRequest(Request $request): bool
{
return 'IntentRequest' === $request->request->type
&& 'HelloWorldIntent' === $request->request->intent->name;
}
public function handleRequest(Request $request): Response
{
$response = new Response();
$response->addOutputSpeech(new OutputSpeech('Hello from Symfony'));
return $response;
}
}
Register it with the bundle’s handler tag (check the bundle’s compiler pass for the exact name; commonly amazon_alexa.request_handler):
# config/services.yaml
services:
App\Alexa\HelloWorldHandler:
tags: ['amazon_alexa.request_handler']
If the bundle does not ship a controller/route, expose a thin endpoint that converts the raw HTTP request into an Alexa request and delegates to the registry:
namespace App\Controller;
use MaxBeckers\AmazonAlexa\Request\Request as AlexaRequest;
use MaxBeckers\AmazonAlexa\RequestHandler\RequestHandlerRegistry;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\Routing\Attribute\Route;
class AlexaController
{
#[Route('/alexa', methods: ['POST'])]
public function __invoke(HttpRequest $http, RequestHandlerRegistry $registry): JsonResponse
{
$alexaRequest = AlexaRequest::fromAmazonRequest(
$http->getContent(),
$http->headers->get('SignatureCertChainUrl', ''),
$http->headers->get('Signature', '')
);
$response = $registry->getRequestHandler($alexaRequest)->handleRequest($alexaRequest);
return new JsonResponse($response);
}
}
Point your skill’s endpoint at this route, expose it over HTTPS, and test through the Alexa Developer Console simulator or ask CLI.
One handler per intent, with dedicated fallback handlers
The cleanest structure is a handler per intent plus separate handlers for the built-in request types every skill needs:
LaunchRequest — welcome message, optionally keep the session open with a reprompt.IntentRequest — one class per intent, supportsRequest() checks the intent name.SessionEndedRequest — cleanup/logging; no meaningful response.AMAZON.HelpIntent, AMAZON.StopIntent, AMAZON.CancelIntent, AMAZON.FallbackIntent — handle these explicitly instead of relying on a generic catch-all.Order matters if your registry is a first-match list. Register the specific handlers before a catch-all fallback.
Keep handlers thin; inject domain services
class OrderStatusHandler implements RequestHandlerInterface
{
public function __construct(private OrderRepository $orders) {}
public function supportsRequest(Request $request): bool
{
return 'IntentRequest' === $request->request->type
&& 'OrderStatusIntent' === $request->request->intent->name;
}
public function handleRequest(Request $request): Response
{
$slot = $request->request->intent->slots['orderNumber'] ?? null;
$order = $slot ? $this->orders->findByNumber($slot->value) : null;
$speech = $order
? sprintf('Order %s is %s.', $order->getNumber(), $order->getStatus())
: 'I could not find that order.';
$response = new Response();
$response->addOutputSpeech(new OutputSpeech($speech));
return $response;
}
}
Slot extraction and validation
Slots live at $request->request->intent->slots, keyed by slot name. Treat value as nullable and validate before querying. Use confirmationStatus if your interaction model uses confirmations. For dates/numbers, normalise explicitly — Alexa sends strings.
Session attributes
Read from $request->session->attributes and write back via the response’s session attributes:
$attributes = $request->session->attributes ?? [];
$attributes['lastOrder'] = $order->getNumber();
$response = new Response();
$response->sessionAttributes = $attributes;
This is the simplest way to keep multi-turn context without a database round trip, but don’t store sensitive data — Alexa session attributes are client-side.
Response construction
OutputSpeech supports plain text and SSML (<speak>...</speak>); use SSML for pauses, emphasis, and audio.addReprompt() when shouldEndSession is false — Alexa requires a reprompt if the session stays open.Card for visual responses in the Alexa app.Testing
Request object (or a fixture-backed one) and assert on the returned Response speech and session attributes.tests/Fixtures/Alexa/ for each intent and built-in request type.Operational integration
requestId, intent name, and user ID for debugging.Use the raw body, not parsed request data
Alexa signature validation is computed over the exact raw JSON body. Read $http->getContent(). Do not use $http->request->all() or a deserialized object, and beware proxies or middleware that rewrite the body.
Headers can be stripped in transit
Signature validation needs Signature and SignatureCertChainUrl. Reverse proxies, load balancers, and some PHP-FPM configurations drop or rename these. Verify they arrive at the controller before blaming the library. If validation fails behind a proxy, fix the proxy rather than disabling validation.
HTTPS is mandatory
Alexa only calls HTTPS endpoints with a valid, publicly trusted certificate. Local development requires a tunnel (ngrok, Expose, etc.) or testing through unit tests. Self-signed certificates will not work with the real service.
Application ID mismatch
If the configured application ID doesn’t match session.application.applicationId, requests should be rejected. This is a common cause of “the skill works in the simulator but not on a device” — double-check the env var per environment, and be aware that development and production skills have different IDs.
Symfony Request name collision
The library and Symfony both have a Request class. Alias one:
use MaxBeckers\AmazonAlexa\Request\Request as AlexaRequest;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
This bites quickly in controllers.
Handler tag and registry wiring
The bundle relies on a compiler pass to collect tagged handlers. If your handler is never invoked, run debug:container --tag=<tag> to confirm it was collected, and check that the service is not private in a way that prevents injection. If there is no registry service, the bundle may expect you to build one yourself from the library.
Always return a valid Alexa response
If your code throws, returning a 500 produces “the skill is not responding.” Catch domain exceptions inside handlers and return a spoken error response. Let infrastructure errors be logged, but keep the Alexa contract intact.
SSML escaping
Ampersands and angle brackets must be escaped inside SSML, or Alexa rejects the response. Use htmlspecialchars() for user-supplied values interpolated into <speak> content.
Locale and i18n
$request->request->locale tells you the user’s locale. If you support multiple languages, route speech strings through Symfony’s translator and set the locale per request rather than assuming en-US.
Small ecosystem — read the source
This bundle has low adoption and no dependents. There is no large body of blog posts or Stack Overflow answers, and no Flex recipe. Budget time to read services.xml, the compiler pass, and the underlying maxbeckers/amazon-alexa-php source. Pin the bundle and library versions in composer.json, and check compatibility with your Symfony major version when upgrading.
Extension points
How can I help you explore Laravel packages today?