Sidecar — Deploy and execute AWS Lambda functions from your Laravel application.

Executing Functions

Executing your Sidecar functions is as easy as calling execute on your function classes or on the Sidecar facade.

1// Using the function directly
2$result = OgImage::execute();
3
4// Using the facade
5$result = Sidecar::execute(OgImage::class);

Passing Data to Lambda

Most of your functions are going to require some input to operate properly. Anything you pass to execute will be passed on to your Lambda function.

1$result = OgImage::execute([
2 'title' => 'Executing Functions',
3 'url' => 'https://hammerstone.dev/sidecar/docs/main/functions/executing',
4 'template' => 'docs/sidecar'
5]);

The entire payload will be available for use in your Lambda function now:

image.js

1exports.handler = async function (event) {
2 console.log(event);
3 // {
4 // title: 'Executing Functions',
5 // url: 'https://hammerstone.dev/sidecar/docs/main/functions/executing',
6 // template: 'docs/sidecar'
7 // }
8}

Sync vs. Async

By default, all executions of Sidecar functions are synchronous, meaning script execution will stop while the Lambda finishes and returns its result. This is the simplest method and probably fine for the majority of use cases.

1// Synchronous execution.
2$result = OgImage::execute();
3
4echo 'Image has been fully created and returned!';

If you'd like for the execution to be asynchronous, meaning the rest of your script will carry on without waiting, you can use the executeAsync method, or pass a second param of true to the execute method.

1// Async execution using the class.
2$result = OgImage::executeAsync();
3$result = OgImage::execute($payload = [], $async = true);
4
5// Async execution using the facade.
6$result = Sidecar::executeAsync(OgImage::class);
7$result = Sidecar::execute(OgImage::class, $payload = [], $async = true);
8
9echo 'Image may or may not have finished generating yet!';

Settled Results

When your function is executed using one of the sync methods, the return value will be an instance of SettledResult. The Settled Result class is responsible for delivering the result of your Lambda, along with the logs and information about duration, memory used, etc.

You can read more about that in the body and logs & timing sections below.

Pending Results

If you function is invoked using one of the async methods, the return value will be an instance of PendingResult. This class is a thin wrapper around a Guzzle promise that represents your pending function execution.

Given a Pending Result, if you'd like to pause execution until the promise is settled, you can call settled. This will return a SettledResult.

1$result = OgImage::executeAsync();
2
3// Do some other stuff while the Lambda executes...
4// ...
5// ...
6
7// Halt execution now while we wait for the Lambda
8// execution to finish. (It may already be done!)
9$result = $result->settled();
10
11// $result is now a SettledResult.
12dump($result instanceof SettledResult);
13// true

Working With Either

If you're not sure whether a given result is a Settled Result or a Pending Result, you can always called settled.

  • When you call settled on a Settled Result, it will just return itself.
  • When you call settled on a Pending Result, it will wait, and then return the Settled Result.
  • When you call settled on a Pending Result that has already settled, it will return the same Settled Result it returned the first time!
1// Create a Pending Result
2$pending = OgImage::executeAsync();
3
4// Settle a Pending Result
5$result = $pending->settled();
6
7// Only one Settled Result per Pending result,
8// so you can call it over and over again!
9$pending->settled() === $pending->settled();
10// > true
11
12// Settle a Settled Result. No harm done!
13$result = OgImage::execute()->settled();
14
15// Settle a Settled Result over and over.
16// Silly, but not bad!
17$result = OgImage::execute()->settled()->settled()->settled()->settled();

Customizing the Results

If you want more control over the process of creating result classes, you can override the toResult class in your LambdaFunction. That method receives either an Aws\Result, or a Guzzle Promise, depending on whether the request was sync or async.

You may also override the toSettledResult or toPendingResult methods:

Image.php

1class OgImage extends LambdaFunction
2{
3 public function handler()
4 {
5 //
6 }
7
8 public function package()
9 {
10 //
11 }
12
13 public function toSettledResult(Result $raw)
14 {
15 // Use a custom settled result class for this function.
16 return new OgImageResult($raw, $this);
17 }
18}

Result Body

Your Lambda function will likely return some data to be consumed by your Laravel application. You can retrieve this data by calling the body method on the SettledResult class.

In the case of generating an image, the response might be the image itself:

image.js

1exports.handler = async function (event) {
2 const canvas = createCanvas(1200, 630)
3
4 const context = canvas.getContext('2d')
5
6 context.font = 'bold 70pt Helvetica'
7 context.textAlign = 'center'
8 context.fillStyle = '#3574d4'
9 context.fillText(text, 600, 170)
10
11 // Return an image.
12 return canvas.toDataURL('image/jpeg');
13}
1echo OgImage::execute()->body();
2
3// data:image/jpeg;base64,/9j/4AA[.....]cU+ThI/wBH/9k=

If your function returns a JSON object, you can access that via the body as well.

foo.js

1exports.handler = async function (event) {
2 return {
3 foo: 'bar'
4 }
5}
1echo FooFunction::execute()->body()['foo'];
2
3// bar

If you'd like to control how the body is decoded, you can pass any JSON options to the body method.

Because the default is JSON_OBJECT_AS_ARRAY, to decode your JSON into an object, you could simply pass null.

1echo FooFunction::execute()->body($options = null)->foo;
2
3// bar

Logs & Timing

