From 692d8ae909e21e93f78504e6b8067540116bf1a5 Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 19:29:19 +0000 Subject: [PATCH 01/19] Use Pest for testing --- .php-cs-fixer.dist.php | 8 ++ LICENSE | 2 +- composer.json | 8 +- tests/ExampleTest.php | 12 -- tests/Helpers/ConfigValidatorPestTest.php | 77 +++++++++++++ tests/Helpers/ConfigValidatorTest.php | 97 ---------------- tests/Helpers/ZipPestTest.php | 92 +++++++++++++++ tests/Helpers/ZipTest.php | 130 ---------------------- tests/Pest.php | 35 ++---- 9 files changed, 189 insertions(+), 272 deletions(-) delete mode 100644 tests/ExampleTest.php create mode 100644 tests/Helpers/ConfigValidatorPestTest.php delete mode 100644 tests/Helpers/ConfigValidatorTest.php create mode 100644 tests/Helpers/ZipPestTest.php delete mode 100644 tests/Helpers/ZipTest.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index a603d96..110098c 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -37,4 +37,12 @@ 'return_type_declaration' => [ 'space_before' => 'none' ], + 'declare_strict_types' => true, + 'blank_line_after_opening_tag' => true, + 'single_import_per_statement' => true, + 'mb_str_functions' => true, + 'no_superfluous_phpdoc_tags' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_phpdoc' => true, + 'phpdoc_trim' => true, ])->setFinder($finder); diff --git a/LICENSE b/LICENSE index 077d195..9f9905e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Sam Carré +Copyright (c) 2023 Sam Carré Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/composer.json b/composer.json index b00b01d..dba4eea 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,8 @@ "authors": [ { "name": "Sam Carré", - "email": "sam.carre2000@gmail.com" + "email": "29132017+Sammyjo20@users.noreply.github.com", + "role": "Developer" } ], "require": { @@ -21,11 +22,10 @@ "league/flysystem": "^3.0" }, "require-dev": { - "phpunit/phpunit": "^9.3", "orchestra/testbench": "^7.0 || ^8.0", "friendsofphp/php-cs-fixer": "^3.1.0", "spatie/ray": "^1.33", - "pestphp/pest": "^1.21" + "pestphp/pest": "^2.25" }, "extra": { "laravel": { @@ -45,7 +45,7 @@ "./vendor/bin/pest" ], "fix-code": [ - "./vendor/bin/php-cs-fixer fix" + "./vendor/bin/php-cs-fixer fix --allow-risky=yes" ] }, "minimum-stability": "stable", diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php deleted file mode 100644 index 19eb2cf..0000000 --- a/tests/ExampleTest.php +++ /dev/null @@ -1,12 +0,0 @@ -assertTrue(true); - } -} diff --git a/tests/Helpers/ConfigValidatorPestTest.php b/tests/Helpers/ConfigValidatorPestTest.php new file mode 100644 index 0000000..9a8403d --- /dev/null +++ b/tests/Helpers/ConfigValidatorPestTest.php @@ -0,0 +1,77 @@ +set([ + 'filesystems.disks' => [ + 'assets' => [ + 'driver' => 's3', + ], + ], + ]); +}); + +test('it throws exception when storage push to git is set', function () { + $this->expectException(ConfigFailedValidation::class); + $this->expectExceptionMessageMatches('/push_to_git/'); + + config()->set(['lasso.storage.push_to_git' => true]); + + (new ConfigValidator)->validate(); +}); + +test('it throws an exception when the compile script is not set', function () { + $this->expectException(ConfigFailedValidation::class); + $this->expectExceptionMessageMatches('/npm run production/'); + + config()->set(['lasso.compiler.script' => null]); + + (new ConfigValidator)->validate(); +}); + +test('it throws an exception when an invalid compiler output is provided', function () { + $this->expectException(ConfigFailedValidation::class); + $this->expectExceptionMessage('You must specify a valid output setting. Available options: all, progress, disable.'); + + config()->set(['lasso.compiler.output' => 'abc']); + + (new ConfigValidator)->validate(); +}); + +test('it throws exception when disk does not exist', function () { + $this->expectException(ConfigFailedValidation::class); + $this->expectExceptionMessageMatches('/not a valid disk/'); + + config()->set(['filesystems.disks' => null]); + + (new ConfigValidator)->validate(); +}); + +test('it throws exception when bundle count is missing', function () { + $this->expectException(ConfigFailedValidation::class); + $this->expectExceptionMessageMatches('/how many bundles/'); + + config()->set(['lasso.storage.max_bundles' => null]); + + (new ConfigValidator)->validate(); +}); + +test('it throws exception when bundle count is less than one', function () { + $this->expectException(ConfigFailedValidation::class); + $this->expectExceptionMessageMatches('/how many bundles/'); + + config()->set(['lasso.storage.max_bundles' => 0]); + + (new ConfigValidator)->validate(); +}); + +test('it throws exception when public path is inaccessible', function () { + $this->expectException(ConfigFailedValidation::class); + $this->expectExceptionMessageMatches('/accessible directory/'); + + config()->set(['lasso.public_path' => 'a_non_existing_path']); + + (new ConfigValidator)->validate(); +}); diff --git a/tests/Helpers/ConfigValidatorTest.php b/tests/Helpers/ConfigValidatorTest.php deleted file mode 100644 index 5b94056..0000000 --- a/tests/Helpers/ConfigValidatorTest.php +++ /dev/null @@ -1,97 +0,0 @@ -set([ - 'filesystems.disks' => [ - 'assets' => [ - 'driver' => 's3', - ], - ], - ]); - } - - /** @test */ - public function it_throws_exception_when_storage_push_to_git_is_set() - { - $this->expectException(ConfigFailedValidation::class); - $this->expectExceptionMessageMatches('/push_to_git/'); - config()->set(['lasso.storage.push_to_git' => true]); - (new ConfigValidator())->validate(); - } - - /** @test */ - public function it_throws_exception_when_compile_script_is_not_set() - { - $this->expectException(ConfigFailedValidation::class); - $this->expectExceptionMessageMatches('/npm run production/'); - - config()->set(['lasso.compiler.script' => null]); - - (new ConfigValidator())->validate(); - } - - /** @test */ - public function it_throws_exception_when_invalid_compiler_output_is_provided() - { - $this->expectException(ConfigFailedValidation::class); - $this->expectExceptionMessage('You must specify a valid output setting. Available options: all, progress, disable.'); - - config()->set(['lasso.compiler.output' => 'abc']); - - (new ConfigValidator())->validate(); - } - - /** @test */ - public function it_throws_exception_when_disk_does_not_exist() - { - $this->expectException(ConfigFailedValidation::class); - $this->expectExceptionMessageMatches('/not a valid disk/'); - - config()->set(['filesystems.disks' => null]); - - (new ConfigValidator())->validate(); - } - - /** @test */ - public function it_throws_exception_when_bundle_count_is_missing() - { - $this->expectException(ConfigFailedValidation::class); - $this->expectExceptionMessageMatches('/how many bundles/'); - - config()->set(['lasso.storage.max_bundles' => null]); - - (new ConfigValidator())->validate(); - } - - /** @test */ - public function it_throws_exception_when_bundle_count_is_less_than_one() - { - $this->expectException(ConfigFailedValidation::class); - $this->expectExceptionMessageMatches('/how many bundles/'); - - config()->set(['lasso.storage.max_bundles' => 0]); - - (new ConfigValidator())->validate(); - } - - /** @test */ - public function it_throws_exception_when_public_path_is_inaccessible() - { - $this->expectException(ConfigFailedValidation::class); - $this->expectExceptionMessageMatches('/accessible directory/'); - - config()->set(['lasso.public_path' => 'a_non_existing_path']); - - (new ConfigValidator())->validate(); - } -} diff --git a/tests/Helpers/ZipPestTest.php b/tests/Helpers/ZipPestTest.php new file mode 100644 index 0000000..8f353aa --- /dev/null +++ b/tests/Helpers/ZipPestTest.php @@ -0,0 +1,92 @@ +addFilesFromDirectory(sourceDirectory() . '/SingleFile') + ->closeZip(); + + assertFileExists($zipFile); + assertZipFileContains($sourceFile, $zipFile); +}); + +test('it adds all files within a source directory to a zip file', function () { + $sourceFiles = [ + 'MultipleFiles/1.txt', + 'MultipleFiles/2.txt', + 'MultipleFiles/3.txt', + ]; + + $zipFile = destinationDirectory() . '/MultiFile.zip'; + + assertFileDoesNotExist($zipFile); + + (new Zip($zipFile)) + ->addFilesFromDirectory(sourceDirectory() .'/MultipleFiles') + ->closeZip(); + + assertFileExists($zipFile); + + assertZipFileContains($sourceFiles, $zipFile); +}); + +test('it adds all files within a source directory including sub folders to a zip file', function () { + $sourceFiles = [ + 'FilesWithSubFolder/SubFolder/in_sub_folder.txt', + 'FilesWithSubFolder/SubFolder/logo_in_sub_folder.png', + 'FilesWithSubFolder/logo_in_root_folder.png', + 'FilesWithSubFolder/in_root_folder.txt', + ]; + + $zipFile = destinationDirectory() . '/WithSubFolder.zip'; + + assertFileDoesNotExist($zipFile); + + (new Zip($zipFile)) + ->addFilesFromDirectory(sourceDirectory() .'/FilesWithSubFolder') + ->closeZip(); + + assertFileExists($zipFile); + + assertZipFileContains($sourceFiles, $zipFile); +}); + +function assertZipFileContains(array $sourceFiles, string $destinationZipFile): void +{ + $inspectZipFile = new ZipArchive(); + $inspectZipFile->open($destinationZipFile); + + collect($sourceFiles)->each(function (string $filePath) use ($inspectZipFile) { + $relativePath = getRelativePath($filePath); + + assertStringEqualsFile( + sourceDirectory() . '/' . $filePath, $inspectZipFile->getFromName($relativePath) + ); + }); +} + +function getRelativePath(string $filePath): string +{ + // Find the root directory (first directory listed) + $rootDirectory = explode('/', $filePath)[0]; + + // Remove the root directory from the given path + $normalizedPath = str_replace($rootDirectory, '', $filePath); + + // Remove preliminary forward slash "/Dir" -> "Dir" + return substr($normalizedPath, 1); +} diff --git a/tests/Helpers/ZipTest.php b/tests/Helpers/ZipTest.php deleted file mode 100644 index 364f593..0000000 --- a/tests/Helpers/ZipTest.php +++ /dev/null @@ -1,130 +0,0 @@ -sourceDirectory = __DIR__ . '/Support/Zip/Source'; - $this->destinationDirectory = __DIR__ . '/Support/Zip/Destination'; - - $this->cleanUpPreviousArtifacts(); - } - - /** @test */ - public function it_can_add_a_single_file_from_a_source_directory_to_a_zip_file(): void - { - $sourceFile = ['SingleFile/logo.png']; - $zipFile = $this->destinationDirectory . '/SingleFile.zip'; - - $this->assertFileDoesNotExist($zipFile); - - (new Zip($zipFile)) - ->addFilesFromDirectory($this->sourceDirectory .'/SingleFile') - ->closeZip(); - - $this->assertFileExists($zipFile); - - $this->assertZipFileContains($sourceFile, $zipFile); - } - - /** @test */ - public function it_adds_all_files_within_a_source_directory_to_a_zip_file(): void - { - $sourceFiles = [ - 'MultipleFiles/1.txt', - 'MultipleFiles/2.txt', - 'MultipleFiles/3.txt', - ]; - - $zipFile = $this->destinationDirectory . '/MultiFile.zip'; - - $this->assertFileDoesNotExist($zipFile); - - (new Zip($zipFile)) - ->addFilesFromDirectory($this->sourceDirectory .'/MultipleFiles') - ->closeZip(); - - $this->assertFileExists($zipFile); - - $this->assertZipFileContains($sourceFiles, $zipFile); - } - - /** @test */ - public function it_adds_all_files_within_a_source_directory_including_sub_folders_to_a_zip_file(): void - { - $sourceFiles = [ - 'FilesWithSubFolder/SubFolder/in_sub_folder.txt', - 'FilesWithSubFolder/SubFolder/logo_in_sub_folder.png', - 'FilesWithSubFolder/logo_in_root_folder.png', - 'FilesWithSubFolder/in_root_folder.txt', - ]; - - $zipFile = $this->destinationDirectory . '/WithSubFolder.zip'; - - $this->assertFileDoesNotExist($zipFile); - - (new Zip($zipFile)) - ->addFilesFromDirectory($this->sourceDirectory .'/FilesWithSubFolder') - ->closeZip(); - - $this->assertFileExists($zipFile); - - $this->assertZipFileContains($sourceFiles, $zipFile); - } - - private function assertZipFileContains(array $sourceFiles, string $destinationZipFile): void - { - $inspectZipFile = new ZipArchive(); - $inspectZipFile->open($destinationZipFile); - - collect($sourceFiles)->each(function (string $filePath) use ($inspectZipFile) { - $relativePath = $this->getRelativePath($filePath); - - $this->assertSame( - file_get_contents($this->sourceDirectory . '/' . $filePath), - $inspectZipFile->getFromName($relativePath) - ); - }); - } - - /** - * Clean working directory at the start by purging files generated in previous tests, - * to be able to inspect the files manually if needed after a specific test has run. - */ - private function cleanUpPreviousArtifacts(): void - { - File::cleanDirectory($this->destinationDirectory); - } - - /** - * @param string $filePath - * @return false|string - * - * Returns the relative path of source files, to be located in the zip file. - */ - private function getRelativePath(string $filePath): string - { - // Find the root directory (first directory listed) - $rootDirectory = explode('/', $filePath)[0]; - - // Remove the root directory from the given path - $normalizedPath = str_replace($rootDirectory, '', $filePath); - - // Remove preliminary forward slash "/Dir" -> "Dir" - $relativePath = substr($normalizedPath, 1); - - return $relativePath; - } -} diff --git a/tests/Pest.php b/tests/Pest.php index 7183e93..e564a93 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -13,35 +13,14 @@ use Sammyjo20\Lasso\Tests\TestCase; -uses(TestCase::class)->in('Feature'); +uses(TestCase::class)->in('Helpers'); -/* -|-------------------------------------------------------------------------- -| Expectations -|-------------------------------------------------------------------------- -| -| When you're writing tests, you often need to check that values meet certain conditions. The -| "expect()" function gives you access to a set of "expectations" methods that you can use -| to assert different things. Of course, you may extend the Expectation API at any time. -| -*/ - -expect()->extend('toBeOne', function () { - return $this->toBe(1); -}); - -/* -|-------------------------------------------------------------------------- -| Functions -|-------------------------------------------------------------------------- -| -| While Pest is very powerful out-of-the-box, you may have some testing code specific to your -| project that you don't want to repeat in every file. Here you can also expose helpers as -| global functions to help you to reduce the number of lines of code in your test files. -| -*/ +function sourceDirectory(): string +{ + return __DIR__ . '/Helpers/Support/Zip/Source'; +} -function something() +function destinationDirectory(): string { - // .. + return __DIR__ . '/Helpers/Support/Zip/Destination'; } From c56c5769adc0c124830cfdb37d0db571de25126b Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 19:31:27 +0000 Subject: [PATCH 02/19] Modern code style --- config/lasso.php | 2 + src/Actions/Compiler.php | 6 +-- src/Commands/BaseCommand.php | 6 +-- src/Commands/PublishCommand.php | 5 +-- src/Commands/PullCommand.php | 7 ++-- src/Container/Artisan.php | 6 +-- src/Exceptions/BaseException.php | 3 +- src/Exceptions/ConfigFailedValidation.php | 2 + src/Exceptions/ConsoleMethodException.php | 2 + src/Exceptions/GitHashException.php | 2 + src/Exceptions/PullJobFailed.php | 2 + src/Exceptions/RestoreFailed.php | 2 + src/Exceptions/VersioningFailed.php | 2 + src/Helpers/Bundle.php | 8 ++-- src/Helpers/BundleIntegrityHelper.php | 13 ++---- src/Helpers/Cloud.php | 17 ++------ src/Helpers/CompilerOutputFormatter.php | 5 +-- src/Helpers/ConfigValidator.php | 38 +++++------------- src/Helpers/FileLister.php | 4 +- src/Helpers/Filesystem.php | 33 ++++----------- src/Helpers/Git.php | 5 ++- src/Helpers/Unzipper.php | 15 ++----- src/Helpers/Zip.php | 9 ++--- src/Interfaces/JobInterface.php | 6 +-- src/LassoServiceProvider.php | 2 + src/Services/ArchiveService.php | 12 ++---- src/Services/BackupService.php | 16 ++------ src/Services/VersioningService.php | 29 ++++--------- src/Tasks/BaseJob.php | 2 + src/Tasks/Command.php | 4 +- src/Tasks/Publish/BundleJob.php | 5 +-- src/Tasks/Publish/PublishJob.php | 12 ++---- src/Tasks/Pull/PullJob.php | 27 +++---------- src/Tasks/Webhook.php | 5 +-- tests/Pest.php | 8 ++-- tests/TestCase.php | 2 + .../ConfigValidatorPestTest.php | 2 + .../Support/Zip/Destination/WithSubFolder.zip | Bin .../SubFolder/in_sub_folder.txt | 0 .../SubFolder/logo_in_sub_folder.png | Bin .../FilesWithSubFolder/in_root_folder.txt | 0 .../logo_in_root_folder.png | Bin .../Support/Zip/Source/MultipleFiles/1.txt | 0 .../Support/Zip/Source/MultipleFiles/2.txt | 0 .../Support/Zip/Source/MultipleFiles/3.txt | 0 .../Support/Zip/Source/SingleFile/logo.png | Bin tests/{Helpers => Unit}/ZipPestTest.php | 11 +++-- 47 files changed, 122 insertions(+), 215 deletions(-) rename tests/{Helpers => Unit}/ConfigValidatorPestTest.php (98%) rename tests/{Helpers => Unit}/Support/Zip/Destination/WithSubFolder.zip (100%) rename tests/{Helpers => Unit}/Support/Zip/Source/FilesWithSubFolder/SubFolder/in_sub_folder.txt (100%) rename tests/{Helpers => Unit}/Support/Zip/Source/FilesWithSubFolder/SubFolder/logo_in_sub_folder.png (100%) rename tests/{Helpers => Unit}/Support/Zip/Source/FilesWithSubFolder/in_root_folder.txt (100%) rename tests/{Helpers => Unit}/Support/Zip/Source/FilesWithSubFolder/logo_in_root_folder.png (100%) rename tests/{Helpers => Unit}/Support/Zip/Source/MultipleFiles/1.txt (100%) rename tests/{Helpers => Unit}/Support/Zip/Source/MultipleFiles/2.txt (100%) rename tests/{Helpers => Unit}/Support/Zip/Source/MultipleFiles/3.txt (100%) rename tests/{Helpers => Unit}/Support/Zip/Source/SingleFile/logo.png (100%) rename tests/{Helpers => Unit}/ZipPestTest.php (93%) diff --git a/config/lasso.php b/config/lasso.php index 13390ab..295c96e 100644 --- a/config/lasso.php +++ b/config/lasso.php @@ -1,5 +1,7 @@ [ diff --git a/src/Actions/Compiler.php b/src/Actions/Compiler.php index dce149b..a80c980 100644 --- a/src/Actions/Compiler.php +++ b/src/Actions/Compiler.php @@ -1,5 +1,7 @@ withCommit(substr($withCommit, 0, 12)); + $job->withCommit(mb_substr($withCommit, 0, 12)); } $job->run(); diff --git a/src/Container/Artisan.php b/src/Container/Artisan.php index 0d74f19..96ca4eb 100644 --- a/src/Container/Artisan.php +++ b/src/Container/Artisan.php @@ -1,5 +1,7 @@ getUploadPath($name); @@ -78,9 +75,6 @@ public function uploadFile(string $path, string $name): void /** * Returns the Lasso upload directory. You can specify a file * to create a fully qualified URL. - * - * @param string|null $file - * @return string */ public function getUploadPath(string $file = null): string { @@ -95,16 +89,13 @@ public function getUploadPath(string $file = null): string return $dir . '/' . ltrim($file, '/'); } - /** - * @return string - */ + public function getCloudDisk(): string { return $this->cloudDisk; } /** - * @param string $disk * @return $this */ public function setCloudDisk(string $disk): self diff --git a/src/Helpers/CompilerOutputFormatter.php b/src/Helpers/CompilerOutputFormatter.php index db5584c..588d3f3 100644 --- a/src/Helpers/CompilerOutputFormatter.php +++ b/src/Helpers/CompilerOutputFormatter.php @@ -1,5 +1,7 @@ filesystem = new Filesystem(); } - /** - * @param string $item - * @return mixed - */ + private function get(string $item) { return config('lasso.' . $item, null); } - /** - * @param $value - * @return bool - */ + private function checkCompilerScript($value): bool { return ! is_null($value); } - /** - * @param $value - * @return bool - */ + private function checkCompilerScriptType($value): bool { return is_string($value); } - /** - * @param $value - * @return bool - */ + private function checkCompilerOutputSetting($value): bool { if (is_null($value)) { @@ -60,28 +50,19 @@ private function checkCompilerOutputSetting($value): bool return in_array($value, ['all', 'progress', 'disable']); } - /** - * @param $value - * @return bool - */ + private function checkIfPublicPathExists($value): bool { return $this->filesystem->exists($value) && $this->filesystem->isReadable($value) && $this->filesystem->isWritable($value); } - /** - * @param $value - * @return bool - */ + private function checkDiskExists($value): bool { return ! is_null(config('filesystems.disks.' . $value, null)); } - /** - * @param $value - * @return bool - */ + private function checkBundleToKeepCount($value): bool { return is_int($value) && $value > 0; @@ -89,7 +70,6 @@ private function checkBundleToKeepCount($value): bool /** * @throws ConfigFailedValidation - * @return void */ public function validate(): void { diff --git a/src/Helpers/FileLister.php b/src/Helpers/FileLister.php index 5fba6bd..e36b34f 100644 --- a/src/Helpers/FileLister.php +++ b/src/Helpers/FileLister.php @@ -1,5 +1,7 @@ setPublicPath($publicPath); } - /** - * @param $resource - * @param string $destination - * @return bool - */ + public function putStream($resource, string $destination): bool { $stream = fopen($destination, 'w+b'); @@ -51,9 +49,7 @@ public function putStream($resource, string $destination): bool return true; } - /** - * @param array $bundle - */ + public function createFreshLocalBundle(array $bundle): void { $this->deleteLocalBundle(); @@ -61,9 +57,7 @@ public function createFreshLocalBundle(array $bundle): void $this->put(base_path('lasso-bundle.json'), json_encode($bundle)); } - /** - * @return bool - */ + public function deleteLocalBundle(): bool { return $this->delete(base_path('lasso-bundle.json')); @@ -71,24 +65,19 @@ public function deleteLocalBundle(): bool /** * Delete Lasso's base directory (.lasso) - * - * @return bool */ public function deleteBaseLassoDirectory(): bool { return $this->deleteDirectory('.lasso'); } - /** - * @return string - */ + public function getLassoEnvironment(): string { return $this->lassoEnvironment; } /** - * @param string $environment * @return $this */ public function setLassoEnvironment(string $environment): self @@ -98,16 +87,13 @@ public function setLassoEnvironment(string $environment): self return $this; } - /** - * @return string - */ + public function getPublicPath(): string { return $this->publicPath; } /** - * @param string $publicPath * @return $this */ public function setPublicPath(string $publicPath): self @@ -117,16 +103,13 @@ public function setPublicPath(string $publicPath): self return $this; } - /** - * @return string - */ + public function getCloudDisk(): string { return $this->cloudDisk; } /** - * @param string $disk * @return $this */ public function setCloudDisk(string $disk): self diff --git a/src/Helpers/Git.php b/src/Helpers/Git.php index 6611389..09846e5 100644 --- a/src/Helpers/Git.php +++ b/src/Helpers/Git.php @@ -1,5 +1,7 @@ setDestination($destination); } - /** - * @return void - */ + public function run(): void { $this->filesystem->ensureDirectoryExists($this->destination); @@ -61,7 +58,6 @@ private function setFilesystem(): self } /** - * @param string $destination * @return $this */ private function createBaseZip(string $destination): self @@ -73,7 +69,6 @@ private function createBaseZip(string $destination): self } /** - * @param string $destination * @return $this */ private function setDestination(string $destination): self @@ -83,9 +78,7 @@ private function setDestination(string $destination): self return $this; } - /** - * @return void - */ + public function closeZip(): void { $this->zip->close(); diff --git a/src/Helpers/Zip.php b/src/Helpers/Zip.php index b9f013b..b89920b 100644 --- a/src/Helpers/Zip.php +++ b/src/Helpers/Zip.php @@ -1,5 +1,7 @@ zip->close(); diff --git a/src/Interfaces/JobInterface.php b/src/Interfaces/JobInterface.php index cc55c16..bd3cb52 100644 --- a/src/Interfaces/JobInterface.php +++ b/src/Interfaces/JobInterface.php @@ -1,11 +1,11 @@ closeZip(); } - /** - * @param string $source - * @param string $destination - */ + public static function extract(string $source, string $destination) { (new Unzipper($source, $destination)) diff --git a/src/Services/BackupService.php b/src/Services/BackupService.php index f57c00d..7d5cbc4 100644 --- a/src/Services/BackupService.php +++ b/src/Services/BackupService.php @@ -1,5 +1,7 @@ backupPath); } /** - * @param string $path * @return $this */ public function setBackupPath(string $path): self diff --git a/src/Services/VersioningService.php b/src/Services/VersioningService.php index a2e1044..e5ca69b 100644 --- a/src/Services/VersioningService.php +++ b/src/Services/VersioningService.php @@ -1,5 +1,7 @@ getUploadPath($path); } - /** - * @return string - */ + private static function getDisk(): string { return config('lasso.storage.disk'); } - /** - * @return int - */ + private static function getMaxBundlesAllowed(): int { return config('lasso.storage.max_bundles'); diff --git a/src/Tasks/BaseJob.php b/src/Tasks/BaseJob.php index 71ddcb0..bed952e 100644 --- a/src/Tasks/BaseJob.php +++ b/src/Tasks/BaseJob.php @@ -1,5 +1,7 @@ deleteLassoDirectory(); } /** - * @param Exception $exception * @throws Exception */ private function rollBack(Exception $exception) @@ -145,9 +143,7 @@ private function rollBack(Exception $exception) throw $exception; } - /** - * @param array $webhooks - */ + public function dispatchWebhooks(array $webhooks = []): void { if (! count($webhooks)) { diff --git a/src/Tasks/Pull/PullJob.php b/src/Tasks/Pull/PullJob.php index 5d63aaa..3544d36 100644 --- a/src/Tasks/Pull/PullJob.php +++ b/src/Tasks/Pull/PullJob.php @@ -1,5 +1,7 @@ dispatchWebhooks($webhooks); } - /** - * @return void - */ + public function cleanUp(): void { $this->filesystem->deleteBaseLassoDirectory(); } - /** - * @param array $webhooks - */ + public function dispatchWebhooks(array $webhooks = []): void { if (! count($webhooks)) { @@ -130,7 +127,6 @@ public function dispatchWebhooks(array $webhooks = []): void } /** - * @return array * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException */ private function getLatestBundleInfo(): array @@ -170,8 +166,6 @@ private function getLatestBundleInfo(): array } /** - * @param array $bundle - * @return bool * @throws \Exception */ private function validateBundle(array $bundle): bool @@ -186,9 +180,6 @@ private function validateBundle(array $bundle): bool } /** - * @param string $file - * @param string $checksum - * @return string * @throws PullJobFailed|\Exception */ private function downloadBundleZip(string $file, string $checksum): string @@ -236,7 +227,6 @@ private function downloadBundleZip(string $file, string $checksum): string } /** - * @param \Exception $exception * @throws \Exception */ private function rollBack(\Exception $exception) @@ -277,9 +267,7 @@ private function setBackup(): self return $this; } - /** - * @return void - */ + private function cleanLassoDirectory(): void { $this->filesystem->deleteBaseLassoDirectory(); @@ -287,10 +275,7 @@ private function cleanLassoDirectory(): void $this->filesystem->ensureDirectoryExists(base_path('.lasso')); } - /** - * @param string $file - * @return string - */ + private function getBundlePath(string $file): string { if ($this->commitHash) { diff --git a/src/Tasks/Webhook.php b/src/Tasks/Webhook.php index a6f7795..97a3ca3 100644 --- a/src/Tasks/Webhook.php +++ b/src/Tasks/Webhook.php @@ -1,5 +1,7 @@ in('Helpers'); +uses(TestCase::class)->in(__DIR__); function sourceDirectory(): string { - return __DIR__ . '/Helpers/Support/Zip/Source'; + return __DIR__ . '/Unit/Support/Zip/Source'; } function destinationDirectory(): string { - return __DIR__ . '/Helpers/Support/Zip/Destination'; + return __DIR__ . '/Unit/Support/Zip/Destination'; } diff --git a/tests/TestCase.php b/tests/TestCase.php index aeb7399..37f3c9a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,5 +1,7 @@ getFromName($relativePath) + sourceDirectory() . '/' . $filePath, + $inspectZipFile->getFromName($relativePath) ); }); } @@ -88,5 +91,5 @@ function getRelativePath(string $filePath): string $normalizedPath = str_replace($rootDirectory, '', $filePath); // Remove preliminary forward slash "/Dir" -> "Dir" - return substr($normalizedPath, 1); + return mb_substr($normalizedPath, 1); } From fb0c0911790432178b792743e2416214730214dd Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 20:15:27 +0000 Subject: [PATCH 03/19] All helpers tidied up --- src/Commands/BaseCommand.php | 8 +- src/Commands/PublishCommand.php | 3 +- src/Commands/PullCommand.php | 1 + src/Container/Artisan.php | 81 +++++++++-------- src/Exceptions/BaseException.php | 14 +-- src/Exceptions/ConfigFailedValidation.php | 4 +- src/Helpers/Bundle.php | 18 ++-- src/Helpers/BundleIntegrityHelper.php | 11 ++- src/Helpers/Cloud.php | 106 +++++++--------------- src/{Actions => Helpers}/Compiler.php | 35 ++++--- src/Helpers/ConfigValidator.php | 34 +++++-- src/Helpers/FileLister.php | 10 +- src/Helpers/Filesystem.php | 63 +++++-------- src/Helpers/Git.php | 15 ++- src/Helpers/Unzipper.php | 58 ++++-------- src/Helpers/Zip.php | 41 +++------ src/Interfaces/JobInterface.php | 11 --- src/LassoServiceProvider.php | 65 ++++--------- src/Services/ArchiveService.php | 13 ++- src/Tasks/Command.php | 63 ------------- src/Tasks/Publish/PublishJob.php | 10 +- tests/Feature/LassoPublishTest.php | 5 + tests/Feature/LassoPullTest.php | 5 + 23 files changed, 266 insertions(+), 408 deletions(-) rename src/{Actions => Helpers}/Compiler.php (65%) delete mode 100644 src/Interfaces/JobInterface.php delete mode 100644 src/Tasks/Command.php create mode 100644 tests/Feature/LassoPublishTest.php create mode 100644 tests/Feature/LassoPullTest.php diff --git a/src/Commands/BaseCommand.php b/src/Commands/BaseCommand.php index b4e6244..45654b2 100644 --- a/src/Commands/BaseCommand.php +++ b/src/Commands/BaseCommand.php @@ -15,16 +15,16 @@ class BaseCommand extends Command */ protected function configureApplication(Artisan $artisan, Filesystem $filesystem, bool $checkFilesystem = false): void { - $noPrompt = $this->option('silent') === true; - $lassoEnvironment = config('lasso.storage.environment', null); + $silent = $this->option('silent') === true; + $lassoEnvironment = config('lasso.storage.environment'); $artisan->setCommand($this); - if ($noPrompt) { + if ($silent) { $artisan->silent(); } - if ($checkFilesystem === true && $noPrompt === false && ! is_null($lassoEnvironment)) { + if ($checkFilesystem === true && $silent === false && ! is_null($lassoEnvironment)) { $definedEnv = $this->ask('🐎 Which Lasso environment would you like to publish to?', $lassoEnvironment); $filesystem->setLassoEnvironment($definedEnv); diff --git a/src/Commands/PublishCommand.php b/src/Commands/PublishCommand.php index 67be9ee..70b302b 100644 --- a/src/Commands/PublishCommand.php +++ b/src/Commands/PublishCommand.php @@ -32,14 +32,13 @@ final class PublishCommand extends BaseCommand */ public function handle(Artisan $artisan, Filesystem $filesystem): int { - (new ConfigValidator())->validate(); + (new ConfigValidator)->validate(); $this->configureApplication($artisan, $filesystem, true); $dontUseGit = $this->option('no-git') === true; $useCommit = $this->option('use-commit') === true; $withCommit = $this->option('with-commit'); - $this->configureApplication($artisan, $filesystem); $job = new PublishJob; diff --git a/src/Commands/PullCommand.php b/src/Commands/PullCommand.php index 28c1ce4..36229ef 100644 --- a/src/Commands/PullCommand.php +++ b/src/Commands/PullCommand.php @@ -40,6 +40,7 @@ public function handle(Artisan $artisan, Filesystem $filesystem): int $useCommit = $this->option('use-commit') === true; $withCommit = $this->option('with-commit'); + $this->configureApplication($artisan, $filesystem); $artisan->setCommand($this); diff --git a/src/Container/Artisan.php b/src/Container/Artisan.php index 96ca4eb..59bf5bf 100644 --- a/src/Container/Artisan.php +++ b/src/Container/Artisan.php @@ -12,45 +12,45 @@ final class Artisan { /** - * @var Command + * Command Line */ - protected $command; + protected Command $command; /** - * @var bool + * Check if the console is running in silent mode */ - protected $isSilent = false; + protected bool $isSilent = false; /** - * @var bool + * Check the compiler output mode */ - private $compilerOutputMode = 'progress'; + protected string $compilerOutputMode = 'progress'; /** - * @var ProgressBar|null + * The progress bar */ - private $progressBar = null; + private ?ProgressBar $progressBar = null; + /** + * Constructor + */ public function __construct() { - $this->setCompilerOutputMode(config('lasso.compiler.output', 'progress')); + $this->compilerOutputMode = config('lasso.compiler.output', 'progress'); } /** - * @return mixed|void - * @throws ConsoleMethodException + * Handle a method call + * + * @throws \Sammyjo20\Lasso\Exceptions\ConsoleMethodException */ - public function __call($name, $arguments) + public function __call($name, $arguments): mixed { if (method_exists($this->command, $name)) { return call_user_func_array([$this->command, $name], $arguments); } - throw new ConsoleMethodException(sprintf( - 'Method %s::%s does not exist.', - get_class($this->command), - $name - )); + throw new ConsoleMethodException(sprintf('Method %s::%s does not exist.', get_class($this->command), $name)); } /** @@ -62,19 +62,22 @@ public function note(string $message, bool $error = false): self { if (! $this->isSilent) { $command = $error === true ? 'error' : 'info'; + $this->$command($message); } return $this; } - public function compilerOutput(string $line): void + /** + * Show compiler output + */ + public function showCompilerOutput(string $line): void { $mode = $this->compilerOutputMode; if ($mode === 'all') { $this->note($line); - return; } @@ -87,14 +90,26 @@ public function compilerOutput(string $line): void } } + /** + * Mark compiler as complete + * + * @return void + */ public function compilerComplete(): void { - if ($this->progressBar instanceof ProgressBar) { - $this->progressBar->finish(); - $this->command->getOutput()->newLine(); + if (! $this->progressBar instanceof ProgressBar) { + return; } + + $this->progressBar->finish(); + $this->command->getOutput()->newLine(); } + /** + * Get the progress bar + * + * @return \Symfony\Component\Console\Helper\ProgressBar + */ private function getProgressBar(): ProgressBar { if ($bar = $this->progressBar) { @@ -108,11 +123,14 @@ private function getProgressBar(): ProgressBar $bar->setEmptyBarCharacter('-'); $bar->start(); - $this->setProgressBar($bar); + $this->progressBar = $bar; return $bar; } + /** + * Set the command being run + */ public function setCommand(Command $command): self { $this->command = $command; @@ -120,24 +138,13 @@ public function setCommand(Command $command): self return $this; } + /** + * Run the artisan console in silent mode + */ public function silent(): self { $this->isSilent = true; return $this; } - - private function setCompilerOutputMode(string $mode): self - { - $this->compilerOutputMode = $mode; - - return $this; - } - - private function setProgressBar(ProgressBar $bar): self - { - $this->progressBar = $bar; - - return $this; - } } diff --git a/src/Exceptions/BaseException.php b/src/Exceptions/BaseException.php index 2cc654d..274ccc5 100644 --- a/src/Exceptions/BaseException.php +++ b/src/Exceptions/BaseException.php @@ -7,19 +7,15 @@ class BaseException extends \Exception { /** - * @var string + * Default Event */ - public static $event = 'An exception was thrown.'; + public static string $event = 'An exception was thrown.'; /** - * @return static + * Create a new exception with a reason */ - public static function because(string $reason) + public static function because(string $reason): self { - return new static(sprintf( - '%s Reason: %s', - static::$event, - $reason - )); + return new static(sprintf('%s Reason: %s', static::$event, $reason)); } } diff --git a/src/Exceptions/ConfigFailedValidation.php b/src/Exceptions/ConfigFailedValidation.php index 8c81458..a744724 100644 --- a/src/Exceptions/ConfigFailedValidation.php +++ b/src/Exceptions/ConfigFailedValidation.php @@ -7,7 +7,7 @@ class ConfigFailedValidation extends BaseException { /** - * @var string + * Default Event */ - public static $event = 'Failed to parse configuration.'; + public static string $event = 'Failed to parse configuration.'; } diff --git a/src/Helpers/Bundle.php b/src/Helpers/Bundle.php index 19baf77..27c2fb8 100644 --- a/src/Helpers/Bundle.php +++ b/src/Helpers/Bundle.php @@ -7,16 +7,20 @@ class Bundle { /** - * @var string + * Bundle ID */ - protected $bundleId; + protected string $bundleId; /** - * @var string + * ZIP Path */ - protected $zipPath; + protected string $zipPath; - + /** + * Create a bundle + * + * @return array + */ public function create(): array { // Now we will generate a checksum for the file. This is useful @@ -29,7 +33,7 @@ public function create(): array } /** - * @return $this + * Set the bundle ID */ public function setBundleId(string $bundleId): self { @@ -39,7 +43,7 @@ public function setBundleId(string $bundleId): self } /** - * @return $this + * Set the ZIP path */ public function setZipPath(string $path): self { diff --git a/src/Helpers/BundleIntegrityHelper.php b/src/Helpers/BundleIntegrityHelper.php index 434f7db..db418c8 100644 --- a/src/Helpers/BundleIntegrityHelper.php +++ b/src/Helpers/BundleIntegrityHelper.php @@ -6,15 +6,22 @@ class BundleIntegrityHelper { + /** + * Hashing Algorithm + */ public const ALGORITHM = 'md5'; - + /** + * Generate the checksum + */ public static function generateChecksum(string $path): string { return hash_file(self::ALGORITHM, $path); } - + /** + * Verify the checksum + */ public static function verifyChecksum(string $path, string $checksum): bool { return self::generateChecksum($path) === $checksum; diff --git a/src/Helpers/Cloud.php b/src/Helpers/Cloud.php index c5c9116..78c46fc 100644 --- a/src/Helpers/Cloud.php +++ b/src/Helpers/Cloud.php @@ -6,122 +6,82 @@ use Illuminate\Support\Facades\Storage; use League\Flysystem\UnableToWriteFile; +use LogicException; use Sammyjo20\Lasso\Exceptions\ConsoleMethodException; -use Sammyjo20\Lasso\Helpers\Filesystem as LocalFilesystem; +use Illuminate\Contracts\Filesystem\Filesystem; class Cloud { /** - * @var \Illuminate\Filesystem\Filesystem + * Lasso Filesystem */ - protected $cloudFilesystem; + protected Filesystem $cloudFilesystem; /** - * @var string + * Local Filesystem */ - protected $cloudDisk; + protected Filesystem $localFilesystem; /** - * @var LocalFilesystem - */ - protected $localFilesystem; - - /** - * Cloud constructor. + * Constructor. */ public function __construct() { - $this->setCloudDisk(config('lasso.storage.disk')); - - $this->initCloudFilesystem() - ->initLocalFilesystem(); + $this->cloudFilesystem = Storage::disk(config('lasso.storage.disk')); + $this->localFilesystem = resolve(Filesystem::class); } /** - * @return mixed|void - * @throws ConsoleMethodException + * Upload a file to the cloud filesystem */ - public function __call($name, $arguments) - { - if (method_exists($this->cloudFilesystem, $name)) { - return call_user_func_array([$this->cloudFilesystem, $name], $arguments); - } - - throw new ConsoleMethodException(sprintf( - 'Method %s::%s does not exist.', - get_class($this->cloudFilesystem), - $name - )); - } - - public function uploadFile(string $path, string $name): void { - $upload_path = $this->getUploadPath($name); + $uploadPath = $this->getUploadPath($name); $stream = fopen($path, 'rb'); - // Use the stream to write the bundle to the Filesystem. - if ($this->cloudFilesystem->writeStream($upload_path, $stream) === false) { - throw new UnableToWriteFile('Unable to write file at location ' . $upload_path); + if (! $stream) { + throw new LogicException('Unable to create a stream from the file path'); } - // Close the Stream pointer because it's good practice. - if (is_resource($stream)) { - fclose($stream); + // Use the stream to write the bundle to the Filesystem. + + if ($this->cloudFilesystem->writeStream($uploadPath, $stream) === false) { + throw new UnableToWriteFile(sprintf('Unable to write file at location [%s]', $uploadPath)); } + + fclose($stream); } /** - * Returns the Lasso upload directory. You can specify a file - * to create a fully qualified URL. + * Returns the Lasso upload directory. + * + * You can specify a file to create a fully qualified URL. */ public function getUploadPath(string $file = null): string { $uploadPath = config('lasso.storage.upload_to'); - $dir = sprintf('%s/%s', $uploadPath, $this->localFilesystem->getLassoEnvironment()); + $directory = sprintf('%s/%s', $uploadPath, $this->localFilesystem->getLassoEnvironment()); if (is_null($file)) { - return $dir; + return $directory; } - return $dir . '/' . ltrim($file, '/'); - } - - - public function getCloudDisk(): string - { - return $this->cloudDisk; - } - - /** - * @return $this - */ - public function setCloudDisk(string $disk): self - { - $this->cloudDisk = $disk; - - return $this; - } - - /** - * @return $this - */ - public function initCloudFilesystem(): self - { - $this->cloudFilesystem = Storage::disk($this->cloudDisk); - - return $this; + return $directory . '/' . ltrim($file, '/'); } /** - * @return $this + * Call a method on the base Filesystem + * + * @throws \Sammyjo20\Lasso\Exceptions\ConsoleMethodException */ - public function initLocalFilesystem(): self + public function __call(string $name, array $arguments) { - $this->localFilesystem = resolve(LocalFilesystem::class); + if (method_exists($this->cloudFilesystem, $name)) { + return call_user_func_array([$this->cloudFilesystem, $name], $arguments); + } - return $this; + throw new ConsoleMethodException(sprintf('Method %s::%s does not exist.', get_class($this->cloudFilesystem), $name)); } } diff --git a/src/Actions/Compiler.php b/src/Helpers/Compiler.php similarity index 65% rename from src/Actions/Compiler.php rename to src/Helpers/Compiler.php index a80c980..477a9da 100644 --- a/src/Actions/Compiler.php +++ b/src/Helpers/Compiler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Sammyjo20\Lasso\Actions; +namespace Sammyjo20\Lasso\Helpers; use Sammyjo20\Lasso\Container\Artisan; use Symfony\Component\Process\Process; @@ -12,20 +12,23 @@ class Compiler /** * The Process shell command. */ - protected $command; + protected string $command; /** * The Process command timeout, if something happens to hang for too long. */ - protected $timeout; + protected int $timeout; /** * The time it has taken for the compiler to Lasso up the assets. * * @var float */ - protected $compilationTime; + protected float $compilationTime = 0; + /** + * Execute the compiler + */ public function execute(): self { $artisan = resolve(Artisan::class); @@ -35,39 +38,41 @@ public function execute(): self Process::fromShellCommandline($this->command) ->setTimeout($this->timeout) ->mustRun(function ($type, $line) use ($artisan) { - $artisan->compilerOutput($line); + $artisan->showCompilerOutput($line); }); $processTime = microtime(true) - $startTime; $artisan->compilerComplete(); - $this->setCompilationTime($processTime); + $this->compilationTime = round($processTime, 2); return $this; } - public function setCommand($command): self + /** + * Set the command for the compiler + */ + public function setCommand(string $command): self { $this->command = $command; return $this; } - public function setTimeout($timeout): self + /** + * Set the timeout for the compiler + */ + public function setTimeout(int $timeout): self { $this->timeout = $timeout; return $this; } - private function setCompilationTime(float $time): self - { - $this->compilationTime = round($time, 2); - - return $this; - } - + /** + * Get the compilation time + */ public function getCompilationTime(): float { return $this->compilationTime; diff --git a/src/Helpers/ConfigValidator.php b/src/Helpers/ConfigValidator.php index 7f92b67..9f335ba 100644 --- a/src/Helpers/ConfigValidator.php +++ b/src/Helpers/ConfigValidator.php @@ -10,9 +10,9 @@ class ConfigValidator { /** - * @var Filesystem + * Filesystem */ - public $filesystem; + public Filesystem $filesystem; /** * ConfigValidator constructor. @@ -22,25 +22,33 @@ public function __construct() $this->filesystem = new Filesystem(); } - + /** + * Get config parameter + */ private function get(string $item) { return config('lasso.' . $item, null); } - + /** + * Check the compiler script + */ private function checkCompilerScript($value): bool { return ! is_null($value); } - + /** + * Check compiler script type + */ private function checkCompilerScriptType($value): bool { return is_string($value); } - + /** + * Check compiler output + */ private function checkCompilerOutputSetting($value): bool { if (is_null($value)) { @@ -50,25 +58,33 @@ private function checkCompilerOutputSetting($value): bool return in_array($value, ['all', 'progress', 'disable']); } - + /** + * Check if public path exists + */ private function checkIfPublicPathExists($value): bool { return $this->filesystem->exists($value) && $this->filesystem->isReadable($value) && $this->filesystem->isWritable($value); } - + /** + * Check disk exists + */ private function checkDiskExists($value): bool { return ! is_null(config('filesystems.disks.' . $value, null)); } - + /** + * Check bundle to keep count + */ private function checkBundleToKeepCount($value): bool { return is_int($value) && $value > 0; } /** + * Validate config + * * @throws ConfigFailedValidation */ public function validate(): void diff --git a/src/Helpers/FileLister.php b/src/Helpers/FileLister.php index e36b34f..6c32f89 100644 --- a/src/Helpers/FileLister.php +++ b/src/Helpers/FileLister.php @@ -9,16 +9,16 @@ class FileLister { /** - * @var Finder + * Symfony Finder */ - public $finder; + public Finder $finder; /** * FileLister constructor. */ public function __construct(string $directory) { - $this->finder = (new Finder()) + $this->finder = (new Finder) ->in($directory) ->ignoreDotFiles(false) ->ignoreUnreadableDirs(true) @@ -27,9 +27,9 @@ public function __construct(string $directory) } /** - * @return Finder + * Return Finder */ - public function getFinder() + public function getFinder(): Finder { return $this->finder; } diff --git a/src/Helpers/Filesystem.php b/src/Helpers/Filesystem.php index 13db95a..4bd6308 100644 --- a/src/Helpers/Filesystem.php +++ b/src/Helpers/Filesystem.php @@ -11,36 +11,36 @@ class Filesystem extends BaseFilesystem /** * @var string */ - protected $lassoEnvironment; + protected string $lassoEnvironment; /** * @var string */ - protected $cloudDisk; + protected string $cloudDisk; /** * @var string */ - protected $publicPath; + protected string $publicPath; /** * Filesystem constructor. */ public function __construct() { - $lassoEnvironment = config('lasso.storage.environment') ?? 'global'; - $cloudDisk = config('lasso.storage.disk', 'assets'); - $publicPath = config('lasso.public_path', public_path()); - - $this->setLassoEnvironment($lassoEnvironment) - ->setCloudDisk($cloudDisk) - ->setPublicPath($publicPath); + $this->lassoEnvironment = config('lasso.storage.environment') ?? 'global'; + $this->cloudDisk = config('lasso.storage.disk', 'assets'); + $this->publicPath = config('lasso.public_path', public_path()); } - + /** + * @param $resource + * @param string $destination + * @return bool + */ public function putStream($resource, string $destination): bool { - $stream = fopen($destination, 'w+b'); + $stream = fopen($destination, 'wb+'); if (! $stream || stream_copy_to_stream($resource, $stream) === false || ! fclose($stream)) { return false; @@ -49,7 +49,10 @@ public function putStream($resource, string $destination): bool return true; } - + /** + * @param array $bundle + * @return void + */ public function createFreshLocalBundle(array $bundle): void { $this->deleteLocalBundle(); @@ -57,7 +60,9 @@ public function createFreshLocalBundle(array $bundle): void $this->put(base_path('lasso-bundle.json'), json_encode($bundle)); } - + /** + * @return bool + */ public function deleteLocalBundle(): bool { return $this->delete(base_path('lasso-bundle.json')); @@ -71,12 +76,6 @@ public function deleteBaseLassoDirectory(): bool return $this->deleteDirectory('.lasso'); } - - public function getLassoEnvironment(): string - { - return $this->lassoEnvironment; - } - /** * @return $this */ @@ -87,35 +86,19 @@ public function setLassoEnvironment(string $environment): self return $this; } - + /** + * @return string + */ public function getPublicPath(): string { return $this->publicPath; } /** - * @return $this + * @return string */ - public function setPublicPath(string $publicPath): self - { - $this->publicPath = $publicPath; - - return $this; - } - - public function getCloudDisk(): string { return $this->cloudDisk; } - - /** - * @return $this - */ - public function setCloudDisk(string $disk): self - { - $this->cloudDisk = $disk; - - return $this; - } } diff --git a/src/Helpers/Git.php b/src/Helpers/Git.php index 09846e5..09b886f 100644 --- a/src/Helpers/Git.php +++ b/src/Helpers/Git.php @@ -4,26 +4,25 @@ namespace Sammyjo20\Lasso\Helpers; +use Exception; use Sammyjo20\Lasso\Exceptions\GitHashException; class Git { /** + * Get the current commit hash + * * @throws GitHashException */ public static function getCommitHash(): ? string { try { - $branch = str_replace("\n", '', last(explode('/', file_get_contents(base_path() . '/.git/HEAD')))); + $branch = str_replace('\n', '', last(explode('/', file_get_contents(base_path() . '/.git/HEAD')))); $hash = file_get_contents(base_path() . '/.git/refs/heads/' . $branch); - } catch (\Exception $exception) { - throw new GitHashException($exception->getMessage(), $exception); + } catch (Exception $exception) { + throw new GitHashException($exception->getMessage(), previous: $exception); } - if ($hash) { - return mb_substr($hash, 0, 12); - } - - return null; + return $hash ? mb_substr($hash, 0, 12) : null; } } diff --git a/src/Helpers/Unzipper.php b/src/Helpers/Unzipper.php index a0c6387..fec7084 100644 --- a/src/Helpers/Unzipper.php +++ b/src/Helpers/Unzipper.php @@ -11,76 +11,54 @@ class Unzipper /** * @var ZipArchive */ - protected $zip; + protected ZipArchive $zip; /** * @var string */ - protected $source; + protected string $source; /** * @var string */ - protected $destination; + protected string $destination; /** * @var Filesystem */ - protected $filesystem; + protected Filesystem $filesystem; /** * Unzipper constructor. */ public function __construct(string $source, string $destination) - { - $this->setFilesystem() - ->createBaseZip($source) - ->setDestination($destination); - } - - - public function run(): void - { - $this->filesystem->ensureDirectoryExists($this->destination); - $this->zip->extractTo($this->destination); - - $this->closeZip(); - } - - /** - * @return $this - */ - private function setFilesystem(): self { $this->filesystem = resolve(Filesystem::class); + $this->destination = $destination; - return $this; + $this->createBaseZip($source); } /** - * @return $this + * Unzip the source into the destination + * + * @return void */ - private function createBaseZip(string $destination): self + public function run(): void { - $this->zip = new ZipArchive(); - $this->zip->open($destination, ZipArchive::CREATE); + $this->filesystem->ensureDirectoryExists($this->destination); - return $this; + $this->zip->extractTo($this->destination); + + $this->zip->close(); } /** - * @return $this + * Create the base ZipArchive */ - private function setDestination(string $destination): self + private function createBaseZip(string $destination): void { - $this->destination = $destination; - - return $this; - } - - - public function closeZip(): void - { - $this->zip->close(); + $this->zip = new ZipArchive; + $this->zip->open($destination, ZipArchive::CREATE); } } diff --git a/src/Helpers/Zip.php b/src/Helpers/Zip.php index b89920b..41635e1 100644 --- a/src/Helpers/Zip.php +++ b/src/Helpers/Zip.php @@ -2,7 +2,6 @@ declare(strict_types=1); - namespace Sammyjo20\Lasso\Helpers; use ZipArchive; @@ -10,18 +9,18 @@ class Zip { /** - * @var ZipArchive + * Base ZipArchive class */ - protected $zip; + protected ZipArchive $zip; /** - * @var Filesystem + * Lasso Filesystem */ - protected $filesystem; + protected Filesystem $filesystem; /** * A big thank you to Spatie for their amazing "Laravel Backup" package. - * A lot of the ZipArchive code is inspired by their code. + * A lot of the Zip code is inspired by their code. * * https://github.com/spatie/laravel-backup/blob/18cf209be56bb086aaeb1397e142c2a7805802b3/src/Tasks/Backup/Zip.php * @@ -29,17 +28,17 @@ class Zip */ public function __construct(string $destinationPath) { - $this->setFilesystem() - ->createBaseZip($destinationPath); + $this->filesystem = resolve(Filesystem::class); + + $this->createBaseZip($destinationPath); } /** - * @return $this + * Add files from a given directory */ public function addFilesFromDirectory(string $directory): self { - $files = (new FileLister($directory)) - ->getFinder(); + $files = (new FileLister($directory))->getFinder(); foreach ($files as $file) { $this->zip->addFile(str_replace('\\', '/', $file->getPathname()), str_replace('\\', '/', $file->getRelativePathname())); @@ -49,27 +48,17 @@ public function addFilesFromDirectory(string $directory): self } /** - * @return $this + * Create base ZipArchive */ - private function setFilesystem(): self + private function createBaseZip(string $destination): void { - $this->filesystem = resolve(Filesystem::class); - - return $this; + $this->zip = (new ZipArchive); + $this->zip->open($destination, ZipArchive::CREATE); } /** - * @return $this + * Close the Zip */ - private function createBaseZip(string $destination): self - { - $this->zip = new ZipArchive(); - $this->zip->open($destination, ZipArchive::CREATE); - - return $this; - } - - public function closeZip(): void { $this->zip->close(); diff --git a/src/Interfaces/JobInterface.php b/src/Interfaces/JobInterface.php deleted file mode 100644 index bd3cb52..0000000 --- a/src/Interfaces/JobInterface.php +++ /dev/null @@ -1,11 +0,0 @@ -mergeConfigFrom( - __DIR__ . '/../config/lasso.php', - 'lasso' - ); - } - - public function boot(): void - { - if ($this->app->runningInConsole()) { - $this->registerCommands() - ->offerPublishing() - ->bindsServices(); - } - } - /** - * @return $this + * Register the Lasso service provider + * + * @return void */ - protected function registerCommands(): self + public function register(): void { - $this->commands([ - PublishCommand::class, - PullCommand::class, - ]); - - return $this; + $this->mergeConfigFrom(__DIR__ . '/../config/lasso.php', 'lasso'); } /** - * @return $this + * Boot the Lasso service provider + * + * @return void */ - protected function offerPublishing(): self + public function boot(): void { + if (! $this->app->runningInConsole()) { + return; + } + + // Publish the config + $this->publishes([ __DIR__ . '/../config/lasso.php' => config_path('lasso.php'), ], 'lasso-config'); - return $this; - } + // Register Lasso's commands - /** - * @return $this - */ - protected function bindsServices(): self - { - $this->app->singleton(Artisan::class, function () { - return new Artisan; - }); - - $this->app->singleton(Filesystem::class, function () { - return new Filesystem; - }); - - return $this; + $this->commands([ + PublishCommand::class, + PullCommand::class, + ]); } } diff --git a/src/Services/ArchiveService.php b/src/Services/ArchiveService.php index 848e2dc..92753f0 100644 --- a/src/Services/ArchiveService.php +++ b/src/Services/ArchiveService.php @@ -9,7 +9,9 @@ final class ArchiveService { - + /** + * Create a Zip File + */ public static function create(string $sourceDirectory, string $destinationDirectory): void { (new Zip($destinationDirectory)) @@ -17,10 +19,11 @@ public static function create(string $sourceDirectory, string $destinationDirect ->closeZip(); } - - public static function extract(string $source, string $destination) + /** + * Extract a Zip File + */ + public static function extract(string $source, string $destination): void { - (new Unzipper($source, $destination)) - ->run(); + (new Unzipper($source, $destination))->run(); } } diff --git a/src/Tasks/Command.php b/src/Tasks/Command.php deleted file mode 100644 index 6e29819..0000000 --- a/src/Tasks/Command.php +++ /dev/null @@ -1,63 +0,0 @@ -script); - - $process->setTimeout($this->timeout) - ->run(); - - if (! $process->isSuccessful()) { - throw new ProcessFailedException($process); - } - } - - /** - * @return $this - */ - public function setScript($script): self - { - if (! is_array($script)) { - $script = explode(' ', $script); - } - - $this->script = $script; - - return $this; - } - - /** - * @return $this - */ - public function setTimeout(int $timeout = 600): self - { - $this->timeout = $timeout; - - return $this; - } -} diff --git a/src/Tasks/Publish/PublishJob.php b/src/Tasks/Publish/PublishJob.php index 139cfb3..676812e 100644 --- a/src/Tasks/Publish/PublishJob.php +++ b/src/Tasks/Publish/PublishJob.php @@ -6,12 +6,12 @@ use Exception; use Illuminate\Support\Str; +use Sammyjo20\Lasso\Exceptions\GitHashException; +use Sammyjo20\Lasso\Helpers\Bundle; +use Sammyjo20\Lasso\Helpers\Compiler; use Sammyjo20\Lasso\Helpers\Git; use Sammyjo20\Lasso\Tasks\BaseJob; use Sammyjo20\Lasso\Tasks\Webhook; -use Sammyjo20\Lasso\Helpers\Bundle; -use Sammyjo20\Lasso\Actions\Compiler; -use Sammyjo20\Lasso\Exceptions\GitHashException; final class PublishJob extends BaseJob { @@ -127,7 +127,7 @@ public function run(): void } } - + public function cleanUp(): void { $this->deleteLassoDirectory(); @@ -143,7 +143,7 @@ private function rollBack(Exception $exception) throw $exception; } - + public function dispatchWebhooks(array $webhooks = []): void { if (! count($webhooks)) { diff --git a/tests/Feature/LassoPublishTest.php b/tests/Feature/LassoPublishTest.php new file mode 100644 index 0000000..5c725b5 --- /dev/null +++ b/tests/Feature/LassoPublishTest.php @@ -0,0 +1,5 @@ + Date: Thu, 23 Nov 2023 20:20:01 +0000 Subject: [PATCH 04/19] Add files to export-ignore --- .gitattributes | 5 ++-- .github/README.md | 2 +- .github/SECURITY.md | 2 +- .github/workflows/php-cs-fixer.yml | 15 ++++++---- .github/workflows/tests.yml | 48 ++++++++++++++++-------------- phpunit.xml | 8 +++++ phpunit.xml.dist | 12 -------- 7 files changed, 49 insertions(+), 43 deletions(-) create mode 100644 phpunit.xml delete mode 100644 phpunit.xml.dist diff --git a/.gitattributes b/.gitattributes index d6a434b..0240a87 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,5 +3,6 @@ .editorconfig export-ignore .gitattributes export-ignore .gitignore export-ignore -CHANGELOG.md export-ignore -phpunit.xml.dist export-ignore +phpunit.xml export-ignore +LICENSE export-ignore +.php-cs-fixer.dist.php export-ignore diff --git a/.github/README.md b/.github/README.md index 161d525..ed2185b 100644 --- a/.github/README.md +++ b/.github/README.md @@ -173,7 +173,7 @@ Special thanks to @codepotato for the logo! ❤️ ## Security -If you find any security related issues, please send me an email to import.lorises_0c@icloud.com. +If you find any security related issues, please send an email to **29132017+Sammyjo20@users.noreply.github.com** ## And that's it! ✨ diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 0fe0b25..4c9663b 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,3 +1,3 @@ # Security Policy -If you discover any security related issues, please email security@yeehaw.dev instead of using the issue tracker. +If you discover any security related issues, please email **29132017+Sammyjo20@users.noreply.github.com** instead of using the issue tracker. diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index 0180a90..08a497f 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -1,10 +1,15 @@ name: Code Style on: - pull_request: - branches: [ 'master', 'v2.0', 'v3.0' ] push: - branches: [ 'master', 'v2.0', 'v3.0' ] + branches: + - 'v3.0' + pull_request: + branches: + - '*' + +permissions: + contents: write jobs: php-cs-fixer: @@ -16,8 +21,8 @@ jobs: - name: Run PHP CS Fixer uses: docker://oskarstark/php-cs-fixer-ga with: - args: --config=.php-cs-fixer.dist.php + args: --config=.php-cs-fixer.dist.php --allow-risky=yes - name: Commit changes uses: stefanzweifel/git-auto-commit-action@v4 with: - commit_message: Apply Code Style Fixes + commit_message: 🪄 Code Style Fixes diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1444365..dcd9504 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,42 +1,46 @@ -name: Tests +name: tests -on: [pull_request, push] +on: + push: + branches: + - 'v3.0' + pull_request: + branches: + - '*' + +permissions: + contents: read jobs: tests: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: true matrix: - php: [8.1, 8.2] - laravel: [^9.0, ^10.0] - dependency-version: [prefer-lowest, prefer-stable] - include: - - laravel: ^9.0 - testbench: ^7.0 - - laravel: ^10.0 - testbench: ^8.0 + os: [ ubuntu-latest, windows-latest ] + php: [ 8.1, 8.2, 8.3 ] + stability: [ prefer-lowest, prefer-stable ] - name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.dependency-version }} + name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v2 - - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ~/.composer/cache/files - key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + extensions: mbstring, zip, fileinfo coverage: none - - name: Install composer dependencies + - name: Setup problem matchers + run: | + echo "::add-matcher::${{ runner.tool_cache }}/php.json" + echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Install dependencies run: | - composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest + composer update --${{ matrix.stability }} --prefer-dist --no-interaction - name: Execute tests - run: composer test + run: vendor/bin/pest -p diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..31b0dda --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,8 @@ + + + + + tests + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist deleted file mode 100644 index 82fb072..0000000 --- a/phpunit.xml.dist +++ /dev/null @@ -1,12 +0,0 @@ - - - - - tests - - - From 06f9bddb2d6957797a348f33d80281bda9587e57 Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 20:54:05 +0000 Subject: [PATCH 05/19] Writing basic tests --- config/lasso.php | 1 - src/Commands/BaseCommand.php | 8 +- src/Commands/PublishCommand.php | 2 +- src/Commands/PullCommand.php | 9 +- src/Container/Artisan.php | 3 + src/Helpers/Bundle.php | 3 + src/Helpers/BundleIntegrityHelper.php | 3 + src/Helpers/Cloud.php | 3 + src/Helpers/Compiler.php | 3 + src/Helpers/CompilerOutputFormatter.php | 3 + src/Helpers/ConfigValidator.php | 3 + src/Helpers/FileLister.php | 3 + src/Helpers/Filesystem.php | 3 + src/Helpers/Git.php | 3 + src/Helpers/Unzipper.php | 3 + src/Helpers/Webhook.php | 27 ++++++ src/Helpers/Zip.php | 3 + src/LassoServiceProvider.php | 7 ++ src/Services/BackupService.php | 32 ++----- src/Services/VersioningService.php | 66 +++++++------ src/Tasks/BaseJob.php | 51 ++--------- src/Tasks/Publish/BundleJob.php | 42 +++------ src/Tasks/Publish/PublishJob.php | 58 +++++++----- src/Tasks/Pull/PullJob.php | 106 ++++++++++----------- src/Tasks/Webhook.php | 22 ----- tests/Feature/LassoPublishTest.php | 6 +- tests/Pest.php | 117 ++++++++++++++++++++++++ tests/TestCase.php | 14 ++- 28 files changed, 370 insertions(+), 234 deletions(-) create mode 100644 src/Helpers/Webhook.php delete mode 100644 src/Tasks/Webhook.php diff --git a/config/lasso.php b/config/lasso.php index 295c96e..bd65d32 100644 --- a/config/lasso.php +++ b/config/lasso.php @@ -3,7 +3,6 @@ declare(strict_types=1); return [ - 'compiler' => [ /* diff --git a/src/Commands/BaseCommand.php b/src/Commands/BaseCommand.php index 45654b2..4ccaa22 100644 --- a/src/Commands/BaseCommand.php +++ b/src/Commands/BaseCommand.php @@ -8,7 +8,7 @@ use Sammyjo20\Lasso\Container\Artisan; use Sammyjo20\Lasso\Helpers\Filesystem; -class BaseCommand extends Command +abstract class BaseCommand extends Command { /** * Configure the Artisan console and the Filesystem, ready for publishing. @@ -16,7 +16,7 @@ class BaseCommand extends Command protected function configureApplication(Artisan $artisan, Filesystem $filesystem, bool $checkFilesystem = false): void { $silent = $this->option('silent') === true; - $lassoEnvironment = config('lasso.storage.environment'); + $environment = config('lasso.storage.environment'); $artisan->setCommand($this); @@ -24,8 +24,8 @@ protected function configureApplication(Artisan $artisan, Filesystem $filesystem $artisan->silent(); } - if ($checkFilesystem === true && $silent === false && ! is_null($lassoEnvironment)) { - $definedEnv = $this->ask('🐎 Which Lasso environment would you like to publish to?', $lassoEnvironment); + if ($checkFilesystem === true && $silent === false && ! is_null($environment)) { + $definedEnv = $this->ask('🐎 Which Lasso environment would you like to publish to?', $environment); $filesystem->setLassoEnvironment($definedEnv); } diff --git a/src/Commands/PublishCommand.php b/src/Commands/PublishCommand.php index 70b302b..fb46ddc 100644 --- a/src/Commands/PublishCommand.php +++ b/src/Commands/PublishCommand.php @@ -66,6 +66,6 @@ public function handle(Artisan $artisan, Filesystem $filesystem): int $filesystem->getCloudDisk() )); - return 0; + return self::SUCCESS; } } diff --git a/src/Commands/PullCommand.php b/src/Commands/PullCommand.php index 36229ef..972e376 100644 --- a/src/Commands/PullCommand.php +++ b/src/Commands/PullCommand.php @@ -30,9 +30,10 @@ final class PullCommand extends BaseCommand * Execute the console command. * * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + * @throws \Psr\Container\ContainerExceptionInterface + * @throws \Psr\Container\NotFoundExceptionInterface * @throws \Sammyjo20\Lasso\Exceptions\BaseException * @throws \Sammyjo20\Lasso\Exceptions\ConfigFailedValidation - * @throws \Sammyjo20\Lasso\Exceptions\PullJobFailed */ public function handle(Artisan $artisan, Filesystem $filesystem): int { @@ -50,7 +51,7 @@ public function handle(Artisan $artisan, Filesystem $filesystem): int $filesystem->getCloudDisk() )); - $job = new PullJob(); + $job = new PullJob; if ($useCommit) { $job->useCommit(); @@ -63,8 +64,8 @@ public function handle(Artisan $artisan, Filesystem $filesystem): int $job->run(); $artisan->note('✅ Successfully downloaded the latest assets. Yee-haw!'); - $artisan->note('❤️ Thank you for using Lasso. https://getlasso.dev'); + $artisan->note('❤️ Thank you for using Lasso.'); - return 0; + return self::SUCCESS; } } diff --git a/src/Container/Artisan.php b/src/Container/Artisan.php index 59bf5bf..582b981 100644 --- a/src/Container/Artisan.php +++ b/src/Container/Artisan.php @@ -9,6 +9,9 @@ use Sammyjo20\Lasso\Helpers\CompilerOutputFormatter; use Sammyjo20\Lasso\Exceptions\ConsoleMethodException; +/** + * @internal + */ final class Artisan { /** diff --git a/src/Helpers/Bundle.php b/src/Helpers/Bundle.php index 27c2fb8..be46975 100644 --- a/src/Helpers/Bundle.php +++ b/src/Helpers/Bundle.php @@ -4,6 +4,9 @@ namespace Sammyjo20\Lasso\Helpers; +/** + * @internal + */ class Bundle { /** diff --git a/src/Helpers/BundleIntegrityHelper.php b/src/Helpers/BundleIntegrityHelper.php index db418c8..b18e510 100644 --- a/src/Helpers/BundleIntegrityHelper.php +++ b/src/Helpers/BundleIntegrityHelper.php @@ -4,6 +4,9 @@ namespace Sammyjo20\Lasso\Helpers; +/** + * @internal + */ class BundleIntegrityHelper { /** diff --git a/src/Helpers/Cloud.php b/src/Helpers/Cloud.php index 78c46fc..40c9c99 100644 --- a/src/Helpers/Cloud.php +++ b/src/Helpers/Cloud.php @@ -10,6 +10,9 @@ use Sammyjo20\Lasso\Exceptions\ConsoleMethodException; use Illuminate\Contracts\Filesystem\Filesystem; +/** + * @internal + */ class Cloud { /** diff --git a/src/Helpers/Compiler.php b/src/Helpers/Compiler.php index 477a9da..35a63c7 100644 --- a/src/Helpers/Compiler.php +++ b/src/Helpers/Compiler.php @@ -7,6 +7,9 @@ use Sammyjo20\Lasso\Container\Artisan; use Symfony\Component\Process\Process; +/** + * @internal + */ class Compiler { /** diff --git a/src/Helpers/CompilerOutputFormatter.php b/src/Helpers/CompilerOutputFormatter.php index 588d3f3..db794d5 100644 --- a/src/Helpers/CompilerOutputFormatter.php +++ b/src/Helpers/CompilerOutputFormatter.php @@ -4,6 +4,9 @@ namespace Sammyjo20\Lasso\Helpers; +/** + * @internal + */ class CompilerOutputFormatter { private const PERCENTAGE_REGEX = '/\b(? $event], $data)); + }, + rescue: false, + report: false + ); + } +} diff --git a/src/Helpers/Zip.php b/src/Helpers/Zip.php index 41635e1..28dbbbb 100644 --- a/src/Helpers/Zip.php +++ b/src/Helpers/Zip.php @@ -6,6 +6,9 @@ use ZipArchive; +/** + * @internal + */ class Zip { /** diff --git a/src/LassoServiceProvider.php b/src/LassoServiceProvider.php index 99ed1ba..b0d794c 100644 --- a/src/LassoServiceProvider.php +++ b/src/LassoServiceProvider.php @@ -4,6 +4,8 @@ namespace Sammyjo20\Lasso; +use Sammyjo20\Lasso\Container\Artisan; +use Sammyjo20\Lasso\Helpers\Filesystem; use Sammyjo20\Lasso\Commands\PullCommand; use Sammyjo20\Lasso\Commands\PublishCommand; use Illuminate\Support\ServiceProvider as BaseServiceProvider; @@ -43,5 +45,10 @@ public function boot(): void PublishCommand::class, PullCommand::class, ]); + + // Bind Artisan and Filesystem to the container + + $this->app->singleton(Artisan::class, static fn() => new Artisan); + $this->app->singleton(Filesystem::class, static fn() => new Filesystem); } } diff --git a/src/Services/BackupService.php b/src/Services/BackupService.php index 7d5cbc4..2fa6c24 100644 --- a/src/Services/BackupService.php +++ b/src/Services/BackupService.php @@ -10,21 +10,21 @@ final class BackupService { /** - * @var Filesystem + * Lasso Filesystem */ - protected $filesystem; + protected Filesystem $filesystem; /** - * @var bool + * Backup path */ - protected $backupPath; + protected string $backupPath; /** - * Backup constructor. + * Constructor. */ public function __construct(Filesystem $filesystem) { - $this->setFilesystem($filesystem); + $this->filesystem = $filesystem; } /** @@ -45,6 +45,8 @@ public function createBackup(string $sourceDirectory, string $destinationDirecto } /** + * Restore a backup + * * @throws \Sammyjo20\Lasso\Exceptions\BaseException */ public function restoreBackup(string $destinationDirectory): bool @@ -61,23 +63,7 @@ public function restoreBackup(string $destinationDirectory): bool } /** - * @return $this - */ - public function setFilesystem(Filesystem $filesystem): self - { - $this->filesystem = $filesystem; - - return $this; - } - - - public function hasBackup(): bool - { - return ! is_null($this->backupPath); - } - - /** - * @return $this + * Set a backup path */ public function setBackupPath(string $path): self { diff --git a/src/Services/VersioningService.php b/src/Services/VersioningService.php index e5ca69b..da3d5a4 100644 --- a/src/Services/VersioningService.php +++ b/src/Services/VersioningService.php @@ -4,10 +4,14 @@ namespace Sammyjo20\Lasso\Services; +use Exception; use Sammyjo20\Lasso\Helpers\Cloud; use Illuminate\Support\Facades\Storage; use Sammyjo20\Lasso\Exceptions\VersioningFailed; +/** + * @internal + */ final class VersioningService { /** @@ -39,7 +43,7 @@ public static function appendNewVersion(string $bundle_url): void private static function getHistoryFromDisk(): array { $disk = self::getDisk(); - $path = self::getFileDirectory('history.json'); + $path = self::getFileDirectory(); // If there is no history to be found in the Filesystem, // that's completely fine. Let's just return an empty @@ -60,48 +64,53 @@ private static function getHistoryFromDisk(): array ksort($history); return $history; - } catch (\Exception $ex) { + } catch (Exception $ex) { throw VersioningFailed::because( 'Lasso could not retrieve the history.json file from the Filesystem.' ); } } - + /** + * @param array $bundles + * @return array + */ private static function deleteExpiredBundles(array $bundles): array { $bundle_limit = self::getMaxBundlesAllowed(); - // If we haven't exceeded our bundle Limit, - // let's just return the bundles. There's nothing - // more we can do here. + // If we haven't exceeded our bundle Limit, let's just return the bundles. + // There's nothing more we can do here. if (count($bundles) <= $bundle_limit) { return $bundles; } - // However, if there's a bundle to be removed - // we need to go Ghostbuster on that bundle. + // However, if there's a bundle to be removed we need to go Ghostbuster on that bundle. + $deletable_count = count($bundles) - $bundle_limit; $deletable = array_slice($bundles, 0, $deletable_count, true); // Now let's delete those bundles! + $deleted = self::deleteBundles(array_values($deletable)); - // Finally, we want to return a new array, with - // the bundles that have been deleted removed. + // Finally, we want to return a new array, with the bundles that have been deleted removed. + return array_diff($bundles, $deleted); } - + /** + * @param array $deletable + * @return array + */ private static function deleteBundles(array $deletable): array { $disk = self::getDisk(); $deleted = []; - // Attempt to delete the bundle. If something - // goes wrong, Lasso isn't precious about it. - // we will simply try to delete the next directory and move on. + // Attempt to delete the bundle. If something goes wrong, Lasso isn't precious about it. + // We will simply try to delete the next directory and move on. foreach ($deletable as $bundle_key => $bundle) { try { @@ -110,7 +119,7 @@ private static function deleteBundles(array $deletable): array if ($success) { $deleted[$bundle_key] = $bundle; } - } catch (\Exception $ex) { + } catch (Exception) { continue; } } @@ -121,35 +130,36 @@ private static function deleteBundles(array $deletable): array /** * @throws \Sammyjo20\Lasso\Exceptions\BaseException */ - private static function updateHistory(array $history): bool + private static function updateHistory(array $history): void { - $disk = self::getDisk(); - $path = self::getFileDirectory('history.json'); - try { - $encoded = json_encode($history); - - return Storage::disk($disk)->put($path, $encoded); - } catch (\Exception $ex) { + Storage::disk(self::getDisk())->put(self::getFileDirectory(), json_encode($history, JSON_THROW_ON_ERROR)); + } catch (Exception) { throw VersioningFailed::because( 'Lasso could not update the history.json on the Filesystem.' ); } } - - private static function getFileDirectory(string $path): string + /** + * @return string + */ + private static function getFileDirectory(): string { - return (new Cloud())->getUploadPath($path); + return (new Cloud)->getUploadPath('history.json'); } - + /** + * @return string + */ private static function getDisk(): string { return config('lasso.storage.disk'); } - + /** + * @return int + */ private static function getMaxBundlesAllowed(): int { return config('lasso.storage.max_bundles'); diff --git a/src/Tasks/BaseJob.php b/src/Tasks/BaseJob.php index bed952e..ee78194 100644 --- a/src/Tasks/BaseJob.php +++ b/src/Tasks/BaseJob.php @@ -7,66 +7,31 @@ use Sammyjo20\Lasso\Helpers\Cloud; use Sammyjo20\Lasso\Container\Artisan; use Sammyjo20\Lasso\Helpers\Filesystem; -use Sammyjo20\Lasso\Interfaces\JobInterface; -abstract class BaseJob implements JobInterface +abstract class BaseJob { /** - * @var Filesystem + * Lasso Filesystem */ - protected $filesystem; + protected Filesystem $filesystem; /** - * @var Cloud + * Lasso Cloud */ - protected $cloud; + protected Cloud $cloud; /** - * @var Artisan + * Lasso Artisan */ - protected $artisan; + protected Artisan $artisan; /** - * BaseJob constructor. + * Constructor */ public function __construct() - { - $this->setArtisan() - ->setFilesystem(); - - // The Cloud class should be defined after the Filesystem as - // it depends on the Filesystem. - - $this->setCloud(); - } - - /** - * @return $this - */ - private function setArtisan(): self { $this->artisan = resolve(Artisan::class); - - return $this; - } - - /** - * @return $this - */ - private function setFilesystem(): self - { $this->filesystem = resolve(Filesystem::class); - - return $this; - } - - /** - * @return $this - */ - private function setCloud(): self - { $this->cloud = new Cloud; - - return $this; } } diff --git a/src/Tasks/Publish/BundleJob.php b/src/Tasks/Publish/BundleJob.php index 25c8926..31e5cc4 100644 --- a/src/Tasks/Publish/BundleJob.php +++ b/src/Tasks/Publish/BundleJob.php @@ -13,12 +13,12 @@ final class BundleJob extends BaseJob /** * @var string */ - protected $bundleId; + protected string $bundleId; /** * @var array */ - protected $forbiddenFiles = [ + protected array $forbiddenFiles = [ '.htaccess', 'web.config', 'index.php', @@ -29,7 +29,7 @@ final class BundleJob extends BaseJob /** * @var array */ - protected $forbiddenDirectories = [ + protected array $forbiddenDirectories = [ 'storage', 'hot', ]; @@ -44,15 +44,15 @@ public function __construct() $userForbiddenFiles = config('lasso.compiler.excluded_files', []); $userForbiddenDirs = config('lasso.compiler.excluded_directories', []); - $this->setForbiddenFiles( - array_merge($this->forbiddenFiles, $userForbiddenFiles) - ); - - $this->setForbiddenDirectories( - array_merge($this->forbiddenDirectories, $userForbiddenDirs) - ); + $this->forbiddenFiles = array_merge($this->forbiddenFiles, $userForbiddenFiles); + $this->forbiddenDirectories = array_merge($this->forbiddenDirectories, $userForbiddenDirs); } + /** + * Run the bundle job + * + * @return void + */ public function run(): void { $filesystem = $this->filesystem; @@ -107,7 +107,7 @@ public function run(): void } /** - * @return $this + * Set the Bundle ID */ public function setBundleId(string $bundleId): self { @@ -115,24 +115,4 @@ public function setBundleId(string $bundleId): self return $this; } - - /** - * @return $this - */ - private function setForbiddenFiles(array $files): self - { - $this->forbiddenFiles = $files; - - return $this; - } - - /** - * @return $this - */ - private function setForbiddenDirectories(array $directories): self - { - $this->forbiddenDirectories = $directories; - - return $this; - } } diff --git a/src/Tasks/Publish/PublishJob.php b/src/Tasks/Publish/PublishJob.php index 676812e..a1fd988 100644 --- a/src/Tasks/Publish/PublishJob.php +++ b/src/Tasks/Publish/PublishJob.php @@ -10,30 +10,30 @@ use Sammyjo20\Lasso\Helpers\Bundle; use Sammyjo20\Lasso\Helpers\Compiler; use Sammyjo20\Lasso\Helpers\Git; +use Sammyjo20\Lasso\Helpers\Webhook; use Sammyjo20\Lasso\Tasks\BaseJob; -use Sammyjo20\Lasso\Tasks\Webhook; final class PublishJob extends BaseJob { /** * @var string */ - protected $bundleId; + protected string $bundleId; /** * @var bool */ - protected $usesGit = true; + protected bool $usesGit = true; /** * @var bool */ - protected $useCommit = false; + protected bool $useCommit = false; /** * @var string? */ - protected $commit = null; + protected ?string $commit = null; /** * PublishJob constructor. @@ -46,52 +46,56 @@ public function __construct() } /** - * @throws Exception + * Run the "publish" job */ public function run(): void { + $artisan = $this->artisan; + $filesystem = $this->filesystem; + $cloud = $this->cloud; + try { $this->generateBundleId(); - $this->artisan->note('⏳ Compiling assets...'); + $artisan->note('⏳ Compiling assets...'); // Start with the compiler. This will run the "script" which // has been defined in the config file (e.g. npm run production). - $compiler = (new Compiler()) + $compiler = (new Compiler) ->setCommand(config('lasso.compiler.script')) ->setTimeout(config('lasso.compiler.timeout', 600)) ->execute(); - $this->artisan->note(sprintf( + $artisan->note(sprintf( '✅ Compiled assets in %s seconds.', $compiler->getCompilationTime() )); - $this->artisan->note('⏳ Copying and zipping compiled assets...'); + $artisan->note('⏳ Copying and zipping compiled assets...'); // Once we have compiled all of our assets, we need to "bundle" - // them up. Todo: Remove this step in the future. + // them up. - (new BundleJob()) + (new BundleJob) ->setBundleId($this->bundleId) ->run(); $zipBundlePath = base_path('.lasso/dist/' . $this->bundleId . '.zip'); - $this->artisan->note('✅ Successfully copied and zipped assets.'); + $artisan->note('✅ Successfully copied and zipped assets.'); // Now we want to create the data which will go inside the // "lasso-bundle.json" file. After that, we will create a Zip file // with all the assets inside. - $bundle = (new Bundle()) + $bundle = (new Bundle) ->setBundleId($this->bundleId) ->setZipPath($zipBundlePath) ->create(); - $this->artisan->note( - sprintf('⏳ Uploading asset bundle to "%s" filesystem...', $this->filesystem->getCloudDisk()) + $artisan->note( + sprintf('⏳ Uploading asset bundle to "%s" filesystem...', $filesystem->getCloudDisk()) ); $bundlePath = base_path('.lasso/dist/lasso-bundle.json'); @@ -100,20 +104,20 @@ public function run(): void // this uses stream to ensure we don't run out of memory // while uploading the file. - $this->cloud->uploadFile($zipBundlePath, $bundle['file']); + $cloud->uploadFile($zipBundlePath, $bundle['file']); // Has the user requested to use/not use Git? Here // we will create the lasso-bundle.json in the right // place. if ($this->usesGit) { - $this->filesystem->createFreshLocalBundle($bundle); + $filesystem->createFreshLocalBundle($bundle); } else { - $this->filesystem->deleteLocalBundle(); + $filesystem->deleteLocalBundle(); - $this->filesystem->put($bundlePath, json_encode($bundle)); + $filesystem->put($bundlePath, json_encode($bundle)); - $this->cloud->uploadFile($bundlePath, config('lasso.storage.prefix') . 'lasso-bundle.json'); + $cloud->uploadFile($bundlePath, config('lasso.storage.prefix') . 'lasso-bundle.json'); } // Done! Let's run some cleanup, and dispatch all the @@ -127,7 +131,9 @@ public function run(): void } } - + /** + * @return void + */ public function cleanUp(): void { $this->deleteLassoDirectory(); @@ -144,6 +150,10 @@ private function rollBack(Exception $exception) } + /** + * @param array $webhooks + * @return void + */ public function dispatchWebhooks(array $webhooks = []): void { if (! count($webhooks)) { @@ -210,6 +220,10 @@ public function useCommit(): self return $this; } + /** + * @param string $commitHash + * @return $this + */ public function withCommit(string $commitHash): self { $this->commit = $commitHash; diff --git a/src/Tasks/Pull/PullJob.php b/src/Tasks/Pull/PullJob.php index 3544d36..723f935 100644 --- a/src/Tasks/Pull/PullJob.php +++ b/src/Tasks/Pull/PullJob.php @@ -4,32 +4,33 @@ namespace Sammyjo20\Lasso\Tasks\Pull; -use Sammyjo20\Lasso\Helpers\Git; -use Sammyjo20\Lasso\Tasks\BaseJob; -use Sammyjo20\Lasso\Tasks\Webhook; +use Exception; +use Sammyjo20\Lasso\Exceptions\PullJobFailed; +use Sammyjo20\Lasso\Helpers\BundleIntegrityHelper; use Sammyjo20\Lasso\Helpers\FileLister; -use Sammyjo20\Lasso\Services\BackupService; +use Sammyjo20\Lasso\Helpers\Git; +use Sammyjo20\Lasso\Helpers\Webhook; use Sammyjo20\Lasso\Services\ArchiveService; -use Sammyjo20\Lasso\Exceptions\PullJobFailed; +use Sammyjo20\Lasso\Services\BackupService; use Sammyjo20\Lasso\Services\VersioningService; -use Sammyjo20\Lasso\Helpers\BundleIntegrityHelper; +use Sammyjo20\Lasso\Tasks\BaseJob; final class PullJob extends BaseJob { /** - * @var BackupService + * Backup Service */ - protected $backup; + protected BackupService $backup; /** - * @var bool + * Should Lasso use the latest commit from Git? */ - protected $useCommit = false; + protected bool $useCommit = false; /** - * @var string? + * Specify commit hash to use */ - protected $commitHash = null; + protected ?string $commitHash = null; /** * PullJob constructor. @@ -38,13 +39,16 @@ public function __construct() { parent::__construct(); - $this->setBackup(); + $this->backup = new BackupService($this->filesystem); } /** + * Run the "pull" job + * * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + * @throws \Psr\Container\ContainerExceptionInterface + * @throws \Psr\Container\NotFoundExceptionInterface * @throws \Sammyjo20\Lasso\Exceptions\BaseException - * @throws PullJobFailed */ public function run(): void { @@ -86,7 +90,7 @@ public function run(): void $this->filesystem ->copy($source, $destination); } - } catch (\Exception $ex) { + } catch (Exception $ex) { // If anything goes wrong inside this try block, // we will "roll back" which means we will restore // our backup. @@ -101,16 +105,21 @@ public function run(): void $this->cleanUp(); $webhooks = config('lasso.webhooks.pull', []); + $this->dispatchWebhooks($webhooks); } - + /** + * Cleanup the command + */ public function cleanUp(): void { $this->filesystem->deleteBaseLassoDirectory(); } - + /** + * Dispatch the webhooks + */ public function dispatchWebhooks(array $webhooks = []): void { if (! count($webhooks)) { @@ -127,15 +136,18 @@ public function dispatchWebhooks(array $webhooks = []): void } /** + * Get the latest bundle info + * * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + * @throws \Psr\Container\ContainerExceptionInterface + * @throws \Psr\Container\NotFoundExceptionInterface */ private function getLatestBundleInfo(): array { $localPath = base_path('lasso-bundle.json'); $cloudPath = $this->cloud->getUploadPath(config('lasso.storage.prefix') . 'lasso-bundle.json'); - // Firstly, let's check if the local filesystem has a "lasso-bundle.json" - // file in it's root directory. + // Firstly, let's check if the local filesystem has a "lasso-bundle.json" file in its root directory. if ($this->filesystem->exists($localPath)) { $file = $this->filesystem->get($localPath); @@ -146,10 +158,9 @@ private function getLatestBundleInfo(): array return $bundle; } - // If there isn't a "lasso-bundle.json" file in the root directory, - // that's okay - this means that the commit is in "non-git" mode. So - // let's just grab that file. If we don't have a file on the server - // however; we need to throw an exception. + // If there isn't a "lasso-bundle.json" file in the root directory, that's okay - this means + // that the commit is in "non-git" mode. So let's just grab that file. If we don't have a + // file on the server however; we need to throw an exception. if (! $this->cloud->exists($cloudPath)) { $this->rollBack( @@ -166,11 +177,11 @@ private function getLatestBundleInfo(): array } /** - * @throws \Exception + * Validate the bundle checksum */ private function validateBundle(array $bundle): bool { - if (! isset($bundle['file']) || ! isset($bundle['checksum'])) { + if (! isset($bundle['file'], $bundle['checksum'])) { $this->rollBack( PullJobFailed::because('The bundle info was missing the required data.') ); @@ -180,7 +191,7 @@ private function validateBundle(array $bundle): bool } /** - * @throws PullJobFailed|\Exception + * Download Zip bundle file */ private function downloadBundleZip(string $file, string $checksum): string { @@ -203,7 +214,7 @@ private function downloadBundleZip(string $file, string $checksum): string if (is_resource($bundleZip)) { fclose($bundleZip); } - } catch (\Exception $ex) { + } catch (Exception $ex) { $this->rollBack($ex); } @@ -227,17 +238,17 @@ private function downloadBundleZip(string $file, string $checksum): string } /** + * Roll back and throw an exception + * * @throws \Exception */ - private function rollBack(\Exception $exception) + private function rollBack(Exception $exception): Exception { - if ($this->backup->hasBackup()) { - $this->artisan->note('⏳ Restoring backup...'); + $this->artisan->note('⏳ Restoring backup...'); - $this->backup->restoreBackup($this->filesystem->getPublicPath()); + $this->backup->restoreBackup($this->filesystem->getPublicPath()); - $this->artisan->note('✅ Successfully restored backup.'); - } + $this->artisan->note('✅ Successfully restored backup.'); $this->filesystem->deleteBaseLassoDirectory(); @@ -245,29 +256,16 @@ private function rollBack(\Exception $exception) } /** - * @return $this + * Backup the source data */ - private function runBackup(): self + private function runBackup(): void { - $this->backup->createBackup( - $this->filesystem->getPublicPath(), - base_path('.lasso/backup') - ); - - return $this; + $this->backup->createBackup($this->filesystem->getPublicPath(), base_path('.lasso/backup')); } /** - * @return $this + * Cleanup the Lasso directory */ - private function setBackup(): self - { - $this->backup = new BackupService($this->filesystem); - - return $this; - } - - private function cleanLassoDirectory(): void { $this->filesystem->deleteBaseLassoDirectory(); @@ -275,7 +273,9 @@ private function cleanLassoDirectory(): void $this->filesystem->ensureDirectoryExists(base_path('.lasso')); } - + /** + * Get the Lasso bundle path + */ private function getBundlePath(string $file): string { if ($this->commitHash) { @@ -290,7 +290,7 @@ private function getBundlePath(string $file): string } /** - * @return $this + * Should Lasso use the latest commit from Git? */ public function useCommit(): self { @@ -300,7 +300,7 @@ public function useCommit(): self } /** - * @return $this + * Specify a custom commit hash */ public function withCommit(string $commitHash): self { diff --git a/src/Tasks/Webhook.php b/src/Tasks/Webhook.php deleted file mode 100644 index 97a3ca3..0000000 --- a/src/Tasks/Webhook.php +++ /dev/null @@ -1,22 +0,0 @@ - $event], $data)); - }, false, false); - - return true; - } -} diff --git a/tests/Feature/LassoPublishTest.php b/tests/Feature/LassoPublishTest.php index 5c725b5..6755464 100644 --- a/tests/Feature/LassoPublishTest.php +++ b/tests/Feature/LassoPublishTest.php @@ -1,5 +1,9 @@ artisan('lasso:publish'); }); diff --git a/tests/Pest.php b/tests/Pest.php index 037b536..9e4770a 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -26,3 +26,120 @@ function destinationDirectory(): string { return __DIR__ . '/Unit/Support/Zip/Destination'; } + +function defaultConfig(): array +{ + return [ + 'compiler' => [ + + /* + * Configure which command Lasso should run in its deployment + * phase. This will most likely be "npm run production" but + * you may choose what you would like to execute. + */ + 'script' => 'npm run production', + + /* + * Configure the amount of time (in seconds) the compiler + * should run before it times out. By default, this is set + * to 600 seconds (10 minutes). + */ + 'timeout' => 600, + + /* + * Lasso will attempt to display the compilation progress + * from webpack. If your progress bar isn't incrementing, it's + * likely you have the `--no-progress` flag on your script + * (e.g npm run production). Change it to `--progress`. + * + * Available progress options are: + * + * - 'all': Display everything from the compiler. + * - 'progress': Display compilation progress. + * - 'disable': Disable the progress. + */ + 'output' => 'progress', + + /* + * If there are any directories/files you would like to Lasso to + * exclude when uploading to the Filesystem, specify them below. + */ + 'excluded_files' => [], + + 'excluded_directories' => [], + + ], + + 'storage' => [ + + /* + * Specify the filesystem Lasso should use to store + * and retrieve its files. + */ + 'disk' => 'assets', + + /* + * Specify the directory Lasso should store all of its + * files within. + * + * WARNING: If you have multiple projects all using Lasso, + * make sure this is unique for each project. + */ + 'upload_to' => 'lasso', + + /* + * Lasso can also create a separate directory containing + * the environment the files will be stored in. Specify this + * here. + */ + 'environment' => null, + + /* + * Lasso can add a prefix to the bundle file, in order to store + * multiple bundle files in the same filesystem for different + * environments + */ + 'prefix' => '', + + /* + * Lasso will automatically version the assets. This is useful if you + * suddenly need to roll back a deployment and use an older version + * of built files. You can set the maximum amount of files stored here. + */ + 'max_bundles' => 5, + + ], + + /* + * Lasso can also trigger Webhooks after its commands have been + * successfully executed. You may specify URLs that Lasso will POST + * to, for each of the commands. + */ + 'webhooks' => [ + + /* + * Specify which webhooks should be triggered after a successful + * "php artisan lasso:publish" command execution. + */ + 'publish' => [ + // + ], + + /* + * Specify which webhooks should be triggered after a successful + * "php artisan lasso:pull" command execution. + */ + 'pull' => [ + // + ], + + ], + + /* + * Where are your assets stored? Most of the time, they will + * be stored within the /public directory in Laravel - but if + * you have changed this - please specify it below. + */ + 'public_path' => public_path(), + ]; +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 37f3c9a..a54591c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -10,7 +10,7 @@ abstract class TestCase extends Orchestra { /** - * @param \Illuminate\Foundation\Application $app + * Get the package providers */ protected function getPackageProviders($app): array { @@ -18,4 +18,16 @@ protected function getPackageProviders($app): array LassoServiceProvider::class, ]; } + + /** + * Get environment setup + */ + protected function getEnvironmentSetUp($app) + { + $app['config']->set('filesystems.disks.assets', [ + 'driver' => 'local', + 'root' => __DIR__ . '/Fixtures/Cloud', + 'throw' => false, + ]); + } } From 90f02f1a9bacb5280185123d3d7ece6e5f62f75d Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:33:23 +0000 Subject: [PATCH 06/19] Tests for Lasso woo --- .gitattributes | 2 ++ .gitignore | 4 +++ mix-manifest.json | 4 +++ package.json | 11 +++++++ src/Container/Artisan.php | 5 +--- src/Helpers/Cloud.php | 6 ++-- src/Helpers/Compiler.php | 2 -- src/Helpers/Filesystem.php | 40 +++++++++----------------- src/Helpers/Unzipper.php | 18 +++--------- src/LassoServiceProvider.php | 8 ++---- src/Services/VersioningService.php | 22 ++++---------- src/Tasks/Publish/BundleJob.php | 14 ++------- src/Tasks/Publish/PublishJob.php | 30 ++++++------------- src/Tasks/Pull/PullJob.php | 10 +++---- tests/Feature/LassoPublishTest.php | 9 ------ tests/Feature/LassoPullTest.php | 5 ---- tests/Feature/LassoTest.php | 43 ++++++++++++++++++++++++++++ tests/Fixtures/Local/app.css | 3 ++ tests/Fixtures/Local/lasso-logo.png | Bin 0 -> 121746 bytes tests/Pest.php | 4 +-- tests/TestCase.php | 4 ++- webpack.mix.js | 3 ++ 22 files changed, 120 insertions(+), 127 deletions(-) create mode 100644 mix-manifest.json create mode 100644 package.json delete mode 100644 tests/Feature/LassoPublishTest.php delete mode 100644 tests/Feature/LassoPullTest.php create mode 100644 tests/Feature/LassoTest.php create mode 100644 tests/Fixtures/Local/app.css create mode 100644 tests/Fixtures/Local/lasso-logo.png create mode 100644 webpack.mix.js diff --git a/.gitattributes b/.gitattributes index 0240a87..2b5ac9b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,3 +6,5 @@ phpunit.xml export-ignore LICENSE export-ignore .php-cs-fixer.dist.php export-ignore +package.json export-ignore +webpack.mix.js export-ignore diff --git a/.gitignore b/.gitignore index 8ec15d1..3d4bc8b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ composer.lock node_modules vendor .php-cs-fixer.cache +package-lock.json +tests/Fixtures/Cloud +tests/Fixtures/Public +lasso-bundle.json diff --git a/mix-manifest.json b/mix-manifest.json new file mode 100644 index 0000000..c3ee70e --- /dev/null +++ b/mix-manifest.json @@ -0,0 +1,4 @@ +{ + "/tests/Fixtures/Public/app.css": "/tests/Fixtures/Public/app.css", + "/tests/Fixtures/Public/lasso-logo.png": "/tests/Fixtures/Public/lasso-logo.png" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ce915d9 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "lasso", + "version": "1.0.0", + "description": "Yeehaw", + "scripts": { + "production": "echo \"Error: no test specified\" && exit 1" + }, + "devDependencies": { + "laravel-mix": "^6.0.49" + } +} diff --git a/src/Container/Artisan.php b/src/Container/Artisan.php index 582b981..a595e71 100644 --- a/src/Container/Artisan.php +++ b/src/Container/Artisan.php @@ -81,6 +81,7 @@ public function showCompilerOutput(string $line): void if ($mode === 'all') { $this->note($line); + return; } @@ -95,8 +96,6 @@ public function showCompilerOutput(string $line): void /** * Mark compiler as complete - * - * @return void */ public function compilerComplete(): void { @@ -110,8 +109,6 @@ public function compilerComplete(): void /** * Get the progress bar - * - * @return \Symfony\Component\Console\Helper\ProgressBar */ private function getProgressBar(): ProgressBar { diff --git a/src/Helpers/Cloud.php b/src/Helpers/Cloud.php index 40c9c99..6934e53 100644 --- a/src/Helpers/Cloud.php +++ b/src/Helpers/Cloud.php @@ -4,11 +4,11 @@ namespace Sammyjo20\Lasso\Helpers; +use LogicException; use Illuminate\Support\Facades\Storage; use League\Flysystem\UnableToWriteFile; -use LogicException; use Sammyjo20\Lasso\Exceptions\ConsoleMethodException; -use Illuminate\Contracts\Filesystem\Filesystem; +use Illuminate\Contracts\Filesystem\Filesystem as BaseFilesystem; /** * @internal @@ -18,7 +18,7 @@ class Cloud /** * Lasso Filesystem */ - protected Filesystem $cloudFilesystem; + protected BaseFilesystem $cloudFilesystem; /** * Local Filesystem diff --git a/src/Helpers/Compiler.php b/src/Helpers/Compiler.php index 35a63c7..59e5850 100644 --- a/src/Helpers/Compiler.php +++ b/src/Helpers/Compiler.php @@ -24,8 +24,6 @@ class Compiler /** * The time it has taken for the compiler to Lasso up the assets. - * - * @var float */ protected float $compilationTime = 0; diff --git a/src/Helpers/Filesystem.php b/src/Helpers/Filesystem.php index 3a99fa1..9509301 100644 --- a/src/Helpers/Filesystem.php +++ b/src/Helpers/Filesystem.php @@ -11,19 +11,13 @@ */ class Filesystem extends BaseFilesystem { - /** - * @var string - */ + protected string $lassoEnvironment; - /** - * @var string - */ + protected string $cloudDisk; - /** - * @var string - */ + protected string $publicPath; /** @@ -36,11 +30,7 @@ public function __construct() $this->publicPath = config('lasso.public_path', public_path()); } - /** - * @param $resource - * @param string $destination - * @return bool - */ + public function putStream($resource, string $destination): bool { $stream = fopen($destination, 'wb+'); @@ -52,10 +42,7 @@ public function putStream($resource, string $destination): bool return true; } - /** - * @param array $bundle - * @return void - */ + public function createFreshLocalBundle(array $bundle): void { $this->deleteLocalBundle(); @@ -63,9 +50,7 @@ public function createFreshLocalBundle(array $bundle): void $this->put(base_path('lasso-bundle.json'), json_encode($bundle)); } - /** - * @return bool - */ + public function deleteLocalBundle(): bool { return $this->delete(base_path('lasso-bundle.json')); @@ -89,17 +74,18 @@ public function setLassoEnvironment(string $environment): self return $this; } - /** - * @return string - */ + public function getLassoEnvironment(): string + { + return $this->lassoEnvironment; + } + + public function getPublicPath(): string { return $this->publicPath; } - /** - * @return string - */ + public function getCloudDisk(): string { return $this->cloudDisk; diff --git a/src/Helpers/Unzipper.php b/src/Helpers/Unzipper.php index c5ce9b9..c670303 100644 --- a/src/Helpers/Unzipper.php +++ b/src/Helpers/Unzipper.php @@ -11,24 +11,16 @@ */ class Unzipper { - /** - * @var ZipArchive - */ + protected ZipArchive $zip; - /** - * @var string - */ + protected string $source; - /** - * @var string - */ + protected string $destination; - /** - * @var Filesystem - */ + protected Filesystem $filesystem; /** @@ -44,8 +36,6 @@ public function __construct(string $source, string $destination) /** * Unzip the source into the destination - * - * @return void */ public function run(): void { diff --git a/src/LassoServiceProvider.php b/src/LassoServiceProvider.php index b0d794c..c3f630d 100644 --- a/src/LassoServiceProvider.php +++ b/src/LassoServiceProvider.php @@ -14,8 +14,6 @@ class LassoServiceProvider extends BaseServiceProvider { /** * Register the Lasso service provider - * - * @return void */ public function register(): void { @@ -24,8 +22,6 @@ public function register(): void /** * Boot the Lasso service provider - * - * @return void */ public function boot(): void { @@ -48,7 +44,7 @@ public function boot(): void // Bind Artisan and Filesystem to the container - $this->app->singleton(Artisan::class, static fn() => new Artisan); - $this->app->singleton(Filesystem::class, static fn() => new Filesystem); + $this->app->singleton(Artisan::class, static fn () => new Artisan); + $this->app->singleton(Filesystem::class, static fn () => new Filesystem); } } diff --git a/src/Services/VersioningService.php b/src/Services/VersioningService.php index da3d5a4..08fdad2 100644 --- a/src/Services/VersioningService.php +++ b/src/Services/VersioningService.php @@ -71,10 +71,7 @@ private static function getHistoryFromDisk(): array } } - /** - * @param array $bundles - * @return array - */ + private static function deleteExpiredBundles(array $bundles): array { $bundle_limit = self::getMaxBundlesAllowed(); @@ -100,10 +97,7 @@ private static function deleteExpiredBundles(array $bundles): array return array_diff($bundles, $deleted); } - /** - * @param array $deletable - * @return array - */ + private static function deleteBundles(array $deletable): array { $disk = self::getDisk(); @@ -141,25 +135,19 @@ private static function updateHistory(array $history): void } } - /** - * @return string - */ + private static function getFileDirectory(): string { return (new Cloud)->getUploadPath('history.json'); } - /** - * @return string - */ + private static function getDisk(): string { return config('lasso.storage.disk'); } - /** - * @return int - */ + private static function getMaxBundlesAllowed(): int { return config('lasso.storage.max_bundles'); diff --git a/src/Tasks/Publish/BundleJob.php b/src/Tasks/Publish/BundleJob.php index 31e5cc4..ad9d0c1 100644 --- a/src/Tasks/Publish/BundleJob.php +++ b/src/Tasks/Publish/BundleJob.php @@ -10,14 +10,10 @@ final class BundleJob extends BaseJob { - /** - * @var string - */ + protected string $bundleId; - /** - * @var array - */ + protected array $forbiddenFiles = [ '.htaccess', 'web.config', @@ -26,9 +22,7 @@ final class BundleJob extends BaseJob 'storage', ]; - /** - * @var array - */ + protected array $forbiddenDirectories = [ 'storage', 'hot', @@ -50,8 +44,6 @@ public function __construct() /** * Run the bundle job - * - * @return void */ public function run(): void { diff --git a/src/Tasks/Publish/PublishJob.php b/src/Tasks/Publish/PublishJob.php index a1fd988..b03b54f 100644 --- a/src/Tasks/Publish/PublishJob.php +++ b/src/Tasks/Publish/PublishJob.php @@ -6,28 +6,22 @@ use Exception; use Illuminate\Support\Str; -use Sammyjo20\Lasso\Exceptions\GitHashException; -use Sammyjo20\Lasso\Helpers\Bundle; -use Sammyjo20\Lasso\Helpers\Compiler; use Sammyjo20\Lasso\Helpers\Git; -use Sammyjo20\Lasso\Helpers\Webhook; use Sammyjo20\Lasso\Tasks\BaseJob; +use Sammyjo20\Lasso\Helpers\Bundle; +use Sammyjo20\Lasso\Helpers\Webhook; +use Sammyjo20\Lasso\Helpers\Compiler; +use Sammyjo20\Lasso\Exceptions\GitHashException; final class PublishJob extends BaseJob { - /** - * @var string - */ + protected string $bundleId; - /** - * @var bool - */ + protected bool $usesGit = true; - /** - * @var bool - */ + protected bool $useCommit = false; /** @@ -131,9 +125,7 @@ public function run(): void } } - /** - * @return void - */ + public function cleanUp(): void { $this->deleteLassoDirectory(); @@ -150,10 +142,7 @@ private function rollBack(Exception $exception) } - /** - * @param array $webhooks - * @return void - */ + public function dispatchWebhooks(array $webhooks = []): void { if (! count($webhooks)) { @@ -221,7 +210,6 @@ public function useCommit(): self } /** - * @param string $commitHash * @return $this */ public function withCommit(string $commitHash): self diff --git a/src/Tasks/Pull/PullJob.php b/src/Tasks/Pull/PullJob.php index 723f935..91556cc 100644 --- a/src/Tasks/Pull/PullJob.php +++ b/src/Tasks/Pull/PullJob.php @@ -5,15 +5,15 @@ namespace Sammyjo20\Lasso\Tasks\Pull; use Exception; -use Sammyjo20\Lasso\Exceptions\PullJobFailed; -use Sammyjo20\Lasso\Helpers\BundleIntegrityHelper; -use Sammyjo20\Lasso\Helpers\FileLister; use Sammyjo20\Lasso\Helpers\Git; +use Sammyjo20\Lasso\Tasks\BaseJob; use Sammyjo20\Lasso\Helpers\Webhook; -use Sammyjo20\Lasso\Services\ArchiveService; +use Sammyjo20\Lasso\Helpers\FileLister; use Sammyjo20\Lasso\Services\BackupService; +use Sammyjo20\Lasso\Services\ArchiveService; +use Sammyjo20\Lasso\Exceptions\PullJobFailed; use Sammyjo20\Lasso\Services\VersioningService; -use Sammyjo20\Lasso\Tasks\BaseJob; +use Sammyjo20\Lasso\Helpers\BundleIntegrityHelper; final class PullJob extends BaseJob { diff --git a/tests/Feature/LassoPublishTest.php b/tests/Feature/LassoPublishTest.php deleted file mode 100644 index 6755464..0000000 --- a/tests/Feature/LassoPublishTest.php +++ /dev/null @@ -1,9 +0,0 @@ -artisan('lasso:publish'); -}); diff --git a/tests/Feature/LassoPullTest.php b/tests/Feature/LassoPullTest.php deleted file mode 100644 index e272647..0000000 --- a/tests/Feature/LassoPullTest.php +++ /dev/null @@ -1,5 +0,0 @@ -toBeFalse(); + expect(File::isEmptyDirectory('./tests/Fixtures/Cloud'))->toBeTrue(); + + Config::set('lasso', defaultConfig()); + + // Kick off the publish which will compile the asset + + $this->artisan('lasso:publish'); + + expect(File::exists('./lasso-bundle.json'))->toBeTrue(); + + $bundleName = json_decode(File::get('./lasso-bundle.json'), true, 512, JSON_THROW_ON_ERROR)['file']; + + // Check the bundle exists + + expect(File::exists('./tests/Fixtures/Cloud/lasso/global/' . $bundleName))->toBeTrue(); + + File::cleanDirectory('./tests/Fixtures/Public'); + + expect(File::isEmptyDirectory('./tests/Fixtures/Public'))->toBeTrue(); + + $this->artisan('lasso:pull'); + + // Now we'll ensure that the public directory has contents! + + expect(File::isEmptyDirectory('./tests/Fixtures/Public'))->toBeFalse(); + expect(File::exists('./tests/Fixtures/Public/app.css'))->toBeTrue(); + expect(File::exists('./tests/Fixtures/Public/lasso-logo.png'))->toBeTrue(); +}); diff --git a/tests/Fixtures/Local/app.css b/tests/Fixtures/Local/app.css new file mode 100644 index 0000000..7bb2d46 --- /dev/null +++ b/tests/Fixtures/Local/app.css @@ -0,0 +1,3 @@ +/*! tailwindcss v2.2.4 | MIT License | https://tailwindcss.com*/ + +/*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */html{-webkit-text-size-adjust:100%;line-height:1.15;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;margin:0}hr{color:inherit;height:0}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}button{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{font-family:inherit;line-height:inherit}*,:after,:before{border:0 solid;box-sizing:border-box}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{color:inherit;line-height:inherit;padding:0}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}*,:after,:before{--tw-border-opacity:1;border-color:rgba(229,231,235,var(--tw-border-opacity))}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.absolute{position:absolute}.relative{position:relative}.top-0{top:0}.left-0{left:0}.z-0{z-index:0}.z-10{z-index:10}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mt-2{margin-top:.5rem}.mt-6{margin-top:1.5rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.mb-12{margin-bottom:3rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.hidden{display:none}.h-full{height:100%}.w-8{width:2rem}.w-12{width:3rem}.w-full{width:100%}.transform{--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;transform:translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100,.transform{--tw-scale-x:1;--tw-scale-y:1}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05}.active\:scale-95:active{--tw-scale-x:.95;--tw-scale-y:.95}@-webkit-keyframes spin{to{transform:rotate(1turn)}}@keyframes spin{to{transform:rotate(1turn)}}@-webkit-keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}@-webkit-keyframes bounce{0%,to{-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}@keyframes bounce{0%,to{-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}.cursor-pointer{cursor:pointer}.flex-row{flex-direction:row}.items-center{align-items:center}.overflow-y-hidden{overflow-y:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded-lg{border-radius:.5rem}.bg-primary{--tw-bg-opacity:1;background-color:rgba(249,199,46,var(--tw-bg-opacity))}.bg-faded-leather{--tw-bg-opacity:1;background-color:rgba(130,95,77,var(--tw-bg-opacity))}.bg-burned-leather{--tw-bg-opacity:1;background-color:rgba(74,53,43,var(--tw-bg-opacity))}.p-4{padding:1rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-16{padding-left:4rem;padding-right:4rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-8{padding-bottom:2rem;padding-top:2rem}.py-16{padding-bottom:4rem;padding-top:4rem}.pt-24{padding-top:6rem}.pb-16{padding-bottom:4rem}.text-center{text-align:center}.font-roboto{font-family:Roboto,sans-serif}.font-jetbrains-mono{font-family:JetBrains Mono,sans-serif}.text-sm{font-size:.875rem;line-height:1.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem}.text-lg,.text-xl{line-height:1.75rem}.text-xl{font-size:1.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.leading-tight{line-height:1.25}.tracking-wide{letter-spacing:.025em}.text-white{--tw-text-opacity:1;color:rgba(255,255,255,var(--tw-text-opacity))}.text-primary{--tw-text-opacity:1;color:rgba(249,199,46,var(--tw-text-opacity))}.text-leather{--tw-text-opacity:1;color:rgba(99,71,57,var(--tw-text-opacity))}.opacity-25{opacity:.25}.opacity-75{opacity:.75}*,:after,:before{--tw-shadow:0 0 #0000}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,0.1),0 2px 4px -1px rgba(0,0,0,0.06);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus,.outline-none{outline:2px solid transparent;outline-offset:2px}*,:after,:before{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000}.duration-200{transition-duration:.2s}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400;src:url(../fonts/JetBrainsMono-Regular.eot) format("embedded-opentype"),url(../fonts/JetBrainsMono-Regular.woff2) format("woff2"),url(../fonts/JetBrainsMono-Regular.woff) format("woff"),url(../fonts/JetBrainsMono-Regular.ttf) format("truetype")}*{font-family:Yeseva One,sans-serif}body{--tw-bg-opacity:1;background-color:rgba(99,71,57,var(--tw-bg-opacity))}.github-icon{box-sizing:border-box;display:block;height:4rem;padding:1rem;position:absolute;right:0;top:0;width:4rem}.github-icon.docs{position:relative}@media (min-width:768px){.github-icon{height:5rem;width:5rem}}.kendrick-lamar{margin-left:auto;margin-right:auto;max-width:700px;width:100%}.svg-bg{background-color:#634739;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='56' height='28'%3E%3Cpath fill='%23825f4d' fill-opacity='.4' d='M56 26v2h-7.75c2.3-1.27 4.94-2 7.75-2zm-26 2a2 2 0 1 0-4 0h-4.09A25.98 25.98 0 0 0 0 16v-2c.67 0 1.34.02 2 .07V14a2 2 0 0 0-2-2v-2a4 4 0 0 1 3.98 3.6 28.09 28.09 0 0 1 2.8-3.86A8 8 0 0 0 0 6V4a9.99 9.99 0 0 1 8.17 4.23c.94-.95 1.96-1.83 3.03-2.63A13.98 13.98 0 0 0 0 0h7.75c2 1.1 3.73 2.63 5.1 4.45 1.12-.72 2.3-1.37 3.53-1.93A20.1 20.1 0 0 0 14.28 0h2.7c.45.56.88 1.14 1.29 1.74 1.3-.48 2.63-.87 4-1.15-.11-.2-.23-.4-.36-.59H26v.07a28.4 28.4 0 0 1 4 0V0h4.09l-.37.59c1.38.28 2.72.67 4.01 1.15.4-.6.84-1.18 1.3-1.74h2.69a20.1 20.1 0 0 0-2.1 2.52c1.23.56 2.41 1.2 3.54 1.93A16.08 16.08 0 0 1 48.25 0H56c-4.58 0-8.65 2.2-11.2 5.6 1.07.8 2.09 1.68 3.03 2.63A9.99 9.99 0 0 1 56 4v2a8 8 0 0 0-6.77 3.74c1.03 1.2 1.97 2.5 2.79 3.86A4 4 0 0 1 56 10v2a2 2 0 0 0-2 2.07 28.4 28.4 0 0 1 2-.07v2c-9.2 0-17.3 4.78-21.91 12H30zM7.75 28H0v-2c2.81 0 5.46.73 7.75 2zM56 20v2c-5.6 0-10.65 2.3-14.28 6h-2.7c4.04-4.89 10.15-8 16.98-8zm-39.03 8h-2.69C10.65 24.3 5.6 22 0 22v-2c6.83 0 12.94 3.11 16.97 8zm15.01-.4a28.09 28.09 0 0 1 2.8-3.86 8 8 0 0 0-13.55 0c1.03 1.2 1.97 2.5 2.79 3.86a4 4 0 0 1 7.96 0zm14.29-11.86c1.3-.48 2.63-.87 4-1.15a25.99 25.99 0 0 0-44.55 0c1.38.28 2.72.67 4.01 1.15a21.98 21.98 0 0 1 36.54 0zm-5.43 2.71c1.13-.72 2.3-1.37 3.54-1.93a19.98 19.98 0 0 0-32.76 0c1.23.56 2.41 1.2 3.54 1.93a15.98 15.98 0 0 1 25.68 0zm-4.67 3.78c.94-.95 1.96-1.83 3.03-2.63a13.98 13.98 0 0 0-22.4 0c1.07.8 2.09 1.68 3.03 2.63a9.99 9.99 0 0 1 16.34 0z'/%3E%3C/svg%3E")}.header-bg .logo{display:block;margin-left:auto;margin-right:auto;pointer-events:none}.header-bg .logo,.header-bg .strapline{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:100%}.header-bg .strapline{--tw-text-opacity:1;color:rgba(255,255,255,var(--tw-text-opacity));font-size:1.25rem;line-height:1.75rem;margin-top:1.5rem;text-align:center}@media (min-width:640px){.header-bg .strapline{font-size:1.5rem;line-height:2rem}}@media (min-width:768px){.header-bg .strapline{bottom:16px;margin-top:0;position:absolute;right:-64px;width:auto}}.content{max-width:1000px}.flow{flex-direction:row;width:100%}.flow,.flow .step .circle{display:flex;justify-content:center}.flow .step .circle{--tw-bg-opacity:1;background-color:rgba(74,53,43,var(--tw-bg-opacity));border-radius:9999px;flex-direction:column;height:90px;text-align:center;width:90px}@media (min-width:768px){.flow .step .circle{height:140px;width:140px}}.flow .step .circle .icon{height:48px;margin-left:auto;margin-right:auto;width:48px}@media (min-width:768px){.flow .step .circle .icon{height:64px;width:64px}}.flow .breadcrumbs{align-items:center;display:flex;flex-direction:row;margin-left:.5rem;margin-right:.5rem}@media (min-width:768px){.flow .breadcrumbs{margin-left:1.5rem;margin-right:1.5rem}}.flow .breadcrumbs .arrow{--tw-text-opacity:1;color:rgba(255,255,255,var(--tw-text-opacity));font-size:1.875rem;line-height:2.25rem}@media (min-width:768px){.flow .breadcrumbs .arrow{display:none}}.flow .breadcrumbs .crumb{--tw-bg-opacity:1;background-color:rgba(255,255,255,var(--tw-bg-opacity));border-radius:9999px;display:none;height:16px;margin-left:.25rem;margin-right:.25rem;width:16px}@media (min-width:768px){.flow .breadcrumbs .crumb{display:block;margin-left:.75rem;margin-right:.75rem}}.flow .breadcrumbs .crumb:first-child{margin-left:0}.flow .breadcrumbs .crumb:last-child{margin-right:0}.cactus{bottom:0;display:none;opacity:.75;position:absolute;z-index:0}.cactus.cactus-one{left:0;margin-bottom:-5px;padding-left:8rem}.cactus.cactus-two{margin-bottom:-10px;padding-right:8rem;right:0}@media (min-width:1280px){.cactus{display:block}}.docs-font *{font-family:Lato,sans-serif}.check,.times{height:28px;width:28px}@media (min-width:768px){.md\:mx-12{margin-left:3rem;margin-right:3rem}.md\:mb-0{margin-bottom:0}.md\:mb-12{margin-bottom:3rem}.md\:mb-16{margin-bottom:4rem}.md\:ml-12{margin-left:3rem}.md\:flex{display:flex}.md\:grid{display:grid}.md\:hidden{display:none}.md\:w-1\/2{width:50%}.md\:grid-flow-col{grid-auto-flow:column}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:flex-col{flex-direction:column}.md\:justify-center{justify-content:center}.md\:gap-6{gap:1.5rem}.md\:bg-burned-leather{--tw-bg-opacity:1;background-color:rgba(74,53,43,var(--tw-bg-opacity))}.md\:px-6{padding-left:1.5rem;padding-right:1.5rem}.md\:px-8{padding-left:2rem;padding-right:2rem}.md\:px-12{padding-left:3rem;padding-right:3rem}.md\:py-2{padding-bottom:.5rem;padding-top:.5rem}.md\:py-3{padding-bottom:.75rem;padding-top:.75rem}.md\:py-4{padding-bottom:1rem;padding-top:1rem}.md\:py-24{padding-bottom:6rem;padding-top:6rem}.md\:py-32{padding-bottom:8rem;padding-top:8rem}.md\:text-center{text-align:center}.md\:text-lg{font-size:1.125rem;line-height:1.75rem}.md\:text-xl{font-size:1.25rem;line-height:1.75rem}.md\:text-2xl{font-size:1.5rem;line-height:2rem}.md\:text-5xl{font-size:3rem;line-height:1}} diff --git a/tests/Fixtures/Local/lasso-logo.png b/tests/Fixtures/Local/lasso-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..83bf8cae0f1f577ac41956dae67700bffad482b8 GIT binary patch literal 121746 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

Sw0#8tXFfr`n5ULBi;Ts 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 aBb`#h^47nyzSQySCpBd)rJ_4#Fa8hIj#%&j literal 0 HcmV?d00001 diff --git a/tests/Pest.php b/tests/Pest.php index 9e4770a..7c8afb0 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -37,7 +37,7 @@ function defaultConfig(): array * phase. This will most likely be "npm run production" but * you may choose what you would like to execute. */ - 'script' => 'npm run production', + 'script' => 'npx mix --production', /* * Configure the amount of time (in seconds) the compiler @@ -140,6 +140,6 @@ function defaultConfig(): array * be stored within the /public directory in Laravel - but if * you have changed this - please specify it below. */ - 'public_path' => public_path(), + 'public_path' => __DIR__ . '/Fixtures/Public', ]; } diff --git a/tests/TestCase.php b/tests/TestCase.php index a54591c..2288fe8 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -26,8 +26,10 @@ protected function getEnvironmentSetUp($app) { $app['config']->set('filesystems.disks.assets', [ 'driver' => 'local', - 'root' => __DIR__ . '/Fixtures/Cloud', + 'root' => realpath(__DIR__ . '/Fixtures/Cloud'), 'throw' => false, ]); + + $app->setBasePath(__DIR__ . '/../'); } } diff --git a/webpack.mix.js b/webpack.mix.js new file mode 100644 index 0000000..e94772e --- /dev/null +++ b/webpack.mix.js @@ -0,0 +1,3 @@ +let mix = require('laravel-mix'); + +mix.copyDirectory('tests/Fixtures/Local', './tests/Fixtures/Public'); From d1317447e824d52e6fac6c721cb58b5acfb7468f Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:36:10 +0000 Subject: [PATCH 07/19] Allow writing contents --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dcd9504..346ac44 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ on: - '*' permissions: - contents: read + contents: write jobs: tests: From cc72fa06914c63222ad43154c9ba8acd39e380b8 Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:41:37 +0000 Subject: [PATCH 08/19] Ignor lasso --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3d4bc8b..09e6016 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ package-lock.json tests/Fixtures/Cloud tests/Fixtures/Public lasso-bundle.json +.lasso From 8f365578453ab35f18d549c8e2ef85ee2f605ca3 Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:44:43 +0000 Subject: [PATCH 09/19] More ensure directory exists --- .github/workflows/tests.yml | 2 +- tests/Feature/LassoTest.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 346ac44..10c8e0a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,4 +43,4 @@ jobs: run: | composer update --${{ matrix.stability }} --prefer-dist --no-interaction - name: Execute tests - run: vendor/bin/pest -p + run: ./vendor/bin/pest diff --git a/tests/Feature/LassoTest.php b/tests/Feature/LassoTest.php index ce9cdee..fe17d8c 100644 --- a/tests/Feature/LassoTest.php +++ b/tests/Feature/LassoTest.php @@ -9,6 +9,8 @@ File::delete('./lasso-bundle.json'); File::ensureDirectoryExists('./tests/Fixtures/Public'); File::ensureDirectoryExists('./tests/Fixtures/Cloud'); + File::ensureDirectoryExists('./tests/Fixtures/Local'); + File::ensureDirectoryExists('.lasso'); File::cleanDirectory('./tests/Fixtures/Cloud'); From 4b6c16729c3233176ba09c0df5f9a270be8e234e Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:49:39 +0000 Subject: [PATCH 10/19] Permissionss --- .github/workflows/tests.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 10c8e0a..bacf388 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,5 +42,10 @@ jobs: - name: Install dependencies run: | composer update --${{ matrix.stability }} --prefer-dist --no-interaction + + - name: Configure Permissions + run: | + sudo chown -R $USER:$USER /home/github/actions-runner/_work/lasso + - name: Execute tests run: ./vendor/bin/pest From 46232edb1af4bbb055daf8c4a3070ee2ae1db88c Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:50:50 +0000 Subject: [PATCH 11/19] Try something else --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bacf388..8cb1406 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -45,7 +45,7 @@ jobs: - name: Configure Permissions run: | - sudo chown -R $USER:$USER /home/github/actions-runner/_work/lasso + sudo chown -R $USER:$USER $GITHUB_WORKSPACE - name: Execute tests run: ./vendor/bin/pest From 7990deeb7eb0ed6d0e3df72d200658b8a45e88c3 Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:51:44 +0000 Subject: [PATCH 12/19] Current directoory --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8cb1406..70862a3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -45,7 +45,7 @@ jobs: - name: Configure Permissions run: | - sudo chown -R $USER:$USER $GITHUB_WORKSPACE + sudo chown -R $USER:$USER ./ - name: Execute tests run: ./vendor/bin/pest From de95dcffce145c54e1e8efbd4431da0acbbdf81a Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:53:18 +0000 Subject: [PATCH 13/19] Remove realpath --- tests/TestCase.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index 2288fe8..298fd35 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -24,12 +24,12 @@ protected function getPackageProviders($app): array */ protected function getEnvironmentSetUp($app) { + $app->setBasePath(__DIR__ . '/../'); + $app['config']->set('filesystems.disks.assets', [ 'driver' => 'local', - 'root' => realpath(__DIR__ . '/Fixtures/Cloud'), + 'root' => __DIR__ . '/Fixtures/Cloud', 'throw' => false, ]); - - $app->setBasePath(__DIR__ . '/../'); } } From 13d5f51ed2dd219a6fe0b4cc041dfcbd620e5b51 Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:55:17 +0000 Subject: [PATCH 14/19] Setup node --- .github/workflows/tests.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 70862a3..60ae1c2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,6 +34,14 @@ jobs: extensions: mbstring, zip, fileinfo coverage: none + - name: Setup Node + uses: actions/setup-node@v4 + with: + # Version Spec of the version to use in SemVer notation. + # It also emits such aliases as lts, latest, nightly and canary builds + # Examples: 12.x, 10.15.1, >=10.15.0, lts/Hydrogen, 16-nightly, latest, node + node-version: '' + - name: Setup problem matchers run: | echo "::add-matcher::${{ runner.tool_cache }}/php.json" From 9a6678b752152ebb0655a91e1a9abb96766d653f Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:56:24 +0000 Subject: [PATCH 15/19] Node dependencies --- .github/workflows/tests.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 60ae1c2..5988a49 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,13 +34,9 @@ jobs: extensions: mbstring, zip, fileinfo coverage: none - - name: Setup Node - uses: actions/setup-node@v4 - with: - # Version Spec of the version to use in SemVer notation. - # It also emits such aliases as lts, latest, nightly and canary builds - # Examples: 12.x, 10.15.1, >=10.15.0, lts/Hydrogen, 16-nightly, latest, node - node-version: '' + - name: Install Node Dependencies + run: | + npm install - name: Setup problem matchers run: | From 75e4931f34c93be461422e948379effc0149816e Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:59:53 +0000 Subject: [PATCH 16/19] Remove permisison --- .github/workflows/tests.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5988a49..8769290 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,9 +47,5 @@ jobs: run: | composer update --${{ matrix.stability }} --prefer-dist --no-interaction - - name: Configure Permissions - run: | - sudo chown -R $USER:$USER ./ - - name: Execute tests run: ./vendor/bin/pest From a00ffb38f1721b83398964e87ed41cf39085576b Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:05:11 +0000 Subject: [PATCH 17/19] More cleanup --- src/Services/VersioningService.php | 38 ++++++++++++++---------- src/Tasks/Publish/PublishJob.php | 47 ++++++++++++++++++++---------- 2 files changed, 53 insertions(+), 32 deletions(-) diff --git a/src/Services/VersioningService.php b/src/Services/VersioningService.php index 08fdad2..54661fe 100644 --- a/src/Services/VersioningService.php +++ b/src/Services/VersioningService.php @@ -15,16 +15,18 @@ final class VersioningService { /** + * Append a new version to the history + * * @throws \Sammyjo20\Lasso\Exceptions\BaseException */ - public static function appendNewVersion(string $bundle_url): void + public static function appendNewVersion(string $bundleUrl): void { // 1. We need to fetch the current history $history = self::getHistoryFromDisk(); // 2. We then need to append the new version to the history, keyed with a unix timestamp - if (! in_array($bundle_url, $history, true)) { - $history[time()] = $bundle_url; + if (! in_array($bundleUrl, $history, true)) { + $history[time()] = $bundleUrl; } // 3. Then we need to delete any old bundles. @@ -71,21 +73,23 @@ private static function getHistoryFromDisk(): array } } - + /** + * Delete expired bundles + */ private static function deleteExpiredBundles(array $bundles): array { - $bundle_limit = self::getMaxBundlesAllowed(); + $bundleLimit = config('lasso.storage.max_bundles'); // If we haven't exceeded our bundle Limit, let's just return the bundles. // There's nothing more we can do here. - if (count($bundles) <= $bundle_limit) { + if (count($bundles) <= $bundleLimit) { return $bundles; } // However, if there's a bundle to be removed we need to go Ghostbuster on that bundle. - $deletable_count = count($bundles) - $bundle_limit; + $deletable_count = count($bundles) - $bundleLimit; $deletable = array_slice($bundles, 0, $deletable_count, true); // Now let's delete those bundles! @@ -97,7 +101,9 @@ private static function deleteExpiredBundles(array $bundles): array return array_diff($bundles, $deleted); } - + /** + * Delete bundles + */ private static function deleteBundles(array $deletable): array { $disk = self::getDisk(); @@ -122,6 +128,8 @@ private static function deleteBundles(array $deletable): array } /** + * Update history file + * * @throws \Sammyjo20\Lasso\Exceptions\BaseException */ private static function updateHistory(array $history): void @@ -135,21 +143,19 @@ private static function updateHistory(array $history): void } } - + /** + * Get the file directory + */ private static function getFileDirectory(): string { return (new Cloud)->getUploadPath('history.json'); } - + /** + * Get the disk + */ private static function getDisk(): string { return config('lasso.storage.disk'); } - - - private static function getMaxBundlesAllowed(): int - { - return config('lasso.storage.max_bundles'); - } } diff --git a/src/Tasks/Publish/PublishJob.php b/src/Tasks/Publish/PublishJob.php index b03b54f..317492c 100644 --- a/src/Tasks/Publish/PublishJob.php +++ b/src/Tasks/Publish/PublishJob.php @@ -15,17 +15,23 @@ final class PublishJob extends BaseJob { - + /** + * Bundle ID + */ protected string $bundleId; - + /** + * Should Lasso use Git? + */ protected bool $usesGit = true; - + /** + * Should Lasso use the latest commit? + */ protected bool $useCommit = false; /** - * @var string? + * The latest commit */ protected ?string $commit = null; @@ -125,24 +131,30 @@ public function run(): void } } - + + /** + * Clean up the publish + */ public function cleanUp(): void { $this->deleteLassoDirectory(); } /** + * Roll back the publish + * * @throws Exception */ - private function rollBack(Exception $exception) + private function rollBack(Exception $exception): void { $this->deleteLassoDirectory(); throw $exception; } - - + /** + * Dispatch the webhookss + */ public function dispatchWebhooks(array $webhooks = []): void { if (! count($webhooks)) { @@ -159,10 +171,11 @@ public function dispatchWebhooks(array $webhooks = []): void } /** - * @return $this + * Generate the Bundle ID + * * @throws GitHashException */ - private function generateBundleId(): self + private function generateBundleId(): void { $id = Str::random(20); @@ -175,21 +188,19 @@ private function generateBundleId(): self } $this->bundleId = $id; - - return $this; } /** - * @return $this + * Delete the Lasso directory */ - private function deleteLassoDirectory(): self + private function deleteLassoDirectory(): void { $this->filesystem->deleteBaseLassoDirectory(); - - return $this; } /** + * Disable Git with Lasso + * * @return $this */ public function dontUseGit(): self @@ -200,6 +211,8 @@ public function dontUseGit(): self } /** + * Should Lasso use the latest commit from Git? + * * @return $this */ public function useCommit(): self @@ -210,6 +223,8 @@ public function useCommit(): self } /** + * Specify a commit that is used for the bundle + * * @return $this */ public function withCommit(string $commitHash): self From 108ffb0e7df37672c0f250bd07588cc317865545 Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:30:57 +0000 Subject: [PATCH 18/19] PHPStan Level 7 Pass --- composer.json | 6 +++- phpstan.dist.neon | 14 +++++++++ src/Commands/PublishCommand.php | 4 +-- src/Commands/PullCommand.php | 4 +-- src/Container/Artisan.php | 30 +++++++++--------- src/Exceptions/BaseException.php | 13 +++++++- src/Helpers/BundleIntegrityHelper.php | 2 +- src/Helpers/Cloud.php | 6 ++-- src/Helpers/ConfigValidator.php | 20 ++++++------ src/Helpers/Filesystem.php | 45 ++++++++++++++++++++------- src/Helpers/Git.php | 2 +- src/Helpers/Webhook.php | 2 ++ src/Services/VersioningService.php | 8 +++++ src/Tasks/Publish/BundleJob.php | 16 ++++++++-- src/Tasks/Publish/PublishJob.php | 6 ++-- src/Tasks/Pull/PullJob.php | 6 ++++ 16 files changed, 134 insertions(+), 50 deletions(-) create mode 100644 phpstan.dist.neon diff --git a/composer.json b/composer.json index dba4eea..2834d4c 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,8 @@ "orchestra/testbench": "^7.0 || ^8.0", "friendsofphp/php-cs-fixer": "^3.1.0", "spatie/ray": "^1.33", - "pestphp/pest": "^2.25" + "pestphp/pest": "^2.25", + "phpstan/phpstan": "^1.10" }, "extra": { "laravel": { @@ -46,6 +47,9 @@ ], "fix-code": [ "./vendor/bin/php-cs-fixer fix --allow-risky=yes" + ], + "pstan": [ + "./vendor/bin/phpstan analyse" ] }, "minimum-stability": "stable", diff --git a/phpstan.dist.neon b/phpstan.dist.neon new file mode 100644 index 0000000..074c0e6 --- /dev/null +++ b/phpstan.dist.neon @@ -0,0 +1,14 @@ +############################################################################################# +# Don't edit this config file directly. # +# For temporary, or local overrides, create a file named 'phpstan.neon' alongside this one. # +############################################################################################# + +parameters: + # Add one of the editorUrl's in your phpstan.neon file for direct editor/IDE links in the terminal. + #editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%' + #editorUrl: 'vscode://file/%%file%%:%%line%%' + + level: 7 + + paths: + - src diff --git a/src/Commands/PublishCommand.php b/src/Commands/PublishCommand.php index fb46ddc..6f1042c 100644 --- a/src/Commands/PublishCommand.php +++ b/src/Commands/PublishCommand.php @@ -50,8 +50,8 @@ public function handle(Artisan $artisan, Filesystem $filesystem): int $job->useCommit(); } - if ($withCommit) { - $job->withCommit($withCommit); + if (is_string($withCommit)) { + $job->withCommit(mb_substr($withCommit, 0, 12)); } $artisan->note(sprintf( diff --git a/src/Commands/PullCommand.php b/src/Commands/PullCommand.php index 972e376..135f943 100644 --- a/src/Commands/PullCommand.php +++ b/src/Commands/PullCommand.php @@ -57,8 +57,8 @@ public function handle(Artisan $artisan, Filesystem $filesystem): int $job->useCommit(); } - if ($withCommit) { - $job->withCommit(mb_substr($withCommit, 0, 12)); + if (is_string($withCommit)) { + $job->withCommit(mb_substr((string)$withCommit, 0, 12)); } $job->run(); diff --git a/src/Container/Artisan.php b/src/Container/Artisan.php index a595e71..216e087 100644 --- a/src/Container/Artisan.php +++ b/src/Container/Artisan.php @@ -10,6 +10,7 @@ use Sammyjo20\Lasso\Exceptions\ConsoleMethodException; /** + * @mixin Command * @internal */ final class Artisan @@ -42,20 +43,6 @@ public function __construct() $this->compilerOutputMode = config('lasso.compiler.output', 'progress'); } - /** - * Handle a method call - * - * @throws \Sammyjo20\Lasso\Exceptions\ConsoleMethodException - */ - public function __call($name, $arguments): mixed - { - if (method_exists($this->command, $name)) { - return call_user_func_array([$this->command, $name], $arguments); - } - - throw new ConsoleMethodException(sprintf('Method %s::%s does not exist.', get_class($this->command), $name)); - } - /** * Create a note for the front end, set the second parameter to true for an error. * @@ -147,4 +134,19 @@ public function silent(): self return $this; } + + /** + * Handle a method call + * + * @param array $arguments + * @throws \Sammyjo20\Lasso\Exceptions\ConsoleMethodException + */ + public function __call(string $name, array $arguments): mixed + { + if (method_exists($this->command, $name)) { + return $this->command->$name(...$arguments); + } + + throw new ConsoleMethodException(sprintf('Method %s::%s does not exist.', get_class($this->command), $name)); + } } diff --git a/src/Exceptions/BaseException.php b/src/Exceptions/BaseException.php index 274ccc5..1cda26a 100644 --- a/src/Exceptions/BaseException.php +++ b/src/Exceptions/BaseException.php @@ -4,13 +4,24 @@ namespace Sammyjo20\Lasso\Exceptions; -class BaseException extends \Exception +use Exception; +use Throwable; + +class BaseException extends Exception { /** * Default Event */ public static string $event = 'An exception was thrown.'; + /** + * Constructor + */ + final public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } + /** * Create a new exception with a reason */ diff --git a/src/Helpers/BundleIntegrityHelper.php b/src/Helpers/BundleIntegrityHelper.php index b18e510..218c472 100644 --- a/src/Helpers/BundleIntegrityHelper.php +++ b/src/Helpers/BundleIntegrityHelper.php @@ -19,7 +19,7 @@ class BundleIntegrityHelper */ public static function generateChecksum(string $path): string { - return hash_file(self::ALGORITHM, $path); + return (string)hash_file(self::ALGORITHM, $path); } /** diff --git a/src/Helpers/Cloud.php b/src/Helpers/Cloud.php index 6934e53..7e6738a 100644 --- a/src/Helpers/Cloud.php +++ b/src/Helpers/Cloud.php @@ -12,6 +12,7 @@ /** * @internal + * @mixin BaseFilesystem */ class Cloud { @@ -77,12 +78,13 @@ public function getUploadPath(string $file = null): string /** * Call a method on the base Filesystem * + * @param array $arguments * @throws \Sammyjo20\Lasso\Exceptions\ConsoleMethodException */ - public function __call(string $name, array $arguments) + public function __call(string $name, array $arguments): mixed { if (method_exists($this->cloudFilesystem, $name)) { - return call_user_func_array([$this->cloudFilesystem, $name], $arguments); + return $this->cloudFilesystem->$name(...$arguments); } throw new ConsoleMethodException(sprintf('Method %s::%s does not exist.', get_class($this->cloudFilesystem), $name)); diff --git a/src/Helpers/ConfigValidator.php b/src/Helpers/ConfigValidator.php index 3dabc9d..2708274 100644 --- a/src/Helpers/ConfigValidator.php +++ b/src/Helpers/ConfigValidator.php @@ -28,15 +28,15 @@ public function __construct() /** * Get config parameter */ - private function get(string $item) + private function get(string $item): mixed { - return config('lasso.' . $item, null); + return config('lasso.' . $item); } /** * Check the compiler script */ - private function checkCompilerScript($value): bool + private function checkCompilerScript(mixed $value): bool { return ! is_null($value); } @@ -44,7 +44,7 @@ private function checkCompilerScript($value): bool /** * Check compiler script type */ - private function checkCompilerScriptType($value): bool + private function checkCompilerScriptType(mixed $value): bool { return is_string($value); } @@ -52,7 +52,7 @@ private function checkCompilerScriptType($value): bool /** * Check compiler output */ - private function checkCompilerOutputSetting($value): bool + private function checkCompilerOutputSetting(mixed $value): bool { if (is_null($value)) { return true; @@ -64,7 +64,7 @@ private function checkCompilerOutputSetting($value): bool /** * Check if public path exists */ - private function checkIfPublicPathExists($value): bool + private function checkIfPublicPathExists(mixed $value): bool { return $this->filesystem->exists($value) && $this->filesystem->isReadable($value) && $this->filesystem->isWritable($value); } @@ -72,15 +72,15 @@ private function checkIfPublicPathExists($value): bool /** * Check disk exists */ - private function checkDiskExists($value): bool + private function checkDiskExists(mixed $value): bool { - return ! is_null(config('filesystems.disks.' . $value, null)); + return ! is_null(config('filesystems.disks.' . $value)); } /** * Check bundle to keep count */ - private function checkBundleToKeepCount($value): bool + private function checkBundleToKeepCount(mixed $value): bool { return is_int($value) && $value > 0; } @@ -88,7 +88,7 @@ private function checkBundleToKeepCount($value): bool /** * Validate config * - * @throws ConfigFailedValidation + * @throws ConfigFailedValidation|\Sammyjo20\Lasso\Exceptions\BaseException */ public function validate(): void { diff --git a/src/Helpers/Filesystem.php b/src/Helpers/Filesystem.php index 9509301..255a931 100644 --- a/src/Helpers/Filesystem.php +++ b/src/Helpers/Filesystem.php @@ -11,13 +11,19 @@ */ class Filesystem extends BaseFilesystem { - + /** + * Lasso Environment + */ protected string $lassoEnvironment; - + /** + * Cloud Disk Path + */ protected string $cloudDisk; - + /** + * Public Path + */ protected string $publicPath; /** @@ -30,8 +36,12 @@ public function __construct() $this->publicPath = config('lasso.public_path', public_path()); } - - public function putStream($resource, string $destination): bool + /** + * Store a file as a stream + * + * @param resource $resource + */ + public function putStream(mixed $resource, string $destination): bool { $stream = fopen($destination, 'wb+'); @@ -42,15 +52,21 @@ public function putStream($resource, string $destination): bool return true; } - + /** + * Create fresh from local bundle + * + * @param array $bundle + */ public function createFreshLocalBundle(array $bundle): void { $this->deleteLocalBundle(); - $this->put(base_path('lasso-bundle.json'), json_encode($bundle)); + $this->put(base_path('lasso-bundle.json'), (string)json_encode($bundle)); } - + /** + * Delete local bundle + */ public function deleteLocalBundle(): bool { return $this->delete(base_path('lasso-bundle.json')); @@ -65,7 +81,7 @@ public function deleteBaseLassoDirectory(): bool } /** - * @return $this + * Set the Lasso environment */ public function setLassoEnvironment(string $environment): self { @@ -74,18 +90,25 @@ public function setLassoEnvironment(string $environment): self return $this; } + /** + * Get the Lasso environment + */ public function getLassoEnvironment(): string { return $this->lassoEnvironment; } - + /** + * Get the public path + */ public function getPublicPath(): string { return $this->publicPath; } - + /** + * Get the cloud disk + */ public function getCloudDisk(): string { return $this->cloudDisk; diff --git a/src/Helpers/Git.php b/src/Helpers/Git.php index b4f328b..13b38a3 100644 --- a/src/Helpers/Git.php +++ b/src/Helpers/Git.php @@ -20,7 +20,7 @@ class Git public static function getCommitHash(): ? string { try { - $branch = str_replace('\n', '', last(explode('/', file_get_contents(base_path() . '/.git/HEAD')))); + $branch = str_replace('\n', '', last(explode('/', (string)file_get_contents(base_path() . '/.git/HEAD')))); $hash = file_get_contents(base_path() . '/.git/refs/heads/' . $branch); } catch (Exception $exception) { throw new GitHashException($exception->getMessage(), previous: $exception); diff --git a/src/Helpers/Webhook.php b/src/Helpers/Webhook.php index 48027d5..a5a6432 100644 --- a/src/Helpers/Webhook.php +++ b/src/Helpers/Webhook.php @@ -13,6 +13,8 @@ class Webhook { /** * Send a Webhook to a URL + * + * @param array $data */ public static function send(string $url, string $event, array $data = []): void { diff --git a/src/Services/VersioningService.php b/src/Services/VersioningService.php index 54661fe..98ebafb 100644 --- a/src/Services/VersioningService.php +++ b/src/Services/VersioningService.php @@ -40,6 +40,7 @@ public static function appendNewVersion(string $bundleUrl): void * Get the versioning file from the Filesystem Disk. * This is a file Lasso stores to keep track of its versions. * + * @return array * @throws \Sammyjo20\Lasso\Exceptions\BaseException */ private static function getHistoryFromDisk(): array @@ -75,6 +76,9 @@ private static function getHistoryFromDisk(): array /** * Delete expired bundles + * + * @param array $bundles + * @return array */ private static function deleteExpiredBundles(array $bundles): array { @@ -103,6 +107,9 @@ private static function deleteExpiredBundles(array $bundles): array /** * Delete bundles + * + * @param array $deletable + * @return array */ private static function deleteBundles(array $deletable): array { @@ -130,6 +137,7 @@ private static function deleteBundles(array $deletable): array /** * Update history file * + * @param array $history * @throws \Sammyjo20\Lasso\Exceptions\BaseException */ private static function updateHistory(array $history): void diff --git a/src/Tasks/Publish/BundleJob.php b/src/Tasks/Publish/BundleJob.php index ad9d0c1..97a5ba9 100644 --- a/src/Tasks/Publish/BundleJob.php +++ b/src/Tasks/Publish/BundleJob.php @@ -10,10 +10,16 @@ final class BundleJob extends BaseJob { - + /** + * Bundle ID + */ protected string $bundleId; - + /** + * Forbidden files + * + * @var array + */ protected array $forbiddenFiles = [ '.htaccess', 'web.config', @@ -22,7 +28,11 @@ final class BundleJob extends BaseJob 'storage', ]; - + /** + * Forbidden Directories + * + * @var array + */ protected array $forbiddenDirectories = [ 'storage', 'hot', diff --git a/src/Tasks/Publish/PublishJob.php b/src/Tasks/Publish/PublishJob.php index 317492c..1ac5e49 100644 --- a/src/Tasks/Publish/PublishJob.php +++ b/src/Tasks/Publish/PublishJob.php @@ -115,7 +115,7 @@ public function run(): void } else { $filesystem->deleteLocalBundle(); - $filesystem->put($bundlePath, json_encode($bundle)); + $filesystem->put($bundlePath, (string)json_encode($bundle)); $cloud->uploadFile($bundlePath, config('lasso.storage.prefix') . 'lasso-bundle.json'); } @@ -153,7 +153,9 @@ private function rollBack(Exception $exception): void } /** - * Dispatch the webhookss + * Dispatch the webhooks + * + * @param array $webhooks */ public function dispatchWebhooks(array $webhooks = []): void { diff --git a/src/Tasks/Pull/PullJob.php b/src/Tasks/Pull/PullJob.php index 91556cc..69bbb88 100644 --- a/src/Tasks/Pull/PullJob.php +++ b/src/Tasks/Pull/PullJob.php @@ -119,6 +119,8 @@ public function cleanUp(): void /** * Dispatch the webhooks + * + * @param array $webhooks */ public function dispatchWebhooks(array $webhooks = []): void { @@ -138,6 +140,7 @@ public function dispatchWebhooks(array $webhooks = []): void /** * Get the latest bundle info * + * @return array * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface @@ -178,6 +181,9 @@ private function getLatestBundleInfo(): array /** * Validate the bundle checksum + * + * @param array $bundle + * @throws \Exception */ private function validateBundle(array $bundle): bool { From af6351175e2b95c93773493ea2e77a009d0842bf Mon Sep 17 00:00:00 2001 From: Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:31:35 +0000 Subject: [PATCH 19/19] PHPStan --- .github/workflows/phpstan.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/phpstan.yml diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..512c212 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,28 @@ +name: PHPStan + +on: + push: + branches: + - 'main' + pull_request: + branches: + - '*' + +jobs: + phpstan: + name: phpstan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none + + - name: Install composer dependencies + uses: ramsey/composer-install@v2 + + - name: Run PHPStan + run: ./vendor/bin/phpstan analyse --error-format=github