Compare commits

...

10 commits

Author SHA1 Message Date
1cb9d51c2f
Update to Laravel 12 2025-02-25 20:39:36 -08:00
71d401b138
testing improvements 2025-02-25 18:42:57 -08:00
7ec1c2b7f0
testing 2025-02-24 20:31:25 -08:00
de6c64bb14
testing 2025-02-23 21:14:39 -08:00
450dba1d74
testing 2025-02-23 20:44:23 -08:00
9385a4b7f1
restore deleted categories 2025-02-23 17:41:50 -08:00
5f97900e30
disable mailpit 2025-02-23 16:34:33 -08:00
350fdd3520
programs cont 2025-02-23 13:26:13 -08:00
a3bebf3976
programs 2025-02-23 13:13:47 -08:00
494fe698b0
session data 2025-02-23 12:57:03 -08:00
49 changed files with 1475 additions and 163 deletions

View file

@ -6,6 +6,7 @@
use App\Models\Category;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\View;
class CategoryController extends Controller
@ -16,8 +17,10 @@ public function index()
{
$this->authorize('viewAny', Category::class);
return View::make('categories.index',
['categories' => Category::all()]);
return View::make('categories.index', [
'categories' => Category::all(),
'trashedCategories' => Category::onlyTrashed()->get()
]);
}
public function create()
@ -33,6 +36,7 @@ public function store(CategoryRequest $request)
Category::create($request->validated());
$request->session()->flash('status', 'Category added!');
return Redirect::route('categories.index');
}
@ -40,6 +44,8 @@ public function show(Category $category)
{
$this->authorize('view', $category);
$category->load('programs');
return View::make('categories.show', ['category' => $category]);
}
@ -52,9 +58,17 @@ public function edit(Category $category)
public function update(CategoryRequest $request, Category $category)
{
$this->authorize('update', $category);
if ($request->has('restore')) {
$this->authorize('restore', $category);
$category->update($request->validated());
$category->restore();
$request->session()->flash('status', 'Category restored!');
} else {
$this->authorize('update', $category);
$category->update($request->validated());
$request->session()->flash('status', 'Category updated!');
}
return Redirect::route('categories.index');
}
@ -65,6 +79,7 @@ public function destroy(Category $category)
$category->delete();
return response()->json();
Request::session()->flash('status', 'Category deleted!');
return Redirect::route('categories.index');
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ProgramRequest;
use App\Models\Program;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\View;
class ProgramController extends Controller
{
use AuthorizesRequests;
public function index()
{
$this->authorize('viewAny', Program::class);
return View::make('programs.index', ['programs' => Program::all()]);
}
public function store(ProgramRequest $request)
{
$this->authorize('create', Program::class);
return Program::create($request->validated());
}
public function show(Program $program)
{
$this->authorize('view', $program);
return $program;
}
public function update(ProgramRequest $request, Program $program)
{
$this->authorize('update', $program);
$program->update($request->validated());
return $program;
}
public function destroy(Program $program)
{
$this->authorize('delete', $program);
$program->delete();
return response()->json();
}
}

View file

@ -2,7 +2,6 @@
namespace App\Http\Requests;
use App\Models\Category;
use Illuminate\Foundation\Http\FormRequest;
class CategoryRequest extends FormRequest
@ -10,18 +9,13 @@ class CategoryRequest extends FormRequest
public function rules(): array
{
return [
'name' => ['required'],
'name' => ['sometimes', 'required', 'string'],
'restore' => ['sometimes', 'required', 'boolean']
];
}
public function authorize(): bool
{
if ($this->routeIs('categories.store')) {
return $this->user()->can('create', Category::class);
} elseif ($this->routeIs('categories.update')) {
return $this->user()->can('update', $this->route('category'));
}
return false;
return true;
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use App\Models\Program;
use Illuminate\Foundation\Http\FormRequest;
class ProgramRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required'],
'category_id' => ['required', 'exists:categories'],
'description' => ['nullable'],
'website' => ['nullable'],
];
}
public function authorize(): bool
{
if ($this->routeIs('programs.store')) {
return $this->user()->can('create', Program::class);
} elseif ($this->routeIs('programs.update')) {
return $this->user()->can('update', $this->route('program'));
}
return false;
}
}

View file

@ -4,6 +4,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Category extends Model
@ -13,4 +14,9 @@ class Category extends Model
protected $fillable = [
'name',
];
public function programs(): HasMany
{
return $this->hasMany(Program::class);
}
}

26
app/Models/Program.php Normal file
View file

@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class Program extends Model
{
use HasFactory;
use SoftDeletes;
protected $fillable = [
'name',
'category_id',
'description',
'website',
];
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
}

View file

