Skip to content

Commit 3f7ebbc

Browse files
committed
feat: polymorphic subscriber resolution — works with any model
The unsubscribe route now embeds the model's morph class alongside its route key, removing the hard-wired userModel concept entirely. Route: unsubscribe/{subscriberType}/{subscriberId}/{mailingList?} - MailSubscriber::unsubscribeLink() uses getMorphClass() / getRouteKey() so signed URLs automatically reference whichever Eloquent model the trait is applied to (User, Contact, Subscriber, etc.) - UnsubscribeController resolves the model via Relation::getMorphedModel() (honours Laravel morph maps) and falls back to the raw class name; the resolved class must implement CanUnsubscribe or the request is rejected with 403, preventing abuse of arbitrary class names in URLs - Subscriber::$userModel, Subscriber::userModel(), and the userModel() wiring in SubscribableApplicationServiceProvider are removed — model identity now comes from the URL, not package config - FakeSubscriber::$userModel / userModel() removed accordingly https://claude.ai/code/session_01R4pAjWwGY8xKspsdU8xnsy
1 parent f6625e7 commit 3f7ebbc

12 files changed

Lines changed: 37 additions & 105 deletions

src/Controllers/UnsubscribeController.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
namespace YlsIdeas\SubscribableNotifications\Controllers;
44