To retrieve the logs from your function execution, you can call logs on the SettledResult class. Everything that is logged from your function will be returned for your inspection.

foo.js

1exports.handler = async function (event) {
2 console.log('Hi from Lambda!');
3
4 return {
5 foo: 'bar'
6 }
7}
1FooFunction::execute()->logs();
2
3// [
4// [
5// "timestamp" => 1619990695
6// "level" => "INFO"
7// "body" => "Hi from Lambda!"
8// ]
9// ]

To see a report on the timing and memory usage of your function, call the report method.

1FooFunction::execute()->report();
2
3// [
4// "request" => "75d3e393-f4ab-4528-a8d3-ee5c41c470c7"
5// "billed_duration" => 2
6// "execution_duration" => 1.06
7// "cold_boot_delay" => 0
8// "total_duration" => 1.06
9// "max_memory" => 66
10// "memory" => 512
11// ]

HTTP Responses

If you want to directly return a result as a proper HTTP response, you may override the toResponse method on your function.

By default, the toResponse function returns the body or your result:

1abstract class LambdaFunction
2{
3 public function toResponse($request, SettledResult $result)
4 {
5 $result->throw();
6
7 return response($result->body());
8 }
9}

This allows you to return directly from your controllers or routes:

web.php

1Route::get('/ogimage', function (Request $request) {
2 return OgImage::execute($request->query());
3});

In the case of our image, we'll want to customize the response a little bit so that the browser will render an image instead of a string of text. We can do this by customizing the toResponse method:

App\Sidecar\OgImage.php

1class OgImage extends LambdaFunction
2{
3 public function handler()
4 {
5 // Define your handler function.
6 // (Javascript file + export name.)
7 return 'lambda/image.handler';
8 }
9
10 public function package()
11 {
12 // All files and folders needed for the function.
13 return [
14 'lambda',
15 ];
16 }
17
18 public function toResponse($request, SettledResult $result)
19 {
20 // Throw an exception if it failed.
21 $result->throw();
22
23 $image = base64_decode($result->body());
24
25 // Set an appropriate header
26 return response($image)->header('Content-type', 'image/jpg');
27 }
28}

Now we can create images on Lambda and return them directly to the browser, and they will render as images!

Executing Multiple

To execute multiple functions at the same time, you can use the executeMany method on your LambdaFunction.

With No Payload

If you want to execute the function 5 times, with no payload, you can just pass in the integer 5.

1// $results will be an array of SettledResults
2$results = OgImage::executeMany(5);

This will return an array full of SettledResults.

With Payloads

More likely, you'll need to execute multiple functions with distinct payloads, in which case you can pass them as the first param.

1// $results will be an array of SettledResults
2$results = OgImage::executeMany([[
3 'text' => 'Creating Functions'
4], [
5 'text' => 'Deploying Functions'
6], [
7 'text' => 'Executing Functions'
8]]);

Without Waiting

By default the executions are all run in parallel, but then Sidecar waits until they are all settled to return anything.

To execute many functions without waiting for anything, pass true as the second parameter. This will return an array full of PendingResults to you.

1// $results will be an array of PendingResults
2$results = OgImage::executeMany([[
3 'text' => 'Creating Functions'
4], [
5 'text' => 'Deploying Functions'
6], [
7 'text' => 'Executing Functions'
8]], $async = true);

You can also call executeManyAsync:

1// $results will be an array of PendingResults
2$results = OgImage::executeManyAsync([[
3 'text' => 'Creating Functions'
4], [
5 'text' => 'Deploying Functions'
6], [
7 'text' => 'Executing Functions'
8]]);

Execution Exceptions

If your Lambda throws an exception or otherwise errors out, you'll need to be able to act on that back in your Laravel application.

We'll take a very basic example of a Node function that simply throws an error:

errors.js

1exports.handler = async function (event) {
2 throw new Error('Error from Lambda!');
3}

When executing that function from PHP, Sidecar will not throw an exception unless explicitly asked to.

1// Execute synchronously. No error thrown yet.
2$result = ErrorFunction::execute();
3
4// When asked to, Sidecar will throw a PHP Exception
5// if there was a runtime error.
6$result->throw();
7
8// > Hammerstone\Sidecar\Exceptions\LambdaExecutionException
9// > Lambda Execution Exception for App\Sidecar\FooFunction: "Error from Lambda!.
10// > [TRACE] Error: Error from Lambda!
11// > at Runtime.exports.handler (/var/task/lambda/error.js:2:11)
12// > at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)".

When you call throw, Sidecar will throw a LambdaExecutionException if there is one. If there isn't, nothing will happen.

1// Execute synchronously.
2$result = NonErrorFunction::execute();
3
4// No error? No problem. Call it just in case.
5return $result->throw()->body();

If you don't want to throw an exception, but want to handle it in another way, you may check the isError method.

1// Execute synchronously. No error thrown yet.
2$result = ErrorFunction::execute();
3
4if ($result->isError()) {
5 // Do something, anything!
6}

Sidecar also provides the trace to you:

1// Execute synchronously. Nothing will happen.
2$result = ErrorFunction::execute();
3
4// Dump the error trace.
5dd($result->trace());
6
7// [
8// "Error: Error from Lambda!"
9// " at Runtime.exports.handler (/var/task/lambda/error.js:2:11)"
10// " at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)"
11// ]