@ -11,12 +11,12 @@ class CategoryPolicy
{
use HandlesAuthorization;
public function viewAny(User $user): bool
public function viewAny(?User $user): bool
{
return true;
}
public function view(User $user, Category $category): bool
public function view(?User $user, Category $category): bool
{
return true;
}

View file

@ -0,0 +1,48 @@
<?php
namespace App\Policies;
use App\Models\Program;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Support\Facades\Auth;
class ProgramPolicy
{
use HandlesAuthorization;
public function viewAny(User $user): bool
{
return true;
}
public function view(User $user, Program $program): bool
{
return true;
}
public function create(User $user): bool
{
return Auth::check();
}
public function update(User $user, Program $program): bool
{
return Auth::check();
}
public function delete(User $user, Program $program): bool
{
return Auth::check();
}
public function restore(User $user, Program $program): bool
{
return Auth::check();
}
public function forceDelete(User $user, Program $program): bool
{
return Auth::check();
}
}

View file

@ -9,19 +9,20 @@
],
"license": "MIT",
"require": {
"php": "^8.2",
"laravel/framework": "^11.31",
"laravel/tinker": "^2.9"
"php": "^8.4",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/breeze": "^2.3",
"laravel/pail": "^1.1",
"laravel/pint": "^1.13",
"laravel/sail": "^1.26",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.1",
"pestphp/pest": "^3.7",
"defstudio/pest-plugin-laravel-expectations": "^2.4",
"fakerphp/faker": "^1.24.1",
"laravel/breeze": "^2.3.5",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.21",
"laravel/sail": "^1.41",
"mockery/mockery": "^1.6.12",
"nunomaduro/collision": "^8.6.1",
"pestphp/pest": "^3.7.4",
"pestphp/pest-plugin-laravel": "^3.1"
},
"autoload": {

225
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "a739267a391adcba09c97097cbad9f52",
"content-hash": "eb1e2c5c0d22bcee0ed64e63a3619f9f",
"packages": [
{
"name": "brick/math",
@ -1056,20 +1056,20 @@
},
{
"name": "laravel/framework",
"version": "v11.43.0",
"version": "v12.0.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "70760d976486310b11d8e487e873077db069e77a"
"reference": "d99e2385a6d4324782d52f4423891966425641be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/70760d976486310b11d8e487e873077db069e77a",
"reference": "70760d976486310b11d8e487e873077db069e77a",
"url": "https://api.github.com/repos/laravel/framework/zipball/d99e2385a6d4324782d52f4423891966425641be",
"reference": "d99e2385a6d4324782d52f4423891966425641be",
"shasum": ""
},
"require": {
"brick/math": "^0.9.3|^0.10.2|^0.11|^0.12",
"brick/math": "^0.11|^0.12",
"composer-runtime-api": "^2.2",
"doctrine/inflector": "^2.0.5",
"dragonmantank/cron-expression": "^3.4",
@ -1084,32 +1084,32 @@
"fruitcake/php-cors": "^1.3",
"guzzlehttp/guzzle": "^7.8.2",
"guzzlehttp/uri-template": "^1.0",
"laravel/prompts": "^0.1.18|^0.2.0|^0.3.0",
"laravel/prompts": "^0.3.0",
"laravel/serializable-closure": "^1.3|^2.0",
"league/commonmark": "^2.6",
"league/flysystem": "^3.25.1",
"league/flysystem-local": "^3.25.1",
"league/uri": "^7.5.1",
"monolog/monolog": "^3.0",
"nesbot/carbon": "^2.72.6|^3.8.4",
"nesbot/carbon": "^3.8.4",
"nunomaduro/termwind": "^2.0",
"php": "^8.2",
"psr/container": "^1.1.1|^2.0.1",
"psr/log": "^1.0|^2.0|^3.0",
"psr/simple-cache": "^1.0|^2.0|^3.0",
"ramsey/uuid": "^4.7",
"symfony/console": "^7.0.3",
"symfony/error-handler": "^7.0.3",
"symfony/finder": "^7.0.3",
"symfony/console": "^7.2.0",
"symfony/error-handler": "^7.2.0",
"symfony/finder": "^7.2.0",
"symfony/http-foundation": "^7.2.0",
"symfony/http-kernel": "^7.0.3",
"symfony/mailer": "^7.0.3",
"symfony/mime": "^7.0.3",
"symfony/http-kernel": "^7.2.0",
"symfony/mailer": "^7.2.0",
"symfony/mime": "^7.2.0",
"symfony/polyfill-php83": "^1.31",
"symfony/process": "^7.0.3",
"symfony/routing": "^7.0.3",
"symfony/uid": "^7.0.3",
"symfony/var-dumper": "^7.0.3",
"symfony/process": "^7.2.0",
"symfony/routing": "^7.2.0",
"symfony/uid": "^7.2.0",
"symfony/var-dumper": "^7.2.0",
"tijsverkoyen/css-to-inline-styles": "^2.2.5",
"vlucas/phpdotenv": "^5.6.1",
"voku/portable-ascii": "^2.0.2"
@ -1173,17 +1173,17 @@
"league/flysystem-read-only": "^3.25.1",
"league/flysystem-sftp-v3": "^3.25.1",
"mockery/mockery": "^1.6.10",
"orchestra/testbench-core": "^9.9.4",
"orchestra/testbench-core": "^10.0",
"pda/pheanstalk": "^5.0.6",
"php-http/discovery": "^1.15",
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.5.35|^11.3.6|^12.0.1",
"phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1",
"predis/predis": "^2.3",
"resend/resend-php": "^0.10.0",
"symfony/cache": "^7.0.3",
"symfony/http-client": "^7.0.3",
"symfony/psr-http-message-bridge": "^7.0.3",
"symfony/translation": "^7.0.3"
"symfony/cache": "^7.2.0",
"symfony/http-client": "^7.2.0",
"symfony/psr-http-message-bridge": "^7.2.0",
"symfony/translation": "^7.2.0"
},
"suggest": {
"ably/ably-php": "Required to use the Ably broadcast driver (^1.0).",
@ -1209,22 +1209,22 @@
"mockery/mockery": "Required to use mocking (^1.6).",
"pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).",
"php-http/discovery": "Required to use PSR-7 bridging features (^1.15).",
"phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.3.6|^12.0.1).",
"phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).",
"predis/predis": "Required to use the predis connector (^2.3).",
"psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).",
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).",
"resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).",
"symfony/cache": "Required to PSR-6 cache bridge (^7.0).",
"symfony/filesystem": "Required to enable support for relative symbolic links (^7.0).",
"symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.0).",
"symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.0).",
"symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.0).",
"symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.0)."
"symfony/cache": "Required to PSR-6 cache bridge (^7.2).",
"symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).",
"symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).",
"symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).",
"symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).",
"symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "11.x-dev"
"dev-master": "12.x-dev"
}
},
"autoload": {
@ -1267,7 +1267,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-02-18T15:37:56+00:00"
"time": "2025-02-24T13:31:23+00:00"
},
{
"name": "laravel/prompts",
@ -2111,16 +2111,16 @@
},
{
"name": "nesbot/carbon",
"version": "3.8.5",
"version": "3.8.6",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon.git",
"reference": "b1a53a27898639579a67de42e8ced5d5386aa9a4"
"reference": "ff2f20cf83bd4d503720632ce8a426dc747bf7fd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/b1a53a27898639579a67de42e8ced5d5386aa9a4",
"reference": "b1a53a27898639579a67de42e8ced5d5386aa9a4",
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ff2f20cf83bd4d503720632ce8a426dc747bf7fd",
"reference": "ff2f20cf83bd4d503720632ce8a426dc747bf7fd",
"shasum": ""
},
"require": {
@ -2213,7 +2213,7 @@
"type": "tidelift"
}
],
"time": "2025-02-11T16:28:45+00:00"
"time": "2025-02-20T17:33:38+00:00"
},
{
"name": "nette/schema",
@ -5893,6 +5893,103 @@
],
"time": "2024-12-11T14:50:44+00:00"
},
{
"name": "defstudio/pest-plugin-laravel-expectations",
"version": "v2.4.0",
"source": {
"type": "git",
"url": "https://github.com/defstudio/pest-plugin-laravel-expectations.git",
"reference": "4bfa314db13cba3271e25cb571aa8e8f73f8a2b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/defstudio/pest-plugin-laravel-expectations/zipball/4bfa314db13cba3271e25cb571aa8e8f73f8a2b4",
"reference": "4bfa314db13cba3271e25cb571aa8e8f73f8a2b4",
"shasum": ""
},
"require": {
"illuminate/contracts": "^10.0|^11.0.3|^12.0",
"illuminate/database": "^10.0|^11.0.3|^12.0",
"illuminate/http": "^10.0|^11.0.3|^12.0",
"illuminate/support": "^10.0|^11.0.3|^12.0",
"illuminate/testing": "^10.0|^11.0.3|^12.0",
"pestphp/pest": "^2.0|^3.0",
"pestphp/pest-plugin": "^2.0|^3.0",
"pestphp/pest-plugin-laravel": "^2.0|^3.0",
"php": "^8.1.0"
},
"require-dev": {
"ergebnis/phpstan-rules": "^2.1.0",
"laravel/pint": "^1.11.0",
"nesbot/carbon": "^2.62.1",
"orchestra/testbench": "^8.0|^9.0",
"phpstan/phpstan": "^1.10.29",
"phpstan/phpstan-strict-rules": "^1.5.1",
"rector/rector": "^1.0.3",
"symfony/var-dumper": "^6.3.3|^v7.0.4",
"symplify/phpstan-rules": "^12.1.4.72",
"thecodingmachine/phpstan-strict-rules": "^1.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
},
"autoload": {
"files": [
"src/Autoload.php"
],
"psr-4": {
"DefStudio\\PestLaravelExpectations\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabio Ivona",
"email": "fabio.ivona@defstudio.it",
"homepage": "https://defstudio.it",
"role": "Developer"
},
{
"name": "Daniele Romeo",
"email": "danieleromeo@defstudio.it",
"homepage": "https://defstudio.it",
"role": "Developer"
}
],
"description": "A plugin to add laravel tailored expectations to Pest",
"keywords": [
"expectations",
"framework",
"laravel",
"pest",
"php",
"plugin",
"test",
"testing",
"unit"
],
"support": {
"issues": "https://github.com/defstudio/pest-plugin-laravel-expectations/issues",
"source": "https://github.com/defstudio/pest-plugin-laravel-expectations/tree/v2.4.0"
},
"funding": [
{
"url": "https://github.com/defstudio",
"type": "github"
},
{
"url": "https://github.com/fabio-ivona",
"type": "github"
}
],
"time": "2025-02-22T11:50:10+00:00"
},
{
"name": "doctrine/deprecations",
"version": "1.1.4",
@ -6245,29 +6342,29 @@
},
{
"name": "laravel/breeze",
"version": "v2.3.4",
"version": "v2.3.5",
"source": {
"type": "git",
"url": "https://github.com/laravel/breeze.git",
"reference": "e456fe0db93d1f9f5ce3b2043739a0777404395c"
"reference": "1d85805c4aecc425a0ce157147384d4becea3fa2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/breeze/zipball/e456fe0db93d1f9f5ce3b2043739a0777404395c",
"reference": "e456fe0db93d1f9f5ce3b2043739a0777404395c",
"url": "https://api.github.com/repos/laravel/breeze/zipball/1d85805c4aecc425a0ce157147384d4becea3fa2",
"reference": "1d85805c4aecc425a0ce157147384d4becea3fa2",
"shasum": ""
},
"require": {
"illuminate/console": "^11.0",
"illuminate/filesystem": "^11.0",
"illuminate/support": "^11.0",
"illuminate/validation": "^11.0",
"illuminate/console": "^11.0|^12.0",
"illuminate/filesystem": "^11.0|^12.0",
"illuminate/support": "^11.0|^12.0",
"illuminate/validation": "^11.0|^12.0",
"php": "^8.2.0",
"symfony/console": "^7.0"
},
"require-dev": {
"laravel/framework": "^11.0",
"orchestra/testbench-core": "^9.0",
"laravel/framework": "^11.0|^12.0",
"orchestra/testbench-core": "^9.0|^10.0",
"phpstan/phpstan": "^2.0"
},
"type": "library",
@ -6302,7 +6399,7 @@
"issues": "https://github.com/laravel/breeze/issues",
"source": "https://github.com/laravel/breeze"
},
"time": "2025-02-11T13:19:28+00:00"
"time": "2025-02-19T23:49:42+00:00"
},
{
"name": "laravel/pail",
@ -7445,16 +7542,16 @@
},
{
"name": "phpstan/phpdoc-parser",
"version": "2.0.2",
"version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "51087f87dcce2663e1fed4dfd4e56eccd580297e"
"reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/51087f87dcce2663e1fed4dfd4e56eccd580297e",
"reference": "51087f87dcce2663e1fed4dfd4e56eccd580297e",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68",
"reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68",
"shasum": ""
},
"require": {
@ -7486,29 +7583,29 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.2"
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0"
},
"time": "2025-02-17T20:25:51+00:00"
"time": "2025-02-19T13:28:12+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "11.0.8",
"version": "11.0.9",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "418c59fd080954f8c4aa5631d9502ecda2387118"
"reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/418c59fd080954f8c4aa5631d9502ecda2387118",
"reference": "418c59fd080954f8c4aa5631d9502ecda2387118",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/14d63fbcca18457e49c6f8bebaa91a87e8e188d7",
"reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
"nikic/php-parser": "^5.3.1",
"nikic/php-parser": "^5.4.0",
"php": ">=8.2",
"phpunit/php-file-iterator": "^5.1.0",
"phpunit/php-text-template": "^4.0.1",
@ -7520,7 +7617,7 @@
"theseer/tokenizer": "^1.2.3"
},
"require-dev": {
"phpunit/phpunit": "^11.5.0"
"phpunit/phpunit": "^11.5.2"
},
"suggest": {
"ext-pcov": "PHP extension that provides line coverage",
@ -7558,7 +7655,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.8"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.9"
},
"funding": [
{
@ -7566,7 +7663,7 @@
"type": "github"
}
],
"time": "2024-12-11T12:34:27+00:00"
"time": "2025-02-25T13:26:39+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -9080,7 +9177,7 @@
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.2"
"php": "^8.4"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"

