diff --git a/.gitignore b/.gitignore index 7166c792c..c7c94da5a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,5 @@ /tests/Bridge/Symfony/cache /tests/Bridge/Symfony/logs /.bref/ -/.php_cs.cache +/.phpcs-cache /composer.lock diff --git a/.php_cs b/.php_cs deleted file mode 100644 index 2e0b42b84..000000000 --- a/.php_cs +++ /dev/null @@ -1,17 +0,0 @@ -in(__DIR__) - ->exclude([ - '.bref', - 'vendor', - 'tests/Bridge/Symfony/cache', - 'tests/Bridge/Symfony/logs', - ]); - -return PhpCsFixer\Config::create() - ->setRules([ - '@PSR2' => true, - '@PHP70Migration' => true, - ]) - ->setFinder($finder); diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 000000000..b90a62cce --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,146 @@ + + + + + + + + + + + src + tests + tests/Bridge/Symfony/cache + tests/Bridge/Symfony/logs + tests/Bridge/Laravel/bootstrap/cache + + + + + + + 0 + + + + + 0 + + + + + + + + + + + + + + + + + + + + 0 + + + + + 0 + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + 0 + + + + + 0 + + + + + 0 + + + 0 + + + 0 + + + + + + + + + tests/* + + + + + tests/* + + + diff --git a/.prettyci.composer.json b/.prettyci.composer.json new file mode 100644 index 000000000..5ae6e7a5a --- /dev/null +++ b/.prettyci.composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "doctrine/coding-standard": "^5.0" + } +} diff --git a/.travis.yml b/.travis.yml index c926d335c..59880043b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ notifications: on_success: never php: - - 7.1 - 7.2 - nightly @@ -14,7 +13,7 @@ matrix: allow_failures: - php: nightly include: - - php: 7.1 + - php: 7.2 env: dependencies=lowest cache: @@ -27,3 +26,4 @@ before_script: script: - vendor/bin/phpunit + - vendor/bin/phpstan analyse diff --git a/bref b/bref index 9e6d1bc47..1f8256873 100755 --- a/bref +++ b/bref @@ -8,8 +8,6 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Process\Process; -const DEFAULT_PHP_TARGET_VERSION = '7.2.5'; - if (file_exists(__DIR__ . '/vendor/autoload.php')) { require_once __DIR__ . '/vendor/autoload.php'; } elseif (file_exists(__DIR__ . '/../autoload.php')) { diff --git a/composer.json b/composer.json index 097c63175..16ecfc5dd 100644 --- a/composer.json +++ b/composer.json @@ -10,11 +10,16 @@ "src/functions.php" ] }, + "autoload-dev": { + "psr-4": { + "Bref\\Test\\": "tests" + } + }, "bin": [ "bref" ], "require": { - "php": "^7.1.0", + "php": "^7.2.0", "ext-json": "*", "mnapoli/silly": "^1.7", "symfony/filesystem": "^3.1|^4.0", @@ -28,16 +33,18 @@ "zendframework/zend-diactoros": "^1.6", "jolicode/jolinotif": "^2.0", "matomo/ini": "^2.0", - "riverline/multipart-parser": "^1.2" + "riverline/multipart-parser": "^1.2", + "innmind/json": "^1.0" }, "require-dev": { "phpunit/phpunit": "^6.5", "symfony/symfony": "^3.4|^4.0", "symfony/debug": "^3.1|^4.0", "slim/slim": "^3.9", - "friendsofphp/php-cs-fixer": "^2.4", "illuminate/contracts": "^5.0", "illuminate/http": "^5.0", - "laravel/framework": "^5.6" + "laravel/framework": "^5.6", + "doctrine/coding-standard": "^5.0", + "phpstan/phpstan": "^0.10.5" } } diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 000000000..e1608abc4 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,10 @@ +parameters: + level: 5 + paths: + - src + - tests + excludes_analyse: + - %rootDir%/../../../tests/Bridge/Laravel/bootstrap/cache/* + - %rootDir%/../../../tests/Bridge/Laravel/storage/* + - %rootDir%/../../../tests/Bridge/Symfony/cache/* + - %rootDir%/../../../tests/Bridge/Symfony/logs/* diff --git a/src/Application.php b/src/Application.php index 1ac589cff..00cff101a 100644 --- a/src/Application.php +++ b/src/Application.php @@ -1,5 +1,4 @@ - - */ class Application { /** @@ -27,19 +24,13 @@ class Application private const BREF_DIRECTORY = '/tmp/.bref'; private const OUTPUT_FILE_NAME = self::BREF_DIRECTORY . '/output.json'; - /** - * @var callable - */ + /** @var callable */ private $simpleHandler; - /** - * @var RequestHandlerInterface - */ + /** @var RequestHandlerInterface */ private $httpHandler; - /** - * @var \Symfony\Component\Console\Application - */ + /** @var \Symfony\Component\Console\Application */ private $cliHandler; public function __construct() @@ -58,7 +49,7 @@ public function __construct() * * @param callable $handler This callable takes a $event parameter (array) and must return anything serializable to JSON. */ - public function simpleHandler(callable $handler) : void + public function simpleHandler(callable $handler): void { $this->simpleHandler = $handler; } @@ -69,7 +60,7 @@ public function simpleHandler(callable $handler) : void * The handler must be a PSR-15 request handler, it can be any * framework that is compatible with PSR-15 for example. */ - public function httpHandler(RequestHandlerInterface $handler) : void + public function httpHandler(RequestHandlerInterface $handler): void { $this->httpHandler = $handler; } @@ -84,7 +75,7 @@ public function httpHandler(RequestHandlerInterface $handler) : void * That can also be a Silly (https://github.com/mnapoli/silly) application * since Silly is based on the Symfony Console. */ - public function cliHandler(\Symfony\Component\Console\Application $console) : void + public function cliHandler(\Symfony\Component\Console\Application $console): void { // Necessary to avoid any `exit()` call :) $console->setAutoExit(false); @@ -103,10 +94,10 @@ public function cliHandler(\Symfony\Component\Console\Application $console) : vo * The application will detect how the lambda is being invoked (HTTP, * CLI, direct invocation, etc.) and execute the proper handler. */ - public function run() : void + public function run(): void { - if (!$this->isRunningInAwsLambda()) { - if (php_sapi_name() == "cli") { + if (! $this->isRunningInAwsLambda()) { + if (PHP_SAPI === 'cli') { $this->cliHandler->setAutoExit(true); $this->cliHandler->run(); } else { @@ -132,20 +123,20 @@ public function run() : void $cliInput = new StringInput($event['cli']); $cliOutput = new BufferedOutput; $exitCode = $this->cliHandler->run($cliInput, $cliOutput); - $output = json_encode([ + $output = Json::encode([ 'exitCode' => $exitCode, 'output' => $cliOutput->fetch(), ]); } else { // Simple invocation $output = ($this->simpleHandler)($event); - $output = json_encode($output); + $output = Json::encode($output); } $this->writeLambdaOutput($output); } - private function ensureTempDirectoryExists() : void + private function ensureTempDirectoryExists(): void { $filesystem = new Filesystem; if (! $filesystem->exists(self::BREF_DIRECTORY)) { @@ -153,19 +144,19 @@ private function ensureTempDirectoryExists() : void } } - private function readLambdaEvent() : array + private function readLambdaEvent(): array { // The lambda event is passed as JSON by `handler.js` as a CLI argument - global $argv; - return json_decode($argv[1], true) ?: []; + $argv = $_SERVER['argv']; + return Json::decode($argv[1]) ?: []; } - private function writeLambdaOutput(string $json) : void + private function writeLambdaOutput(string $json): void { file_put_contents(self::OUTPUT_FILE_NAME, $json); } - private function isRunningInAwsLambda() : bool + private function isRunningInAwsLambda(): bool { // LAMBDA_TASK_ROOT is a constant defined by AWS // TODO: use a solution that would work with other hosts? diff --git a/src/Bridge/Laravel/Application.php b/src/Bridge/Laravel/Application.php index 4393b8080..915de48ba 100644 --- a/src/Bridge/Laravel/Application.php +++ b/src/Bridge/Laravel/Application.php @@ -1,5 +1,4 @@ -isArtisanConsole !== null) { @@ -55,7 +55,7 @@ public function runningInConsole() return parent::runningInConsole(); } - public function overrideRunningInConsole(bool $value) + public function overrideRunningInConsole(bool $value): void { $this->isArtisanConsole = $value; } diff --git a/src/Bridge/Laravel/LaravelAdapter.php b/src/Bridge/Laravel/LaravelAdapter.php index 9c2e4106c..991fcab11 100644 --- a/src/Bridge/Laravel/LaravelAdapter.php +++ b/src/Bridge/Laravel/LaravelAdapter.php @@ -1,9 +1,10 @@ - */ class LaravelAdapter implements RequestHandlerInterface { - /** - * @var Kernel - */ + /** @var Kernel */ private $kernel; public function __construct(Kernel $kernel) @@ -27,7 +24,7 @@ public function __construct(Kernel $kernel) $this->kernel = $kernel; } - public function handle(ServerRequestInterface $request) : ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { // Create a Symfony request that will be used by Laravel $httpFoundationFactory = new HttpFoundationFactory; @@ -36,19 +33,17 @@ public function handle(ServerRequestInterface $request) : ResponseInterface // We create Laravel's HTTP request from the Symfony request // We cannot use Symfony's request directly because the Kernel's implementation // expects a `Illuminate\Http\Request` implementation. - $laravelRequest = \Illuminate\Http\Request::createFromBase($symfonyRequest); + $laravelRequest = Request::createFromBase($symfonyRequest); // Laravel does not forward the headers from the Symfony request // we need to do that explicitly :'( $laravelRequest->headers->replace($symfonyRequest->headers->all()); - /** @var \Illuminate\Http\Response $laravelResponse */ + /** @var Response $laravelResponse */ $laravelResponse = $this->kernel->handle($laravelRequest); $this->kernel->terminate($laravelRequest, $laravelResponse); $psr7Factory = new DiactorosFactory; // The Laravel response extends Symfony so this works fine here - $response = $psr7Factory->createResponse($laravelResponse); - - return $response; + return $psr7Factory->createResponse($laravelResponse); } } diff --git a/src/Bridge/Psr7/RequestFactory.php b/src/Bridge/Psr7/RequestFactory.php index 5a69a6c35..04b315dee 100644 --- a/src/Bridge/Psr7/RequestFactory.php +++ b/src/Bridge/Psr7/RequestFactory.php @@ -1,5 +1,4 @@ - */ class RequestFactory { /** * Create a PSR-7 server request from an AWS Lambda HTTP event. */ - public static function fromLambdaEvent(array $event) : ServerRequestInterface + public static function fromLambdaEvent(array $event): ServerRequestInterface { $method = $event['httpMethod'] ?? 'GET'; $query = []; @@ -63,13 +60,16 @@ public static function fromLambdaEvent(array $event) : ServerRequestInterface if ($contentType === 'application/x-www-form-urlencoded') { parse_str($bodyString, $parsedBody); } else { - $document = new Part("Content-type: $contentType\r\n\r\n".$bodyString); + $document = new Part("Content-type: $contentType\r\n\r\n" . $bodyString); if ($document->isMultiPart()) { $parsedBody = []; foreach ($document->getParts() as $part) { if ($part->isFile()) { $tmpPath = tempnam(sys_get_temp_dir(), 'bref_upload_'); + if ($tmpPath === false) { + throw new \RuntimeException('Unable to create a temporary directory'); + } file_put_contents($tmpPath, $part->getBody()); $file = new UploadedFile($tmpPath, filesize($tmpPath), UPLOAD_ERR_OK, $part->getFileName(), $part->getMimeType()); @@ -110,7 +110,7 @@ public static function fromLambdaEvent(array $event) : ServerRequestInterface ); } - private static function createBodyStream(string $body) : StreamInterface + private static function createBodyStream(string $body): StreamInterface { $stream = fopen('php://memory', 'r+'); fwrite($stream, $body); @@ -121,9 +121,10 @@ private static function createBodyStream(string $body) : StreamInterface /** * Parse a string key like "files[id_cards][jpg][]" and do $array['files']['id_cards']['jpg'][] = $value + * * @param mixed $value */ - private static function parseKeyAndInsertValueInArray(array &$array, string $key, $value) : void + private static function parseKeyAndInsertValueInArray(array &$array, string $key, $value): void { if (strpos($key, '[') === false) { $array[$key] = $value; diff --git a/src/Bridge/Slim/SlimAdapter.php b/src/Bridge/Slim/SlimAdapter.php index 11e88b448..bbd045331 100644 --- a/src/Bridge/Slim/SlimAdapter.php +++ b/src/Bridge/Slim/SlimAdapter.php @@ -1,5 +1,4 @@ - */ class SlimAdapter implements RequestHandlerInterface { - /** - * @var App - */ + /** @var App */ private $slim; public function __construct(App $slim) @@ -25,7 +20,7 @@ public function __construct(App $slim) $this->slim = $slim; } - public function handle(ServerRequestInterface $request) : ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { $response = $this->slim->getContainer()->get('response'); diff --git a/src/Bridge/Symfony/SymfonyAdapter.php b/src/Bridge/Symfony/SymfonyAdapter.php index a8a43a28d..d37c3b23b 100644 --- a/src/Bridge/Symfony/SymfonyAdapter.php +++ b/src/Bridge/Symfony/SymfonyAdapter.php @@ -1,5 +1,4 @@ - */ class SymfonyAdapter implements RequestHandlerInterface { - /** - * @var HttpKernelInterface - */ + /** @var KernelInterface */ private $httpKernel; - public function __construct(HttpKernelInterface $httpKernel) + public function __construct(KernelInterface $httpKernel) { $this->httpKernel = $httpKernel; } @@ -54,9 +50,11 @@ private function loadSessionFromRequest(Request $symfonyRequest): string return ''; } - $this->httpKernel->getContainer()->get('session')->setId( - $sessionId = $symfonyRequest->cookies->get(session_name(), '') - ); + $sessionId = $symfonyRequest->cookies->get(session_name(), ''); + + /** @var SessionInterface $session */ + $session = $this->httpKernel->getContainer()->get('session'); + $session->setId($sessionId); return $sessionId; } @@ -67,7 +65,9 @@ private function addSessionCookieToResponseIfChanged(?string $requestSessionId, return; } - $responseSessionId = $this->httpKernel->getContainer()->get('session')->getId(); + /** @var SessionInterface $session */ + $session = $this->httpKernel->getContainer()->get('session'); + $responseSessionId = $session->getId(); if ($requestSessionId === $responseSessionId) { return; @@ -92,6 +92,6 @@ private function addSessionCookieToResponseIfChanged(?string $requestSessionId, private function hasSessionsDisabled(): bool { - return false === $this->httpKernel->getContainer()->has('session'); + return $this->httpKernel->getContainer()->has('session') === false; } } diff --git a/src/Cli/InvokeCommand.php b/src/Cli/InvokeCommand.php index 1e3dc8faf..3dae4329a 100644 --- a/src/Cli/InvokeCommand.php +++ b/src/Cli/InvokeCommand.php @@ -1,8 +1,9 @@ - */ class InvokeCommand extends Command { - /** - * @var callable - */ + /** @var callable */ private $invokerLocator; public function __construct(callable $invokerLocator) @@ -27,46 +24,66 @@ public function __construct(callable $invokerLocator) parent::__construct(); } - protected function configure() + protected function configure(): void { $this ->setName('bref:invoke') ->setDescription('Invoke the lambda locally when testing it in a development environment.') ->setHelp('This command does NOT run the lambda on a serverless provider. It can be used to test the lambda in a "direct invocation" mode on a development machine.') ->addOption('event', 'e', InputOption::VALUE_REQUIRED, 'Event data as JSON') - ->addOption('path', 'p', InputOption::VALUE_REQUIRED, 'Event data as file') - ; + ->addOption('path', 'p', InputOption::VALUE_REQUIRED, 'Event data as file'); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): void { $simpleHandler = ($this->invokerLocator)(); $event = []; - if ($option = $input->getOption('event')) { - if (null === $event = json_decode($option, true)) { - throw new \RuntimeException('The `--event` option provided contains invalid JSON: ' . $option); + $eventOption = $input->getOption('event'); + if ($eventOption) { + $eventOption = (string) $eventOption; + try { + $event = Json::decode($eventOption); + } catch (RuntimeException $e) { + throw new \RuntimeException("The `--event` option provided contains invalid JSON: $eventOption", 0, $e); } } - if ($option = $input->getOption('path')) { - if (!$path = realpath($option)) { - throw new \RuntimeException('The `--path` option is an invalid path: ' . $option); - } - if (!is_readable($path)) { - throw new \RuntimeException('The `--path` option reference an invalid file path or misses permission: ' . $option); - } - if (!$fileContent = file_get_contents($path)) { - throw new \RuntimeException('Unable to get file content:' . $option); - } - if (null === $event = json_decode($fileContent, true)) { - throw new \RuntimeException('The `--path` option provided an file with invalid JSON content: ' . $option); - } + $pathOption = $input->getOption('path'); + if ($pathOption) { + $event = $this->readPathOption($pathOption); } $payload = $simpleHandler($event); $output->writeln(json_encode($payload, JSON_PRETTY_PRINT)); } + + /** + * @return mixed + */ + private function readPathOption(string $option) + { + $path = realpath($option); + if (! $path) { + throw new \RuntimeException('The `--path` option is an invalid path: ' . $option); + } + + if (! is_readable($path)) { + throw new \RuntimeException('The `--path` option reference an invalid file path or misses permission: ' . $option); + } + + $fileContent = file_get_contents($path); + if (! $fileContent) { + throw new \RuntimeException('Unable to get file content:' . $option); + } + + $event = json_decode($fileContent, true); + if ($event === null) { + throw new \RuntimeException('The `--path` option provided an file with invalid JSON content: ' . $option); + } + + return $event; + } } diff --git a/src/Cli/WelcomeApplication.php b/src/Cli/WelcomeApplication.php index 34bde3826..c858b6ff7 100644 --- a/src/Cli/WelcomeApplication.php +++ b/src/Cli/WelcomeApplication.php @@ -1,5 +1,4 @@ - */ class WelcomeApplication extends Application { @@ -17,7 +14,7 @@ public function __construct() { parent::__construct(); - $this->command('hello', function (SymfonyStyle $io) { + $this->command('hello', function (SymfonyStyle $io): void { $io->writeln('Welcome! This CLI application is working but has no commands.'); $io->writeln([ 'Add your own CLI application by registering a Symfony Console application' @@ -31,7 +28,7 @@ public function __construct() /** * Disable the default commands (help and list). */ - protected function getDefaultCommands() + protected function getDefaultCommands(): array { return []; } diff --git a/src/Console/Deployer.php b/src/Console/Deployer.php index 03f070860..b234b7179 100644 --- a/src/Console/Deployer.php +++ b/src/Console/Deployer.php @@ -1,5 +1,4 @@ -createProgressBar($io, 7); @@ -46,7 +47,7 @@ public function invoke(SymfonyStyle $io, string $function, ?string $data, bool $ '--raw' => $raw, ]); - $p = join(' ', array_map( + $p = implode(' ', array_map( function ($value, $key) { if ($value === true) { // Support for "flag" arguments @@ -67,23 +68,23 @@ function ($value, $key) { return $process->getOutput(); } - public function deploy(SymfonyStyle $io, bool $dryRun, ?string $stage) : void + public function deploy(SymfonyStyle $io, bool $dryRun, ?string $stage): void { $progress = $this->createProgressBar($io, 8); $this->generateArchive($progress); - if (!$dryRun) { + if (! $dryRun) { $progress->setMessage('Uploading the lambda'); $progress->display(); $serverlessCommand = 'serverless deploy'; - if (null !== $stage) { + if ($stage !== null) { $serverlessCommand .= ' --stage ' . escapeshellarg($stage); } $process = new Process($serverlessCommand, '.bref/output'); $process->setTimeout(null); $completeDeployOutput = ''; - $process->mustRun(function ($type, $buffer) use ($io, $progress, &$completeDeployOutput) { + $process->mustRun(function ($type, $buffer) use ($io, $progress, &$completeDeployOutput): void { $completeDeployOutput .= $buffer; $progress->clear(); $io->writeln($buffer); @@ -109,9 +110,9 @@ public function deploy(SymfonyStyle $io, bool $dryRun, ?string $stage) : void * @param ProgressBar $progress The progress bar will advance of 7 steps. * @throws \Exception */ - private function generateArchive(ProgressBar $progress) : void + private function generateArchive(ProgressBar $progress): void { - if (!$this->fs->exists('serverless.yml') || !$this->fs->exists('bref.php')) { + if (! $this->fs->exists('serverless.yml') || ! $this->fs->exists('bref.php')) { throw new \Exception('The files `bref.php` and `serverless.yml` are required to deploy, run `bref init` to create them'); } @@ -125,7 +126,7 @@ private function generateArchive(ProgressBar $progress) : void * error if there are unknown keys. Using the Symfony Config component * for that could make sense. */ - $projectConfig = Yaml::parse(file_get_contents('.bref.yml')); + $projectConfig = Yaml::parse($this->readContent('.bref.yml')); } $progress->advance(); @@ -136,11 +137,11 @@ private function generateArchive(ProgressBar $progress) : void // Cache PHP's binary in `.bref/bin/php` to avoid downloading it // on every deploy. - $phpVersion = $projectConfig['php']['version'] ?? DEFAULT_PHP_TARGET_VERSION; + $phpVersion = $projectConfig['php']['version'] ?? self::DEFAULT_PHP_TARGET_VERSION; $progress->setMessage('Downloading PHP in the `.bref/bin/` directory'); $progress->display(); - if (!$this->fs->exists('.bref/bin/php/php-' . $phpVersion . '.tar.gz')) { + if (! $this->fs->exists('.bref/bin/php/php-' . $phpVersion . '.tar.gz')) { $this->fs->mkdir('.bref/bin/php'); /* * TODO This option allows to customize the PHP binary used. It should be documented @@ -150,7 +151,7 @@ private function generateArchive(ProgressBar $progress) : void */ $defaultUrl = 'https://s3.amazonaws.com/bref-php/bin/php-' . $phpVersion . '.tar.gz'; $url = $projectConfig['php']['url'] ?? $defaultUrl; - (new Process("curl -sSL $url -o .bref/bin/php/php-" . $phpVersion . ".tar.gz")) + (new Process("curl -sSL $url -o .bref/bin/php/php-$phpVersion.tar.gz")) ->setTimeout(null) ->mustRun(); } @@ -159,13 +160,13 @@ private function generateArchive(ProgressBar $progress) : void $progress->setMessage('Installing the PHP binary'); $progress->display(); $this->fs->mkdir('.bref/output/.bref/bin'); - (new Process('tar -xzf .bref/bin/php/php-' . $phpVersion . '.tar.gz -C .bref/output/.bref/bin')) + (new Process("tar -xzf .bref/bin/php/php-$phpVersion.tar.gz -C .bref/output/.bref/bin")) ->mustRun(); // Set correct permissions on the file $this->fs->chmod('.bref/output/.bref/bin', 0755); // Install our custom php.ini and merge it with user configuration $phpConfig = $this->buildPhpConfig( - __DIR__ . '/../../template/php.ini', + __DIR__ . '/../../template/php.ini', '.bref/output/.bref/php.ini', $projectConfig['php']['configuration'] ?? [], $projectConfig['php']['extensions'] ?? [] @@ -200,14 +201,14 @@ private function generateArchive(ProgressBar $progress) : void $progress->advance(); } - private function runLocally(string $command) : void + private function runLocally(string $command): void { $process = new Process($command, '.bref/output'); $process->setTimeout(null); $process->mustRun(); } - private function createProgressBar(SymfonyStyle $io, int $max) : ProgressBar + private function createProgressBar(SymfonyStyle $io, int $max): ProgressBar { ProgressBar::setFormatDefinition('bref', "%message%\n %current%/%max% [%bar%] %elapsed:6s%"); @@ -223,9 +224,9 @@ private function createProgressBar(SymfonyStyle $io, int $max) : ProgressBar /** * Pre-process the `serverless.yml` file and copy it in the lambda directory. */ - private function copyServerlessYml() : void + private function copyServerlessYml(): void { - $serverlessYml = Yaml::parse(file_get_contents('serverless.yml')); + $serverlessYml = Yaml::parse($this->readContent('serverless.yml')); // Force deploying the files used by Bref without having the user know about them $serverlessYml['package']['include'][] = 'handler.js'; @@ -234,9 +235,9 @@ private function copyServerlessYml() : void file_put_contents('.bref/output/serverless.yml', Yaml::dump($serverlessYml, 10)); } - private function copyProjectToOutputDirectory() : void + private function copyProjectToOutputDirectory(): void { - if (!$this->fs->exists('.bref/output')) { + if (! $this->fs->exists('.bref/output')) { $this->fs->mkdir('.bref/output'); } @@ -261,14 +262,13 @@ private function copyProjectToOutputDirectory() : void private function buildPhpConfig(string $sourceFile, string $targetFile, array $flags, array $extensions): array { $config = array_merge( - ['flags' => array_merge( - (new IniReader())->readFile($sourceFile), - $flags - )], + [ + 'flags' => array_merge((new IniReader)->readFile($sourceFile), $flags), + ], array_combine( $extensions, array_map(function ($extension) { - if (!$this->fs->exists('.bref/output/.bref/bin/ext/' . $extension . '.so')) { + if (! $this->fs->exists(".bref/output/.bref/bin/ext/$extension.so")) { throw new \Exception("The PHP extension '$extension' is not available yet in Bref, please open an issue or a pull request on GitHub to add that extension"); } @@ -277,30 +277,31 @@ private function buildPhpConfig(string $sourceFile, string $targetFile, array $f ) ); - (new IniWriter())->writeToFile($targetFile, $config); + (new IniWriter)->writeToFile($targetFile, $config); return $config; } - private function removeUnusedExtensions(array $phpConfig) + private function removeUnusedExtensions(array $phpConfig): void { foreach (glob('.bref/output/.bref/bin/ext/*.so') as $extensionFile) { if ($extensionFile === '.bref/output/.bref/bin/ext/opcache.so') { continue; } - if (!array_key_exists(basename($extensionFile, '.so'), $phpConfig)) { + if (! array_key_exists(basename($extensionFile, '.so'), $phpConfig)) { $this->fs->remove($extensionFile); } } } - private function removeUnusedLibraries(array $extensions) + private function removeUnusedLibraries(array $extensions): void { $dependencies = []; $dependenciesFile = '.bref/output/.bref/bin/dependencies.yml'; if ($this->fs->exists($dependenciesFile)) { - $dependencies = Yaml::parse(file_get_contents($dependenciesFile))['extensions'] ?? []; + $dependenciesConfig = Yaml::parse($this->readContent($dependenciesFile)); + $dependencies = $dependenciesConfig['extensions'] ?? []; $this->fs->remove($dependenciesFile); } @@ -309,9 +310,19 @@ private function removeUnusedLibraries(array $extensions) )); foreach (glob('.bref/output/.bref/bin/lib/**') as $library) { - if (!in_array(basename($library), $requiredLibraries)) { + if (! in_array(basename($library), $requiredLibraries)) { $this->fs->remove($library); } } } + + private function readContent(string $file): string + { + $content = file_get_contents($file); + if ($content === false) { + throw new \RuntimeException("Unable to read the `$file` file"); + } + + return $content; + } } diff --git a/src/Filesystem/DirectoryMirror.php b/src/Filesystem/DirectoryMirror.php index 1988c081d..a0bbec3b0 100644 --- a/src/Filesystem/DirectoryMirror.php +++ b/src/Filesystem/DirectoryMirror.php @@ -1,5 +1,4 @@ - + * Mirrors a directory into another on the filesystem. */ class DirectoryMirror { - /** - * @var Filesystem - */ + /** @var Filesystem */ private $fs; public function __construct(Filesystem $fs) @@ -30,14 +27,14 @@ public function __construct(Filesystem $fs) * * @throws IOException */ - public function mirror(Finder $source, Finder $target) : void + public function mirror(Finder $source, Finder $target): void { [$sourceFiles, $targetFiles] = $this->indexArraysByRelativePath($source, $target); $filesToCreate = array_diff_key($sourceFiles, $targetFiles); $filesToDelete = array_diff_key($targetFiles, $sourceFiles); $filesToVerify = array_intersect_key($sourceFiles, $targetFiles); - $filesToUpdate = array_filter($filesToVerify, function (int $sourceCTime, string $sourceRelativePath) use ($targetFiles) : bool { + $filesToUpdate = array_filter($filesToVerify, function (int $sourceCTime, string $sourceRelativePath) use ($targetFiles): bool { assert(isset($targetFiles[$sourceRelativePath])); $targetCTime = $targetFiles[$sourceRelativePath]; /* @@ -57,9 +54,9 @@ public function mirror(Finder $source, Finder $target) : void } /** - * @param SplFileInfo[] $filesToCreate + * @param int[] $filesToCreate */ - private function createMissingFiles(array $filesToCreate) : void + private function createMissingFiles(array $filesToCreate): void { foreach ($filesToCreate as $relativePath => $cTime) { $targetPath = '.bref/output/' . $relativePath; @@ -73,9 +70,9 @@ private function createMissingFiles(array $filesToCreate) : void } /** - * @param SplFileInfo[] $filesToDelete + * @param int[] $filesToDelete */ - private function deleteExtraFiles(array $filesToDelete) : void + private function deleteExtraFiles(array $filesToDelete): void { foreach ($filesToDelete as $relativePath => $cTime) { $targetPath = '.bref/output/' . $relativePath; @@ -85,9 +82,9 @@ private function deleteExtraFiles(array $filesToDelete) : void } /** - * @param SplFileInfo[] $filesToUpdate + * @param int[] $filesToUpdate */ - private function updateChangedFiles(array $filesToUpdate) : void + private function updateChangedFiles(array $filesToUpdate): void { foreach ($filesToUpdate as $relativePath => $cTime) { $targetPath = '.bref/output/' . $relativePath; @@ -96,11 +93,10 @@ private function updateChangedFiles(array $filesToUpdate) : void $this->fs->remove($targetPath); $this->fs->copy($relativePath, $targetPath); } else { + // TODO sync permissions if $targetPath is a directory? if (is_file($targetPath)) { $this->fs->remove($targetPath); $this->fs->mkdir($targetPath); - } else { - // TODO sync permissions? } } } @@ -109,7 +105,7 @@ private function updateChangedFiles(array $filesToUpdate) : void /** * @return int[][] */ - private function indexArraysByRelativePath(Finder $source, Finder $target) : array + private function indexArraysByRelativePath(Finder $source, Finder $target): array { $sourceFiles = iterator_to_array($this->indexFilesCTimeByRelativePath($source)); $targetFiles = iterator_to_array($this->indexFilesCTimeByRelativePath($target)); @@ -117,7 +113,7 @@ private function indexArraysByRelativePath(Finder $source, Finder $target) : arr return [$sourceFiles, $targetFiles]; } - private function indexFilesCTimeByRelativePath(iterable $files) : \Traversable + private function indexFilesCTimeByRelativePath(iterable $files): \Traversable { foreach ($files as $file) { /** @var SplFileInfo $file */ diff --git a/src/Http/LambdaResponse.php b/src/Http/LambdaResponse.php index 170b63f83..8a5087563 100644 --- a/src/Http/LambdaResponse.php +++ b/src/Http/LambdaResponse.php @@ -1,30 +1,22 @@ - */ class LambdaResponse { - /** - * @var int - */ + /** @var int */ private $statusCode = 200; - /** - * @var array - */ + /** @var array */ private $headers; - /** - * @var string - */ + /** @var string */ private $body; public function __construct(int $statusCode, array $headers, string $body) @@ -34,7 +26,7 @@ public function __construct(int $statusCode, array $headers, string $body) $this->body = $body; } - public static function fromPsr7Response(ResponseInterface $response) : self + public static function fromPsr7Response(ResponseInterface $response): self { // The lambda proxy integration does not support arrays in headers $headers = []; @@ -54,7 +46,7 @@ public static function fromPsr7Response(ResponseInterface $response) : self return new self($response->getStatusCode(), $headers, $body); } - public static function fromHtml(string $html) : self + public static function fromHtml(string $html): self { return new self( 200, @@ -65,7 +57,7 @@ public static function fromHtml(string $html) : self ); } - public function toJson() : string + public function toJson(): string { // The headers must be a JSON object. If the PHP array is empty it is // serialized to `[]` (we want `{}`) so we force it to an empty object. @@ -73,7 +65,7 @@ public function toJson() : string // This is the format required by the AWS_PROXY lambda integration // See https://stackoverflow.com/questions/43708017/aws-lambda-api-gateway-error-malformed-lambda-proxy-response - return json_encode([ + return Json::encode([ 'isBase64Encoded' => false, 'statusCode' => $this->statusCode, 'headers' => $headers, diff --git a/src/Http/WelcomeHandler.php b/src/Http/WelcomeHandler.php index 241375096..08d2ede80 100644 --- a/src/Http/WelcomeHandler.php +++ b/src/Http/WelcomeHandler.php @@ -1,5 +1,4 @@ - */ class WelcomeHandler implements RequestHandlerInterface { - public function handle(ServerRequestInterface $request) : ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { $html = file_get_contents(__DIR__ . '/welcome.html'); + if ($html === false) { + throw new \RuntimeException('Unable to read the `welcome.html` template'); + } return new HtmlResponse($html); } diff --git a/src/functions.php b/src/functions.php index 3f2413455..4dbacd30f 100644 --- a/src/functions.php +++ b/src/functions.php @@ -1,5 +1,6 @@ -simpleHandler($handler); $app->run(); } diff --git a/template/bref.php b/template/bref.php index c720a1a26..984753c1c 100644 --- a/template/bref.php +++ b/template/bref.php @@ -1,4 +1,4 @@ -getBody()); } - private function createLaravel() : Application + private function createLaravel(): Application { $app = new Application(__DIR__); $app->singleton(Kernel::class, \Illuminate\Foundation\Http\Kernel::class); diff --git a/tests/Bridge/Laravel/config/app.php b/tests/Bridge/Laravel/config/app.php index b62512838..f0b60b479 100644 --- a/tests/Bridge/Laravel/config/app.php +++ b/tests/Bridge/Laravel/config/app.php @@ -1,4 +1,3 @@ - '/test', 'requestTimeEpoch' => $currentTimestamp, ], - 'headers' => [ - ], + 'headers' => [], ]); self::assertEquals('GET', $request->getMethod()); @@ -108,7 +106,7 @@ public function test multipart form data is supported() 'Content-Type' => 'multipart/form-data; boundary=testBoundary', ], 'body' => -"--testBoundary\r + "--testBoundary\r Content-Disposition: form-data; name=\"foo\"\r \r bar\r @@ -134,7 +132,7 @@ public function test cookies are supported() self::assertEquals([ 'tz' => 'Europe/Paris', 'four' => 'two + 2', - 'theme' => 'light' + 'theme' => 'light', ], $request->getCookieParams()); } @@ -151,10 +149,8 @@ public function test arrays in query string are supported() self::assertEquals([ 'vars' => [ 'val1' => 'foo', - 'val2' => [ - 'bar', - ] - ] + 'val2' => ['bar'], + ], ], $request->getQueryParams()); } @@ -166,7 +162,7 @@ public function test arrays in name are supported with multipart form d 'Content-Type' => 'multipart/form-data; boundary=testBoundary', ], 'body' => -"--testBoundary\r + "--testBoundary\r Content-Disposition: form-data; name=\"delete[categories][]\"\r \r 123\r @@ -180,10 +176,10 @@ public function test arrays in name are supported with multipart form d self::assertEquals('POST', $request->getMethod()); self::assertEquals( [ - 'delete' => [ - 'categories' => [ - '123', - '456', + 'delete' => [ + 'categories' => [ + '123', + '456', ], ], ], @@ -199,7 +195,7 @@ public function test files are supported with multipart form data() 'Content-Type' => 'multipart/form-data; boundary=testBoundary', ], 'body' => -"--testBoundary\r + "--testBoundary\r Content-Disposition: form-data; name=\"foo\"; filename=\"lorem.txt\"\r Content-Type: text/plain\r \r diff --git a/tests/Bridge/Slim/SlimAdapterTest.php b/tests/Bridge/Slim/SlimAdapterTest.php index 515037e17..86d0692e1 100644 --- a/tests/Bridge/Slim/SlimAdapterTest.php +++ b/tests/Bridge/Slim/SlimAdapterTest.php @@ -1,5 +1,4 @@ -handle(new ServerRequest([], [], self::ROUTE_WITH_SESSION)); - $symfonySessionId = $kernel->getContainer()->get('session')->getId(); + /** @var SessionInterface $session */ + $session = $kernel->getContainer()->get('session'); + $symfonySessionId = $session->getId(); + self::assertEquals($symfonySessionId, (string) $response->getBody()); self::assertEquals( - sprintf("%s=%s; path=/", \session_name(), $symfonySessionId), + sprintf('%s=%s; path=/', \session_name(), $symfonySessionId), $response->getHeaders()['Set-Cookie'][0] ); } @@ -98,47 +101,47 @@ public function test an existing session is used when session provided() self::assertEquals('SESSIONID', (string) $response->getBody()); } - private function createKernel(): HttpKernelInterface + private function createKernel(): KernelInterface { $kernel = new class('dev', false) extends Kernel implements EventSubscriberInterface { use MicroKernelTrait; - public function registerBundles() + public function registerBundles(): array { return [new FrameworkBundle]; } - protected function configureContainer(ContainerBuilder $c) + protected function configureContainer(ContainerBuilder $c): void { $c->register('session_storage', MockArraySessionStorage::class); $c->loadFromExtension('framework', [ 'secret' => 'foo', 'session' => [ - 'storage_id' => 'session_storage' - ] + 'storage_id' => 'session_storage', + ], ]); } - protected function configureRoutes(RouteCollectionBuilder $routes) + protected function configureRoutes(RouteCollectionBuilder $routes): void { $routes->add('/', 'kernel:testActionWithoutSession'); $routes->add('/session', 'kernel:testActionWithSession'); } - public function testActionWithoutSession() + public function testActionWithoutSession(): Response { return new Response('Hello world!'); } - public function testActionWithSession(Session $session) + public function testActionWithSession(Session $session): Response { $session->set('ACTIVATE', 'SESSIONS'); // ensure that Symfony starts/uses sessions return new Response($session->getId()); } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [KernelEvents::EXCEPTION => 'onKernelException']; } @@ -146,7 +149,7 @@ public static function getSubscribedEvents() /** * We have to handle NotFound exceptions ourselves because they are not handled by the micro-kernel */ - public function onKernelException(GetResponseForExceptionEvent $event) + public function onKernelException(GetResponseForExceptionEvent $event): void { if ($event->getException() instanceof NotFoundHttpException) { $event->setResponse(new Response('Not found', 404)); diff --git a/tests/Cli/InvokeCommandTest.php b/tests/Cli/InvokeCommandTest.php index 56a9bb33a..ef2ad5697 100644 --- a/tests/Cli/InvokeCommandTest.php +++ b/tests/Cli/InvokeCommandTest.php @@ -1,58 +1,47 @@ -command = new InvokeCommand(function () { + return function (array $event): string { + return 'Hello ' . $event['foo']; }; }); } - public function setUp() - { - $this->inputInterface = $this->prophesize(InputInterface::class); - $this->inputInterface->bind(Argument::any())->willReturn(null); - $this->inputInterface->isInteractive()->willReturn(false); - $this->inputInterface->hasArgument(Argument::any())->willReturn(false); - $this->inputInterface->validate()->willReturn(null); - $this->outputInterface = $this->prophesize(OutputInterface::class); - } - + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage The `--event` option provided contains invalid JSON: {fooo + */ public function test event json_decode fail() { - $this->expectException(\RuntimeException::class); - $this->inputInterface->getOption(Argument::exact('event'))->willReturn(self::INVALID_JSON); - self::$invokeCommand->run($this->inputInterface->reveal(), $this->outputInterface->reveal()); + $this->command->run(new StringInput('--event {fooo'), new NullOutput); } public function test event json_decode success() { - $this->inputInterface->getOption(Argument::exact('event'))->willReturn(self::VALID_JSON); - $this->inputInterface->getOption(Argument::exact('path'))->willReturn(null); - $this->outputInterface->writeln(Argument::any())->shouldBeCalled(1); - self::$invokeCommand->run($this->inputInterface->reveal(), $this->outputInterface->reveal()); + $output = new BufferedOutput; + + $this->command->run(new ArrayInput([ + '--event' => '{"foo": "bar"}', + ]), $output); + + self::assertEquals('"Hello bar"', trim($output->fetch())); } } diff --git a/tests/CliTest.php b/tests/CliTest.php index 0d4c76e7f..56d6566fb 100644 --- a/tests/CliTest.php +++ b/tests/CliTest.php @@ -1,5 +1,4 @@ -assertJsonPayload($response, [ 'isBase64Encoded' => false, 'statusCode' => 204, - 'headers' => [ - 'Foo' => 'baz', - ], + 'headers' => ['Foo' => 'baz'], 'body' => '', ]); } diff --git a/tests/JsHandler/JsHandlerTest.php b/tests/JsHandler/JsHandlerTest.php index 39ce98573..fe72ff0fa 100644 --- a/tests/JsHandler/JsHandlerTest.php +++ b/tests/JsHandler/JsHandlerTest.php @@ -1,5 +1,4 @@ -setEnv([ @@ -77,7 +76,7 @@ private static function assertProcessResult( string $stdout, string $stderr = '', int $exitCode = 0 - ) { + ): void { $fullOutput = $process->getOutput() . $process->getErrorOutput(); self::assertEquals($exitCode, $process->getExitCode(), $fullOutput); @@ -85,13 +84,19 @@ private static function assertProcessResult( self::assertEquals($stderr, $process->getErrorOutput()); } - private static function assertLambdaResponse($expected) : void + /** + * @param mixed $expected + */ + private static function assertLambdaResponse($expected): void { $response = json_decode(file_get_contents(__DIR__ . '/tmp/testResponse.json'), true); self::assertEquals($expected, $response); } - private static function assertLambdaError($expected) : void + /** + * @param mixed $expected + */ + private static function assertLambdaError($expected): void { $error = json_decode(file_get_contents(__DIR__ . '/tmp/testError.json'), true); self::assertEquals($expected, $error); diff --git a/tests/JsHandler/bref.array-response.php b/tests/JsHandler/bref.array-response.php index 6a7b722c1..b4380034e 100644 --- a/tests/JsHandler/bref.array-response.php +++ b/tests/JsHandler/bref.array-response.php @@ -1,4 +1,4 @@ -