From 005396635fb794100dcd9c69b464283f16759cf9 Mon Sep 17 00:00:00 2001 From: Ben Sherred Date: Thu, 29 Aug 2024 08:28:38 +0100 Subject: [PATCH] feat: add initial install command --- composer.json | 8 +- config/fabricate.php | 5 - database/factories/.gitkeep | 0 database/migrations/.gitkeep | 0 phpstan.neon.dist | 10 +- resources/views/.gitkeep | 0 src/Actions/ReplaceInFileAction.php | 22 +++ src/Actions/RequireComposerPackagesAction.php | 28 ++++ src/Console/InstallCommand.php | 21 +++ src/Data/InstallData.php | 13 ++ src/FabricateServiceProvider.php | 30 ++-- src/Install.php | 34 +++++ src/Pipes/ConfigureEloquentModels.php | 70 +++++++++ src/Pipes/ConfigureTestingDatabase.php | 41 +++++ src/Pipes/FixLarastanErrors.php | 41 +++++ src/Pipes/InstallComposerDependencies.php | 36 +++++ src/Pipes/InstallYarnDependencies.php | 30 ++++ src/Pipes/PublishStubs.php | 62 ++++++++ src/Pipes/RegisterComposerScripts.php | 42 ++++++ src/Pipes/RegisterHelpersFile.php | 33 ++++ src/Pipes/RemoveDefaultController.php | 26 ++++ src/Pipes/RemoveDownMigrations.php | 32 ++++ src/Pipes/RemoveMigrationComments.php | 32 ++++ src/Pipes/RunLintScript.php | 19 +++ src/Pipes/RunRefactorScript.php | 19 +++ stubs/default/.github/dependabot.yml | 21 +++ stubs/default/.github/semantic.yml | 5 + .../workflows/dependabot-auto-merge.yml | 33 ++++ .../.github/workflows/php-security.yml | 24 +++ stubs/default/.github/workflows/tests.yml | 74 +++++++++ stubs/default/README.md | 104 +++++++++++++ stubs/default/app/Support/Type.php | 141 ++++++++++++++++++ stubs/default/app/Support/helpers.php | 18 +++ stubs/default/gitignore | 39 +++++ stubs/default/phpstan.neon.dist | 13 ++ stubs/default/rector.php | 27 ++++ stubs/default/stubs/migration.create.stub | 18 +++ stubs/default/stubs/migration.stub | 15 ++ stubs/default/stubs/migration.update.stub | 17 +++ stubs/default/tests/Arch/ConsoleTest.php | 9 ++ stubs/default/tests/Arch/ContractsTest.php | 7 + stubs/default/tests/Arch/DataTest.php | 16 ++ stubs/default/tests/Arch/EnumsTest.php | 8 + stubs/default/tests/Arch/EventsTest.php | 8 + stubs/default/tests/Arch/ExceptionsTest.php | 7 + stubs/default/tests/Arch/GlobalTest.php | 15 ++ stubs/default/tests/Arch/HttpTest.php | 30 ++++ stubs/default/tests/Arch/JobsTest.php | 10 ++ stubs/default/tests/Arch/ListenersTest.php | 8 + stubs/default/tests/Arch/ModelsTest.php | 8 + .../default/tests/Arch/NotificationsTest.php | 12 ++ stubs/default/tests/Arch/ObserversTest.php | 7 + stubs/default/tests/Arch/ProvidersTest.php | 9 ++ stubs/default/tests/Arch/RulesTest.php | 9 ++ stubs/default/tests/Arch/ServicesTest.php | 8 + stubs/default/tests/Arch/ViewModelsTest.php | 9 ++ stubs/default/tests/Arch/ViewTest.php | 9 ++ 57 files changed, 1363 insertions(+), 29 deletions(-) delete mode 100644 config/fabricate.php delete mode 100644 database/factories/.gitkeep delete mode 100644 database/migrations/.gitkeep delete mode 100644 resources/views/.gitkeep create mode 100644 src/Actions/ReplaceInFileAction.php create mode 100644 src/Actions/RequireComposerPackagesAction.php create mode 100644 src/Console/InstallCommand.php create mode 100644 src/Data/InstallData.php create mode 100644 src/Install.php create mode 100644 src/Pipes/ConfigureEloquentModels.php create mode 100644 src/Pipes/ConfigureTestingDatabase.php create mode 100644 src/Pipes/FixLarastanErrors.php create mode 100644 src/Pipes/InstallComposerDependencies.php create mode 100644 src/Pipes/InstallYarnDependencies.php create mode 100644 src/Pipes/PublishStubs.php create mode 100644 src/Pipes/RegisterComposerScripts.php create mode 100644 src/Pipes/RegisterHelpersFile.php create mode 100644 src/Pipes/RemoveDefaultController.php create mode 100644 src/Pipes/RemoveDownMigrations.php create mode 100644 src/Pipes/RemoveMigrationComments.php create mode 100644 src/Pipes/RunLintScript.php create mode 100644 src/Pipes/RunRefactorScript.php create mode 100644 stubs/default/.github/dependabot.yml create mode 100644 stubs/default/.github/semantic.yml create mode 100644 stubs/default/.github/workflows/dependabot-auto-merge.yml create mode 100644 stubs/default/.github/workflows/php-security.yml create mode 100644 stubs/default/.github/workflows/tests.yml create mode 100644 stubs/default/README.md create mode 100644 stubs/default/app/Support/Type.php create mode 100644 stubs/default/app/Support/helpers.php create mode 100644 stubs/default/gitignore create mode 100644 stubs/default/phpstan.neon.dist create mode 100644 stubs/default/rector.php create mode 100644 stubs/default/stubs/migration.create.stub create mode 100644 stubs/default/stubs/migration.stub create mode 100644 stubs/default/stubs/migration.update.stub create mode 100644 stubs/default/tests/Arch/ConsoleTest.php create mode 100644 stubs/default/tests/Arch/ContractsTest.php create mode 100644 stubs/default/tests/Arch/DataTest.php create mode 100644 stubs/default/tests/Arch/EnumsTest.php create mode 100644 stubs/default/tests/Arch/EventsTest.php create mode 100644 stubs/default/tests/Arch/ExceptionsTest.php create mode 100644 stubs/default/tests/Arch/GlobalTest.php create mode 100644 stubs/default/tests/Arch/HttpTest.php create mode 100644 stubs/default/tests/Arch/JobsTest.php create mode 100644 stubs/default/tests/Arch/ListenersTest.php create mode 100644 stubs/default/tests/Arch/ModelsTest.php create mode 100644 stubs/default/tests/Arch/NotificationsTest.php create mode 100644 stubs/default/tests/Arch/ObserversTest.php create mode 100644 stubs/default/tests/Arch/ProvidersTest.php create mode 100644 stubs/default/tests/Arch/RulesTest.php create mode 100644 stubs/default/tests/Arch/ServicesTest.php create mode 100644 stubs/default/tests/Arch/ViewModelsTest.php create mode 100644 stubs/default/tests/Arch/ViewTest.php diff --git a/composer.json b/composer.json index ecaa393..1f2e339 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "red-explosion/fabricate", - "description": "Laravel scaffolding/starter kit for building new applications.", + "description": "An opionated yet flexible package for scaffolding new Laravel applications.", "license": "MIT", "homepage": "https://github.com/red-explosion/fabricate", "type": "library", @@ -16,12 +16,12 @@ } ], "require": { - "php": "^8.2", - "illuminate/support": "^10.0|^11.0" + "php": "^8.3", + "illuminate/support": "^11.0" }, "require-dev": { "laravel/pint": "^1.10", - "orchestra/testbench": "^8.0|^9.0", + "orchestra/testbench": "^9.0", "pestphp/pest": "^2.6", "pestphp/pest-plugin-arch": "^2.1", "phpstan/phpstan": "^1.10", diff --git a/config/fabricate.php b/config/fabricate.php deleted file mode 100644 index 0dae23d..0000000 --- a/config/fabricate.php +++ /dev/null @@ -1,5 +0,0 @@ -filesystem->get($path)); + + $this->filesystem->put($path, $contents); + } +} diff --git a/src/Actions/RequireComposerPackagesAction.php b/src/Actions/RequireComposerPackagesAction.php new file mode 100644 index 0000000..3c74c16 --- /dev/null +++ b/src/Actions/RequireComposerPackagesAction.php @@ -0,0 +1,28 @@ + $packages + * @param bool $asDev + * @return bool + */ + public function handle(array $packages, bool $asDev = false): bool + { + $command = array_merge( + ['composer', 'require'], + $packages, + $asDev ? ['--dev'] : [], + ); + + return (new Process($command, base_path(), ['COMPOSER_MEMORY_LIMIT' => '-1'])) + ->setTimeout(null) + ->run() === 0; // TODO: log the output + } +} diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php new file mode 100644 index 0000000..1bc7d3e --- /dev/null +++ b/src/Console/InstallCommand.php @@ -0,0 +1,21 @@ +process(new InstallData()); + } +} diff --git a/src/Data/InstallData.php b/src/Data/InstallData.php new file mode 100644 index 0000000..6ba11cb --- /dev/null +++ b/src/Data/InstallData.php @@ -0,0 +1,13 @@ +mergeConfigFrom( - path: __DIR__ . '/../config/fabricate.php', - key: 'fabricate', - ); } public function boot(): void { - if ($this->app->runningInConsole()) { - $this->publishes( - paths: [ - __DIR__ . '/../config/fabricate.php' => config_path('fabricate.php'), - ], - groups: 'fabricate-config', - ); + if (! $this->app->runningInConsole()) { + return; } + + $this->commands([ + Console\InstallCommand::class, + ]); + } + + /** + * @return array + */ + public function provides(): array + { + return [ + Console\InstallCommand::class, + ]; } } diff --git a/src/Install.php b/src/Install.php new file mode 100644 index 0000000..7f56c06 --- /dev/null +++ b/src/Install.php @@ -0,0 +1,34 @@ +send($data) + ->through([ + Pipes\InstallComposerDependencies::class, + Pipes\InstallYarnDependencies::class, + Pipes\PublishStubs::class, + Pipes\RegisterComposerScripts::class, + Pipes\RunRefactorScript::class, + Pipes\RunLintScript::class, + Pipes\RegisterHelpersFile::class, + Pipes\FixLarastanErrors::class, + Pipes\ConfigureTestingDatabase::class, + Pipes\RemoveDefaultController::class, + Pipes\RemoveMigrationComments::class, + Pipes\RemoveDownMigrations::class, + Pipes\ConfigureEloquentModels::class, + Pipes\RunRefactorScript::class, + Pipes\RunLintScript::class, + ]) + ->thenReturn(); + } +} diff --git a/src/Pipes/ConfigureEloquentModels.php b/src/Pipes/ConfigureEloquentModels.php new file mode 100644 index 0000000..98f35f6 --- /dev/null +++ b/src/Pipes/ConfigureEloquentModels.php @@ -0,0 +1,70 @@ +replaceInFile->handle( + <<replaceInFile->handle( + <<app->isProduction()); + Model::preventSilentlyDiscardingAttributes(); + Model::preventAccessingMissingAttributes(); + + if (\$this->app->isProduction()) { + Model::handleLazyLoadingViolationUsing(function (Model \$model, string \$relation): void { + \$class = \$model::class; + + Log::warning("Attempted to lazy load [{\$relation}] on model [{\$class}]."); + }); + } + + Relation::enforceMorphMap([ + 'user' => User::class, + ]); + } + EOT, + base_path('app/Providers/AppServiceProvider.php'), + ); + + return $next($data); + } +} diff --git a/src/Pipes/ConfigureTestingDatabase.php b/src/Pipes/ConfigureTestingDatabase.php new file mode 100644 index 0000000..2032eb9 --- /dev/null +++ b/src/Pipes/ConfigureTestingDatabase.php @@ -0,0 +1,41 @@ +filesystem->move( + base_path('phpunit.xml'), + base_path('phpunit.xml.dist'), + ); + + $this->replaceInFile->handle( + << --> + + EOT, + << + EOT, + base_path('phpunit.xml.dist'), + ); + + return $next($data); + } +} + diff --git a/src/Pipes/FixLarastanErrors.php b/src/Pipes/FixLarastanErrors.php new file mode 100644 index 0000000..f587be2 --- /dev/null +++ b/src/Pipes/FixLarastanErrors.php @@ -0,0 +1,41 @@ +replaceInFile->handle( + 'use Illuminate\Database\Eloquent\Factories\HasFactory;', + <<replaceInFile->handle( + 'use HasFactory;', + << */ + use HasFactory; + EOT, + app_path('Models/User.php'), + ); + + return $next($data); + } +} diff --git a/src/Pipes/InstallComposerDependencies.php b/src/Pipes/InstallComposerDependencies.php new file mode 100644 index 0000000..dc75087 --- /dev/null +++ b/src/Pipes/InstallComposerDependencies.php @@ -0,0 +1,36 @@ +requireComposerPackages->handle([ + // 'filament/filament', + 'laravel/horizon', + 'laravel/pulse', + 'red-explosion/laravel-sqids', + 'spatie/laravel-data', + ]); + + $this->requireComposerPackages->handle([ + 'larastan/larastan', + 'rector/rector', + 'red-explosion/pint-config', + ], asDev: true); + + return $next($data); + } +} diff --git a/src/Pipes/InstallYarnDependencies.php b/src/Pipes/InstallYarnDependencies.php new file mode 100644 index 0000000..00492d5 --- /dev/null +++ b/src/Pipes/InstallYarnDependencies.php @@ -0,0 +1,30 @@ +filesystem->deleteDirectory(base_path('node_modules')); + $this->filesystem->delete(base_path('package.lock')); + + // TODO: log that we are installing Yarn dependencies + + (new Process(['yarn', 'install'], base_path()))->run(); + + return $next($data); + } +} diff --git a/src/Pipes/PublishStubs.php b/src/Pipes/PublishStubs.php new file mode 100644 index 0000000..9c22305 --- /dev/null +++ b/src/Pipes/PublishStubs.php @@ -0,0 +1,62 @@ +filesystem->copyDirectory( + __DIR__ . '/../../stubs/default/.github', + base_path('.github'), + ); + + $this->filesystem->copyDirectory( + __DIR__ . '/../../stubs/default/app/Support', + app_path('Support'), + ); + + $this->filesystem->copyDirectory( + __DIR__ . '/../../stubs/default/stubs', + base_path('stubs'), + ); + + $this->filesystem->copyDirectory( + __DIR__ . '/../../stubs/default/tests/Arch', + base_path('tests/Arch'), + ); + + $this->filesystem->copy( + __DIR__ . '/../../stubs/default/gitignore', + base_path('.gitignore'), + ); + + $this->filesystem->copy( + __DIR__ . '/../../stubs/default/phpstan.neon.dist', + base_path('phpunit.xml.dist'), + ); + + $this->filesystem->copy( + __DIR__ . '/../../stubs/default/README.md', + base_path('README.md'), + ); + + $this->filesystem->copy( + __DIR__ . '/../../stubs/default/rector.php', + base_path('rector.php'), + ); + + return $next($data); + } +} diff --git a/src/Pipes/RegisterComposerScripts.php b/src/Pipes/RegisterComposerScripts.php new file mode 100644 index 0000000..a436b12 --- /dev/null +++ b/src/Pipes/RegisterComposerScripts.php @@ -0,0 +1,42 @@ +filesystem->json(base_path('composer.json')); + + $contents['scripts'] = array_merge($contents['scripts'], [ + 'lint' => 'pint --config vendor/red-explosion/pint-config/pint.json', + 'refactor' => 'rector', + 'test:lint' => 'pint --config vendor/red-explosion/pint-config/pint.json --test', + 'test:refactor' => 'rector --dry-run', + 'test:types' => 'phpstan analyse', + 'test:arch' => 'pest --filter=arch', + 'test:unit' => 'pest --parallel', + 'test' => [ + '@test:lint', + '@test:refactor', + '@test:types', + '@test:unit', + ], + ]); + + $this->filesystem->put(base_path('composer.json'), json_encode($contents, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + return $next($data); + } +} diff --git a/src/Pipes/RegisterHelpersFile.php b/src/Pipes/RegisterHelpersFile.php new file mode 100644 index 0000000..b9d954f --- /dev/null +++ b/src/Pipes/RegisterHelpersFile.php @@ -0,0 +1,33 @@ +filesystem->json(base_path('composer.json')); + + $composer['autoload']['files'] = [ + 'app/Support/helpers.php', + ]; + + $this->filesystem->put(base_path('composer.json'), json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + (new Process(['composer', 'dump-autoload'], base_path()))->run(); + + return $next($data); + } +} diff --git a/src/Pipes/RemoveDefaultController.php b/src/Pipes/RemoveDefaultController.php new file mode 100644 index 0000000..dc200e2 --- /dev/null +++ b/src/Pipes/RemoveDefaultController.php @@ -0,0 +1,26 @@ +filesystem->delete(base_path('app/Http/Controllers/Controller.php')); + + $this->filesystem->put(base_path('app/Http/Controllers/.gitkeep'), ''); + + return $next($data); + } +} diff --git a/src/Pipes/RemoveDownMigrations.php b/src/Pipes/RemoveDownMigrations.php new file mode 100644 index 0000000..ac1426d --- /dev/null +++ b/src/Pipes/RemoveDownMigrations.php @@ -0,0 +1,32 @@ +filesystem->files(database_path('migrations')); + + foreach ($migrations as $migration) { + $contents = $this->filesystem->get($migration->getPathname()); + + $contents = preg_replace('/public function down\(\): void\s*\{[^}]*\}/s', '', $contents); + + $this->filesystem->put($migration->getPathname(), $contents); + } + + return $next($data); + } +} diff --git a/src/Pipes/RemoveMigrationComments.php b/src/Pipes/RemoveMigrationComments.php new file mode 100644 index 0000000..707b836 --- /dev/null +++ b/src/Pipes/RemoveMigrationComments.php @@ -0,0 +1,32 @@ +filesystem->files(database_path('migrations')); + + foreach ($migrations as $migration) { + $contents = $this->filesystem->get($migration->getPathname()); + + $contents = preg_replace('/\/\*\*[\s\S]*?\*\//', '', $contents); + + $this->filesystem->put($migration->getPathname(), $contents); + } + + return $next($data); + } +} diff --git a/src/Pipes/RunLintScript.php b/src/Pipes/RunLintScript.php new file mode 100644 index 0000000..f11a378 --- /dev/null +++ b/src/Pipes/RunLintScript.php @@ -0,0 +1,19 @@ +run(); + + return $next($data); + } +} diff --git a/src/Pipes/RunRefactorScript.php b/src/Pipes/RunRefactorScript.php new file mode 100644 index 0000000..7dd68e9 --- /dev/null +++ b/src/Pipes/RunRefactorScript.php @@ -0,0 +1,19 @@ +run(); + + return $next($data); + } +} diff --git a/stubs/default/.github/dependabot.yml b/stubs/default/.github/dependabot.yml new file mode 100644 index 0000000..7ac7d31 --- /dev/null +++ b/stubs/default/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 +updates: + # Fetch and update latest `composer` packages + - package-ecosystem: composer + directory: '/' + schedule: + interval: weekly + commit-message: + prefix: fix + prefix-development: chore + include: scope + + # Fetch and update latest `github-actions` packages + - package-ecosystem: github-actions + directory: '/' + schedule: + interval: weekly + commit-message: + prefix: fix + prefix-development: chore + include: scope diff --git a/stubs/default/.github/semantic.yml b/stubs/default/.github/semantic.yml new file mode 100644 index 0000000..40b6927 --- /dev/null +++ b/stubs/default/.github/semantic.yml @@ -0,0 +1,5 @@ +# Always validate the PR title AND all the commits +titleAndCommits: true +# Allows use of Merge commits (eg on github: "Merge branch 'master' into feature/ride-unicorns") +# this is only relevant when using commitsOnly: true (or titleAndCommits: true) +allowMergeCommits: true diff --git a/stubs/default/.github/workflows/dependabot-auto-merge.yml b/stubs/default/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..8da1401 --- /dev/null +++ b/stubs/default/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,33 @@ +name: dependabot-auto-merge + +on: pull_request_target + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v2.2.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Auto-merge Dependabot PRs for semver-minor updates + if: ${{ steps.metadata.outputs.update-type == 'version-update:semver-minor' }} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Auto-merge Dependabot PRs for semver-patch updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/stubs/default/.github/workflows/php-security.yml b/stubs/default/.github/workflows/php-security.yml new file mode 100644 index 0000000..8928e94 --- /dev/null +++ b/stubs/default/.github/workflows/php-security.yml @@ -0,0 +1,24 @@ +name: php security + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + lint: + name: PHP Security + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[ci skip]')" + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run security checks + uses: symfonycorp/security-checker-action@v5 diff --git a/stubs/default/.github/workflows/tests.yml b/stubs/default/.github/workflows/tests.yml new file mode 100644 index 0000000..68d9853 --- /dev/null +++ b/stubs/default/.github/workflows/tests.yml @@ -0,0 +1,74 @@ +name: tests + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + tests: + name: Tests + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[ci skip]')" + + services: + mysql: + image: mysql/mysql-server:8.0 + env: + MYSQL_ROOT_HOST: '%' + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: testing + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + + - name: Cache Dependencies + uses: actions/cache@v4 + with: + path: ~/.composer/cache/files + key: dependencies-php-composer-${{ hashFiles('composer.lock') }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + extensions: json, dom, curl, libxml, mbstring, zip + tools: composer:v2 + coverage: none + + - name: Setup node env + uses: actions/setup-node@v4 + with: + node-version: 22 + check-latest: true + cache: 'yarn' + + - name: Copy example .env + run: cp .env.example .env + + - name: Install PHP dependencies + run: composer install --no-interaction --no-progress --ansi + + - name: Generate application key + run: php artisan key:generate + + - name: Install yarn dependencies + run: yarn install + + - name: Build frontend + run: yarn build + + - name: Run tests + run: composer test + env: + DB_PASSWORD: password + +# - name: Deploy staging +# if: github.ref_name == 'main' +# run: curl ${{ secrets.FORGE_STAGING_HOOK }} diff --git a/stubs/default/README.md b/stubs/default/README.md new file mode 100644 index 0000000..e42ec46 --- /dev/null +++ b/stubs/default/README.md @@ -0,0 +1,104 @@ +# Project Name + +## Holla 👋 + +Hello and welcome to the Project Name codebase. This README has been written to provide you with all the information +you need to get up and running with the project. Like any piece of software, things are constantly changing and it's +easy for information to become outdated. + +At Red Explosion, we take pride in the code we write to help deliver our companies mission. We love what we do and we +take pride in our work. We're so excited to see what contributions you make to this project, but all we ask is that you +keep information such as installation steps, documentation and tests up to date 🙇 + +## Installation + +Project Name is a regular Laravel application; it's build on top of Laravel 11 and uses X for the frontend. If you are +familiar with Laravel, you should feel right at home. + +In terms of local development, you will need the following requirements: + +- PHP 8.3 +- Node.js 22 +- Yarn + +> [!NOTE] +> We recommend using either [Laravel Herd](https://herd.laravel.com/) or [Laravel Valet](https://laravel.com/docs/11.x/valet) +> for local development. + +Once you meet these requirements, you can start by cloning the repository and installing the dependencies: + +```bash +git clone git@github.com/red-explosion/project-name.git + +cd project-name +``` + +Next, install the dependencies using [Composer](https://getcomposer.org) and [Yarn](https://yarnpkg.com): + +```bash +composer install + +yarn install +``` + +After that, set up your `.env` file: + +```bash +cp .env.example .env + +php artisan key:generate +``` + +Run the migrations: + +```bash +php artisan migrate +``` + +Link the storage to the public folder: + +```bash +php artisan storage:link +``` + +In a **separate terminal**, build the assets in watch mode: + +```bash +yarn dev +``` + +Also in a **separate terminal**, run the queue worker: + +```bash +php artisan queue:listen --queue=default +``` + +## Tooling + +Red Explosion uses a few tools to ensure the code quality and consistency. [Pest](https://pestphp.com) is the testing +framework of choice, and we also use [PHPStan](https://phpstan.org) for static analysis. + +For code style, we use [Laravel Pint](https://laravel.com/docs/11.x/pint) to ensure the code is consistent and follows +the Red Explosion project conventions. We also use [Rector](https://getrector.org) to ensure the code is up to date +with the latest PHP version. + +You run these tools individually using the following commands: + +```bash +# Lint the code using Pint +composer lint +composer test:lint + +# Refactor the code using Rector +composer refactor +composer test:refactor + +# Run PHPStan +composer test:types + +# Run the test suite +composer test:unit + +# Run all the tools +composer test +``` diff --git a/stubs/default/app/Support/Type.php b/stubs/default/app/Support/Type.php new file mode 100644 index 0000000..af2e1c0 --- /dev/null +++ b/stubs/default/app/Support/Type.php @@ -0,0 +1,141 @@ +variable + * + * @param class-string $type + * @return TAs + */ + public function as(string $type): mixed + { + if (! is_object($this->variable) || ! $this->variable instanceof $type) { + throw new TypeError("Variable is not a [{$type}]"); + } + + return $this->variable; + } + + /** + * Asserts and narrow down the type to string. + * + * @phpstan-assert-if-true string $this->variable + */ + public function asString(): string + { + if (! is_string($this->variable)) { + throw new TypeError('Variable is not a [string].'); + } + + return $this->variable; + } + + /** + * Asserts and narrow down the type to integer. + * + * @phpstan-assert-if-true int $this->variable + */ + public function asInt(): int + { + if (! is_int($this->variable)) { + throw new TypeError('Variable is not an [integer].'); + } + + return $this->variable; + } + + /** + * Asserts and narrow down the type to float. + * + * @phpstan-assert-if-true float $this->variable + */ + public function asFloat(): float + { + if (! is_float($this->variable)) { + throw new TypeError('Variable is not a [float].'); + } + + return $this->variable; + } + + /** + * Asserts and narrow down the type to boolean. + * + * @phpstan-assert-if-true bool $this->variable + */ + public function asBool(): bool + { + if (! is_bool($this->variable)) { + throw new TypeError('Variable is not a [boolean].'); + } + + return $this->variable; + + } + + /** + * Asserts and narrow down the type to null. + * + * @phpstan-assert-if-true null $this->variable + */ + public function asNull(): null + { + if (! is_null($this->variable)) { + throw new TypeError('Variable is not a [null].'); + } + + return $this->variable; + } + + /** + * Asserts and narrow down the type to array. + * + * @phpstan-assert-if-true array $this->variable + * + * @return (TVariable is array ? TVariable : never) + */ + public function asArray(): array + { + if (! is_array($this->variable)) { + throw new TypeError('Variable is not an array.'); + } + + return $this->variable; + } + + /** + * Asserts and narrow down the type to a callable. + * + * @phpstan-assert callable $this->variable + */ + public function asCallable(): callable + { + if (! is_callable($this->variable)) { + throw new TypeError('Variable is not a [callable].'); + } + + return $this->variable; + } +} diff --git a/stubs/default/app/Support/helpers.php b/stubs/default/app/Support/helpers.php new file mode 100644 index 0000000..a528859 --- /dev/null +++ b/stubs/default/app/Support/helpers.php @@ -0,0 +1,18 @@ + + */ + function type(mixed $variable): Type + { + return new Type($variable); + } +} diff --git a/stubs/default/gitignore b/stubs/default/gitignore new file mode 100644 index 0000000..eab9f1c --- /dev/null +++ b/stubs/default/gitignore @@ -0,0 +1,39 @@ +# OS +.DS_Store + +# IDE +/.fleet +/.idea +/.vscode + +# Package Managers +/node_modules +/vendor + +# Environment +.env +.env.backup +.env.production +.env.staging + +# Framework +/bootstrap/ssr +/public/build +/public/hot +/public/storage +/storage/*.key + +# Laravel Octane +/caddy +frankenphp +frankenphp-worker.php + +# Local Dev +.phpactor.json +.phpunit.cache +.phpunit.result.cache +_ide_helper.php +_ide_helper_models.php +auth.json +phpstan.neon +phpunit.xml.dist diff --git a/stubs/default/phpstan.neon.dist b/stubs/default/phpstan.neon.dist new file mode 100644 index 0000000..0259272 --- /dev/null +++ b/stubs/default/phpstan.neon.dist @@ -0,0 +1,13 @@ +includes: + - vendor/larastan/larastan/extension.neon + +parameters: + checkMissingIterableValueType: true + + level: max + + paths: + - app + - bootstrap + - database/factories + - routes diff --git a/stubs/default/rector.php b/stubs/default/rector.php new file mode 100644 index 0000000..039b62b --- /dev/null +++ b/stubs/default/rector.php @@ -0,0 +1,27 @@ +withPaths([ + __DIR__.'/app', + __DIR__.'/bootstrap/app.php', + __DIR__.'/config', + __DIR__.'/database', + __DIR__.'/public', + ]) + ->withSkip([ + AddOverrideAttributeToOverriddenMethodsRector::class, + ]) + ->withPreparedSets( + deadCode: true, + codeQuality: true, + typeDeclarations: true, + privatization: true, + earlyReturn: true, + strictBooleans: true, + ) + ->withPhpSets(); diff --git a/stubs/default/stubs/migration.create.stub b/stubs/default/stubs/migration.create.stub new file mode 100644 index 0000000..6ec5997 --- /dev/null +++ b/stubs/default/stubs/migration.create.stub @@ -0,0 +1,18 @@ +id(); + $table->timestamps(); + }); + } +}; diff --git a/stubs/default/stubs/migration.stub b/stubs/default/stubs/migration.stub new file mode 100644 index 0000000..cb1b9f6 --- /dev/null +++ b/stubs/default/stubs/migration.stub @@ -0,0 +1,15 @@ +expect('App\Console\Commands') + ->toHaveSuffix('Command') + ->toExtend('Illuminate\Console\Command') + ->toHaveMethod('handle'); diff --git a/stubs/default/tests/Arch/ContractsTest.php b/stubs/default/tests/Arch/ContractsTest.php new file mode 100644 index 0000000..59be6c5 --- /dev/null +++ b/stubs/default/tests/Arch/ContractsTest.php @@ -0,0 +1,7 @@ +expect('App\Contracts') + ->toBeInterfaces(); diff --git a/stubs/default/tests/Arch/DataTest.php b/stubs/default/tests/Arch/DataTest.php new file mode 100644 index 0000000..a30f23e --- /dev/null +++ b/stubs/default/tests/Arch/DataTest.php @@ -0,0 +1,16 @@ +expect('App\Data') + ->toHaveSuffix('Data') + ->ignoring('App\Data\Casts') + ->toExtend('Spatie\LaravelData\Data') + ->ignoring('App\Data\Casts') + ->toHaveConstructor() + ->ignoring('App\Data\Casts'); + +arch('data casts') + ->expect('App\Data\Casts') + ->toImplement('Spatie\LaravelData\Casts\Cast'); diff --git a/stubs/default/tests/Arch/EnumsTest.php b/stubs/default/tests/Arch/EnumsTest.php new file mode 100644 index 0000000..fc68d7b --- /dev/null +++ b/stubs/default/tests/Arch/EnumsTest.php @@ -0,0 +1,8 @@ +expect('App\Enums') + ->toBeEnums() + ->toExtendNothing(); diff --git a/stubs/default/tests/Arch/EventsTest.php b/stubs/default/tests/Arch/EventsTest.php new file mode 100644 index 0000000..632431b --- /dev/null +++ b/stubs/default/tests/Arch/EventsTest.php @@ -0,0 +1,8 @@ +expect('App\Events') + ->toExtendNothing() + ->toHaveConstructor(); diff --git a/stubs/default/tests/Arch/ExceptionsTest.php b/stubs/default/tests/Arch/ExceptionsTest.php new file mode 100644 index 0000000..ef64359 --- /dev/null +++ b/stubs/default/tests/Arch/ExceptionsTest.php @@ -0,0 +1,7 @@ +expect('App\Exceptions') + ->toExtend('Throwable'); diff --git a/stubs/default/tests/Arch/GlobalTest.php b/stubs/default/tests/Arch/GlobalTest.php new file mode 100644 index 0000000..1c834d3 --- /dev/null +++ b/stubs/default/tests/Arch/GlobalTest.php @@ -0,0 +1,15 @@ +expect(['dd', 'ddd', 'dump', 'die', 'var_dump', 'sleep', 'ray']) + ->not->toBeUsed(); + +arch('env helper not to be used') + ->expect('env') + ->not->toBeUsed(); + +arch('strict types are used') + ->expect('App') + ->toUseStrictTypes(); diff --git a/stubs/default/tests/Arch/HttpTest.php b/stubs/default/tests/Arch/HttpTest.php new file mode 100644 index 0000000..4a4386c --- /dev/null +++ b/stubs/default/tests/Arch/HttpTest.php @@ -0,0 +1,30 @@ +expect('App\Http\Controllers') + ->toHaveSuffix('Controller') + ->toExtendNothing(); + +arch('integration connectors') + ->expect([]) + ->toExtend('Saloon\Http\Connector') + ->toOnlyBeUsedIn('App\Services'); + +arch('integration requests') + ->expect([]) + ->toHaveSuffix('Request') + ->toExtend('Saloon\Http\Request') + ->toOnlyBeUsedIn('App\Services'); + +arch('middleware') + ->expect('App\Http\Middleware') + ->toHaveMethod('handle'); + +arch('requests') + ->expect('App\Http\Requests') + ->toHaveSuffix('Request') + ->toExtend('Illuminate\Foundation\Http\FormRequest') + ->toHaveMethod('rules') + ->toOnlyBeUsedIn('App\Http\Controllers'); diff --git a/stubs/default/tests/Arch/JobsTest.php b/stubs/default/tests/Arch/JobsTest.php new file mode 100644 index 0000000..88c0ba0 --- /dev/null +++ b/stubs/default/tests/Arch/JobsTest.php @@ -0,0 +1,10 @@ +expect('App\Jobs') + ->toHaveSuffix('Job') + ->toHaveMethod('handle') + ->toExtendNothing() + ->toImplement('Illuminate\Contracts\Queue\ShouldQueue'); diff --git a/stubs/default/tests/Arch/ListenersTest.php b/stubs/default/tests/Arch/ListenersTest.php new file mode 100644 index 0000000..72784e2 --- /dev/null +++ b/stubs/default/tests/Arch/ListenersTest.php @@ -0,0 +1,8 @@ +expect('App\Listeners') + ->toHaveMethod('handle') + ->not->toBeUsed(); diff --git a/stubs/default/tests/Arch/ModelsTest.php b/stubs/default/tests/Arch/ModelsTest.php new file mode 100644 index 0000000..023410c --- /dev/null +++ b/stubs/default/tests/Arch/ModelsTest.php @@ -0,0 +1,8 @@ +expect('App\Models') + ->toExtend('Illuminate\Database\Eloquent\Model') + ->toHaveMethod('casts'); diff --git a/stubs/default/tests/Arch/NotificationsTest.php b/stubs/default/tests/Arch/NotificationsTest.php new file mode 100644 index 0000000..a2cfc77 --- /dev/null +++ b/stubs/default/tests/Arch/NotificationsTest.php @@ -0,0 +1,12 @@ +expect('App\Notifications') + ->toHaveConstructor() + ->ignoring([ + 'App\Notifications\ResetPassword', + 'App\Notifications\VerifyEmail', + ]) + ->toExtend('Illuminate\Notifications\Notification'); diff --git a/stubs/default/tests/Arch/ObserversTest.php b/stubs/default/tests/Arch/ObserversTest.php new file mode 100644 index 0000000..a7415d3 --- /dev/null +++ b/stubs/default/tests/Arch/ObserversTest.php @@ -0,0 +1,7 @@ +expect('App\Observers') + ->toHaveSuffix('Observer'); diff --git a/stubs/default/tests/Arch/ProvidersTest.php b/stubs/default/tests/Arch/ProvidersTest.php new file mode 100644 index 0000000..2246709 --- /dev/null +++ b/stubs/default/tests/Arch/ProvidersTest.php @@ -0,0 +1,9 @@ +expect('App\Providers') + ->toHaveSuffix('Provider') + ->toExtend('Illuminate\Support\ServiceProvider') + ->not->toBeUsed(); diff --git a/stubs/default/tests/Arch/RulesTest.php b/stubs/default/tests/Arch/RulesTest.php new file mode 100644 index 0000000..c0e6f09 --- /dev/null +++ b/stubs/default/tests/Arch/RulesTest.php @@ -0,0 +1,9 @@ +expect('App\Rules') + ->toImplement('Illuminate\Contracts\Validation\ValidationRule') + ->toHaveMethod('validate') + ->toOnlyBeUsedIn('App\Http\Requests'); diff --git a/stubs/default/tests/Arch/ServicesTest.php b/stubs/default/tests/Arch/ServicesTest.php new file mode 100644 index 0000000..d7c1d3e --- /dev/null +++ b/stubs/default/tests/Arch/ServicesTest.php @@ -0,0 +1,8 @@ +expect('App\Services') + ->toHaveSuffix('Service') + ->toOnlyBeUsedIn('App\Providers'); diff --git a/stubs/default/tests/Arch/ViewModelsTest.php b/stubs/default/tests/Arch/ViewModelsTest.php new file mode 100644 index 0000000..4e070d1 --- /dev/null +++ b/stubs/default/tests/Arch/ViewModelsTest.php @@ -0,0 +1,9 @@ +expect('App\ViewModels') + ->toHaveSuffix('ViewModel') + ->toExtend('Spatie\LaravelData\Data') + ->toHaveConstructor(); diff --git a/stubs/default/tests/Arch/ViewTest.php b/stubs/default/tests/Arch/ViewTest.php new file mode 100644 index 0000000..0eabfd3 --- /dev/null +++ b/stubs/default/tests/Arch/ViewTest.php @@ -0,0 +1,9 @@ +expect('App\View\Components') + ->toExtend('Illuminate\View\Component') + ->toHaveMethod('render') + ->not->toBeUsed();