View file

@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use App\Models\Program;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
class ProgramFactory extends Factory
{
protected $model = Program::class;
public function definition(): array
{
return [
'name' => $this->faker->word(),
'description' => $this->faker->text(),
'website' => $this->faker->url(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
}
}

View file

@ -0,0 +1,27 @@
<?php
use App\Models\Category;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('programs', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->foreignIdFor(Category::class)->constrained('categories');
$table->text('description')->nullable();
$table->string('website')->nullable();
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('programs');
}
};

View file

@ -3,6 +3,7 @@
namespace Database\Seeders;
use App\Models\Category;
use App\Models\Program;
use App\Models\User;
use Illuminate\Database\Seeder;
@ -23,6 +24,15 @@ public function run(): void
'password' => 'password',
]);
Category::factory(20)->create();
Category::factory(20)
->has(Program::factory()->count(3))
->has(Program::factory()->trashed()->count(1))
->create();
Category::factory(10)
->has(Program::factory()->count(1))
->has(Program::factory()->trashed()->count(1))
->trashed()
->create();
}
}

View file

@ -1,7 +1,7 @@
services:
laravel.test:
build:
context: './vendor/laravel/sail/runtimes/8.4'
context: './docker/8.4'
dockerfile: Dockerfile
args:
WWWGROUP: '${WWWGROUP}'
@ -25,7 +25,7 @@ services:
- pgsql
- redis
- meilisearch
- mailpit
# - mailpit
- selenium
pgsql:
image: 'postgres:17'
@ -38,7 +38,7 @@ services:
POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}'
volumes:
- 'sail-pgsql:/var/lib/postgresql/data'
- './vendor/laravel/sail/database/pgsql/create-testing-database.sql:/docker-entrypoint-initdb.d/10-create-testing-database.sql'
- './docker/pgsql/create-testing-database.sql:/docker-entrypoint-initdb.d/10-create-testing-database.sql'
networks:
- sail
healthcheck:
@ -86,13 +86,13 @@ services:
- 'http://127.0.0.1:7700/health'
retries: 3
timeout: 5s
mailpit:
image: 'axllent/mailpit:latest'
ports:
- '${FORWARD_MAILPIT_PORT:-1025}:1025'
- '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025'
networks:
- sail
# mailpit:
# image: 'axllent/mailpit:latest'
# ports:
# - '${FORWARD_MAILPIT_PORT:-1025}:1025'
# - '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025'
# networks:
# - sail
selenium:
image: selenium/standalone-chromium
extra_hosts:

