From aaf8ee4249f28ead4a39bcee84f0a8b5428f5729 Mon Sep 17 00:00:00 2001 From: Refringe Date: Wed, 11 Sep 2024 15:09:27 -0400 Subject: [PATCH 01/14] Chaperone Add chaperone calls to all of the relationships that support it. --- app/Models/License.php | 3 ++- app/Models/Mod.php | 9 ++++++--- app/Models/ModDependency.php | 3 ++- app/Models/ModVersion.php | 3 ++- app/Models/UserRole.php | 3 ++- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/Models/License.php b/app/Models/License.php index c678a35..7c3118b 100644 --- a/app/Models/License.php +++ b/app/Models/License.php @@ -16,6 +16,7 @@ class License extends Model */ public function mods(): HasMany { - return $this->hasMany(Mod::class); + return $this->hasMany(Mod::class) + ->chaperone(); } } diff --git a/app/Models/Mod.php b/app/Models/Mod.php index 84d6e22..7d81d9d 100644 --- a/app/Models/Mod.php +++ b/app/Models/Mod.php @@ -72,7 +72,8 @@ class Mod extends Model { return $this->hasMany(ModVersion::class) ->whereHas('latestSptVersion') - ->orderByDesc('version'); + ->orderByDesc('version') + ->chaperone(); } /** @@ -82,7 +83,8 @@ class Mod extends Model { return $this->hasOne(ModVersion::class) ->whereHas('latestSptVersion') - ->orderByDesc('updated_at'); + ->orderByDesc('updated_at') + ->chaperone(); } /** @@ -114,7 +116,8 @@ class Mod extends Model ->whereHas('sptVersions') ->orderByDesc('version') ->orderByDesc('updated_at') - ->take(1); + ->take(1) + ->chaperone(); } /** diff --git a/app/Models/ModDependency.php b/app/Models/ModDependency.php index e147196..1a0ec75 100644 --- a/app/Models/ModDependency.php +++ b/app/Models/ModDependency.php @@ -31,7 +31,8 @@ class ModDependency extends Model */ public function resolvedDependencies(): HasMany { - return $this->hasMany(ModResolvedDependency::class, 'dependency_id'); + return $this->hasMany(ModResolvedDependency::class, 'dependency_id') + ->chaperone(); } /** diff --git a/app/Models/ModVersion.php b/app/Models/ModVersion.php index c834a41..3221173 100644 --- a/app/Models/ModVersion.php +++ b/app/Models/ModVersion.php @@ -42,7 +42,8 @@ class ModVersion extends Model */ public function dependencies(): HasMany { - return $this->hasMany(ModDependency::class); + return $this->hasMany(ModDependency::class) + ->chaperone(); } /** diff --git a/app/Models/UserRole.php b/app/Models/UserRole.php index e27e57f..bcd2146 100644 --- a/app/Models/UserRole.php +++ b/app/Models/UserRole.php @@ -15,6 +15,7 @@ class UserRole extends Model */ public function users(): HasMany { - return $this->hasMany(User::class); + return $this->hasMany(User::class) + ->chaperone(); } } From df013666976fa0216785adac9e74416ecf225bfd Mon Sep 17 00:00:00 2001 From: Refringe Date: Wed, 11 Sep 2024 16:01:07 -0400 Subject: [PATCH 02/14] Possible Gitea Action Fix Changes MySQL host to `mysql`. --- .env.ci | 4 +-- {.github => .gitea}/README.md | 0 {.github => .gitea}/logo.spt.png | Bin {.github => .gitea}/workflows/quality.yaml | 13 +++++++ {.github => .gitea}/workflows/tests.yaml | 14 ++++++++ .github/dependabot.yml | 40 --------------------- 6 files changed, 29 insertions(+), 42 deletions(-) rename {.github => .gitea}/README.md (100%) rename {.github => .gitea}/logo.spt.png (100%) rename {.github => .gitea}/workflows/quality.yaml (99%) rename {.github => .gitea}/workflows/tests.yaml (99%) delete mode 100644 .github/dependabot.yml diff --git a/.env.ci b/.env.ci index 08b91e4..4138eee 100644 --- a/.env.ci +++ b/.env.ci @@ -9,13 +9,13 @@ LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug DB_CONNECTION=mysql -DB_HOST=127.0.0.1 +DB_HOST=mysql DB_PORT=33306 DB_DATABASE=testing DB_USERNAME=user DB_PASSWORD=password -SCOUT_DRIVER=null +SCOUT_DRIVER=collection FILESYSTEM_DISK=local QUEUE_CONNECTION=sync CACHE_STORE=array diff --git a/.github/README.md b/.gitea/README.md similarity index 100% rename from .github/README.md rename to .gitea/README.md diff --git a/.github/logo.spt.png b/.gitea/logo.spt.png similarity index 100% rename from .github/logo.spt.png rename to .gitea/logo.spt.png diff --git a/.github/workflows/quality.yaml b/.gitea/workflows/quality.yaml similarity index 99% rename from .github/workflows/quality.yaml rename to .gitea/workflows/quality.yaml index 739f925..a755f95 100644 --- a/.github/workflows/quality.yaml +++ b/.gitea/workflows/quality.yaml @@ -16,28 +16,34 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.3' extensions: mbstring, dom, fileinfo coverage: none + - name: Get Composer Cache Directory id: composer-cache run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache composer dependencies uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: ${{ runner.os }}-composer- + - name: Install Composer Dependencies run: composer install --no-progress --prefer-dist --optimize-autoloader + - name: Prepare Laravel Environment run: | php -r "file_exists('.env') || copy('.env.ci', '.env');" php artisan key:generate php artisan optimize + - name: Execute Code Static Analysis with Larastan run: ./vendor/bin/phpstan analyse -c ./phpstan.neon --no-progress --error-format=github @@ -48,30 +54,37 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.3' extensions: mbstring, dom, fileinfo coverage: none + - name: Get Composer Cache Directory id: composer-cache run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache composer dependencies uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: ${{ runner.os }}-composer- + - name: Install Composer Dependencies run: composer install --no-progress --prefer-dist --optimize-autoloader + - name: Prepare Laravel Environment run: | php -r "file_exists('.env') || copy('.env.ci', '.env');" php artisan key:generate php artisan optimize + - name: Run Pint Code Style Fixer run: ./vendor/bin/pint + - uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: Pint PHP Style Fixes [no ci] diff --git a/.github/workflows/tests.yaml b/.gitea/workflows/tests.yaml similarity index 99% rename from .github/workflows/tests.yaml rename to .gitea/workflows/tests.yaml index fc8bf60..2fa2410 100644 --- a/.github/workflows/tests.yaml +++ b/.gitea/workflows/tests.yaml @@ -16,50 +16,64 @@ jobs: ports: - 33306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + steps: - name: Checkout uses: actions/checkout@v4 + - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.3' extensions: mbstring, dom, fileinfo coverage: none + - name: Get Composer Cache Directory id: composer-cache run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache composer dependencies uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: ${{ runner.os }}-composer- + - name: Install Composer Dependencies run: composer install --no-progress --prefer-dist --optimize-autoloader + - name: Get NPM Cache Directory id: npm-cache run: echo "NPM_CACHE_DIR=$(npm config get cache)" >> $GITHUB_ENV + - name: Cache NPM Dependencies uses: actions/cache@v4 with: path: ${{ env.NPM_CACHE_DIR }} key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: ${{ runner.os }}-node- + - name: Install npm dependencies run: npm ci + - name: Build Front-end Assets run: npm run build + - name: Prepare Laravel Environment run: | php -r "file_exists('.env') || copy('.env.ci', '.env');" php artisan key:generate php artisan optimize + - name: Run Database Migrations run: php artisan migrate + - name: Link Storage run: php artisan storage:link + - name: Run Tests run: php artisan test + - name: Display Laravel Log if: failure() run: cat storage/logs/laravel.log diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 4f8bc32..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,40 +0,0 @@ -version: 2 -updates: - # Composer dependencies (PHP) - - package-ecosystem: "composer" - directory: "/" - schedule: - interval: "daily" - time: "15:00" # 10am EST - open-pull-requests-limit: 10 - target-branch: "develop" - labels: - - "dependencies" - assignees: - - "Refringe" - - # GitHub Actions - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - time: "15:00" # 10am EST - open-pull-requests-limit: 10 - target-branch: "develop" - labels: - - "dependencies" - assignees: - - "Refringe" - - # npm modules (JavaScript) - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "daily" - time: "15:00" # 10am EST - open-pull-requests-limit: 10 - target-branch: "develop" - labels: - - "dependencies" - assignees: - - "Refringe" From 1c25b3eddeb99705cc81e088346afb694008e89f Mon Sep 17 00:00:00 2001 From: Refringe Date: Wed, 11 Sep 2024 16:37:08 -0400 Subject: [PATCH 03/14] Workflow: Test MySQL Connection Step --- .gitea/workflows/tests.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitea/workflows/tests.yaml b/.gitea/workflows/tests.yaml index 2fa2410..27a845d 100644 --- a/.gitea/workflows/tests.yaml +++ b/.gitea/workflows/tests.yaml @@ -18,6 +18,11 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: + - name: Verify MySQL connection + run: | + mysql --version + mysql --host mysql --port 33306 -uuser -ppassword -e "SHOW DATABASES" + - name: Checkout uses: actions/checkout@v4 From 290db63093b6770f6195b7f9ce9e28fe2639bb26 Mon Sep 17 00:00:00 2001 From: Refringe Date: Wed, 11 Sep 2024 16:40:05 -0400 Subject: [PATCH 04/14] Workflow: Install MySQL Client --- .gitea/workflows/tests.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/tests.yaml b/.gitea/workflows/tests.yaml index 27a845d..5cd870d 100644 --- a/.gitea/workflows/tests.yaml +++ b/.gitea/workflows/tests.yaml @@ -20,8 +20,9 @@ jobs: steps: - name: Verify MySQL connection run: | + sudo apt-get install -y mysql-client mysql --version - mysql --host mysql --port 33306 -uuser -ppassword -e "SHOW DATABASES" + mysql --host 127.0.0.1 --port 3306 -uuser -ppassword -e "SHOW DATABASES" - name: Checkout uses: actions/checkout@v4 From c7114df136fdb612e0c647974c0fb7c240b71953 Mon Sep 17 00:00:00 2001 From: Refringe Date: Wed, 11 Sep 2024 16:41:02 -0400 Subject: [PATCH 05/14] Workflow: Remove Sudo Call --- .gitea/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/tests.yaml b/.gitea/workflows/tests.yaml index 5cd870d..4adf958 100644 --- a/.gitea/workflows/tests.yaml +++ b/.gitea/workflows/tests.yaml @@ -20,7 +20,7 @@ jobs: steps: - name: Verify MySQL connection run: | - sudo apt-get install -y mysql-client + apt-get install -y mysql-client mysql --version mysql --host 127.0.0.1 --port 3306 -uuser -ppassword -e "SHOW DATABASES" From 30f7d60cc454f1d3c2bc227a67f0e4248dcb6f80 Mon Sep 17 00:00:00 2001 From: Refringe Date: Wed, 11 Sep 2024 16:43:53 -0400 Subject: [PATCH 06/14] Workflow: Use Latest Ubuntu Also, apt update before install. --- .gitea/workflows/quality.yaml | 6 +++--- .gitea/workflows/tests.yaml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/quality.yaml b/.gitea/workflows/quality.yaml index a755f95..4c24ed5 100644 --- a/.gitea/workflows/quality.yaml +++ b/.gitea/workflows/quality.yaml @@ -4,7 +4,7 @@ on: [ push, pull_request ] jobs: security-checker: - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 @@ -12,7 +12,7 @@ jobs: uses: symfonycorp/security-checker-action@v5 larastan: - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 @@ -48,7 +48,7 @@ jobs: run: ./vendor/bin/phpstan analyse -c ./phpstan.neon --no-progress --error-format=github pint-fixer: - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest permissions: contents: write steps: diff --git a/.gitea/workflows/tests.yaml b/.gitea/workflows/tests.yaml index 4adf958..76dde6c 100644 --- a/.gitea/workflows/tests.yaml +++ b/.gitea/workflows/tests.yaml @@ -4,7 +4,7 @@ on: [ push, pull_request ] jobs: laravel-tests: - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest services: mysql: image: mysql:8.3 @@ -20,7 +20,7 @@ jobs: steps: - name: Verify MySQL connection run: | - apt-get install -y mysql-client + apt-get update && apt-get install -y mysql-client mysql --version mysql --host 127.0.0.1 --port 3306 -uuser -ppassword -e "SHOW DATABASES" From 001039b6fc46265367d12b4aeba1925354f91c39 Mon Sep 17 00:00:00 2001 From: Refringe Date: Wed, 11 Sep 2024 16:48:08 -0400 Subject: [PATCH 07/14] Workflow: Updated MySQL Package Name --- .gitea/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/tests.yaml b/.gitea/workflows/tests.yaml index 76dde6c..a0202d6 100644 --- a/.gitea/workflows/tests.yaml +++ b/.gitea/workflows/tests.yaml @@ -20,7 +20,7 @@ jobs: steps: - name: Verify MySQL connection run: | - apt-get update && apt-get install -y mysql-client + apt-get update && apt-get install -qy git curl libmcrypt-dev default-mysql-client mysql --version mysql --host 127.0.0.1 --port 3306 -uuser -ppassword -e "SHOW DATABASES" From 711650fa348bb2d87300fa62b0e3d1691cd6d27e Mon Sep 17 00:00:00 2001 From: Refringe Date: Wed, 11 Sep 2024 16:49:41 -0400 Subject: [PATCH 08/14] Workflow: Updates test mysql connection information --- .gitea/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/tests.yaml b/.gitea/workflows/tests.yaml index a0202d6..9d574b8 100644 --- a/.gitea/workflows/tests.yaml +++ b/.gitea/workflows/tests.yaml @@ -22,7 +22,7 @@ jobs: run: | apt-get update && apt-get install -qy git curl libmcrypt-dev default-mysql-client mysql --version - mysql --host 127.0.0.1 --port 3306 -uuser -ppassword -e "SHOW DATABASES" + mysql --host mysql --port 33306 -uuser -ppassword -e "SHOW DATABASES" - name: Checkout uses: actions/checkout@v4 From d4c62817f1e2e92c3146701121883da7732fc669 Mon Sep 17 00:00:00 2001 From: Refringe Date: Wed, 11 Sep 2024 16:57:12 -0400 Subject: [PATCH 09/14] Workflow: Change Port & Dump HealthCMD --- .gitea/workflows/tests.yaml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/tests.yaml b/.gitea/workflows/tests.yaml index 9d574b8..18d4bc6 100644 --- a/.gitea/workflows/tests.yaml +++ b/.gitea/workflows/tests.yaml @@ -14,15 +14,14 @@ jobs: MYSQL_PASSWORD: password MYSQL_ROOT_PASSWORD: password ports: - - 33306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + - 3306:3306 steps: - - name: Verify MySQL connection + - name: Ready MySQL connection run: | - apt-get update && apt-get install -qy git curl libmcrypt-dev default-mysql-client + apt-get update && apt-get install -qy libmcrypt-dev default-mysql-client mysql --version - mysql --host mysql --port 33306 -uuser -ppassword -e "SHOW DATABASES" + mysql --host mysql --port 3306 -uuser -ppassword -e "SHOW DATABASES" - name: Checkout uses: actions/checkout@v4 From 945062d6f163d8e9a60f64fca1db34bd1be7e16d Mon Sep 17 00:00:00 2001 From: Refringe Date: Wed, 11 Sep 2024 17:00:28 -0400 Subject: [PATCH 10/14] Workflow: Change MySQL Port --- .gitea/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/tests.yaml b/.gitea/workflows/tests.yaml index 18d4bc6..a812ed0 100644 --- a/.gitea/workflows/tests.yaml +++ b/.gitea/workflows/tests.yaml @@ -14,7 +14,7 @@ jobs: MYSQL_PASSWORD: password MYSQL_ROOT_PASSWORD: password ports: - - 3306:3306 + - 33306:3306 steps: - name: Ready MySQL connection From 1bce328db5cfb9172130f30a1f4a60c80eb97f5b Mon Sep 17 00:00:00 2001 From: Refringe Date: Wed, 11 Sep 2024 17:10:26 -0400 Subject: [PATCH 11/14] Workflow: Removes Port Map & Adds HealthCMD --- .gitea/workflows/tests.yaml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.gitea/workflows/tests.yaml b/.gitea/workflows/tests.yaml index a812ed0..7803c49 100644 --- a/.gitea/workflows/tests.yaml +++ b/.gitea/workflows/tests.yaml @@ -13,16 +13,9 @@ jobs: MYSQL_USER: user MYSQL_PASSWORD: password MYSQL_ROOT_PASSWORD: password - ports: - - 33306:3306 + options: --health-cmd="mysql -u user -D testing -ppassword -h mysql -e ''" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - - name: Ready MySQL connection - run: | - apt-get update && apt-get install -qy libmcrypt-dev default-mysql-client - mysql --version - mysql --host mysql --port 3306 -uuser -ppassword -e "SHOW DATABASES" - - name: Checkout uses: actions/checkout@v4 From 0dc21378ab1ed68de47c00045f68d58f29f0c228 Mon Sep 17 00:00:00 2001 From: Refringe Date: Wed, 11 Sep 2024 17:13:38 -0400 Subject: [PATCH 12/14] Workflow: Updates MySQL CI Port --- .env.ci | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.ci b/.env.ci index 4138eee..857f7af 100644 --- a/.env.ci +++ b/.env.ci @@ -10,7 +10,7 @@ LOG_LEVEL=debug DB_CONNECTION=mysql DB_HOST=mysql -DB_PORT=33306 +DB_PORT=3306 DB_DATABASE=testing DB_USERNAME=user DB_PASSWORD=password From bd2d38b4e486773f37ca2247a418fed63acf9ba6 Mon Sep 17 00:00:00 2001 From: Refringe Date: Wed, 11 Sep 2024 23:07:45 -0400 Subject: [PATCH 13/14] Seeder Updaes - Commented out the follower seeding function as it's not yet merged. - SPT Versions are now being generated through the ModVersionFactory. - After initial data has been generated, jobs are called to get the site to a 'ready' state. - Clears cache --- database/seeders/DatabaseSeeder.php | 97 +++++++++++++++++------------ 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6becca0..e2fda8a 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -6,10 +6,11 @@ use App\Models\License; use App\Models\Mod; use App\Models\ModDependency; use App\Models\ModVersion; -use App\Models\SptVersion; use App\Models\User; use App\Models\UserRole; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Artisan; +use Laravel\Prompts\Progress; use function Laravel\Prompts\progress; @@ -20,42 +21,44 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - // how many of each "thing" to make during seeding - $userCount = 100; - $modCount = 300; - $modVersionCount = 3000; + // How many of each entity to create. + $counts = [ + 'license' => 10, + 'administrator' => 5, + 'moderator' => 5, + 'user' => 100, + 'mod' => 200, + 'modVersion' => 1500, + ]; - // Create a few SPT versions. - $spt_versions = SptVersion::factory(30)->create(); + // Licenses + $licenses = License::factory($counts['license'])->create(); - // Create some code licenses. - $licenses = License::factory(10)->create(); - - // Add administrators. + // Administrator Users $administratorRole = UserRole::factory()->administrator()->create(); $testAccount = User::factory()->for($administratorRole, 'role')->create([ 'email' => 'test@example.com', ]); + User::factory($counts['administrator'] - 1)->for($administratorRole, 'role')->create(); - $this->command->outputComponents()->info("test account created: $testAccount->email"); + $this->command->outputComponents()->info("Test account created: {$testAccount->email}"); - User::factory(4)->for($administratorRole, 'role')->create(); - - // Add moderators. + // Moderator Users $moderatorRole = UserRole::factory()->moderator()->create(); - User::factory(5)->for($moderatorRole, 'role')->create(); + User::factory($counts['moderator'])->for($moderatorRole, 'role')->create(); - // Add users + // Users progress( - label: 'adding users...', - steps: $userCount, + label: 'Adding Users...', + steps: $counts['user'], callback: fn () => User::factory()->create() ); - // get all users + // All Users $allUsers = User::all(); - // Add user follows + /* We got a little ahead of ourselves here. This hasn't been merged yet! + // User Follows progress( label: 'adding user follows ...', steps: $allUsers, @@ -73,18 +76,20 @@ class DatabaseSeeder extends Seeder $user->following()->attach($following); } }); + */ + // Mods $mods = collect(progress( - label: 'adding mods...', - steps: $modCount, + label: 'Adding Mods...', + steps: $counts['mod'], callback: fn () => Mod::factory()->recycle([$licenses])->create() )); - // attach users to mods + // Attach users to mods progress( - label: 'attaching mod users ...', + label: 'Attaching users to mods...', steps: $mods, - callback: function ($mod) use ($allUsers) { + callback: function (Mod $mod, Progress $progress) use ($allUsers) { $userIds = $allUsers->random(rand(1, 3))->pluck('id')->toArray(); $mod->users()->attach($userIds); } @@ -92,26 +97,40 @@ class DatabaseSeeder extends Seeder // Add mod versions, assigning them to the mods we just created. $modVersions = collect(progress( - label: 'adding mods versions ...', - steps: $modVersionCount, - callback: fn () => ModVersion::factory()->recycle([$mods, $spt_versions])->create() + label: 'Adding Mod Versions...', + steps: $counts['modVersion'], + callback: fn () => ModVersion::factory()->recycle([$mods])->create() )); - // Add ModDependencies to a subset of ModVersions. + // Add mod dependencies to *some* mod versions. progress( - label: 'adding mods dependencies ...', + label: 'Adding Mod Dependencies...', steps: $modVersions, - callback: function ($modVersion) use ($mods) { - $hasDependencies = rand(0, 100) < 30; // 30% chance to have dependencies - if ($hasDependencies) { - $dependencyMods = $mods->random(rand(1, 3)); // 1 to 3 dependencies - foreach ($dependencyMods as $dependencyMod) { - ModDependency::factory()->recycle([$modVersion, $dependencyMod])->create(); - } + callback: function (ModVersion $modVersion, Progress $progress) use ($mods) { + // 70% chance to not have dependencies + if (rand(0, 9) >= 3) { + return; + } + + // Choose 1-3 random mods to be dependencies. + $dependencyMods = $mods->random(rand(1, 3)); + foreach ($dependencyMods as $dependencyMod) { + ModDependency::factory()->recycle([$modVersion, $dependencyMod])->create(); } } ); - $this->command->outputComponents()->success('Database seeded'); + $this->command->outputComponents()->success('Initial seeding complete'); + + Artisan::call('app:search-sync'); + Artisan::call('app:resolve-versions'); + Artisan::call('app:count-mods'); + Artisan::call('app:update-downloads'); + $this->command->outputComponents()->warn('Jobs added to queue. Ensure Horizon is running!'); + + Artisan::call('cache:clear'); + $this->command->outputComponents()->info('Cache cleared'); + + $this->command->outputComponents()->success('Database seeding complete'); } } From bbb8fab1a1fdf6131d299522ac41e13526b2f9c3 Mon Sep 17 00:00:00 2001 From: Refringe Date: Wed, 11 Sep 2024 23:08:58 -0400 Subject: [PATCH 14/14] Resolves Some Larastan Issues --- app/Http/Controllers/Api/V0/ApiController.php | 10 +++++- app/Http/Controllers/Api/V0/ModController.php | 21 ++++------- .../Controllers/Api/V0/UsersController.php | 21 ++++------- app/Http/Controllers/ModController.php | 18 ++++------ app/Http/Controllers/UserController.php | 3 +- app/Http/Filters/ModFilter.php | 22 ++++++++++-- app/Models/UserRole.php | 4 +++ app/Notifications/ResetPassword.php | 5 +++ app/Notifications/VerifyEmail.php | 5 +++ app/Services/DependencyVersionService.php | 2 ++ app/Services/SptVersionService.php | 2 ++ app/Traits/ApiResponses.php | 13 +++++++ app/Traits/HasCoverPhoto.php | 14 ++++---- app/View/Components/ModList.php | 13 ++++++- app/View/Components/ModListSection.php | 35 +++++++++++++++++++ app/View/Components/ModListStats.php | 6 ++-- database/factories/LicenseFactory.php | 3 ++ database/factories/ModDependencyFactory.php | 3 ++ database/factories/ModFactory.php | 12 +++---- database/factories/ModVersionFactory.php | 3 ++ database/factories/SptVersionFactory.php | 3 ++ database/factories/UserFactory.php | 6 ++++ database/factories/UserRoleFactory.php | 3 ++ .../2024_05_14_040126_create_pulse_tables.php | 6 ++-- 24 files changed, 170 insertions(+), 63 deletions(-) diff --git a/app/Http/Controllers/Api/V0/ApiController.php b/app/Http/Controllers/Api/V0/ApiController.php index 588e047..152e1fe 100644 --- a/app/Http/Controllers/Api/V0/ApiController.php +++ b/app/Http/Controllers/Api/V0/ApiController.php @@ -4,16 +4,24 @@ namespace App\Http\Controllers\Api\V0; use App\Http\Controllers\Controller; use Illuminate\Support\Str; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; class ApiController extends Controller { /** * Determine if the given relationship should be included in the request. If more than one relationship is provided, * only one needs to be present in the request for this method to return true. + * + * @param string|string[] $relationships */ public static function shouldInclude(string|array $relationships): bool { - $param = request()->get('include'); + try { + $param = request()->get('include'); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + return false; + } if (! $param) { return false; diff --git a/app/Http/Controllers/Api/V0/ModController.php b/app/Http/Controllers/Api/V0/ModController.php index 9065bbc..29738af 100644 --- a/app/Http/Controllers/Api/V0/ModController.php +++ b/app/Http/Controllers/Api/V0/ModController.php @@ -7,13 +7,15 @@ use App\Http\Requests\Api\V0\StoreModRequest; use App\Http\Requests\Api\V0\UpdateModRequest; use App\Http\Resources\Api\V0\ModResource; use App\Models\Mod; +use Illuminate\Http\Resources\Json\AnonymousResourceCollection; +use Illuminate\Http\Resources\Json\JsonResource; class ModController extends ApiController { /** * Display a listing of the resource. */ - public function index(ModFilter $filters) + public function index(ModFilter $filters): AnonymousResourceCollection { return ModResource::collection(Mod::filter($filters)->paginate()); } @@ -21,15 +23,12 @@ class ModController extends ApiController /** * Store a newly created resource in storage. */ - public function store(StoreModRequest $request) - { - // - } + public function store(StoreModRequest $request): void {} /** * Display the specified resource. */ - public function show(Mod $mod) + public function show(Mod $mod): JsonResource { return new ModResource($mod); } @@ -37,16 +36,10 @@ class ModController extends ApiController /** * Update the specified resource in storage. */ - public function update(UpdateModRequest $request, Mod $mod) - { - // - } + public function update(UpdateModRequest $request, Mod $mod): void {} /** * Remove the specified resource from storage. */ - public function destroy(Mod $mod) - { - // - } + public function destroy(Mod $mod): void {} } diff --git a/app/Http/Controllers/Api/V0/UsersController.php b/app/Http/Controllers/Api/V0/UsersController.php index 8bf7824..ca1cd62 100644 --- a/app/Http/Controllers/Api/V0/UsersController.php +++ b/app/Http/Controllers/Api/V0/UsersController.php @@ -7,13 +7,15 @@ use App\Http\Requests\Api\V0\StoreUserRequest; use App\Http\Requests\Api\V0\UpdateUserRequest; use App\Http\Resources\Api\V0\UserResource; use App\Models\User; +use Illuminate\Http\Resources\Json\AnonymousResourceCollection; +use Illuminate\Http\Resources\Json\JsonResource; class UsersController extends ApiController { /** * Display a listing of the resource. */ - public function index(UserFilter $filters) + public function index(UserFilter $filters): AnonymousResourceCollection { return UserResource::collection(User::filter($filters)->paginate()); } @@ -21,15 +23,12 @@ class UsersController extends ApiController /** * Store a newly created resource in storage. */ - public function store(StoreUserRequest $request) - { - // - } + public function store(StoreUserRequest $request): void {} /** * Display the specified resource. */ - public function show(User $user) + public function show(User $user): JsonResource { return new UserResource($user); } @@ -37,16 +36,10 @@ class UsersController extends ApiController /** * Update the specified resource in storage. */ - public function update(UpdateUserRequest $request, User $user) - { - // - } + public function update(UpdateUserRequest $request, User $user): void {} /** * Remove the specified resource from storage. */ - public function destroy(User $user) - { - // - } + public function destroy(User $user): void {} } diff --git a/app/Http/Controllers/ModController.php b/app/Http/Controllers/ModController.php index 939d5ae..22b4922 100644 --- a/app/Http/Controllers/ModController.php +++ b/app/Http/Controllers/ModController.php @@ -6,26 +6,27 @@ use App\Http\Requests\ModRequest; use App\Http\Resources\ModResource; use App\Models\Mod; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\View\View; class ModController extends Controller { use AuthorizesRequests; - public function index() + public function index(): View { $this->authorize('viewAny', Mod::class); return view('mod.index'); } - public function store(ModRequest $request) + public function store(ModRequest $request): ModResource { $this->authorize('create', Mod::class); return new ModResource(Mod::create($request->validated())); } - public function show(int $modId, string $slug) + public function show(int $modId, string $slug): View { $mod = Mod::with([ 'versions', @@ -47,7 +48,7 @@ class ModController extends Controller return view('mod.show', compact(['mod'])); } - public function update(ModRequest $request, Mod $mod) + public function update(ModRequest $request, Mod $mod): ModResource { $this->authorize('update', $mod); @@ -56,12 +57,5 @@ class ModController extends Controller return new ModResource($mod); } - public function destroy(Mod $mod) - { - $this->authorize('delete', $mod); - - $mod->delete(); - - return response()->json(); - } + public function destroy(Mod $mod): void {} } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index a8e86d5..b0554cc 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -5,12 +5,13 @@ namespace App\Http\Controllers; use App\Models\User; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Http\Request; +use Illuminate\View\View; class UserController extends Controller { use AuthorizesRequests; - public function show(Request $request, User $user, string $username) + public function show(Request $request, User $user, string $username): View { if ($user->slug() !== $username) { abort(404); diff --git a/app/Http/Filters/ModFilter.php b/app/Http/Filters/ModFilter.php index 0fa8a39..b4ca066 100644 --- a/app/Http/Filters/ModFilter.php +++ b/app/Http/Filters/ModFilter.php @@ -11,14 +11,21 @@ class ModFilter { /** * The query builder instance for the mod model. + * + * @var Builder */ protected Builder $builder; /** * The filter that should be applied to the query. + * + * @var array */ protected array $filters; + /** + * @param array $filters + */ public function __construct(array $filters) { $this->builder = $this->baseQuery(); @@ -27,6 +34,8 @@ class ModFilter /** * The base query for the mod listing. + * + * @return Builder */ private function baseQuery(): Builder { @@ -49,6 +58,8 @@ class ModFilter /** * Apply the filters to the query. + * + * @return Builder */ public function apply(): Builder { @@ -58,13 +69,13 @@ class ModFilter } } - //dd($this->builder->toRawSql()); - return $this->builder; } /** * Order the query by the given type. + * + * @return Builder */ private function order(string $type): Builder { @@ -92,6 +103,8 @@ class ModFilter /** * Filter the results by the given search term. + * + * @return Builder */ private function query(string $term): Builder { @@ -100,6 +113,8 @@ class ModFilter /** * Filter the results by the featured status. + * + * @return Builder */ private function featured(string $option): Builder { @@ -112,6 +127,9 @@ class ModFilter /** * Filter the results to specific SPT versions. + * + * @param array $versions + * @return Builder */ private function sptVersions(array $versions): Builder { diff --git a/app/Models/UserRole.php b/app/Models/UserRole.php index bcd2146..e56dfe3 100644 --- a/app/Models/UserRole.php +++ b/app/Models/UserRole.php @@ -2,16 +2,20 @@ namespace App\Models; +use Database\Factories\UserFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; class UserRole extends Model { + /** @use HasFactory */ use HasFactory; /** * The relationship between a user role and users. + * + * @return HasMany */ public function users(): HasMany { diff --git a/app/Notifications/ResetPassword.php b/app/Notifications/ResetPassword.php index 85c4838..9545c9a 100644 --- a/app/Notifications/ResetPassword.php +++ b/app/Notifications/ResetPassword.php @@ -18,6 +18,11 @@ class ResetPassword extends OriginalResetPassword implements ShouldQueue parent::__construct($token); } + /** + * Get the array representation of the notification. + * + * @return array + */ public function toArray(object $notifiable): array { return []; diff --git a/app/Notifications/VerifyEmail.php b/app/Notifications/VerifyEmail.php index 4dde9e5..fc3fd55 100644 --- a/app/Notifications/VerifyEmail.php +++ b/app/Notifications/VerifyEmail.php @@ -13,6 +13,11 @@ class VerifyEmail extends OriginalVerifyEmail implements ShouldQueue { use Queueable; + /** + * Get the array representation of the notification. + * + * @return array + */ public function toArray(object $notifiable): array { return []; diff --git a/app/Services/DependencyVersionService.php b/app/Services/DependencyVersionService.php index 556314e..9598ade 100644 --- a/app/Services/DependencyVersionService.php +++ b/app/Services/DependencyVersionService.php @@ -18,6 +18,8 @@ class DependencyVersionService /** * Satisfies all dependency constraints of a ModVersion. + * + * @return array> */ private function satisfyConstraint(ModVersion $modVersion): array { diff --git a/app/Services/SptVersionService.php b/app/Services/SptVersionService.php index 8eba213..2a8aa53 100644 --- a/app/Services/SptVersionService.php +++ b/app/Services/SptVersionService.php @@ -19,6 +19,8 @@ class SptVersionService /** * Satisfies the version constraint of a given ModVersion. Returns the ID of the satisfying SptVersion. + * + * @return array */ private function satisfyConstraint(ModVersion $modVersion): array { diff --git a/app/Traits/ApiResponses.php b/app/Traits/ApiResponses.php index f26781d..dfd4345 100644 --- a/app/Traits/ApiResponses.php +++ b/app/Traits/ApiResponses.php @@ -6,11 +6,21 @@ use Illuminate\Http\JsonResponse; trait ApiResponses { + /** + * Return a success JSON response. + * + * @param array $data + */ protected function success(string $message, ?array $data = []): JsonResponse { return $this->baseResponse(message: $message, data: $data, code: 200); } + /** + * The base response. + * + * @param array $data + */ private function baseResponse(?string $message = '', ?array $data = [], ?int $code = 200): JsonResponse { return response()->json([ @@ -19,6 +29,9 @@ trait ApiResponses ], $code); } + /** + * Return an error JSON response. + */ protected function error(string $message, int $code): JsonResponse { return $this->baseResponse(message: $message, code: $code); diff --git a/app/Traits/HasCoverPhoto.php b/app/Traits/HasCoverPhoto.php index ce429e3..d52adb4 100644 --- a/app/Traits/HasCoverPhoto.php +++ b/app/Traits/HasCoverPhoto.php @@ -11,7 +11,7 @@ trait HasCoverPhoto /** * Update the user's cover photo. */ - public function updateCoverPhoto(UploadedFile $cover, $storagePath = 'cover-photos'): void + public function updateCoverPhoto(UploadedFile $cover, string $storagePath = 'cover-photos'): void { tap($this->cover_photo_path, function ($previous) use ($cover, $storagePath) { $this->forceFill([ @@ -51,15 +51,17 @@ trait HasCoverPhoto } /** - * Get the URL to the user's cover photo. + * Get the cover photo URL for the user. + * + * @return Attribute */ public function coverPhotoUrl(): Attribute { - return Attribute::get(function (): string { - return $this->cover_photo_path + return new Attribute( + get: fn (): string => $this->cover_photo_path ? Storage::disk($this->coverPhotoDisk())->url($this->cover_photo_path) - : $this->defaultCoverPhotoUrl(); - }); + : $this->defaultCoverPhotoUrl() + ); } /** diff --git a/app/View/Components/ModList.php b/app/View/Components/ModList.php index 6d46793..d4a8369 100644 --- a/app/View/Components/ModList.php +++ b/app/View/Components/ModList.php @@ -2,17 +2,28 @@ namespace App\View\Components; +use App\Models\Mod; use Illuminate\Contracts\View\View; use Illuminate\Database\Eloquent\Collection; use Illuminate\View\Component; class ModList extends Component { + /** + * The mods to display. + * + * @var Collection + */ public Collection $mods; public string $versionScope; - public function __construct($mods, $versionScope) + /** + * Create a new component instance. + * + * @param Collection $mods + */ + public function __construct(Collection $mods, string $versionScope) { $this->mods = $mods; $this->versionScope = $versionScope; diff --git a/app/View/Components/ModListSection.php b/app/View/Components/ModListSection.php index f9c257c..be87d7c 100644 --- a/app/View/Components/ModListSection.php +++ b/app/View/Components/ModListSection.php @@ -11,10 +11,25 @@ use Illuminate\View\Component; class ModListSection extends Component { + /** + * The featured mods listed on the homepage. + * + * @var Collection + */ public Collection $modsFeatured; + /** + * The latest mods listed on the homepage. + * + * @var Collection + */ public Collection $modsLatest; + /** + * The last updated mods listed on the homepage. + * + * @var Collection + */ public Collection $modsUpdated; public function __construct() @@ -24,6 +39,11 @@ class ModListSection extends Component $this->modsUpdated = $this->fetchUpdatedMods(); } + /** + * Fetches the featured mods homepage listing. + * + * @return Collection + */ private function fetchFeaturedMods(): Collection { return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'downloads']) @@ -39,6 +59,11 @@ class ModListSection extends Component ->get(); } + /** + * Fetches the latest mods homepage listing. + * + * @return Collection + */ private function fetchLatestMods(): Collection { return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at', 'downloads']) @@ -53,6 +78,11 @@ class ModListSection extends Component ->get(); } + /** + * Fetches the recently updated mods homepage listing. + * + * @return Collection + */ private function fetchUpdatedMods(): Collection { return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'downloads']) @@ -81,6 +111,11 @@ class ModListSection extends Component ]); } + /** + * Prepare the sections for the homepage mod lists. + * + * @return array> + */ public function getSections(): array { return [ diff --git a/app/View/Components/ModListStats.php b/app/View/Components/ModListStats.php index a267d7c..3591b3c 100644 --- a/app/View/Components/ModListStats.php +++ b/app/View/Components/ModListStats.php @@ -2,14 +2,16 @@ namespace App\View\Components; +use App\Models\Mod; +use App\Models\ModVersion; use Illuminate\Contracts\View\View; use Illuminate\View\Component; class ModListStats extends Component { public function __construct( - public $mod, - public $modVersion + public Mod $mod, + public ModVersion $modVersion ) {} public function render(): View diff --git a/database/factories/LicenseFactory.php b/database/factories/LicenseFactory.php index 0bb8660..42d7fbb 100644 --- a/database/factories/LicenseFactory.php +++ b/database/factories/LicenseFactory.php @@ -6,6 +6,9 @@ use App\Models\License; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Carbon; +/** + * @extends Factory + */ class LicenseFactory extends Factory { protected $model = License::class; diff --git a/database/factories/ModDependencyFactory.php b/database/factories/ModDependencyFactory.php index b3318ac..22a0508 100644 --- a/database/factories/ModDependencyFactory.php +++ b/database/factories/ModDependencyFactory.php @@ -8,6 +8,9 @@ use App\Models\ModVersion; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Carbon; +/** + * @extends Factory + */ class ModDependencyFactory extends Factory { protected $model = ModDependency::class; diff --git a/database/factories/ModFactory.php b/database/factories/ModFactory.php index a3167b4..c818a49 100644 --- a/database/factories/ModFactory.php +++ b/database/factories/ModFactory.php @@ -7,25 +7,23 @@ use App\Models\Mod; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Carbon; use Illuminate\Support\Str; -use Random\RandomException; +/** + * @extends Factory + */ class ModFactory extends Factory { protected $model = Mod::class; - /** - * @throws RandomException - */ public function definition(): array { - - $name = fake()->catchPhrase(); + $name = fake()->sentence(rand(3, 5)); return [ 'name' => $name, 'slug' => Str::slug($name), 'teaser' => fake()->sentence(), - 'description' => fake()->paragraphs(random_int(4, 20), true), + 'description' => fake()->paragraphs(rand(4, 20), true), 'license_id' => License::factory(), 'source_code_link' => fake()->url(), 'featured' => fake()->boolean(), diff --git a/database/factories/ModVersionFactory.php b/database/factories/ModVersionFactory.php index 716fd20..ade126e 100644 --- a/database/factories/ModVersionFactory.php +++ b/database/factories/ModVersionFactory.php @@ -8,6 +8,9 @@ use App\Models\SptVersion; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Carbon; +/** + * @extends Factory + */ class ModVersionFactory extends Factory { protected $model = ModVersion::class; diff --git a/database/factories/SptVersionFactory.php b/database/factories/SptVersionFactory.php index 5adf33a..707ed46 100644 --- a/database/factories/SptVersionFactory.php +++ b/database/factories/SptVersionFactory.php @@ -6,6 +6,9 @@ use App\Models\SptVersion; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Carbon; +/** + * @extends Factory + */ class SptVersionFactory extends Factory { protected $model = SptVersion::class; diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 75f3298..3b311ce 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -2,10 +2,14 @@ namespace Database\Factories; +use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; +/** + * @extends Factory + */ class UserFactory extends Factory { /** @@ -13,6 +17,8 @@ class UserFactory extends Factory */ protected static ?string $password; + protected $model = User::class; + /** * Define the user's default state. */ diff --git a/database/factories/UserRoleFactory.php b/database/factories/UserRoleFactory.php index 0e7d165..20b0a2b 100644 --- a/database/factories/UserRoleFactory.php +++ b/database/factories/UserRoleFactory.php @@ -6,6 +6,9 @@ use App\Models\UserRole; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Carbon; +/** + * @extends Factory + */ class UserRoleFactory extends Factory { protected $model = UserRole::class; diff --git a/database/migrations/2024_05_14_040126_create_pulse_tables.php b/database/migrations/2024_05_14_040126_create_pulse_tables.php index 5d194e2..3335394 100644 --- a/database/migrations/2024_05_14_040126_create_pulse_tables.php +++ b/database/migrations/2024_05_14_040126_create_pulse_tables.php @@ -23,7 +23,7 @@ return new class extends PulseMigration match ($this->driver()) { 'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), - 'sqlite' => $table->string('key_hash'), + default => $table->string('key_hash'), }; $table->mediumText('value'); @@ -40,7 +40,7 @@ return new class extends PulseMigration match ($this->driver()) { 'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), - 'sqlite' => $table->string('key_hash'), + default => $table->string('key_hash'), }; $table->bigInteger('value')->nullable(); @@ -59,7 +59,7 @@ return new class extends PulseMigration match ($this->driver()) { 'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), - 'sqlite' => $table->string('key_hash'), + default => $table->string('key_hash'), }; $table->string('aggregate'); $table->decimal('value', 20, 2);