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 App\Models\Category;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Redirect; use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\View;
class CategoryController extends Controller class CategoryController extends Controller
@ -16,8 +17,10 @@ public function index()
{ {
$this->authorize('viewAny', Category::class); $this->authorize('viewAny', Category::class);
return View::make('categories.index', return View::make('categories.index', [
['categories' => Category::all()]); 'categories' => Category::all(),
'trashedCategories' => Category::onlyTrashed()->get()
]);
} }
public function create() public function create()
@ -33,6 +36,7 @@ public function store(CategoryRequest $request)
Category::create($request->validated()); Category::create($request->validated());
$request->session()->flash('status', 'Category added!');
return Redirect::route('categories.index'); return Redirect::route('categories.index');
} }
@ -40,6 +44,8 @@ public function show(Category $category)
{ {
$this->authorize('view', $category); $this->authorize('view', $category);
$category->load('programs');
return View::make('categories.show', ['category' => $category]); return View::make('categories.show', ['category' => $category]);
} }
@ -52,9 +58,17 @@ public function edit(Category $category)
public function update(CategoryRequest $request, 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'); return Redirect::route('categories.index');
} }
@ -65,6 +79,7 @@ public function destroy(Category $category)
$category->delete(); $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; namespace App\Http\Requests;
use App\Models\Category;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
class CategoryRequest extends FormRequest class CategoryRequest extends FormRequest
@ -10,18 +9,13 @@ class CategoryRequest extends FormRequest
public function rules(): array public function rules(): array
{ {
return [ return [
'name' => ['required'], 'name' => ['sometimes', 'required', 'string'],
'restore' => ['sometimes', 'required', 'boolean']
]; ];
} }
public function authorize(): bool public function authorize(): bool
{ {
if ($this->routeIs('categories.store')) { return true;
return $this->user()->can('create', Category::class);
} elseif ($this->routeIs('categories.update')) {
return $this->user()->can('update', $this->route('category'));
}
return false;
} }
} }

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\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
class Category extends Model class Category extends Model
@ -13,4 +14,9 @@ class Category extends Model
protected $fillable = [ protected $fillable = [
'name', '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; use HandlesAuthorization;
public function viewAny(User $user): bool public function viewAny(?User $user): bool
{ {
return true; return true;
} }
public function view(User $user, Category $category): bool public function view(?User $user, Category $category): bool
{ {
return true; 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", "license": "MIT",
"require": { "require": {
"php": "^8.2", "php": "^8.4",
"laravel/framework": "^11.31", "laravel/framework": "^12.0",
"laravel/tinker": "^2.9" "laravel/tinker": "^2.10.1"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.23", "defstudio/pest-plugin-laravel-expectations": "^2.4",
"laravel/breeze": "^2.3", "fakerphp/faker": "^1.24.1",
"laravel/pail": "^1.1", "laravel/breeze": "^2.3.5",
"laravel/pint": "^1.13", "laravel/pail": "^1.2.2",
"laravel/sail": "^1.26", "laravel/pint": "^1.21",
"mockery/mockery": "^1.6", "laravel/sail": "^1.41",
"nunomaduro/collision": "^8.1", "mockery/mockery": "^1.6.12",
"pestphp/pest": "^3.7", "nunomaduro/collision": "^8.6.1",
"pestphp/pest": "^3.7.4",
"pestphp/pest-plugin-laravel": "^3.1" "pestphp/pest-plugin-laravel": "^3.1"
}, },
"autoload": { "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", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "a739267a391adcba09c97097cbad9f52", "content-hash": "eb1e2c5c0d22bcee0ed64e63a3619f9f",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@ -1056,20 +1056,20 @@
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v11.43.0", "version": "v12.0.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/framework.git", "url": "https://github.com/laravel/framework.git",
"reference": "70760d976486310b11d8e487e873077db069e77a" "reference": "d99e2385a6d4324782d52f4423891966425641be"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/70760d976486310b11d8e487e873077db069e77a", "url": "https://api.github.com/repos/laravel/framework/zipball/d99e2385a6d4324782d52f4423891966425641be",
"reference": "70760d976486310b11d8e487e873077db069e77a", "reference": "d99e2385a6d4324782d52f4423891966425641be",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"brick/math": "^0.9.3|^0.10.2|^0.11|^0.12", "brick/math": "^0.11|^0.12",
"composer-runtime-api": "^2.2", "composer-runtime-api": "^2.2",
"doctrine/inflector": "^2.0.5", "doctrine/inflector": "^2.0.5",
"dragonmantank/cron-expression": "^3.4", "dragonmantank/cron-expression": "^3.4",
@ -1084,32 +1084,32 @@
"fruitcake/php-cors": "^1.3", "fruitcake/php-cors": "^1.3",
"guzzlehttp/guzzle": "^7.8.2", "guzzlehttp/guzzle": "^7.8.2",
"guzzlehttp/uri-template": "^1.0", "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", "laravel/serializable-closure": "^1.3|^2.0",
"league/commonmark": "^2.6", "league/commonmark": "^2.6",
"league/flysystem": "^3.25.1", "league/flysystem": "^3.25.1",
"league/flysystem-local": "^3.25.1", "league/flysystem-local": "^3.25.1",
"league/uri": "^7.5.1", "league/uri": "^7.5.1",
"monolog/monolog": "^3.0", "monolog/monolog": "^3.0",
"nesbot/carbon": "^2.72.6|^3.8.4", "nesbot/carbon": "^3.8.4",
"nunomaduro/termwind": "^2.0", "nunomaduro/termwind": "^2.0",
"php": "^8.2", "php": "^8.2",
"psr/container": "^1.1.1|^2.0.1", "psr/container": "^1.1.1|^2.0.1",
"psr/log": "^1.0|^2.0|^3.0", "psr/log": "^1.0|^2.0|^3.0",
"psr/simple-cache": "^1.0|^2.0|^3.0", "psr/simple-cache": "^1.0|^2.0|^3.0",
"ramsey/uuid": "^4.7", "ramsey/uuid": "^4.7",
"symfony/console": "^7.0.3", "symfony/console": "^7.2.0",
"symfony/error-handler": "^7.0.3", "symfony/error-handler": "^7.2.0",
"symfony/finder": "^7.0.3", "symfony/finder": "^7.2.0",
"symfony/http-foundation": "^7.2.0", "symfony/http-foundation": "^7.2.0",
"symfony/http-kernel": "^7.0.3", "symfony/http-kernel": "^7.2.0",
"symfony/mailer": "^7.0.3", "symfony/mailer": "^7.2.0",
"symfony/mime": "^7.0.3", "symfony/mime": "^7.2.0",
"symfony/polyfill-php83": "^1.31", "symfony/polyfill-php83": "^1.31",
"symfony/process": "^7.0.3", "symfony/process": "^7.2.0",
"symfony/routing": "^7.0.3", "symfony/routing": "^7.2.0",
"symfony/uid": "^7.0.3", "symfony/uid": "^7.2.0",
"symfony/var-dumper": "^7.0.3", "symfony/var-dumper": "^7.2.0",
"tijsverkoyen/css-to-inline-styles": "^2.2.5", "tijsverkoyen/css-to-inline-styles": "^2.2.5",
"vlucas/phpdotenv": "^5.6.1", "vlucas/phpdotenv": "^5.6.1",
"voku/portable-ascii": "^2.0.2" "voku/portable-ascii": "^2.0.2"
@ -1173,17 +1173,17 @@
"league/flysystem-read-only": "^3.25.1", "league/flysystem-read-only": "^3.25.1",
"league/flysystem-sftp-v3": "^3.25.1", "league/flysystem-sftp-v3": "^3.25.1",
"mockery/mockery": "^1.6.10", "mockery/mockery": "^1.6.10",
"orchestra/testbench-core": "^9.9.4", "orchestra/testbench-core": "^10.0",
"pda/pheanstalk": "^5.0.6", "pda/pheanstalk": "^5.0.6",
"php-http/discovery": "^1.15", "php-http/discovery": "^1.15",
"phpstan/phpstan": "^2.0", "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", "predis/predis": "^2.3",
"resend/resend-php": "^0.10.0", "resend/resend-php": "^0.10.0",
"symfony/cache": "^7.0.3", "symfony/cache": "^7.2.0",
"symfony/http-client": "^7.0.3", "symfony/http-client": "^7.2.0",
"symfony/psr-http-message-bridge": "^7.0.3", "symfony/psr-http-message-bridge": "^7.2.0",
"symfony/translation": "^7.0.3" "symfony/translation": "^7.2.0"
}, },
"suggest": { "suggest": {
"ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).",
@ -1209,22 +1209,22 @@
"mockery/mockery": "Required to use mocking (^1.6).", "mockery/mockery": "Required to use mocking (^1.6).",
"pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).",
"php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", "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).", "predis/predis": "Required to use the predis connector (^2.3).",
"psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "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).", "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).", "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/cache": "Required to PSR-6 cache bridge (^7.2).",
"symfony/filesystem": "Required to enable support for relative symbolic links (^7.0).", "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.0).", "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.0).", "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.0).", "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.0)." "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)."
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "11.x-dev" "dev-master": "12.x-dev"
} }
}, },
"autoload": { "autoload": {
@ -1267,7 +1267,7 @@
"issues": "https://github.com/laravel/framework/issues", "issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework" "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", "name": "laravel/prompts",
@ -2111,16 +2111,16 @@
}, },
{ {
"name": "nesbot/carbon", "name": "nesbot/carbon",
"version": "3.8.5", "version": "3.8.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/CarbonPHP/carbon.git", "url": "https://github.com/CarbonPHP/carbon.git",
"reference": "b1a53a27898639579a67de42e8ced5d5386aa9a4" "reference": "ff2f20cf83bd4d503720632ce8a426dc747bf7fd"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/b1a53a27898639579a67de42e8ced5d5386aa9a4", "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ff2f20cf83bd4d503720632ce8a426dc747bf7fd",
"reference": "b1a53a27898639579a67de42e8ced5d5386aa9a4", "reference": "ff2f20cf83bd4d503720632ce8a426dc747bf7fd",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2213,7 +2213,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-02-11T16:28:45+00:00" "time": "2025-02-20T17:33:38+00:00"
}, },
{ {
"name": "nette/schema", "name": "nette/schema",
@ -5893,6 +5893,103 @@
], ],
"time": "2024-12-11T14:50:44+00:00" "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", "name": "doctrine/deprecations",
"version": "1.1.4", "version": "1.1.4",
@ -6245,29 +6342,29 @@
}, },
{ {
"name": "laravel/breeze", "name": "laravel/breeze",
"version": "v2.3.4", "version": "v2.3.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/breeze.git", "url": "https://github.com/laravel/breeze.git",
"reference": "e456fe0db93d1f9f5ce3b2043739a0777404395c" "reference": "1d85805c4aecc425a0ce157147384d4becea3fa2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/breeze/zipball/e456fe0db93d1f9f5ce3b2043739a0777404395c", "url": "https://api.github.com/repos/laravel/breeze/zipball/1d85805c4aecc425a0ce157147384d4becea3fa2",
"reference": "e456fe0db93d1f9f5ce3b2043739a0777404395c", "reference": "1d85805c4aecc425a0ce157147384d4becea3fa2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"illuminate/console": "^11.0", "illuminate/console": "^11.0|^12.0",
"illuminate/filesystem": "^11.0", "illuminate/filesystem": "^11.0|^12.0",
"illuminate/support": "^11.0", "illuminate/support": "^11.0|^12.0",
"illuminate/validation": "^11.0", "illuminate/validation": "^11.0|^12.0",
"php": "^8.2.0", "php": "^8.2.0",
"symfony/console": "^7.0" "symfony/console": "^7.0"
}, },
"require-dev": { "require-dev": {
"laravel/framework": "^11.0", "laravel/framework": "^11.0|^12.0",
"orchestra/testbench-core": "^9.0", "orchestra/testbench-core": "^9.0|^10.0",
"phpstan/phpstan": "^2.0" "phpstan/phpstan": "^2.0"
}, },
"type": "library", "type": "library",
@ -6302,7 +6399,7 @@
"issues": "https://github.com/laravel/breeze/issues", "issues": "https://github.com/laravel/breeze/issues",
"source": "https://github.com/laravel/breeze" "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", "name": "laravel/pail",
@ -7445,16 +7542,16 @@
}, },
{ {
"name": "phpstan/phpdoc-parser", "name": "phpstan/phpdoc-parser",
"version": "2.0.2", "version": "2.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git", "url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "51087f87dcce2663e1fed4dfd4e56eccd580297e" "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/51087f87dcce2663e1fed4dfd4e56eccd580297e", "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68",
"reference": "51087f87dcce2663e1fed4dfd4e56eccd580297e", "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7486,29 +7583,29 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types", "description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": { "support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues", "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", "name": "phpunit/php-code-coverage",
"version": "11.0.8", "version": "11.0.9",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "418c59fd080954f8c4aa5631d9502ecda2387118" "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/418c59fd080954f8c4aa5631d9502ecda2387118", "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/14d63fbcca18457e49c6f8bebaa91a87e8e188d7",
"reference": "418c59fd080954f8c4aa5631d9502ecda2387118", "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-dom": "*", "ext-dom": "*",
"ext-libxml": "*", "ext-libxml": "*",
"ext-xmlwriter": "*", "ext-xmlwriter": "*",
"nikic/php-parser": "^5.3.1", "nikic/php-parser": "^5.4.0",
"php": ">=8.2", "php": ">=8.2",
"phpunit/php-file-iterator": "^5.1.0", "phpunit/php-file-iterator": "^5.1.0",
"phpunit/php-text-template": "^4.0.1", "phpunit/php-text-template": "^4.0.1",
@ -7520,7 +7617,7 @@
"theseer/tokenizer": "^1.2.3" "theseer/tokenizer": "^1.2.3"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11.5.0" "phpunit/phpunit": "^11.5.2"
}, },
"suggest": { "suggest": {
"ext-pcov": "PHP extension that provides line coverage", "ext-pcov": "PHP extension that provides line coverage",
@ -7558,7 +7655,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", "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": [ "funding": [
{ {
@ -7566,7 +7663,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-12-11T12:34:27+00:00" "time": "2025-02-25T13:26:39+00:00"
}, },
{ {
"name": "phpunit/php-file-iterator", "name": "phpunit/php-file-iterator",
@ -9080,7 +9177,7 @@
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": "^8.2" "php": "^8.4"
}, },
"platform-dev": {}, "platform-dev": {},
"plugin-api-version": "2.6.0" "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; namespace Database\Seeders;
use App\Models\Category; use App\Models\Category;
use App\Models\Program;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
@ -23,6 +24,15 @@ public function run(): void
'password' => 'password', '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: services:
laravel.test: laravel.test:
build: build:
context: './vendor/laravel/sail/runtimes/8.4' context: './docker/8.4'
dockerfile: Dockerfile dockerfile: Dockerfile
args: args:
WWWGROUP: '${WWWGROUP}' WWWGROUP: '${WWWGROUP}'
@ -25,7 +25,7 @@ services:
- pgsql - pgsql
- redis - redis
- meilisearch - meilisearch
- mailpit # - mailpit
- selenium - selenium
pgsql: pgsql:
image: 'postgres:17' image: 'postgres:17'
@ -38,7 +38,7 @@ services:
POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}' POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}'
volumes: volumes:
- 'sail-pgsql:/var/lib/postgresql/data' - '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: networks:
- sail - sail
healthcheck: healthcheck:
@ -86,13 +86,13 @@ services:
- 'http://127.0.0.1:7700/health' - 'http://127.0.0.1:7700/health'
retries: 3 retries: 3
timeout: 5s timeout: 5s
mailpit: # mailpit:
image: 'axllent/mailpit:latest' # image: 'axllent/mailpit:latest'
ports: # ports:
- '${FORWARD_MAILPIT_PORT:-1025}:1025' # - '${FORWARD_MAILPIT_PORT:-1025}:1025'
- '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025' # - '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025'
networks: # networks:
- sail # - sail
selenium: selenium:
image: selenium/standalone-chromium image: selenium/standalone-chromium
extra_hosts: 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-layout title="Categories">
<x-window> <x-window x-data="{ tab: 'active' }">
<div class="sunken-panel"> @auth
<table class="interactive"> <x-tablist class="mt-3">
<thead> <x-tablist.tab id="active">Active</x-tablist.tab>
<tr> <x-tablist.tab id="deleted">Deleted</x-tablist.tab>
<th>Category</th> </x-tablist>
<th>Actions</th> @endauth
</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>
@auth <div class="window" role="tabpanel" x-show="tab === 'active'">
<a <div class="sunken-panel">
href="{{ route('categories.edit', $category->id) }}" <table class="interactive">
class="ml-1" <thead>
> <tr>
Edit <th>Category</th>
</a> @auth
<th>Actions</th>
<a @endauth
href="{{ route('categories.destroy', $category->id) }}"
class="ml-1"
>
Delete
</a>
@endauth
</td>
</tr> </tr>
@endforeach </thead>
</tbody> <tbody
</table> 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>
<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 @auth
<a href="{{ route('categories.create') }}" class="button">New</a> <a href="{{ route('categories.create') }}" class="button">New</a>
@endauth @endauth

View file

@ -1,5 +1,9 @@
<x-layout :title="'Category: '. $category->name"> <x-layout :title="'Category: '. $category->name">
<x-window> <x-window>
@foreach ($category->programs as $program)
{{ $program->name }}
@endforeach
<a href="{{ route('categories.index') }}">All</a> <a href="{{ route('categories.index') }}">All</a>
</x-window> </x-window>
</x-layout> </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> <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> </head>
<body> <body class="flex gap-6">
<main class="max-w-[600px]"> <main class="min-w-[600px]">
{{ $slot }} {{ $slot }}
<x-layout.navigation />
</main> </main>
@if (session('status'))
<x-window class="h-fit" title="Notification" dismissible>
{{ session('status') }}
</x-window>
@endif
<x-layout.navigation />
</body> </body>
</html> </html>

View file

@ -46,10 +46,7 @@ class="window"
<a <a
href="#" href="#"
x-on:click=" x-on:click.prevent="$el.closest('form').submit()"
$event.preventDefault()
$el.closest('form').submit()
"
> >
<img <img
src="{{ Vite::asset('resources/images/startmenu/key.png') }}" 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, 'restore' => true,
'help' => false, 'help' => false,
'close' => true, 'close' => true,
'dismissible' => false,
]) ])
@aware([ @aware([
@ -14,9 +15,16 @@
if ($restore) { if ($restore) {
$maximize = false; $maximize = false;
} }
if ($dismissible) {
$close = true;
}
@endphp @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">
<div class="title-bar-text">{{ $title }}</div> <div class="title-bar-text">{{ $title }}</div>
@ -39,7 +47,11 @@
@endif @endif
@if ($close) @if ($close)
<button class="close" aria-hidden="true"></button> <button
class="close"
aria-hidden="true"
{!! $dismissible ? 'x-on:click="show = false"' : '' !!}
></button>
@endif @endif
</div> </div>
@endif @endif

View file

View file

@ -2,6 +2,7 @@
use App\Http\Controllers\CategoryController; use App\Http\Controllers\CategoryController;
use App\Http\Controllers\ProfileController; use App\Http\Controllers\ProfileController;
use App\Http\Controllers\ProgramController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/', function () { Route::get('/', function () {
@ -25,4 +26,5 @@
Route::resources([ Route::resources([
'categories' => CategoryController::class, '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) ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
->in('Feature'); ->in('Feature');
arch()->preset()->laravel();
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Expectations | Expectations
@ -26,9 +36,42 @@
| |
*/ */
expect()->extend('toBeOne', function () { // https://github.com/defstudio/pest-plugin-laravel-expectations
return $this->toBe(1);
}); 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 abstract class TestCase extends BaseTestCase
{ {
// /**
* Indicates whether the default seeder should run before each test.
*
* @var bool
* @noinspection PhpMissingFieldTypeInspection
*/
// protected $seed = true;
} }