70
docker/8.0/Dockerfile Normal file
View file

@ -0,0 +1,70 @@
FROM ubuntu:24.04
LABEL maintainer="Taylor Otwell"
ARG WWWGROUP
ARG NODE_VERSION=22
ARG POSTGRES_VERSION=17
WORKDIR /var/www/html
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=UTC
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
ENV SUPERVISOR_PHP_USER="sail"
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
RUN apt-get update && apt-get upgrade -y \
&& mkdir -p /etc/apt/keyrings \
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /usr/share/keyrings/ppa_ondrej_php.gpg > /dev/null \
&& echo "deb [signed-by=/usr/share/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
&& apt-get update \
&& apt-get install -y php8.0-cli php8.0-dev \
php8.0-pgsql php8.0-sqlite3 php8.0-gd php8.0-imagick \
php8.0-curl php8.0-memcached php8.0-mongodb \
php8.0-imap php8.0-mysql php8.0-mbstring \
php8.0-xml php8.0-zip php8.0-bcmath php8.0-soap \
php8.0-intl php8.0-readline php8.0-pcov \
php8.0-msgpack php8.0-igbinary php8.0-ldap \
php8.0-redis php8.0-swoole php8.0-xdebug \
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y nodejs \
&& npm install -g npm \
&& npm install -g bun \
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarnkey.gpg >/dev/null \
&& echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/pgdg.gpg >/dev/null \
&& echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
&& apt-get update \
&& apt-get install -y yarn \
&& apt-get install -y mysql-client \
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
&& apt-get -y autoremove \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN update-alternatives --set php /usr/bin/php8.0
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.0
RUN userdel -r ubuntu
RUN groupadd --force -g $WWWGROUP sail
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
COPY start-container /usr/local/bin/start-container
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY php.ini /etc/php/8.0/cli/conf.d/99-sail.ini
RUN chmod +x /usr/local/bin/start-container
EXPOSE 80/tcp
ENTRYPOINT ["start-container"]

5
docker/8.0/php.ini Normal file
View file

@ -0,0 +1,5 @@
[PHP]
post_max_size = 100M
upload_max_filesize = 100M
variables_order = EGPCS
pcov.directory = .

