diff --git a/bref b/bref
index a5e83f209..0fa00f06d 100755
--- a/bref
+++ b/bref
@@ -2,15 +2,12 @@
command('init', function (SymfonyStyle $io) {
- $exeFinder = new ExecutableFinder();
- if (! $exeFinder->find('serverless')) {
- $io->error(
- 'The `serverless` command is not installed.' . PHP_EOL .
- 'Please follow the instructions at https://bref.sh/docs/installation.html'
- );
-
- return 1;
- }
-
- if (file_exists('serverless.yml') || file_exists('index.php')) {
- $io->error('The directory already contains a `serverless.yml` and/or `index.php` file.');
-
- return 1;
- }
-
- $choice = $io->choice(
- 'What kind of lambda do you want to create? (you will be able to add more functions later by editing `serverless.yml`)',
- [
- 'Web application',
- 'Event-driven function',
- ],
- 'Web application',
- );
- $templateDirectory = [
- 'Web application' => 'http',
- 'Event-driven function' => 'function',
- ][$choice];
-
- $fs = new Filesystem;
- $rootPath = __DIR__ . "/template/$templateDirectory";
-
- $io->writeln("Creating index.php");
- $fs->copy("$rootPath/index.php", 'index.php');
-
- $io->writeln("Creating serverless.yml");
-
- $template = file_get_contents("$rootPath/serverless.yml");
-
- $template = str_replace('PHP_VERSION', PHP_MAJOR_VERSION . PHP_MINOR_VERSION, $template);
-
- file_put_contents('serverless.yml', $template);
-
- $filesToGitAdd = ['index.php', 'serverless.yml'];
-
- /*
- * We check if this is a git repository to automatically add files to git.
- */
- if ((new Process(['git', 'rev-parse', '--is-inside-work-tree']))->run() === 0) {
- foreach ($filesToGitAdd as $file) {
- (new Process(['git', 'add', $file]))->run();
- }
- $io->success([
- 'Project initialized and ready to test or deploy.',
- 'The files created were automatically added to git.',
- ]);
- } else {
- $io->success('Project initialized and ready to test or deploy.');
- }
-
- return 0;
-});
-
-/**
- * Run a CLI command in the remote environment.
- */
-$app->command('cli function [--region=] [--profile=] [arguments]*', function (string $function, ?string $region, ?string $profile, array $arguments, SymfonyStyle $io) {
- $lambda = new SimpleLambdaClient(
- $region ?: getenv('AWS_DEFAULT_REGION') ?: 'us-east-1',
- $profile ?: getenv('AWS_PROFILE') ?: 'default',
- 15 * 60 // maximum duration on Lambda
- );
-
- // Because arguments may contain spaces, and are going to be executed remotely
- // as a separate process, we need to escape all arguments.
- $arguments = array_map(static function (string $arg): string {
- return escapeshellarg($arg);
- }, $arguments);
-
- try {
- $result = $lambda->invoke($function, json_encode(implode(' ', $arguments)));
- } catch (InvocationFailed $e) {
- $io->getErrorStyle()->writeln('' . $e->getInvocationLogs() . '');
- $io->error($e->getMessage());
- return 1;
- }
-
- $payload = $result->getPayload();
- if (isset($payload['output'])) {
- $io->writeln($payload['output']);
- } else {
- $io->error('The command did not return a valid response.');
- $io->writeln('Logs:');
- $io->write('' . $result->getLogs() . '');
- $io->writeln('Lambda result payload:');
- $io->writeln(json_encode($payload, JSON_PRETTY_PRINT));
- return 1;
- }
-
- return (int) ($payload['exitCode'] ?? 1);
-});
-
-/**
- * Invoke a function locally
- */
-$app->command(Local::SIGNATURE, new Local);
-
-$app->command('dashboard [--host=] [--port=] [--profile=] [--stage=]', function (SymfonyStyle $io, string $host = 'localhost', int $port = 8000, string $profile = null, string $stage = null) {
- $io->info('The Bref Dashboard is also available as an application: https://dashboard.bref.sh');
- if ($host === 'localhost') {
- $host = '127.0.0.1';
- }
- if ($profile === null) {
- $profile = getenv('AWS_PROFILE') ?: 'default';
- }
-
- if (! file_exists('serverless.yml')) {
- $io->error('No `serverless.yml` file found.');
-
- return 1;
- }
-
- $exeFinder = new ExecutableFinder();
- if (! $exeFinder->find('docker')) {
- $io->error(
- 'The `docker` command is not installed.' . PHP_EOL .
- 'Please follow the instructions at https://docs.docker.com/install/'
- );
-
- return 1;
- }
-
- if (! $exeFinder->find('serverless')) {
- $io->error(
- 'The `serverless` command is not installed.' . PHP_EOL .
- 'Please follow the instructions at https://bref.sh/docs/installation.html'
- );
-
- return 1;
- }
-
- $serverlessInfo = new Process(['serverless', 'info', '--stage', $stage, '--aws-profile', $profile]);
- $serverlessInfo->start();
- $animation = new LoadingAnimation($io);
- do {
- $animation->tick('Retrieving the stack');
- usleep(100*1000);
- } while ($serverlessInfo->isRunning());
- $animation->clear();
-
- if (!$serverlessInfo->isSuccessful()) {
- $io->error('The command `serverless info` failed' . PHP_EOL . $serverlessInfo->getOutput());
-
- return 1;
- }
-
- $serverlessInfoOutput = $serverlessInfo->getOutput();
-
- $region = [];
- preg_match('/region: ([a-z0-9-]*)/', $serverlessInfoOutput, $region);
- $region = $region[1];
-
- $stack = [];
- preg_match('/stack: ([a-zA-Z0-9-]*)/', $serverlessInfoOutput, $stack);
- $stack = $stack[1];
-
- $io->writeln("Stack: $stack ($region)>");
-
- $dockerPull = new Process(['docker', 'pull', 'bref/dashboard']);
- $dockerPull->setTimeout(null);
- $dockerPull->start();
- do {
- $animation->tick('Retrieving the latest version of the dashboard');
- usleep(100*1000);
- } while ($dockerPull->isRunning());
- $animation->clear();
- if (! $dockerPull->isSuccessful()) {
- $io->error([
- 'The command `docker pull bref/dashboard` failed',
- $dockerPull->getErrorOutput(),
- ]);
-
- return 1;
- }
-
- $process = new Process(['docker', 'run', '--rm', '-p', $host . ':' . $port.':8000', '-v', getenv('HOME').'/.aws:/root/.aws:ro', '--env', 'STACKNAME='.$stack, '--env', 'REGION='.$region, '--env', 'AWS_PROFILE='.$profile, 'bref/dashboard']);
- $process->setTimeout(null);
- $process->start();
- do {
- $animation->tick('Starting the dashboard');
- usleep(100*1000);
- $serverOutput = $process->getOutput() . $process->getErrorOutput();
- $hasStarted = (strpos($serverOutput, 'Development Server') !== false);
- } while ($process->isRunning() && !$hasStarted);
- $animation->clear();
- if (!$process->isRunning()) {
- $io->error([
- 'The dashboard failed to start',
- $process->getErrorOutput(),
- ]);
-
- return 1;
- }
- $url = "http://$host:$port";
- $io->writeln("Dashboard started: $url>");
- OpenUrl::open($url);
- $process->wait(function ($type, $buffer) {
- if (Process::ERR === $type) {
- echo 'ERR > '.$buffer;
- } else {
- echo 'OUT > '.$buffer;
- }
- });
-
- return $process->getExitCode();
-})->descriptions('Start the dashboard');
-
-$app->command('layers region', function (string $region, SymfonyStyle $io) {
- $layers = json_decode(file_get_contents(__DIR__ . '/layers.json'), true);
- $io->title("Layers for the $region region");
-
- $array = [];
- foreach ($layers as $layer => $versions) {
- $version = $versions[$region];
- $array[] = [
- $layer,
- $version,
- "arn:aws:lambda:$region:416566615250:layer:$layer:$version",
- ];
- }
- $io->table([
- 'Layer',
- 'Version',
- 'ARN',
- ], $array);
-
- return 0;
-})->descriptions('Displays the versions of the Bref layers');
+$app = new Application('Deploy serverless PHP applications');
+$app->add(new Init());
+$app->add(new Cli());
+$app->add(new Local());
+$app->add(new Dashboard());
+$app->add(new Layers());
$app->run();
diff --git a/composer.json b/composer.json
index f962caba2..ed9d41d15 100644
--- a/composer.json
+++ b/composer.json
@@ -22,7 +22,7 @@
"php": ">=7.3.0",
"ext-curl": "*",
"ext-json": "*",
- "mnapoli/silly": "^1.7",
+ "symfony/console": "^3.0|^4.0|^5.0|^6.0",
"symfony/filesystem": "^3.1|^4.0|^5.0|^6.0",
"symfony/process": "^4.2|^5.0|^6.0",
"psr/http-message": "^1.0",
diff --git a/demo/console.php b/demo/console.php
index b723076ea..a4723ca0e 100644
--- a/demo/console.php
+++ b/demo/console.php
@@ -1,27 +1,45 @@
command('hello [name]', function (string $name = 'World!', OutputInterface $output) {
- $output->writeln('Hello ' . $name);
-});
-$silly->command('phpinfo', function (OutputInterface $output) {
- ob_start();
- phpinfo();
- $phpinfo = ob_get_clean();
- $output->write($phpinfo);
-});
-$silly->command('error', function (OutputInterface $output) {
- $output->writeln('There was an error!');
- return 1;
-});
-$silly->command('sleep', function () {
- sleep(120);
-});
-
-$silly->run();
+$app = new Application();
+
+$app->register('hello')
+ ->addArgument('name', InputArgument::OPTIONAL, '', 'World!')
+ ->setCode(function (InputInterface $input, OutputInterface $output): int {
+ $output->writeln('Hello, ' . $input->getArgument('name'));
+ return Command::SUCCESS;
+ })
+;
+
+$app->register('phpinfo')
+ ->setCode(function (InputInterface $input, OutputInterface $output): int {
+ ob_start();
+ phpinfo();
+ $phpinfo = ob_get_clean();
+ $output->write($phpinfo);
+ return Command::SUCCESS;
+ })
+;
+
+$app->register('error')
+ ->setCode(function (InputInterface $input, OutputInterface $output): int {
+ $output->writeln('There was an error!');
+ return Command::FAILURE;
+ })
+;
+
+$app->register('sleep')
+ ->setCode(function (InputInterface $input, OutputInterface $output): int {
+ sleep(120);
+ return Command::SUCCESS;
+ })
+;
+
+$app->run();
diff --git a/src/Console/Command/Cli.php b/src/Console/Command/Cli.php
new file mode 100644
index 000000000..77dcbdf03
--- /dev/null
+++ b/src/Console/Command/Cli.php
@@ -0,0 +1,68 @@
+setName('cli')
+ ->setDescription('Runs a CLI command in the remote environment')
+ ->addArgument('function', InputArgument::REQUIRED)
+ ->addArgument('arguments', InputArgument::OPTIONAL)
+ ->addOption('region', 'r')
+ ->addOption('profile', 'p');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+ $function = $input->getArgument('function');
+ $arguments = $input->getArgument('arguments');
+ $region = $input->getOption('region');
+ $profile = $input->getOption('profile');
+
+ $lambda = new SimpleLambdaClient(
+ $region ?: getenv('AWS_DEFAULT_REGION') ?: 'us-east-1',
+ $profile ?: getenv('AWS_PROFILE') ?: 'default',
+ 15 * 60 // maximum duration on Lambda
+ );
+
+ // Because arguments may contain spaces, and are going to be executed remotely
+ // as a separate process, we need to escape all arguments.
+ $arguments = array_map(static function (string $arg): string {
+ return escapeshellarg($arg);
+ }, $arguments);
+
+ try {
+ $result = $lambda->invoke($function, json_encode(implode(' ', $arguments)));
+ } catch (InvocationFailed $e) {
+ $io->getErrorStyle()->writeln('' . $e->getInvocationLogs() . '');
+ $io->error($e->getMessage());
+ return Command::FAILURE;
+ }
+
+ $payload = $result->getPayload();
+ if (isset($payload['output'])) {
+ $io->writeln($payload['output']);
+ } else {
+ $io->error('The command did not return a valid response.');
+ $io->writeln('Logs:');
+ $io->write('' . $result->getLogs() . '');
+ $io->writeln('Lambda result payload:');
+ $io->writeln(json_encode($payload, JSON_PRETTY_PRINT));
+ return Command::FAILURE;
+ }
+
+ return (int) ($payload['exitCode'] ?? Command::FAILURE);
+ }
+}
diff --git a/src/Console/Command/Dashboard.php b/src/Console/Command/Dashboard.php
new file mode 100644
index 000000000..1f8bec496
--- /dev/null
+++ b/src/Console/Command/Dashboard.php
@@ -0,0 +1,144 @@
+setName('dashboard')
+ ->setDescription('Starts the dashboard')
+ ->addOption('host', null, InputOption::VALUE_REQUIRED, '', 'localhost')
+ ->addOption('port', null, InputOption::VALUE_REQUIRED, '', '8000')
+ ->addOption('profile', null, InputOption::VALUE_REQUIRED)
+ ->addOption('stage', null, InputOption::VALUE_REQUIRED);
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+ $host = $input->getOption('host');
+ $port = (int) $input->getOption('port');
+ $profile = $input->getOption('profile');
+ $stage = $input->getOption('stage');
+
+ $io->info('The Bref Dashboard is also available as an application: https://dashboard.bref.sh');
+ if ($host === 'localhost') {
+ $host = '127.0.0.1';
+ }
+ if ($profile === null) {
+ $profile = getenv('AWS_PROFILE') ?: 'default';
+ }
+
+ if (! file_exists('serverless.yml')) {
+ $io->error('No `serverless.yml` file found.');
+
+ return Command::FAILURE;
+ }
+
+ $exeFinder = new ExecutableFinder;
+ if (! $exeFinder->find('docker')) {
+ $io->error(
+ 'The `docker` command is not installed.' . PHP_EOL .
+ 'Please follow the instructions at https://docs.docker.com/install/'
+ );
+
+ return Command::FAILURE;
+ }
+
+ if (! $exeFinder->find('serverless')) {
+ $io->error(
+ 'The `serverless` command is not installed.' . PHP_EOL .
+ 'Please follow the instructions at https://bref.sh/docs/installation.html'
+ );
+
+ return Command::FAILURE;
+ }
+
+ $serverlessInfo = new Process(['serverless', 'info', '--stage', $stage, '--aws-profile', $profile]);
+ $serverlessInfo->start();
+ $animation = new LoadingAnimation($io);
+ do {
+ $animation->tick('Retrieving the stack');
+ usleep(100 * 1000);
+ } while ($serverlessInfo->isRunning());
+ $animation->clear();
+
+ if (! $serverlessInfo->isSuccessful()) {
+ $io->error('The command `serverless info` failed' . PHP_EOL . $serverlessInfo->getOutput());
+
+ return Command::FAILURE;
+ }
+
+ $serverlessInfoOutput = $serverlessInfo->getOutput();
+
+ $region = [];
+ preg_match('/region: ([a-z0-9-]*)/', $serverlessInfoOutput, $region);
+ $region = $region[1];
+
+ $stack = [];
+ preg_match('/stack: ([a-zA-Z0-9-]*)/', $serverlessInfoOutput, $stack);
+ $stack = $stack[1];
+
+ $io->writeln("Stack: $stack ($region)>");
+
+ $dockerPull = new Process(['docker', 'pull', 'bref/dashboard']);
+ $dockerPull->setTimeout(null);
+ $dockerPull->start();
+ do {
+ $animation->tick('Retrieving the latest version of the dashboard');
+ usleep(100 * 1000);
+ } while ($dockerPull->isRunning());
+ $animation->clear();
+ if (! $dockerPull->isSuccessful()) {
+ $io->error([
+ 'The command `docker pull bref/dashboard` failed',
+ $dockerPull->getErrorOutput(),
+ ]);
+
+ return Command::FAILURE;
+ }
+
+ $process = new Process(['docker', 'run', '--rm', '-p', $host . ':' . $port . ':8000', '-v', getenv('HOME') . '/.aws:/root/.aws:ro', '--env', 'STACKNAME=' . $stack, '--env', 'REGION=' . $region, '--env', 'AWS_PROFILE=' . $profile, 'bref/dashboard']);
+ $process->setTimeout(null);
+ $process->start();
+ do {
+ $animation->tick('Starting the dashboard');
+ usleep(100 * 1000);
+ $serverOutput = $process->getOutput() . $process->getErrorOutput();
+ $hasStarted = (strpos($serverOutput, 'Development Server') !== false);
+ } while ($process->isRunning() && ! $hasStarted);
+ $animation->clear();
+ if (! $process->isRunning()) {
+ $io->error([
+ 'The dashboard failed to start',
+ $process->getErrorOutput(),
+ ]);
+
+ return Command::FAILURE;
+ }
+ $url = "http://$host:$port";
+ $io->writeln("Dashboard started: $url>");
+ OpenUrl::open($url);
+ $process->wait(function ($type, $buffer): void {
+ if ($type === Process::ERR) {
+ echo 'ERR > ' . $buffer;
+ } else {
+ echo 'OUT > ' . $buffer;
+ }
+ });
+
+ return $process->getExitCode();
+ }
+}
diff --git a/src/Console/Command/Init.php b/src/Console/Command/Init.php
new file mode 100644
index 000000000..d55f7a39b
--- /dev/null
+++ b/src/Console/Command/Init.php
@@ -0,0 +1,85 @@
+setName('init');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+ $exeFinder = new ExecutableFinder;
+ if (! $exeFinder->find('serverless')) {
+ $io->error(
+ 'The `serverless` command is not installed.' . PHP_EOL .
+ 'Please follow the instructions at https://bref.sh/docs/installation.html'
+ );
+
+ return Command::FAILURE;
+ }
+
+ if (file_exists('serverless.yml') || file_exists('index.php')) {
+ $io->error('The directory already contains a `serverless.yml` and/or `index.php` file.');
+
+ return Command::FAILURE;
+ }
+
+ $choice = $io->choice(
+ 'What kind of lambda do you want to create? (you will be able to add more functions later by editing `serverless.yml`)',
+ [
+ 'Web application',
+ 'Event-driven function',
+ ],
+ 'Web application',
+ );
+ $templateDirectory = [
+ 'Web application' => 'http',
+ 'Event-driven function' => 'function',
+ ][$choice];
+
+ $fs = new Filesystem;
+ $rootPath = dirname(__DIR__, 3) . "/template/$templateDirectory";
+
+ $io->writeln('Creating index.php');
+ $fs->copy("$rootPath/index.php", 'index.php');
+
+ $io->writeln('Creating serverless.yml');
+
+ $template = file_get_contents("$rootPath/serverless.yml");
+
+ $template = str_replace('PHP_VERSION', PHP_MAJOR_VERSION . PHP_MINOR_VERSION, $template);
+
+ file_put_contents('serverless.yml', $template);
+
+ $filesToGitAdd = ['index.php', 'serverless.yml'];
+
+ /*
+ * We check if this is a git repository to automatically add files to git.
+ */
+ if ((new Process(['git', 'rev-parse', '--is-inside-work-tree']))->run() === 0) {
+ foreach ($filesToGitAdd as $file) {
+ (new Process(['git', 'add', $file]))->run();
+ }
+ $io->success([
+ 'Project initialized and ready to test or deploy.',
+ 'The files created were automatically added to git.',
+ ]);
+ } else {
+ $io->success('Project initialized and ready to test or deploy.');
+ }
+
+ return Command::SUCCESS;
+ }
+}
diff --git a/src/Console/Command/Layers.php b/src/Console/Command/Layers.php
new file mode 100644
index 000000000..465e187a0
--- /dev/null
+++ b/src/Console/Command/Layers.php
@@ -0,0 +1,45 @@
+setName('layers')
+ ->setDescription('Displays the versions of the Bref layers')
+ ->addArgument('region');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+ $region = $input->getArgument('region');
+
+ $layers = json_decode(file_get_contents(dirname(__DIR__, 3) . '/layers.json'), true);
+ $io->title("Layers for the $region region");
+
+ $array = [];
+ foreach ($layers as $layer => $versions) {
+ $version = $versions[$region];
+ $array[] = [
+ $layer,
+ $version,
+ "arn:aws:lambda:$region:416566615250:layer:$layer:$version",
+ ];
+ }
+ $io->table([
+ 'Layer',
+ 'Version',
+ 'ARN',
+ ], $array);
+
+ return Command::SUCCESS;
+ }
+}
diff --git a/src/Console/Command/Local.php b/src/Console/Command/Local.php
index 3bef1085b..3b6397955 100644
--- a/src/Console/Command/Local.php
+++ b/src/Console/Command/Local.php
@@ -8,6 +8,11 @@
use Exception;
use JsonException;
use Psr\Container\NotFoundExceptionInterface;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Yaml\Yaml;
use Throwable;
@@ -15,12 +20,28 @@
/**
* Local function invocation.
*/
-class Local
+class Local extends Command
{
- public const SIGNATURE = 'local [function] [data] [--file=] [--handler=] [--config=]';
+ protected function configure(): void
+ {
+ $this
+ ->setName('local')
+ ->addArgument('function', InputArgument::OPTIONAL)
+ ->addArgument('data', InputArgument::OPTIONAL)
+ ->addOption('file', 'f', InputOption::VALUE_REQUIRED)
+ ->addOption('handler', null, InputOption::VALUE_REQUIRED)
+ ->addOption('config', 'c', InputOption::VALUE_REQUIRED);
+ }
- public function __invoke(?string $function, ?string $data, ?string $file, ?string $handler, ?string $config, SymfonyStyle $io): int
+ protected function execute(InputInterface $input, OutputInterface $output): int
{
+ $io = new SymfonyStyle($input, $output);
+ $function = $input->getArgument('function');
+ $data = $input->getArgument('data');
+ $file = $input->getOption('file');
+ $handler = $input->getOption('handler');
+ $config = $input->getOption('config');
+
if ($function === null && $handler === null) {
throw new Exception('Please provide a function name or the --handler= option.');
}
@@ -79,14 +100,14 @@ public function __invoke(?string $function, ?string $data, ?string $file, ?strin
$e->getTraceAsString(),
]);
$io->error($e->getMessage());
- return 1;
+ return Command::FAILURE;
}
$this->logEnd($startTime, $io, $requestId);
// Show the invocation result
$io->block(json_encode($result, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), null, 'fg=black;bg=green', '', true);
- return 0;
+ return Command::SUCCESS;
}
private function handlerFromServerlessYml(string $function, ?string $config): string