5+
use Illuminate\Database\Eloquent\Relations\Relation;
56
use Illuminate\Http\Request;
67
use Illuminate\Http\Response;
78
use Illuminate\Routing\Controller;
9+
use YlsIdeas\SubscribableNotifications\Contracts\CanUnsubscribe;
810
use YlsIdeas\SubscribableNotifications\Events\UserUnsubscribed;
911
use YlsIdeas\SubscribableNotifications\Events\UserUnsubscribing;
1012
use YlsIdeas\SubscribableNotifications\Subscriber;
@@ -33,16 +35,23 @@ public function __construct(Subscriber $subscriber)
3335
* Handle the incoming request.
3436
*
3537
* @param Request $request
36-
* @param mixed $subscriber
38+
* @param string $subscriberType
39+
* @param mixed $subscriberId
3740
* @param string|null $mailingList
3841
* @return Response
3942
*/
40-
public function __invoke(Request $request, $subscriber, ?string $mailingList = null)
43+
public function __invoke(Request $request, string $subscriberType, $subscriberId, ?string $mailingList = null)
4144
{
42-
$model = new $this->subscriber->userModel();
45+
$modelClass = Relation::getMorphedModel($subscriberType) ?? $subscriberType;
46+
47+
if (! class_exists($modelClass) || ! is_a($modelClass, CanUnsubscribe::class, true)) {
48+
abort(403, __('Could not process unsubscribe request'));
49+
}
50+
51+
$model = new $modelClass();
4352

4453
$subscriber = $model
45-
->where($model->getRouteKeyName(), $subscriber)
54+
->where($model->getRouteKeyName(), $subscriberId)
4655
->first();
4756

4857
if (! $subscriber) {

src/Facades/Subscriber.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
*
1414
* @method static void routes()
1515
* @method static string routeName()
16-
* @method static mixed userModel(string $model = null)
1716
* @method static void onCompletion(callable|string $handler)
1817
* @method static void onUnsubscribeFromMailingList(callable|string $handler)
1918
* @method static void onUnsubscribeFromAllMailingLists(callable|string $handler)

src/MailSubscriber.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@ trait MailSubscriber
1313
* @param string|null $mailingList
1414
* @return string
1515
*/
16-
public function unsubscribeLink(?string $mailingList = ''): string
16+
public function unsubscribeLink(?string $mailingList = null): string
1717
{
1818
return URL::signedRoute(
1919
Subscriber::routeName(),
20-
['subscriber' => $this, 'mailingList' => $mailingList]
20+
[
21+
'subscriberType' => $this->getMorphClass(),
22+
'subscriberId' => $this->getRouteKey(),
23+
'mailingList' => $mailingList,
24+
]
2125
);
2226
}
2327

src/SubscribableApplicationServiceProvider.php

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,12 @@ abstract class SubscribableApplicationServiceProvider extends ServiceProvider
1111
*/
1212
protected $loadRoutes = true;
1313

14-
/**
15-
* @var string
16-
*/
17-
protected $model = null;
18-
1914
public function boot()
2015
{
2116
if ($this->loadRoutes === true) {
2217
$this->loadRoutes();
2318
}
2419

25-
\YlsIdeas\SubscribableNotifications\Facades\Subscriber::userModel(
26-
$this->userModel()
27-
);
28-
2920
\YlsIdeas\SubscribableNotifications\Facades\Subscriber::onUnsubscribeFromMailingList(
3021
$this->onUnsubscribeFromMailingList()
3122
);
@@ -43,19 +34,6 @@ public function boot()
4334
);
4435
}
4536

46-
protected function userModel()
47-
{
48-
if ($this->model != null) {
49-
return $this->model;
50-
}
51-
52-
if (version_compare($this->app->version(), '8.0.0', '>=')) {
53-
return '\App\Models\User';
54-
}
55-
56-
return '\App\User';
57-
}
58-
5937
public function loadRoutes()
6038
{
6139
\YlsIdeas\SubscribableNotifications\Facades\Subscriber::routes();

src/Subscriber.php

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class Subscriber
1111
/**
1212
* @var string
1313
*/
14-
public $uri = 'unsubscribe/{subscriber}/{mailingList?}';
14+
public $uri = 'unsubscribe/{subscriberType}/{subscriberId}/{mailingList?}';
1515
/**
1616
* @var string
1717
*/
@@ -20,10 +20,6 @@ class Subscriber
2020
* @var string
2121
*/
2222
public $routeName = 'unsubscribe';
23-
/**
24-
* @var string
25-
*/
26-
public $userModel = '\App\Models\User';
2723
/**
2824
* @var callable
2925
*/
@@ -74,21 +70,6 @@ public function routeName()
7470
return $this->routeName;
7571
}
7672

77-
/**
78-
* @param string|null $model
79-
* @return string|null
80-
*/
81-
public function userModel(?string $model = null)
82-
{
83-
if ($model) {
84-
$this->userModel = $model;
85-
86-
return null;
87-
}
88-
89-
return $this->userModel;
90-
}
91-
9273
/**
9374
* @param string|callable $handler
9475
* @throws \Illuminate\Contracts\Container\BindingResolutionException

src/Testing/FakeSubscriber.php

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77

88
class FakeSubscriber
99
{
10-
public string $userModel = '\App\Models\User';
11-
1210
public string $routeName = 'unsubscribe';
1311

1412
protected array $unsubscribedFromMailingList = [];
@@ -24,17 +22,6 @@ public function routeName(): string
2422
return $this->routeName;
2523
}
2624

27-
public function userModel(?string $model = null): ?string
28-
{
29-
if ($model) {
30-
$this->userModel = $model;
31-
32-
return null;
33-
}
34-
35-
return $this->userModel;
36-
}
37-
3825
public function onUnsubscribeFromMailingList($handler): void {}
3926

4027
public function onUnsubscribeFromAllMailingLists($handler): void {}

stubs/SubscribableServiceProvider.stub

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ use YlsIdeas\SubscribableNotifications\SubscribableApplicationServiceProvider;
66

77
class SubscribableServiceProvider extends SubscribableApplicationServiceProvider
88
{
9-
/**
10-
* @var bool
11-
*/
129
protected $loadRoutes = true;
1310

1411
/**

tests/Controllers/UnsubscribeControllerTest.php

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@ public function test_it_unsubscribes_users_from_all_mailing_lists()
4848
'password' => 'test',
4949
]);
5050

51-
Subscriber::userModel(DummyUser::class);
52-
5351
Subscriber::onUnsubscribeFromAllMailingLists(
5452
function ($user) use (&$expected, $expectedUser) {
5553
$expected = true;
@@ -77,8 +75,6 @@ public function test_it_unsubscribes_users_from_a_mailing_list()
7775
'password' => 'test',
7876
]);
7977

80-
Subscriber::userModel(DummyUser::class);
81-
8278
Subscriber::onUnsubscribeFromMailingList(
8379
function ($user, $mailingList) use (&$expected, $expectedUser) {
8480
$expected = true;
@@ -137,7 +133,11 @@ function () use (&$notExpected) {
137133
$this->get(
138134
URL::signedRoute(
139135
Subscriber::routeName(),
140-
['subscriber' => 1, 'mailingList' => 'test']
136+
[
137+
'subscriberType' => DummyUser::class,
138+
'subscriberId' => 999,
139+
'mailingList' => 'test',
140+
]
141141
)
142142
)
143143
->assertStatus(403);
@@ -177,8 +177,6 @@ public function test_it_returns_200_for_rfc8058_one_click_post_unsubscribe()
177177
'password' => 'test',
178178
]);
179179

180-
Subscriber::userModel(DummyUser::class);
181-
182180
$called = false;
183181
Subscriber::onUnsubscribeFromAllMailingLists(function ($user) use (&$called) {
184182
$called = true;
@@ -201,8 +199,6 @@ public function test_it_returns_200_for_rfc8058_one_click_post_unsubscribe_from_
201199
'password' => 'test',
202200
]);
203201

204-
Subscriber::userModel(DummyUser::class);
205-
206202
$called = false;
207203
Subscriber::onUnsubscribeFromMailingList(function ($user, $list) use (&$called) {
208204
$called = true;

tests/MailSubscriberTest.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ protected function getPackageProviders($app)
3232

3333
public function test_it_generates_a_signed_url_for_users_to_unsubscribe()
3434
{
35-
Route::get('unsubscribe/{subscriber}/{mailingList?}', function () {
35+
Route::get('unsubscribe/{subscriberType}/{subscriberId}/{mailingList?}', function () {
3636
})->name('unsubscribe');
3737

3838
/** @var DummyUser $user */
@@ -45,16 +45,18 @@ public function test_it_generates_a_signed_url_for_users_to_unsubscribe()
4545

4646
$url = $user->unsubscribeLink();
4747

48-
4948
$this->assertEquals(
50-
URL::signedRoute('unsubscribe', ['subscriber' => 1]),
49+
URL::signedRoute('unsubscribe', [
50+
'subscriberType' => $user->getMorphClass(),
51+
'subscriberId' => 1,
52+
]),
5153
$url
5254
);
5355
}
5456

