├── LICENSE ├── README.md ├── composer.json ├── phpstan.neon ├── phpunit.xml ├── pint.json ├── src ├── Cache │ └── CacheService.php ├── Concerns │ └── SendsRequests.php ├── Config │ └── Resolver.php ├── Console │ └── Commands │ │ ├── Make │ │ └── MakeDomainDirectoryCommand.php │ │ ├── Setup │ │ ├── SetupLaravelPintCommand.php │ │ └── SetupPhpstanCommand.php │ │ └── Stubs │ │ └── StubsPublishCommand.php ├── Contracts │ ├── CacheExpiry.php │ ├── CacheKey.php │ ├── FeatureBootLoaderContract.php │ ├── IntegrationContract.php │ ├── PayloadContract.php │ └── SelfRegistersToContainer.php ├── DataObjects │ ├── WebhookDataObject.php │ └── WebhookObject.php ├── Database │ ├── Contracts │ │ └── EloquentEntity.php │ ├── Portal.php │ └── Repositories │ │ └── Concerns │ │ └── FindById.php ├── Http │ ├── Controllers │ │ └── Concerns │ │ │ └── RendersInertiaComponent.php │ ├── Exceptions │ │ └── DuplicateRequestException.php │ ├── Middleware │ │ ├── CacheResponseMiddleware.php │ │ ├── ContentEncodingMiddleware.php │ │ ├── ContentTypeMiddleware.php │ │ └── DeduplicateRequestsMiddleware.php │ ├── Resources │ │ └── DateResource.php │ └── Responses │ │ ├── CollectionResponse.php │ │ ├── Concerns │ │ └── HasResponse.php │ │ ├── MessageResponse.php │ │ └── ModelResponse.php ├── Providers │ └── PackageServiceProvider.php ├── Queue │ └── DispatchableCommandBus.php └── ValueObjects │ ├── Concerns │ ├── HasArrayValue.php │ ├── HasBooleanValue.php │ ├── HasCarbonValue.php │ ├── HasFloatValue.php │ ├── HasNumberValue.php │ └── HasStringValue.php │ ├── Date.php │ ├── Email.php │ ├── EntityID.php │ ├── Name.php │ ├── Password.php │ └── StringValue.php └── stubs ├── console.stub ├── controller.invokable.stub ├── event.stub ├── factory.stub ├── job.queued.stub ├── middleware.stub ├── migration.create.stub ├── migration.update.stub ├── model.pivot.stub ├── model.stub ├── phpstan.neon ├── pint.json ├── provider.stub ├── request.stub └── resource.stub /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Steve McDougall. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Launchpad 2 | 3 | A helpful Laravel package to help me get started in Laravel projects quicker. 4 | 5 | This is still a work in progress, so use at your own risk! 6 | 7 | ## CLI Commands 8 | 9 | - `php artisan setup:phpstan`: This command will publish a default PHPStan configuration file in the root directory of your Laravel Project. 10 | - `php artisan setup:pint`: This command will publish a default Laravel Pint configuration file in the root directory of your Laravel Project. 11 | 12 | ## Helpers 13 | 14 | - `CacheService` - A helper to allow you to use caching underneath an abstracted class. Currently only implements: 15 | - `remember` which accepts: 16 | - `CacheKey` Enum 17 | - `CacheExpiry` Enum 18 | - `Closure` callback 19 | - `Resolver` - A helper to allow you to fetch typed values from config. 20 | - `Portal` - A helper to allow you to interact with the Laravel Database Manager, current methods implemented: 21 | - `transaction` which will allow you to do Database Transactions easily. 22 | - `DispatchableCommandBus` - A helper to allow you to dispatch background jobs using the DI container instead of the Facade. 23 | 24 | ## Traits/Concerns 25 | 26 | - `RendersInertiaComponent` - Add this to your Web Controllers, to have access to the underlying Response Factory for Inertia by using `$this->response->render()`. 27 | 28 | ## Contracts 29 | 30 | - `SelfRegistersToContainer` - A contract that you can add to a class, which is used for self registration into the DI container for classes. 31 | - `CacheExpiry` - A contract that we will add to Enums that are related to Cache Expiry times. 32 | - `CacheKey` - A contract that we will add to Enums that are related to Cache Keys. 33 | 34 | ## API Responses 35 | 36 | - `MessageResponse` - A response class that will return a response with the key `message`. 37 | - `ModelResponse` - A response class that accepts an Eloquent Resource class. 38 | - `CollectionResponse` - A response class that accepts an Eloquent Resource collection class. 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "juststeveking/launchpad", 3 | "description": "A helpful Laravel package to help me get started in Laravel projects quicker.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "role": "Developer", 8 | "name": "Steve McDougall", 9 | "email": "juststevemcd@gmail.com", 10 | "homepage": "https://www.juststeveking.uk/" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-4": { 15 | "JustSteveKing\\Launchpad\\": "src/" 16 | } 17 | }, 18 | "autoload-dev": { 19 | "psr-4": { 20 | "JustSteveKing\\Launchpad\\Tests\\": "tests/" 21 | } 22 | }, 23 | "require": { 24 | "php": "^8.2", 25 | "treblle/treblle-api-tools-laravel": "^0.0.1" 26 | }, 27 | "require-dev": { 28 | "guzzlehttp/guzzle": "^7.5", 29 | "inertiajs/inertia-laravel": "^0.6.9", 30 | "laravel/pint": "^1.9", 31 | "orchestra/testbench": "^8.5", 32 | "pestphp/pest": "^2.5.2", 33 | "phpstan/phpstan": "^1.10.14" 34 | }, 35 | "scripts": { 36 | "pint": [ 37 | "./vendor/bin/pint" 38 | ], 39 | "stan": [ 40 | "./vendor/bin/phpstan analyse" 41 | ], 42 | "test": [ 43 | "./vendor/bin/pest" 44 | ] 45 | }, 46 | "extra": { 47 | "laravel": { 48 | "providers": [ 49 | "JustSteveKing\\Launchpad\\Providers\\PackageServiceProvider" 50 | ] 51 | } 52 | }, 53 | "config": { 54 | "sort-packages": true, 55 | "optimize-autoloader": true, 56 | "allow-plugins": { 57 | "pestphp/pest-plugin": true, 58 | "php-http/discovery": true 59 | } 60 | }, 61 | "minimum-stability": "dev", 62 | "prefer-stable": true 63 | } 64 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | 3 | paths: 4 | - src/ 5 | 6 | level: 9 7 | 8 | ignoreErrors: 9 | 10 | excludePaths: 11 | 12 | checkMissingIterableValueType: false 13 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | ./src 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "psr12", 3 | "rules": { 4 | "align_multiline_comment": true, 5 | "array_indentation": true, 6 | "array_syntax": true, 7 | "blank_line_after_namespace": true, 8 | "blank_line_after_opening_tag": true, 9 | "combine_consecutive_issets": true, 10 | "combine_consecutive_unsets": true, 11 | "concat_space": true, 12 | "declare_parentheses": true, 13 | "declare_strict_types": true, 14 | "explicit_string_variable": true, 15 | "final_class": true, 16 | "final_internal_class": false, 17 | "fully_qualified_strict_types": true, 18 | "global_namespace_import": { 19 | "import_classes": true, 20 | "import_constants": true, 21 | "import_functions": true 22 | }, 23 | "is_null": true, 24 | "lambda_not_used_import": true, 25 | "logical_operators": true, 26 | "mb_str_functions": true, 27 | "method_chaining_indentation": true, 28 | "modernize_strpos": true, 29 | "new_with_braces": true, 30 | "no_empty_comment": true, 31 | "not_operator_with_space": true, 32 | "ordered_traits": true, 33 | "protected_to_private": true, 34 | "simplified_if_return": true, 35 | "strict_comparison": true, 36 | "ternary_to_null_coalescing": true, 37 | "trim_array_spaces": true, 38 | "use_arrow_functions": true, 39 | "void_return": true, 40 | "yoda_style": true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Cache/CacheService.php: -------------------------------------------------------------------------------- 1 | repository->remember( 22 | key: $key->value, 23 | ttl: $expiry->value, 24 | callback: $callback, 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Concerns/SendsRequests.php: -------------------------------------------------------------------------------- 1 | request->send( 27 | method: $method->value, 28 | url: $uri, 29 | options: $options, 30 | )->throw(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Config/Resolver.php: -------------------------------------------------------------------------------- 1 | config->get( 19 | key: $key, 20 | default: $default, 21 | ); 22 | } 23 | 24 | public function number(string $key, null|int $default = null): int 25 | { 26 | return (int) $this->config->get( 27 | key: $key, 28 | default: $default, 29 | ); 30 | } 31 | 32 | public function float(string $key, null|bool $default = null): float 33 | { 34 | return (float) $this->config->get( 35 | key: $key, 36 | default: $default, 37 | ); 38 | } 39 | 40 | public function boolean(string $key, null|bool $default = null): bool 41 | { 42 | return (bool) $this->config->get( 43 | key: $key, 44 | default: $default, 45 | ); 46 | } 47 | 48 | public function array(string $key, null|array $default = null): array 49 | { 50 | return (array) $this->config->get( 51 | key: $key, 52 | default: $default, 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Console/Commands/Make/MakeDomainDirectoryCommand.php: -------------------------------------------------------------------------------- 1 | argument( 19 | key: 'name', 20 | ) ?? $this->components->ask( 21 | question: 'What is the name of the domain you want to create?', 22 | ); 23 | 24 | $parts = Str::of( 25 | string: $name, 26 | )->explode( 27 | delimiter: ' ', 28 | )->map(fn (string $part) => Str::title(value: $part)); 29 | 30 | $domain = Str::of( 31 | string: implode( 32 | separator: ' ', 33 | array: $parts->toArray(), 34 | ), 35 | )->replace( 36 | search: ' ', 37 | replace: '', 38 | )->toString(); 39 | 40 | 41 | 42 | return Command::SUCCESS; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Console/Commands/Setup/SetupLaravelPintCommand.php: -------------------------------------------------------------------------------- 1 | components->info( 20 | string: 'Publishing Laravel Pint configuration.', 21 | ); 22 | 23 | try { 24 | File::put( 25 | path: base_path(path: 'pint.json'), 26 | contents: File::get( 27 | path: __DIR__.'/../../../../stubs/pint.json', 28 | ), 29 | ); 30 | } catch (Throwable $exception) { 31 | $this->components->error( 32 | string: $exception->getMessage(), 33 | ); 34 | 35 | return Command::FAILURE; 36 | } 37 | 38 | return Command::SUCCESS; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Console/Commands/Setup/SetupPhpstanCommand.php: -------------------------------------------------------------------------------- 1 | components->info( 20 | string: 'Publishing PHPStan configuration.', 21 | ); 22 | 23 | try { 24 | File::put( 25 | path: base_path(path: 'phpstan.neon'), 26 | contents: File::get( 27 | path: __DIR__.'/../../../../stubs/phpstan.neon', 28 | ), 29 | ); 30 | } catch (Throwable $exception) { 31 | $this->components->error( 32 | string: $exception->getMessage(), 33 | ); 34 | 35 | return Command::FAILURE; 36 | } 37 | 38 | return Command::SUCCESS; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Console/Commands/Stubs/StubsPublishCommand.php: -------------------------------------------------------------------------------- 1 | confirm(question: 'Are you sure you want to publish these?')) { 24 | return SymfonyCommand::FAILURE; 25 | } 26 | 27 | if (!is_dir($stubsPath = $this->laravel->basePath('stubs'))) { 28 | (new Filesystem())->makeDirectory($stubsPath); 29 | } 30 | 31 | $files = collect( 32 | File::files(__DIR__ . '/../../../stubs') 33 | )->unless( 34 | $this->option('force'), 35 | fn($files) => $this->unpublished($files) 36 | ); 37 | 38 | $published = $this->publish($files); 39 | 40 | $this->components->info( 41 | string: "{$published} / {$files->count()} stubs published.", 42 | ); 43 | 44 | return SymfonyCommand::SUCCESS; 45 | } 46 | 47 | public function unpublished(Collection $files): Collection 48 | { 49 | return $files->reject(function (SplFileInfo $file) { 50 | return file_exists($this->targetPath($file)); 51 | }); 52 | } 53 | 54 | public function publish(Collection $files): int|Closure 55 | { 56 | return $files->reduce( 57 | function (int $published, SplFileInfo $file) { 58 | file_put_contents($this->targetPath($file), file_get_contents($file->getPathname())); 59 | return $published + 1; 60 | }, 61 | 0, 62 | ); 63 | } 64 | 65 | public function targetPath(SplFileInfo $file): string 66 | { 67 | return "{$this->laravel->basePath('stubs')}/{$file->getFilename()}"; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Contracts/CacheExpiry.php: -------------------------------------------------------------------------------- 1 | data; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/DataObjects/WebhookObject.php: -------------------------------------------------------------------------------- 1 | $this->event, 18 | 'data' => $this->data->toArray(), 19 | ]; 20 | } 21 | 22 | public static function fromArray(array $data): WebhookObject 23 | { 24 | return new WebhookObject( 25 | event: $data['event'], 26 | data: new WebhookDataObject( 27 | data: collect($data)->except('event')->toArray(), 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Database/Contracts/EloquentEntity.php: -------------------------------------------------------------------------------- 1 | database->transaction( 30 | callback: $callback, 31 | attempts: $attempts, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Database/Repositories/Concerns/FindById.php: -------------------------------------------------------------------------------- 1 | fingerprint(); 18 | 19 | if (Cache::has($fingerprint)) { 20 | return Cache::get($fingerprint); 21 | } 22 | 23 | $response = $next($request); 24 | 25 | Cache::put($fingerprint, $response, now()->addMinutes(5)); 26 | 27 | return $response; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Http/Middleware/ContentEncodingMiddleware.php: -------------------------------------------------------------------------------- 1 | header('Content-Encoding', 'gzip'); 19 | $response->setContent(gzencode($response->getContent(), 9)); 20 | 21 | return $response; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Http/Middleware/ContentTypeMiddleware.php: -------------------------------------------------------------------------------- 1 | header('Content-Type', 'application/vnd.api+json'); 19 | 20 | return $response; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Http/Middleware/DeduplicateRequestsMiddleware.php: -------------------------------------------------------------------------------- 1 | fingerprint(); 20 | 21 | if (Cache::has($fingerprint)) { 22 | throw new DuplicateRequestException( 23 | message: 'This is a duplicate request', 24 | code: Status::CONFLICT->value, 25 | ); 26 | } 27 | 28 | Cache::put($fingerprint, true, now()->addMinutes(5)); 29 | 30 | return $next($request); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Http/Resources/DateResource.php: -------------------------------------------------------------------------------- 1 | $this->resource->diffForHumans(), 20 | 'string' => $this->resource->toDateTimeString(), 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Http/Responses/CollectionResponse.php: -------------------------------------------------------------------------------- 1 | data, 20 | status: $this->status->value, 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Http/Responses/MessageResponse.php: -------------------------------------------------------------------------------- 1 | commands( 32 | commands: [ 33 | SetupPhpstanCommand::class, 34 | SetupLaravelPintCommand::class, 35 | StubsPublishCommand::class, 36 | ], 37 | ); 38 | $this->app->singleton( 39 | abstract: Portal::class, 40 | concrete: Portal::class, 41 | ); 42 | $this->app->singleton( 43 | abstract: DispatchableCommandBus::class, 44 | concrete: DispatchableCommandBus::class, 45 | ); 46 | $this->app->singleton( 47 | abstract: Resolver::class, 48 | concrete: Resolver::class, 49 | ); 50 | $this->app->singleton( 51 | abstract: CacheService::class, 52 | concrete: CacheService::class, 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Queue/DispatchableCommandBus.php: -------------------------------------------------------------------------------- 1 | value; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ValueObjects/Concerns/HasBooleanValue.php: -------------------------------------------------------------------------------- 1 | value; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ValueObjects/Concerns/HasCarbonValue.php: -------------------------------------------------------------------------------- 1 | value; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ValueObjects/Concerns/HasFloatValue.php: -------------------------------------------------------------------------------- 1 | value; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ValueObjects/Concerns/HasNumberValue.php: -------------------------------------------------------------------------------- 1 | value; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ValueObjects/Concerns/HasStringValue.php: -------------------------------------------------------------------------------- 1 | value; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ValueObjects/Date.php: -------------------------------------------------------------------------------- 1 | value, 19 | filter: FILTER_VALIDATE_EMAIL, 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ValueObjects/EntityID.php: -------------------------------------------------------------------------------- 1 | $this->value, 21 | ], 22 | rules: [ 23 | 'password' => [ 24 | 'required', 25 | PasswordValidation::default(), 26 | ] 27 | ] 28 | )->passes(); 29 | } 30 | 31 | public function check(string $check): bool 32 | { 33 | return Hash::check( 34 | value: $this->value, 35 | hashedValue: $check, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ValueObjects/StringValue.php: -------------------------------------------------------------------------------- 1 | ulid('id')->primary(); 15 | 16 | // 17 | 18 | $table->timestamps(); 19 | }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /stubs/migration.update.stub: -------------------------------------------------------------------------------- 1 | $this->resource->id, 19 | ]; 20 | } 21 | } 22 | --------------------------------------------------------------------------------