Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,47 @@ Use these with HTML forms, for example, in React:
</form>
```

### Multiple Routes To The Same Action

If two or more routes point at the same controller method, Wayfinder can't tell which URL you meant from the action alone, so the generated export becomes a dictionary keyed by URI instead of a callable:

```php
Route::get('clients/{client}/payments', [ClientPaymentsController::class, 'index'])
->name('clients.payments.index');

Route::get('clients/{client}/payments-archive', [ClientPaymentsController::class, 'index'])
->name('clients.payments.archive');
```

```typescript
import { index } from "@/wayfinder/App/Http/Controllers/ClientPaymentsController";

// `index` is not callable directly — pick the URI you want:
index["/clients/{client}/payments"]({ client: 1 });
```

If two of those routes share a URI and differ only by verb, each key is prefixed with the verb, so you can still pick the one you want:

```php
Route::get('/exports/{report}', ExportController::class)
->name('exports.show');

Route::post('/exports/{report}', ExportController::class)
->middleware('throttle:5,1')
->name('exports.run');
```

```typescript
import ExportController from "@/wayfinder/App/Http/Controllers/ExportController";

ExportController["get /exports/{report}"]({ report: 1 });
ExportController["post /exports/{report}"]({ report: 1 });
```

A route that answers to more than one verb joins them with `|`, as in `ExportController["put|patch /exports/{report}"]`. Exports whose URIs are already unique keep the plain URI keys shown above.

In most cases it is easier to import the route by name instead, as described below.

## Named Routes

Wayfinder also generates files organized by route names, making it easy to access routes the same way you would with Laravel's `route()` helper.
Expand Down
34 changes: 28 additions & 6 deletions src/Langs/TypeScript/Converters/RouteMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ public function __construct(
protected bool $named = false,
protected array $relatedRoutes = [],
protected bool $tmpMethod = false,
protected ?string $tmpMethodKey = null,
) {
$this->name = TypeScript::safeMethod($this->jsMethod($route), 'Method');

if ($this->tmpMethod) {
$this->name = $this->tmpMethod($route);
$this->name = $this->tmpMethod($route, $this->tmpMethodKey ?? $route->uri());
}

$this->hasParameters = $route->parameters()->isNotEmpty();
Expand Down Expand Up @@ -93,22 +94,32 @@ protected function multiRouteControllerMethod(): string
{
$output = [];

foreach ($this->relatedRoutes as $route) {
$duplicateUris = collect($this->relatedRoutes)->duplicates(fn (Route $route) => $route->uri());

$keys = array_map(
fn (Route $route) => $duplicateUris->contains($route->uri())
? $this->verbPrefixedUri($route)
: $route->uri(),
$this->relatedRoutes,
);

foreach ($this->relatedRoutes as $index => $route) {
$routeMethod = new static(
route: $route,
withForm: $this->withForm,
withInertiaComponent: $this->withInertiaComponent,
named: $this->named,
tmpMethod: true,
tmpMethodKey: $keys[$index],
);

$output[] = $routeMethod->controllerMethod();
}

$object = TypeScript::object();

foreach ($this->relatedRoutes as $route) {
$object->key($route->uri())->value($this->tmpMethod($route));
foreach ($this->relatedRoutes as $index => $route) {
$object->key($keys[$index])->value($this->tmpMethod($route, $keys[$index]));
}

$const = TypeScript::constant($this->name, $object)->export($this->named || ! $this->route->hasInvokableController());
Expand Down Expand Up @@ -521,9 +532,20 @@ protected function formVariant(): string
return $block;
}

protected function tmpMethod(Route $route): string
protected function tmpMethod(Route $route, string $key): string
{
return $this->jsMethod($route).hash('xxh128', $route->uri());
return $this->jsMethod($route).hash('xxh128', $key);
}

protected function verbPrefixedUri(Route $route): string
{
$verbs = $route->verbs()->pluck('actual');

if ($verbs->contains('get')) {
$verbs = $verbs->reject(fn (string $verb) => $verb === 'head');
}

return $verbs->implode('|').' '.$route->uri();
}

protected function withComponentMethod(): string
Expand Down
28 changes: 28 additions & 0 deletions tests/SharedUriController.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { expect, it } from "vitest";
import SharedUriController from "../workbench/resources/js/wayfinder/App/Http/Controllers/SharedUriController";

it("keys routes sharing a URI by verb", () => {
expect(SharedUriController["get /shared-uri/{name}"].url("test")).toBe(
"/shared-uri/test"
);
expect(SharedUriController["get /shared-uri/{name}"]("test")).toEqual({
url: "/shared-uri/test",
method: "get",
});

expect(SharedUriController["post /shared-uri/{name}"].url("test")).toBe(
"/shared-uri/test"
);
expect(SharedUriController["post /shared-uri/{name}"]("test")).toEqual({
url: "/shared-uri/test",
method: "post",
});

expect(SharedUriController["put|patch /shared-uri/{name}"].url("test")).toBe(
"/shared-uri/test"
);
expect(SharedUriController["put|patch /shared-uri/{name}"]("test")).toEqual({
url: "/shared-uri/test",
method: "put",
});
});
11 changes: 11 additions & 0 deletions workbench/app/Http/Controllers/SharedUriController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace App\Http\Controllers;

class SharedUriController
{
public function __invoke()
{
//
}
}
5 changes: 5 additions & 0 deletions workbench/routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use App\Http\Controllers\Prism\PrismController;
use App\Http\Controllers\ResourceTestController;
use App\Http\Controllers\SecretsController;
use App\Http\Controllers\SharedUriController;
use App\Http\Controllers\TwoRoutesSameActionController;
use App\Http\Controllers\UrlDefaultsController;
use App\Http\Middleware\UrlDefaultsMiddleware;
Expand Down Expand Up @@ -94,6 +95,10 @@
Route::get('/two-routes-one-action-1', [TwoRoutesSameActionController::class, 'same']);
Route::get('/two-routes-one-action-2', [TwoRoutesSameActionController::class, 'same']);

Route::get('/shared-uri/{name}', SharedUriController::class);
Route::post('/shared-uri/{name}', SharedUriController::class);
Route::match(['put', 'patch'], '/shared-uri/{name}', SharedUriController::class);

Route::get('/disallowed/delete', [DisallowedMethodNameController::class, 'delete']);
Route::get('/disallowed/404', [DisallowedMethodNameController::class, '404'])->name('disallowed.404');
Route::get('/disallowed/2fa', [DisallowedMethodNameController::class, '2fa'])->name('2fa.disallowed');
Expand Down