5557
public function test_it_generates_a_signed_url_for_users_to_unsubscribe_from_a_mailing_list()
5658
{
57-
Route::get('unsubscribe/{subscriber}/{mailingList?}', function () {
59+
Route::get('unsubscribe/{subscriberType}/{subscriberId}/{mailingList?}', function () {
5860
})->name('unsubscribe');
5961

6062
/** @var DummyUser $user */
@@ -68,7 +70,11 @@ public function test_it_generates_a_signed_url_for_users_to_unsubscribe_from_a_m
6870
$url = $user->unsubscribeLink('test');
6971

7072
$this->assertEquals(
71-
URL::signedRoute('unsubscribe', ['subscriber' => 1, 'mailingList' => 'test']),
73+
URL::signedRoute('unsubscribe', [
74+
'subscriberType' => $user->getMorphClass(),
75+
'subscriberId' => 1,
76+
'mailingList' => 'test',
77+
]),
7278
$url
7379
);
7480
}

tests/SubscribeApplicationServiceProviderTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ class SubscribeApplicationServiceProviderTest extends TestCase
1111
public function test_it_can_be_configured_to_loads_routes()
1212
{
1313
Subscriber::shouldReceive('routes');
14-
Subscriber::shouldReceive('userModel');
1514
Subscriber::shouldReceive('onUnsubscribeFromMailingList');
1615
Subscriber::shouldReceive('onUnsubscribeFromAllMailingLists');
1716
Subscriber::shouldReceive('onCompletion');

0 commit comments

Comments
 (0)