1 Commits

Author SHA1 Message Date
eb9c8796de Add Larastan level 7 analysis
All checks were successful
Tests / Laravel tests (pull_request) Successful in 2m36s
2026-06-03 22:09:33 +02:00
27 changed files with 341 additions and 71 deletions

View File

@@ -32,3 +32,6 @@ jobs:
- name: Run test suite
run: php artisan test
- name: Run static analysis
run: composer analyse

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
@@ -14,12 +15,18 @@ class VerifyEmailController extends Controller
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
$user = $request->user();
if (! $user instanceof MustVerifyEmail) {
abort(403);
}
if ($user->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
if ($user->markEmailAsVerified()) {
event(new Verified($user));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');

View File

@@ -4,23 +4,25 @@ namespace App\Http\Controllers;
use App\Http\Requests\EducationRequest;
use App\Models\Education;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class EducationController extends Controller
{
public function index()
public function index(): View
{
$educations = Education::with('media')->latest()->get();
return view('educations.index', compact('educations'));
}
public function create()
public function create(): View
{
return view('educations.create');
}
public function store(EducationRequest $request)
public function store(EducationRequest $request): RedirectResponse
{
$education = Education::create($request->validated());
@@ -29,12 +31,12 @@ class EducationController extends Controller
return redirect()->route('educations.index')->with('success', 'Opleiding toegevoegd.');
}
public function edit(Education $education)
public function edit(Education $education): View
{
return view('educations.edit', compact('education'));
}
public function update(EducationRequest $request, Education $education)
public function update(EducationRequest $request, Education $education): RedirectResponse
{
$education->update($request->validated());
@@ -43,7 +45,7 @@ class EducationController extends Controller
return redirect()->route('educations.index')->with('success', 'Opleiding bijgewerkt.');
}
public function destroy(Education $education)
public function destroy(Education $education): RedirectResponse
{
$education->clearMediaCollection('image');
$education->delete();

View File

@@ -8,11 +8,13 @@ use App\Models\Education;
use App\Models\Personalia;
use App\Models\Skill;
use App\Models\WorkExperience;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class FrontendController extends Controller
{
public function index()
public function index(): View
{
$skills = Skill::all()->groupBy('type');
@@ -23,21 +25,20 @@ class FrontendController extends Controller
return view('welcome', compact('skills', 'personalia', 'education', 'experience'));
}
public function getPersonalia($id)
public function getPersonalia(Personalia $personalia): JsonResponse
{
$item = Personalia::findOrFail($id);
NotifyTelegramAboutPersonaliaClick::dispatch(
$item,
$personalia,
request()->ip(),
request()->userAgent()
);
return response()->json([
'value' => $item->value,
'value' => $personalia->value,
]);
}
public function message(Request $request)
public function message(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',

View File

@@ -2,24 +2,26 @@
namespace App\Http\Controllers;
use App\Models\Personalia;
use App\Http\Requests\PersonaliaRequest;
use App\Models\Personalia;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class PersonaliaController extends Controller
{
public function index()
public function index(): View
{
$personalia = Personalia::all();
return view('personalia.index', compact('personalia'));
}
public function create()
public function create(): View
{
return view('personalia.create');
}
public function store(PersonaliaRequest $request)
public function store(PersonaliaRequest $request): RedirectResponse
{
$validated = $request->validated();
@@ -31,13 +33,13 @@ class PersonaliaController extends Controller
return redirect()->route('personalia.index')->with('success', 'Persoonlijk item toegevoegd.');
}
public function edit(Personalia $personalium)
public function edit(Personalia $personalium): View
{
return view('personalia.edit', ['personalia' => $personalium]);
}
public function update(PersonaliaRequest $request, Personalia $personalium)
public function update(PersonaliaRequest $request, Personalia $personalium): RedirectResponse
{
$validated = $request->validated();
@@ -45,10 +47,11 @@ class PersonaliaController extends Controller
...$validated,
'hidden' => $request->boolean('hidden'),
]);
return redirect()->route('personalia.index')->with('success', 'Persoonlijk item bijgewerkt.');
}
public function destroy(Personalia $personalium)
public function destroy(Personalia $personalium): RedirectResponse
{
$personalium->delete();

View File

@@ -2,23 +2,26 @@
namespace App\Http\Controllers;
use App\Models\Skill;
use App\Http\Requests\SkillRequest;
use App\Models\Skill;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class SkillController extends Controller
{
public function index()
public function index(): View
{
$skills = Skill::latest()->get();
return view('skills.index', compact('skills'));
}
public function create()
public function create(): View
{
return view('skills.create');
}
public function store(SkillRequest $request)
public function store(SkillRequest $request): RedirectResponse
{
$skill = Skill::create($request->validated());
@@ -29,12 +32,12 @@ class SkillController extends Controller
return redirect()->route('skills.index')->with('success', 'Vaardigheid toegevoegd.');
}
public function edit(Skill $skill)
public function edit(Skill $skill): View
{
return view('skills.edit', compact('skill'));
}
public function update(SkillRequest $request, Skill $skill)
public function update(SkillRequest $request, Skill $skill): RedirectResponse
{
$skill->update($request->validated());
@@ -46,7 +49,7 @@ class SkillController extends Controller
return redirect()->route('skills.index')->with('success', 'Vaardigheid bijgewerkt.');
}
public function destroy(Skill $skill)
public function destroy(Skill $skill): RedirectResponse
{
$skill->clearMediaCollection('image');
$skill->delete();

View File

@@ -2,24 +2,26 @@
namespace App\Http\Controllers;
use App\Models\WorkExperience;
use App\Http\Requests\WorkExperienceRequest;
use App\Models\WorkExperience;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class WorkExperienceController extends Controller
{
public function index()
public function index(): View
{
$experiences = WorkExperience::with('media')->latest()->get();
return view('work_experiences.index', compact('experiences'));
}
public function create()
public function create(): View
{
return view('work_experiences.create');
}
public function store(WorkExperienceRequest $request)
public function store(WorkExperienceRequest $request): RedirectResponse
{
$experience = WorkExperience::create($request->validated());
@@ -30,12 +32,12 @@ class WorkExperienceController extends Controller
return redirect()->route('work-experiences.index')->with('success', 'Ervaring toegevoegd.');
}
public function edit(WorkExperience $workExperience)
public function edit(WorkExperience $workExperience): View
{
return view('work_experiences.edit', compact('workExperience'));
}
public function update(WorkExperienceRequest $request, WorkExperience $workExperience)
public function update(WorkExperienceRequest $request, WorkExperience $workExperience): RedirectResponse
{
$workExperience->update($request->validated());
@@ -47,7 +49,7 @@ class WorkExperienceController extends Controller
return redirect()->route('work-experiences.index')->with('success', 'Ervaring bijgewerkt.');
}
public function destroy(WorkExperience $workExperience)
public function destroy(WorkExperience $workExperience): RedirectResponse
{
$workExperience->clearMediaCollection('image');
$workExperience->delete();

View File

@@ -11,6 +11,9 @@ class EducationRequest extends FormRequest
return true;
}
/**
* @return array<string, list<string>>
*/
public function rules(): array
{
return [
@@ -23,6 +26,9 @@ class EducationRequest extends FormRequest
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [

View File

@@ -11,6 +11,9 @@ class PersonaliaRequest extends FormRequest
return true;
}
/**
* @return array<string, list<string>>
*/
public function rules(): array
{
return [
@@ -21,6 +24,9 @@ class PersonaliaRequest extends FormRequest
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
@@ -28,5 +34,4 @@ class PersonaliaRequest extends FormRequest
'value.required' => 'Een waarde is verplicht.',
];
}
}

View File

@@ -11,6 +11,9 @@ class SkillRequest extends FormRequest
return true;
}
/**
* @return array<string, list<string>>
*/
public function rules(): array
{
return [
@@ -21,6 +24,10 @@ class SkillRequest extends FormRequest
'type' => ['required', 'in:rating,tag,other'],
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
@@ -29,5 +36,4 @@ class SkillRequest extends FormRequest
'type.in' => 'Het type moet rating, tag of other zijn.',
];
}
}

View File

@@ -11,6 +11,9 @@ class WorkExperienceRequest extends FormRequest
return true;
}
/**
* @return array<string, list<string>>
*/
public function rules(): array
{
return [
@@ -23,6 +26,9 @@ class WorkExperienceRequest extends FormRequest
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [

View File

@@ -21,9 +21,9 @@ class NotifyTelegramAboutContactMessage implements ShouldQueue
protected string $userAgent;
protected string $email;
protected ?string $email;
protected string $phone;
protected ?string $phone;
public function __construct(string $name, string $message, string $ip, string $userAgent, ?string $email = null, ?string $phone = null)
{
@@ -35,7 +35,7 @@ class NotifyTelegramAboutContactMessage implements ShouldQueue
$this->phone = $phone;
}
public function handle()
public function handle(): void
{
$email = $this->email ?? '';
$phone = $this->phone ?? '';

View File

@@ -14,27 +14,30 @@ class NotifyTelegramAboutPersonaliaClick implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $personalia;
protected Personalia $personalia;
protected $ip;
protected ?string $ip;
protected $userAgent;
protected ?string $userAgent;
public function __construct(Personalia $personalia, $ip, $userAgent)
public function __construct(Personalia $personalia, ?string $ip, ?string $userAgent)
{
$this->personalia = $personalia;
$this->ip = $ip;
$this->userAgent = $userAgent;
}
public function handle()
public function handle(): void
{
$ip = $this->ip ?? '';
$userAgent = $this->userAgent ?? '';
$message = <<<TEXT
👤 *Persoonlijke gegevens bekeken*
Naam: {$this->personalia->value}
IP: {$this->ip}
User Agent: `{$this->userAgent}`
IP: {$ip}
User Agent: `{$userAgent}`
📅 Tijdstip: *{now()->format('d-m-Y H:i')}*
TEXT;

View File

@@ -6,10 +6,13 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class Education extends Model implements HasMedia
{
/** @use HasFactory<\Database\Factories\EducationFactory> */
use HasFactory;
use InteractsWithMedia;
protected $table = 'education';
@@ -22,12 +25,12 @@ class Education extends Model implements HasMedia
'beschrijving',
];
public function image()
public function image(): ?Media
{
return $this->getFirstMedia('image');
}
public function imageUrl()
public function imageUrl(): ?string
{
return $this->image() ? $this->image()->getUrl() : null;
}

View File

@@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model;
class Personalia extends Model
{
/** @use HasFactory<\Database\Factories\PersonaliaFactory> */
use HasFactory;
protected $fillable = ['key', 'value', 'hidden', 'icon'];
@@ -16,14 +17,4 @@ class Personalia extends Model
protected $casts = [
'hidden' => 'boolean',
];
public function image()
{
return $this->getFirstMedia('image');
}
public function imageUrl()
{
return $this->image() ? $this->image()->getUrl() : null;
}
}

View File

@@ -6,20 +6,23 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class Skill extends Model implements HasMedia
{
/** @use HasFactory<\Database\Factories\SkillFactory> */
use HasFactory;
use InteractsWithMedia;
protected $fillable = ['title', 'description', 'rating', 'type'];
public function image()
public function image(): ?Media
{
return $this->getFirstMedia('image');
}
public function imageUrl()
public function imageUrl(): ?string
{
return $this->image() ? $this->image()->getUrl() : null;
}

View File

@@ -2,11 +2,12 @@
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
class User extends Authenticatable implements MustVerifyEmail
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;

View File

@@ -6,10 +6,13 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class WorkExperience extends Model implements HasMedia
{
/** @use HasFactory<\Database\Factories\WorkExperienceFactory> */
use HasFactory;
use InteractsWithMedia;
protected $fillable = [
@@ -20,12 +23,12 @@ class WorkExperience extends Model implements HasMedia
'beschrijving',
];
public function image()
public function image(): ?Media
{
return $this->getFirstMedia('image');
}
public function imageUrl()
public function imageUrl(): ?string
{
return $this->image() ? $this->image()->getUrl() : null;
}

View File

@@ -13,6 +13,7 @@
},
"require-dev": {
"fakerphp/faker": "^1.23",
"larastan/larastan": "^3.10",
"laravel/breeze": "^2.3",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.13",
@@ -57,6 +58,9 @@
"test": [
"@php artisan config:clear --ansi",
"@php artisan test"
],
"analyse": [
"phpstan analyse --memory-limit=1G"
]
},
"extra": {

199
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": "0d27ba8c7d653ba6d0cbd49b2eec5d2d",
"content-hash": "2f5e7cf541f786348723b475b25a79c5",
"packages": [
{
"name": "brick/math",
@@ -6685,6 +6685,47 @@
},
"time": "2025-04-30T06:54:44+00:00"
},
{
"name": "iamcal/sql-parser",
"version": "v0.7",
"source": {
"type": "git",
"url": "https://github.com/iamcal/SQLParser.git",
"reference": "610392f38de49a44dab08dc1659960a29874c4b8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/iamcal/SQLParser/zipball/610392f38de49a44dab08dc1659960a29874c4b8",
"reference": "610392f38de49a44dab08dc1659960a29874c4b8",
"shasum": ""
},
"require-dev": {
"php-coveralls/php-coveralls": "^1.0",
"phpunit/phpunit": "^5|^6|^7|^8|^9"
},
"type": "library",
"autoload": {
"psr-4": {
"iamcal\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Cal Henderson",
"email": "cal@iamcal.com"
}
],
"description": "MySQL schema parser",
"support": {
"issues": "https://github.com/iamcal/SQLParser/issues",
"source": "https://github.com/iamcal/SQLParser/tree/v0.7"
},
"time": "2026-01-28T22:20:33+00:00"
},
{
"name": "jean85/pretty-package-versions",
"version": "2.1.1",
@@ -6745,6 +6786,96 @@
},
"time": "2025-03-19T14:43:43+00:00"
},
{
"name": "larastan/larastan",
"version": "v3.10.0",
"source": {
"type": "git",
"url": "https://github.com/larastan/larastan.git",
"reference": "2970f83398154178a739609c244577267c7ee8eb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/larastan/larastan/zipball/2970f83398154178a739609c244577267c7ee8eb",
"reference": "2970f83398154178a739609c244577267c7ee8eb",
"shasum": ""
},
"require": {
"ext-json": "*",
"iamcal/sql-parser": "^0.7.0",
"illuminate/console": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/container": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/contracts": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/database": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/http": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/pipeline": "^11.44.2 || ^12.4.1 || ^13",
"illuminate/support": "^11.44.2 || ^12.4.1 || ^13",
"php": "^8.2",
"phpstan/phpstan": "^2.2.0"
},
"require-dev": {
"doctrine/coding-standard": "^14",
"laravel/framework": "^11.44.2 || ^12.7.2 || ^13",
"mockery/mockery": "^1.6.12",
"nikic/php-parser": "^5.4",
"orchestra/canvas": "^v9.2.2 || ^10.0.1 || ^11",
"orchestra/testbench-core": "^9.12.0 || ^10.1 || ^11",
"phpstan/phpstan-deprecation-rules": "^2.0.1",
"phpunit/phpunit": "^10.5.35 || ^11.5.15 || ^12.5.8 || ^13.1.8"
},
"suggest": {
"orchestra/testbench": "Using Larastan for analysing a package needs Testbench",
"phpmyadmin/sql-parser": "Install to enable Larastan's optional phpMyAdmin-based SQL parser automatically"
},
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"Larastan\\Larastan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Can Vural",
"email": "can9119@gmail.com"
}
],
"description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel",
"keywords": [
"PHPStan",
"code analyse",
"code analysis",
"larastan",
"laravel",
"package",
"php",
"static analysis"
],
"support": {
"issues": "https://github.com/larastan/larastan/issues",
"source": "https://github.com/larastan/larastan/tree/v3.10.0"
},
"funding": [
{
"url": "https://github.com/canvural",
"type": "github"
}
],
"time": "2026-05-28T08:00:58+00:00"
},
{
"name": "laravel/breeze",
"version": "v2.3.7",
@@ -7994,6 +8125,70 @@
},
"time": "2025-02-19T13:28:12+00:00"
},
{
"name": "phpstan/phpstan",
"version": "2.2.1",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/dea9c8f2d25cc849391042b71e429c1a4bf82660",
"reference": "dea9c8f2d25cc849391042b71e429c1a4bf82660",
"shasum": ""
},
"require": {
"php": "^7.4|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ondřej Mirtes"
},
{
"name": "Markus Staab"
},
{
"name": "Vincent Langlet"
}
],
"description": "PHPStan - PHP Static Analysis Tool",
"keywords": [
"dev",
"static analysis"
],
"support": {
"docs": "https://phpstan.org/user-guide/getting-started",
"forum": "https://github.com/phpstan/phpstan/discussions",
"issues": "https://github.com/phpstan/phpstan/issues",
"security": "https://github.com/phpstan/phpstan/security/policy",
"source": "https://github.com/phpstan/phpstan-src"
},
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://github.com/phpstan",
"type": "github"
}
],
"time": "2026-05-28T14:44:12+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "11.0.10",
@@ -9611,5 +9806,5 @@
"php": "^8.2"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}

View File

@@ -9,6 +9,9 @@ use Illuminate\Database\Eloquent\Factories\Factory;
*/
class EducationFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
$startDate = fake()->dateTimeBetween('-8 years', '-2 years');

View File

@@ -9,6 +9,9 @@ use Illuminate\Database\Eloquent\Factories\Factory;
*/
class PersonaliaFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [

View File

@@ -9,6 +9,9 @@ use Illuminate\Database\Eloquent\Factories\Factory;
*/
class SkillFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [

View File

@@ -9,6 +9,9 @@ use Illuminate\Database\Eloquent\Factories\Factory;
*/
class WorkExperienceFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
$startDate = fake()->dateTimeBetween('-8 years', '-1 year');

View File

@@ -9,14 +9,14 @@ return new class extends Migration
/**
* Run the migrations.
*/
public function up()
public function up(): void
{
Schema::table('skills', function (Blueprint $table) {
$table->string('type')->default('rating')->after('id');
});
}
public function down()
public function down(): void
{
Schema::table('skills', function (Blueprint $table) {
$table->dropColumn('type');

11
phpstan.neon Normal file
View File

@@ -0,0 +1,11 @@
includes:
- vendor/larastan/larastan/extension.neon
parameters:
level: 7
paths:
- app
- database
- routes
tmpDir: storage/framework/phpstan

View File

@@ -8,10 +8,10 @@ use App\Http\Controllers\SkillController;
use Illuminate\Support\Facades\Route;
Route::get('/', [FrontendController::class, 'index'])->name('home');
Route::get('/dashboard', function () {
Route::get('/dashboard', function (): \Illuminate\View\View {
return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
Route::get('/getPersonalia/{id}', [FrontendController::class, 'getPersonalia'])->name('personalia');
Route::get('/getPersonalia/{personalia}', [FrontendController::class, 'getPersonalia'])->name('personalia');
Route::post('/contact', [FrontendController::class, 'message'])->name('contact');
Route::middleware('auth')->group(function () {