Laravel Wayfinder: Type-Safe Route'lar ve Form'lar
Laravel Wayfinder, PHP controller'larindan otomatik TypeScript kodu ureten resmi bir paket. Inertia.js + Vue veya React kullaniyorsan, route isimlerini string olarak yazmak yerine type-safe fonksiyonlarla cagirabilirsin. Ziggy'nin yerine geldi ve Laravel starter kit'lerinde varsayilan olarak kullaniliyor.
Sorun Ne?
Klasik yontemle Inertia'da route kullanirken:
// Ziggy ile (eski yontem)
import { route } from 'ziggy-js';
router.get(route('posts.show', { post: 1 }));
// veya dogrudan string
router.get('/posts/1');
Sorunlar:
- Route ismi yanlis yazarsan runtime'da patlar (compile-time hata yok)
- Route parametrelerini unutabilirsin
- Controller metodu ne aliyor, ne donduruyor bilmiyorsun
- Form request'teki validation kurallari frontend'de yok
Wayfinder Nedir?
Wayfinder, PHP kodunu analiz edip TypeScript dosyalari uretiyor. Her controller metodu icin bir fonksiyon, her form request icin bir tip olusturuyor.
Kurulum
composer require laravel/wayfinder
npm install @laravel/wayfinder
Vite Eklentisi
// vite.config.ts
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import { wayfinder } from '@laravel/wayfinder/vite';
export default defineConfig({
plugins: [
laravel({
input: ['resources/js/app.ts'],
refresh: true,
}),
wayfinder(),
],
});
TypeScript Uretimi
Vite dev server'i baslattiginda veya build aldiginda Wayfinder otomatik olarak TypeScript dosyalarini uretir:
npm run dev
# veya
npm run build
Uretilen dosyalar resources/js/actions/ dizinine yazilir (varsayilan).
Uretilen Kod Ornekleri
Diyelim su controller'in var:
// app/Http/Controllers/PostController.php
class PostController extends Controller
{
public function index(): Response
{
return Inertia::render('Posts/Index', [
'posts' => Post::latest()->paginate(15),
]);
}
public function show(Post $post): Response
{
return Inertia::render('Posts/Show', [
'post' => $post->load('comments'),
]);
}
public function store(StorePostRequest $request): RedirectResponse
{
Post::create($request->validated());
return redirect()->route('posts.index');
}
public function update(UpdatePostRequest $request, Post $post): RedirectResponse
{
$post->update($request->validated());
return redirect()->route('posts.show', $post);
}
public function destroy(Post $post): RedirectResponse
{
$post->delete();
return redirect()->route('posts.index');
}
}
Wayfinder su TypeScript kodunu uretir:
// resources/js/actions/PostController.ts (otomatik uretilmis)
import { route, type RoutableAction } from '@laravel/wayfinder';
export const index: RoutableAction = {
url: '/posts',
method: 'get',
};
export const show = (args: { post: number | string }): RoutableAction => ({
url: `/posts/${args.post}`,
method: 'get',
});
export const store: RoutableAction = {
url: '/posts',
method: 'post',
};
export const update = (args: { post: number | string }): RoutableAction => ({
url: `/posts/${args.post}`,
method: 'put',
});
export const destroy = (args: { post: number | string }): RoutableAction => ({
url: `/posts/${args.post}`,
method: 'delete',
});
Vue ile Kullanim
Link Olusturma
<script setup lang="ts">
import { index, show } from '@/actions/PostController';
import { Link } from '@inertiajs/vue3';
</script>
<template>
<!-- Liste sayfasina link -->
<Link :href="index.url">
Tum Yazilar
</Link>
<!-- Detay sayfasina link -->
<Link :href="show({ post: post.id }).url">
{{ post.title }}
</Link>
</template>
Form Gonderme
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { store } from '@/actions/PostController';
const form = useForm({
title: '',
body: '',
published: false,
});
function submit() {
form.submit(store);
// veya
form.post(store.url);
}
</script>
<template>
<form @submit.prevent="submit">
<input v-model="form.title" type="text" />
<textarea v-model="form.body"></textarea>
<label>
<input v-model="form.published" type="checkbox" />
Yayinla
</label>
<button type="submit" :disabled="form.processing">
Kaydet
</button>
</form>
</template>
Method Spoofing
PUT ve DELETE istekleri HTML form'larinda desteklenmiyor. Inertia bunu otomatik hallediyor ama Wayfinder ile daha da temiz:
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { update, destroy } from '@/actions/PostController';
const props = defineProps<{
post: { id: number; title: string; body: string };
}>();
const form = useForm({
title: props.post.title,
body: props.post.body,
});
function submitUpdate() {
// method: 'put' otomatik olarak eklenir
form.submit(update({ post: props.post.id }));
}
function submitDelete() {
if (confirm('Silmek istediginize emin misiniz?')) {
// method: 'delete' otomatik
form.submit(destroy({ post: props.post.id }));
}
}
</script>
React ile Kullanim
import { useForm, Link } from '@inertiajs/react';
import { index, show, store } from '@/actions/PostController';
function PostCreate() {
const { data, setData, submit, processing, errors } = useForm({
title: '',
body: '',
});
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
submit(store);
}
return (
<form onSubmit={handleSubmit}>
<input
value={data.title}
onChange={e => setData('title', e.target.value)}
/>
{errors.title && <span>{errors.title}</span>}
<textarea
value={data.body}
onChange={e => setData('body', e.target.value)}
/>
{errors.body && <span>{errors.body}</span>}
<button type="submit" disabled={processing}>
Kaydet
</button>
</form>
);
}
Form Request Tip Uretimi
Wayfinder, Form Request siniflarindan TypeScript tipleri de uretebilir:
// app/Http/Requests/StorePostRequest.php
class StorePostRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string', 'min:50'],
'category_id' => ['required', 'exists:categories,id'],
'tags' => ['sometimes', 'array'],
'tags.*' => ['exists:tags,id'],
'published_at' => ['nullable', 'date'],
];
}
}
Uretilen tip:
// Otomatik uretilmis
export interface StorePostData {
title: string;
body: string;
category_id: number;
tags?: number[];
published_at?: string | null;
}
Artik form verisini tip-guvenli sekilde kullanabilirsin:
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { store } from '@/actions/PostController';
import type { StorePostData } from '@/types/requests';
const form = useForm<StorePostData>({
title: '',
body: '',
category_id: 0,
tags: [],
published_at: null,
});
</script>
Model Tipleri
Wayfinder, Eloquent modellerinden de TypeScript interface'leri uretebilir:
// app/Models/Post.php
class Post extends Model
{
protected $casts = [
'published_at' => 'datetime',
'is_featured' => 'boolean',
'metadata' => 'array',
'view_count' => 'integer',
];
}
Uretilen:
export interface Post {
id: number;
title: string;
body: string;
slug: string;
published_at: string | null;
is_featured: boolean;
metadata: Record<string, unknown>;
view_count: number;
created_at: string;
updated_at: string;
}
PHP Enum Destegi
PHP 8.1 enum'lari da TypeScript'e cevriliyor:
// app/Enums/PostStatus.php
enum PostStatus: string
{
case Draft = 'draft';
case Published = 'published';
case Archived = 'archived';
}
Uretilen:
export enum PostStatus {
Draft = 'draft',
Published = 'published',
Archived = 'archived',
}
Vue'da kullanim:
<script setup lang="ts">
import { PostStatus } from '@/types/enums';
const statusOptions = Object.entries(PostStatus).map(([key, value]) => ({
label: key,
value: value,
}));
</script>
<template>
<select v-model="form.status">
<option v-for="opt in statusOptions" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
</template>
Ziggy ile Karsilastirma
| Ozellik | Ziggy | Wayfinder |
|---|---|---|
| Route generation | Runtime (JS objesi) | Build-time (TypeScript) |
| Type safety | Yok (string-based) | Tam |
| Bundle size | Route listesi client'a gider | Sadece kullanilan route'lar |
| Form request tipleri | Yok | Var |
| Model tipleri | Yok | Var |
| Enum destegi | Yok | Var |
| Method spoofing | Manuel | Otomatik |
| Inertia entegrasyonu | Ekstra is gerekir | Native |
| Tree-shaking | Zor | Kolay |
| Starter kit'lerde | Eski kit'ler | Yeni kit'ler (2025+) |
Bundle Size Farki
Ziggy tum route listesini JavaScript bundle'ina ekler. 100 route'un varsa o 100 route'un hepsi client'a gider. Wayfinder ise sadece import ettigin route'lari bundle'a ekler (tree-shaking).
// Ziggy - Tum route'lar yuklenir
import { route } from 'ziggy-js';
route('posts.show', { post: 1 }); // Tum route listesi bellekte
// Wayfinder - Sadece PostController yuklenir
import { show } from '@/actions/PostController';
show({ post: 1 }); // Sadece bu action bellekte
Konfigurasyon
// config/wayfinder.php
return [
// TypeScript dosyalarinin uretilecegi dizin
'output' => resource_path('js/actions'),
// Hangi controller'lar dahil edilecek
'include' => [
'App\\Http\\Controllers\\*',
],
// Hangi controller'lar haric tutulacak
'exclude' => [
'App\\Http\\Controllers\\Admin\\*', // Admin route'larini haric tut
],
// Model tip uretimi
'models' => true,
// Enum tip uretimi
'enums' => true,
// Form request tip uretimi
'form_requests' => true,
];
Ziggy'den Wayfinder'a Gecis
1. Paketleri Degistir
# Ziggy'yi kaldir
composer remove tightenco/ziggy
npm uninstall ziggy-js
# Wayfinder'i yukle
composer require laravel/wayfinder
npm install @laravel/wayfinder
2. Vite Config'i Guncelle
// Ziggy eklentisini cikar, Wayfinder ekle
import { wayfinder } from '@laravel/wayfinder/vite';
export default defineConfig({
plugins: [
laravel({ ... }),
wayfinder(),
],
});
3. Import'lari Guncelle
// Eskisi (Ziggy)
import { route } from 'ziggy-js';
const url = route('posts.show', { post: 1 });
// Yenisi (Wayfinder)
import { show } from '@/actions/PostController';
const action = show({ post: 1 });
// action.url = '/posts/1'
// action.method = 'get'
4. Link'leri Guncelle
<!-- Eskisi -->
<Link :href="route('posts.show', { post: post.id })">
<!-- Yenisi -->
<Link :href="show({ post: post.id }).url">
5. Form'lari Guncelle
<!-- Eskisi -->
<script setup>
const form = useForm({ ... });
form.post(route('posts.store'));
</script>
<!-- Yenisi -->
<script setup>
import { store } from '@/actions/PostController';
const form = useForm({ ... });
form.submit(store);
</script>
Pratik Ipuclari
1. IDE Otomatik Tamamlama
TypeScript dosyalari uretildigi icin IDE'de tam otomatik tamamlama calisir. PostController. yazdiginda tum action'lari gorebilirsin.
2. Refactoring Guvenligi
Controller'da bir metodu yeniden adlandirdigin veya sildigin an, TypeScript tarafinda hata alirsin. Bu, kotu route referanslarini deploy'dan once yakalar.
3. Nested Route'lar
// routes/web.php
Route::apiResource('posts.comments', CommentController::class);
// Uretilen
import { store } from '@/actions/CommentController';
// /posts/5/comments'a POST
form.submit(store({ post: 5 }));
4. Route Parametreleri ve Query String
import { index } from '@/actions/PostController';
// Sadece URL
const url = index.url; // '/posts'
// Query string ekle
const urlWithQuery = `${index.url}?page=2&search=laravel`;
Sonuc
Wayfinder, Inertia.js kullanan Laravel projelerinde buyuk bir iyilestirme. String-based route isimleri yerine type-safe fonksiyonlar, otomatik method spoofing, form request tipleri ve model tipleri -- hepsi compile-time'da garanti altinda.
Eger yeni bir Inertia projesi basliyorsan, Wayfinder ile basla. Mevcut Ziggy projeni de kademeli olarak gecis yapabilirsin -- ikisi bir arada da calisabilir gecis surecinde.
TypeScript kullaniyorsan ve Laravel backend'in varsa, Wayfinder'i denemeni kesinlikle tavsiye ederim. O "route ismi yanlis mi yazdim acaba" endisesinden kurtulmak buyuk bir rahatlama.