View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
exit 1
fi
if [ ! -z "$WWWUSER" ]; then
usermod -u $WWWUSER sail
fi
if [ ! -d /.composer ]; then
mkdir /.composer
fi
chmod -R ugo+rw /.composer
if [ $# -gt 0 ]; then
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
exec "$@"
else
exec gosu $WWWUSER "$@"
fi
else
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
fi

View file

@ -0,0 +1,14 @@
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[program:php]
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
user=%(ENV_SUPERVISOR_PHP_USER)s
environment=LARAVEL_SAIL="1"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

69
docker/8.1/Dockerfile Normal file
View file

@ -0,0 +1,69 @@
FROM ubuntu:24.04
LABEL maintainer="Taylor Otwell"
ARG WWWGROUP
ARG NODE_VERSION=22
ARG POSTGRES_VERSION=17
WORKDIR /var/www/html
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=UTC
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
ENV SUPERVISOR_PHP_USER="sail"
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
RUN apt-get update && apt-get upgrade -y \
&& mkdir -p /etc/apt/keyrings \
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /usr/share/keyrings/ppa_ondrej_php.gpg > /dev/null \
&& echo "deb [signed-by=/usr/share/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
&& apt-get update \
&& apt-get install -y php8.1-cli php8.1-dev \
php8.1-pgsql php8.1-sqlite3 php8.1-gd php8.1-imagick \
php8.1-curl php8.1-mongodb \
php8.1-imap php8.1-mysql php8.1-mbstring \
php8.1-xml php8.1-zip php8.1-bcmath php8.1-soap \
php8.1-intl php8.1-readline \
php8.1-ldap \
php8.1-msgpack php8.1-igbinary php8.1-redis php8.1-swoole \
php8.1-memcached php8.1-pcov php8.1-xdebug \
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y nodejs \
&& npm install -g npm \
&& npm install -g bun \
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarn.gpg >/dev/null \
&& echo "deb [signed-by=/usr/share/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/pgdg.gpg >/dev/null \
&& echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
&& apt-get update \
&& apt-get install -y yarn \
&& apt-get install -y mysql-client \
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
&& apt-get -y autoremove \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.1
RUN userdel -r ubuntu
RUN groupadd --force -g $WWWGROUP sail
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
COPY start-container /usr/local/bin/start-container
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY php.ini /etc/php/8.1/cli/conf.d/99-sail.ini
RUN chmod +x /usr/local/bin/start-container
EXPOSE 80/tcp
ENTRYPOINT ["start-container"]

5
docker/8.1/php.ini Normal file
View file

@ -0,0 +1,5 @@
[PHP]
post_max_size = 100M
upload_max_filesize = 100M
variables_order = EGPCS
pcov.directory = .

View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
exit 1
fi
if [ ! -z "$WWWUSER" ]; then
usermod -u $WWWUSER sail
fi
if [ ! -d /.composer ]; then
mkdir /.composer
fi
chmod -R ugo+rw /.composer
if [ $# -gt 0 ]; then
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
exec "$@"
else
exec gosu $WWWUSER "$@"
fi
else
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
fi

View file

@ -0,0 +1,14 @@
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[program:php]
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
user=%(ENV_SUPERVISOR_PHP_USER)s
environment=LARAVEL_SAIL="1"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

70
docker/8.2/Dockerfile Normal file
View file

@ -0,0 +1,70 @@
FROM ubuntu:24.04
LABEL maintainer="Taylor Otwell"
ARG WWWGROUP
ARG NODE_VERSION=22
ARG POSTGRES_VERSION=17
WORKDIR /var/www/html
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=UTC
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
ENV SUPERVISOR_PHP_USER="sail"
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
RUN apt-get update && apt-get upgrade -y \
&& mkdir -p /etc/apt/keyrings \
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
&& echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
&& apt-get update \
&& apt-get install -y php8.2-cli php8.2-dev \
php8.2-pgsql php8.2-sqlite3 php8.2-gd php8.2-imagick \
php8.2-curl php8.2-mongodb \
php8.2-imap php8.2-mysql php8.2-mbstring \
php8.2-xml php8.2-zip php8.2-bcmath php8.2-soap \
php8.2-intl php8.2-readline \
php8.2-ldap \
php8.2-msgpack php8.2-igbinary php8.2-redis php8.2-swoole \
php8.2-memcached php8.2-pcov php8.2-xdebug \
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y nodejs \
&& npm install -g npm \
&& npm install -g pnpm \
&& npm install -g bun \
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
&& echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
&& echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
&& apt-get update \
&& apt-get install -y yarn \
&& apt-get install -y mysql-client \
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
&& apt-get -y autoremove \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.2
RUN userdel -r ubuntu
RUN groupadd --force -g $WWWGROUP sail
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
COPY start-container /usr/local/bin/start-container
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY php.ini /etc/php/8.2/cli/conf.d/99-sail.ini
RUN chmod +x /usr/local/bin/start-container
EXPOSE 80/tcp
ENTRYPOINT ["start-container"]

5
docker/8.2/php.ini Normal file
View file

@ -0,0 +1,5 @@
[PHP]
post_max_size = 100M
upload_max_filesize = 100M
variables_order = EGPCS
pcov.directory = .

View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
exit 1
fi
if [ ! -z "$WWWUSER" ]; then
usermod -u $WWWUSER sail
fi
if [ ! -d /.composer ]; then
mkdir /.composer
fi
chmod -R ugo+rw /.composer
if [ $# -gt 0 ]; then
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
exec "$@"
else
exec gosu $WWWUSER "$@"
fi
else
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
fi

View file

@ -0,0 +1,14 @@
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[program:php]
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
user=%(ENV_SUPERVISOR_PHP_USER)s
environment=LARAVEL_SAIL="1"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

71
docker/8.3/Dockerfile Normal file
View file

@ -0,0 +1,71 @@
FROM ubuntu:24.04
LABEL maintainer="Taylor Otwell"
ARG WWWGROUP
ARG NODE_VERSION=22
ARG MYSQL_CLIENT="mysql-client"
ARG POSTGRES_VERSION=17
WORKDIR /var/www/html
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=UTC
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
ENV SUPERVISOR_PHP_USER="sail"
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
RUN apt-get update && apt-get upgrade -y \
&& mkdir -p /etc/apt/keyrings \
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
&& echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
&& apt-get update \
&& apt-get install -y php8.3-cli php8.3-dev \
php8.3-pgsql php8.3-sqlite3 php8.3-gd \
php8.3-curl php8.3-mongodb \
php8.3-imap php8.3-mysql php8.3-mbstring \
php8.3-xml php8.3-zip php8.3-bcmath php8.3-soap \
php8.3-intl php8.3-readline \
php8.3-ldap \
php8.3-msgpack php8.3-igbinary php8.3-redis \
php8.3-memcached php8.3-pcov php8.3-imagick php8.3-xdebug php8.3-swoole \
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y nodejs \
&& npm install -g npm \
&& npm install -g pnpm \
&& npm install -g bun \
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
&& echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
&& echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
&& apt-get update \
&& apt-get install -y yarn \
&& apt-get install -y $MYSQL_CLIENT \
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
&& apt-get -y autoremove \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.3
RUN userdel -r ubuntu
RUN groupadd --force -g $WWWGROUP sail
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
COPY start-container /usr/local/bin/start-container
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY php.ini /etc/php/8.3/cli/conf.d/99-sail.ini
RUN chmod +x /usr/local/bin/start-container
EXPOSE 80/tcp
ENTRYPOINT ["start-container"]

5
docker/8.3/php.ini Normal file
View file

@ -0,0 +1,5 @@
[PHP]
post_max_size = 100M
upload_max_filesize = 100M
variables_order = EGPCS
pcov.directory = .

View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
exit 1
fi
if [ ! -z "$WWWUSER" ]; then
usermod -u $WWWUSER sail
fi
if [ ! -d /.composer ]; then
mkdir /.composer
fi
chmod -R ugo+rw /.composer
if [ $# -gt 0 ]; then
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
exec "$@"
else
exec gosu $WWWUSER "$@"
fi
else
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
fi

View file

@ -0,0 +1,14 @@
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[program:php]
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
user=%(ENV_SUPERVISOR_PHP_USER)s
environment=LARAVEL_SAIL="1"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

71
docker/8.4/Dockerfile Normal file
View file

@ -0,0 +1,71 @@
FROM ubuntu:24.04
LABEL maintainer="Taylor Otwell"
ARG WWWGROUP
ARG NODE_VERSION=22
ARG MYSQL_CLIENT="mysql-client"
ARG POSTGRES_VERSION=17
WORKDIR /var/www/html
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=UTC
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
ENV SUPERVISOR_PHP_USER="sail"
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
RUN apt-get update && apt-get upgrade -y \
&& mkdir -p /etc/apt/keyrings \
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
&& echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
&& apt-get update \
&& apt-get install -y php8.4-cli php8.4-dev \
php8.4-pgsql php8.4-sqlite3 php8.4-gd \
php8.4-curl php8.4-mongodb \
php8.4-imap php8.4-mysql php8.4-mbstring \
php8.4-xml php8.4-zip php8.4-bcmath php8.4-soap \
php8.4-intl php8.4-readline \
php8.4-ldap \
php8.4-msgpack php8.4-igbinary php8.4-redis php8.4-swoole \
php8.4-memcached php8.4-pcov php8.4-imagick php8.4-xdebug \
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y nodejs \
&& npm install -g npm \
&& npm install -g pnpm \
&& npm install -g bun \
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
&& echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
&& echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
&& apt-get update \
&& apt-get install -y yarn \
&& apt-get install -y $MYSQL_CLIENT \
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
&& apt-get -y autoremove \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.4
RUN userdel -r ubuntu
RUN groupadd --force -g $WWWGROUP sail
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
COPY start-container /usr/local/bin/start-container
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY php.ini /etc/php/8.4/cli/conf.d/99-sail.ini
RUN chmod +x /usr/local/bin/start-container
EXPOSE 80/tcp
ENTRYPOINT ["start-container"]

8
docker/8.4/php.ini Normal file
View file

@ -0,0 +1,8 @@
[PHP]
post_max_size = 100M
upload_max_filesize = 100M
variables_order = EGPCS
pcov.directory = .
[xdebug]
xdebug.mode = ${XDEBUG_MODE}

View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
exit 1
fi
if [ ! -z "$WWWUSER" ]; then
usermod -u $WWWUSER sail
fi
if [ ! -d /.composer ]; then
mkdir /.composer
fi
chmod -R ugo+rw /.composer
if [ $# -gt 0 ]; then
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
exec "$@"
else
exec gosu $WWWUSER "$@"
fi
else
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
fi

View file

@ -0,0 +1,14 @@
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[program:php]
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
user=%(ENV_SUPERVISOR_PHP_USER)s
environment=LARAVEL_SAIL="1"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

View file

@ -0,0 +1,6 @@
#!/usr/bin/env bash
/usr/bin/mariadb --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL
CREATE DATABASE IF NOT EXISTS testing;
GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%';
EOSQL

View file

@ -0,0 +1,6 @@
#!/usr/bin/env bash
mysql --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL
CREATE DATABASE IF NOT EXISTS testing;
GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%';
EOSQL

View file

@ -0,0 +1,2 @@
SELECT 'CREATE DATABASE testing'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'testing')\gexec

View file

@ -1,51 +1,112 @@
<x-layout title="Categories">
<x-window>
<div class="sunken-panel">
<table class="interactive">
<thead>
<tr>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody
x-data="{ hovered: null }"
x-on:mouseleave="hovered = null"
>
@foreach ($categories as $category)
<tr
id="{{ $category->id }}"
x-on:mouseenter="hovered = $event.target.id"
x-bind:class="{ 'highlighted': hovered === $el.id }"
>
<td>{{ $category->name }}</td>
<td>
<a href="{{ route('categories.show', $category->id) }}">
View
</a>
<x-window x-data="{ tab: 'active' }">
@auth
<x-tablist class="mt-3">
<x-tablist.tab id="active">Active</x-tablist.tab>
<x-tablist.tab id="deleted">Deleted</x-tablist.tab>
</x-tablist>
@endauth
@auth
<a
href="{{ route('categories.edit', $category->id) }}"
class="ml-1"
>
Edit
</a>
<a
href="{{ route('categories.destroy', $category->id) }}"
class="ml-1"
>
Delete
</a>
@endauth
</td>
<div class="window" role="tabpanel" x-show="tab === 'active'">
<div class="sunken-panel">
<table class="interactive">
<thead>
<tr>
<th>Category</th>
@auth
<th>Actions</th>
@endauth
</tr>
@endforeach
</tbody>
</table>
</thead>
<tbody
x-data="{ hovered: null }"
x-on:mouseleave="hovered = null"
>
@foreach ($categories as $category)
<tr
id="{{ $category->id }}"
x-on:mouseenter="hovered = $event.target.id"
x-bind:class="{ 'highlighted': hovered === $el.id }"
>
<td>
<a
href="{{ route('categories.show', $category->id) }}"
class="text-inherit no-underline"
>
{{ $category->name }}
</a>
</td>
@auth
<td>
<a
href="{{ route('categories.edit', $category->id) }}"
class="ml-1"
>
Edit
</a>
<form
action="{{ route('categories.destroy', $category->id) }}"
method="POST"
class="ml-1 inline-block"
>
@csrf
@method('DELETE')
<a
href="#"
x-data
x-on:click.prevent="$el.closest('form').submit()"
>
Delete
</a>
</form>
</td>
@endauth
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="window" role="tabpanel" x-show="tab === 'deleted'">
<div class="sunken-panel">
<table class="interactive">
<thead>
<tr>
<td>Category</td>
<td>Actions</td>
</tr>
</thead>
<tbody>
@foreach ($trashedCategories as $category)
<tr>
<td>{{ $category->name }}</td>
<td>
<form
action="{{ route('categories.update', $category->id) }}"
method="POST"
>
@csrf
@method('PATCH')
<input type="hidden" name="restore" value="1" />
<a
href="#"
x-on:click.prevent="$el.closest('form').submit()"
>
Restore
</a>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@auth
<a href="{{ route('categories.create') }}" class="button">New</a>
@endauth

View file

@ -1,5 +1,9 @@
<x-layout :title="'Category: '. $category->name">
<x-window>
@foreach ($category->programs as $program)
{{ $program->name }}
@endforeach
<a href="{{ route('categories.index') }}">All</a>
</x-window>
</x-layout>

View file

@ -21,11 +21,17 @@
<script src="//polyfill.io/v3/polyfill.min.js?flags=gated&features=default,es5,es6,es7,matchMedia,IntersectionObserver,ResizeObserver,NodeList.prototype.forEach,HTMLTemplateElement,Element.prototype.closest,requestAnimationFrame,CustomEvent,URLSearchParams,queueMicrotask"></script>
</head>
<body>
<main class="max-w-[600px]">
<body class="flex gap-6">
<main class="min-w-[600px]">
{{ $slot }}
<x-layout.navigation />
</main>
@if (session('status'))
<x-window class="h-fit" title="Notification" dismissible>
{{ session('status') }}
</x-window>
@endif
<x-layout.navigation />
</body>
</html>

View file

@ -46,10 +46,7 @@ class="window"
<a
href="#"
x-on:click="
$event.preventDefault()
$el.closest('form').submit()
"
x-on:click.prevent="$el.closest('form').submit()"
>
<img
src="{{ Vite::asset('resources/images/startmenu/key.png') }}"

View file

@ -0,0 +1,15 @@
@props([
'id',
'tabVarName' => 'tab',
])
<li
role="tab"
x-bind:aria-selected="{{ $tabVarName }} === $el.id"
id="{{ $id }}"
{{ $attributes }}
>
<a href="#" x-on:click.prevent="{{ $tabVarName }} = $el.parentElement.id">
{{ $slot }}
</a>
</li>

View file

@ -0,0 +1,3 @@
<menu role="tablist" {{ $attributes }}>
{{ $slot }}
</menu>

View file

@ -4,6 +4,7 @@
'restore' => true,
'help' => false,
'close' => true,
'dismissible' => false,
])
@aware([
@ -14,9 +15,16 @@
if ($restore) {
$maximize = false;
}
if ($dismissible) {
$close = true;
}
@endphp
<div {{ $attributes->merge(['class' => 'window']) }}>
<div
{{ $attributes->merge(['class' => 'window']) }}
{!! $dismissible ? 'x-data="{ show: true }" x-show="show"' : '' !!}
>
<div class="title-bar">
<div class="title-bar-text">{{ $title }}</div>
@ -39,7 +47,11 @@
@endif
@if ($close)
<button class="close" aria-hidden="true"></button>
<button
class="close"
aria-hidden="true"
{!! $dismissible ? 'x-on:click="show = false"' : '' !!}
></button>
@endif
</div>
@endif

View file

View file

@ -2,6 +2,7 @@
use App\Http\Controllers\CategoryController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\ProgramController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
@ -25,4 +26,5 @@
Route::resources([
'categories' => CategoryController::class,
]);
'programs' => ProgramController::class
], ['trashed' => []]);

View file

@ -0,0 +1,241 @@
<?php
use App\Http\Controllers\CategoryController;
use App\Models\Category;
use function Pest\Laravel\delete;
use function Pest\Laravel\get;
use function Pest\Laravel\patch;
covers(CategoryController::class);
dataset('invalid-values', [null, true, 50]);
dataset('valid-values', ['hello', 'hello there', 'h#llo', 'h3llo']);
describe('categories.index', function () {
test('can be rendered to guests', function () {
get(route('categories.index'))->assertOk();
});
test('can be rendered to logged in users', function () {
asAdmin()->get(route('categories.index'))->assertOk();
});
test('contains categories', function () {
$response = get(route('categories.index'));
expect($response['categories'])
->toBeEloquentCollection()
->toContainOnlyInstancesOf(Category::class)
->toEqual(Category::all());
});
test('contains trashedCategories', function () {
$response = get(route('categories.index'));
expect($response['trashedCategories'])
->toBeEloquentCollection()
->toContainOnlyInstancesOf(Category::class)
->toEqual(Category::onlyTrashed()->get());
});
});
describe('categories.create', function () {
test("can't be rendered to guests", function () {
get(route('categories.create'))->assertForbidden();
});
test('can be rendered to logged in users', function () {
asAdmin()->get(route('categories.create'))->assertOk();
});
});
describe('categories.store', function () {
test("can't be accessed by guests", function () {
$category = Category::factory()->make();
$this->post(
route('categories.store'),
['name' => $category->name]
)->assertForbidden();
expect($category)->not->toMatchDatabaseRecord();
});
test('can be accessed by logged in users', function () {
$category = Category::factory()->make();
asAdmin()->post(
route('categories.store'),
['name' => $category->name]
)->assertRedirect(route('categories.index'));
expect($category)->toMatchDatabaseRecord();
});
test('stores valid categories', function (string $name) {
$category = Category::factory()->make(['name' => $name]);
asAdmin()->post(
route('categories.store'),
['name' => $name]
)->assertRedirect(route('categories.index'));
expect($category)->toMatchDatabaseRecord();
})->with('valid-values');
test('does not store invalid categories', function (mixed $name) {
$category = Category::factory()->make(['name' => $name]);
asAdmin()->post(
route('categories.store'),
['name' => $name],
)->assertRedirect();
expect($category)->not->toMatchDatabaseRecord();
})->with('invalid-values');
});
describe('categories.show', function () {
test('can be rendered to guests', function () {
get(route('categories.show', Category::factory()->create()))
->assertOk();
});
test('can be rendered to logged in users', function () {
asAdmin()
->get(route('categories.show', Category::factory()->create()))
->assertOk();
});
test('contains the category', function () {
$category = Category::factory()->create();
$response = get(route('categories.show', $category));
expect($response['category'])
->id->toBe($category->id)
->name->toBe($category->name);
});
test('can show trashed categories', function () {
$category = Category::factory()->trashed()->create();
$response = get(route('categories.show', $category));
// @formatter:off
expect($response['category'])
->id->toBe($category->id)
->name->toBe($category->name)
->and($category)->toBeSoftDeleted();
// @formatter:on
});
});
describe('categories.edit', function () {
test("can't be rendered to guests", function () {
$category = Category::factory()->create();
get(route('categories.edit', $category))
->assertForbidden();
});
test('can be rendered to logged in users', function () {
$category = Category::factory()->create();
asAdmin()
->get(route('categories.edit', $category))
->assertOk();
});
test('contains the category', function () {
$category = Category::factory()->create();
$response = asAdmin()->get(route('categories.edit', $category));
expect($response['category'])
->id->toBe($category->id)
->name->toBe($category->name);
});
});
describe('categories.update', function () {
test("can't be accessed by guests", function () {
$category = Category::factory()->create();
$updated = Category::factory()->make();
patch(
route('categories.update', $category),
['name' => $updated->name]
)->assertForbidden();
expect($category)->toBeInDatabaseExactly();
});
test('can be accessed by logged in users', function () {
$category = Category::factory()->create();
$updated = Category::factory()->make();
asAdmin()->patch(
route('categories.update', $category),
['name' => $updated->name]
)->assertRedirect(route('categories.index', absolute: false));
$category->refresh();
expect($category)->toMatchObject($updated);
});
test('updates categories with valid values', function (string $name) {
$category = Category::factory()->create();
asAdmin()->patch(
route('categories.update', $category),
['name' => $name]
)->assertRedirect(route('categories.index', absolute: false));
$category->refresh();
expect($category->name)->toEqual($name);
})->with('valid-values');
test(
'does not update categories with invalid values',
function (mixed $name) {
$category = Category::factory()->create();
asAdmin()->patch(
route('categories.update', $category),
['name' => $name]
)->assertRedirect();
$updated = Category::find($category->id);
expect($updated->name)->toEqual($category->name);
}
)->with('invalid-values');
test('restores trashed categories', function () {
$category = Category::factory()->trashed()->create();
asAdmin()
->patch(route('categories.update', $category), [
'restore' => 1
])->assertRedirect();
$category->refresh();
expect($category)->not->toBeSoftDeleted();
});
});
describe('categories.destroy', function () {
test("can't be accessed by guests", function () {
$category = Category::factory()->create();
delete(route('categories.destroy', $category))
->assertForbidden();
expect($category)->not->toBeSoftDeleted();
});
test('can be accessed by logged in users', function () {
$category = Category::factory()->create();
asAdmin()
->delete(route('categories.destroy', $category))
->assertRedirect(route('categories.index', absolute: false));
expect($category)->toBeSoftDeleted();
});
});

View file

@ -11,10 +11,20 @@
|
*/
pest()->extend(Tests\TestCase::class)
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Pest\Expectation;
use Tests\TestCase;
use function Pest\Laravel\assertDatabaseHas;
use function Pest\Laravel\assertModelExists;
pest()
->extend(Tests\TestCase::class)
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
->in('Feature');
arch()->preset()->laravel();
/*
|--------------------------------------------------------------------------
| Expectations
@ -26,9 +36,42 @@
|
*/
expect()->extend('toBeOne', function () {
return $this->toBe(1);
});
// https://github.com/defstudio/pest-plugin-laravel-expectations
expect()->extend(
'toMatchDatabaseRecord',
function (?string $table = null, ?string $connection = null): Expectation {
$this->toBeInstanceOf(Model::class);
$table = $table ?? $this->value->getTable();
$value = $this->value->attributesToArray();
assertDatabaseHas($table, $value, $connection);
return $this;
}
);
expect()->extend(
'toBeInDatabaseExactly',
function (?string $table = null, ?string $connection = null): Expectation {
assertModelExists($this->value);
return $this->toMatchDatabaseRecord();
}
);
expect()->pipe(
'toMatchObject',
function (Closure $next, mixed $expected) {
if ($expected instanceof Model) {
return expect($this->value)
->toMatchObject($expected->attributesToArray());
}
return $next;
}
);
/*
|--------------------------------------------------------------------------
@ -41,7 +84,7 @@
|
*/
function something()
function asAdmin(): TestCase
{
// ..
return test()->actingAs(User::factory()->create());
}

View file

@ -6,5 +6,11 @@
abstract class TestCase extends BaseTestCase
{
//
/**
* Indicates whether the default seeder should run before each test.
*
* @var bool
* @noinspection PhpMissingFieldTypeInspection
*/
// protected $seed = true;
}