• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daycry / auth / 25552752016

08 May 2026 11:20AM UTC coverage: 71.592% (+13.0%) from 58.608%
25552752016

push

github

web-flow
Merge pull request #48 from daycry/development

Add Laravel-parity authentication features: Gates, Password Confirmation, Basic Auth

198 of 252 new or added lines in 18 files covered. (78.57%)

1 existing line in 1 file now uncovered.

4453 of 6220 relevant lines covered (71.59%)

62.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.76
/src/Authorization/Gate.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of Daycry Auth.
7
 *
8
 * (c) Daycry <daycry9@proton.me>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace Daycry\Auth\Authorization;
15

16
use Closure;
17
use Daycry\Auth\Entities\User;
18

19
/**
20
 * Authorization gateway — the entry point for closure- or class-based
21
 * authorization rules. Inspired by Laravel's `Gate` facade.
22
 *
23
 * Three styles of rules are supported, in this resolution order:
24
 *
25
 *  1. **Closure rules** registered via {@see define()}.
26
 *  2. **Class-based policies** registered via {@see policy()} or
27
 *     auto-discovered for a resource class via the configured
28
 *     `policyNamespace` (defaults to `App\Policies\\`).
29
 *  3. Anything else: the rule is treated as undefined and the check
30
 *     denies by default.
31
 *
32
 * Use:
33
 *
34
 *     // Register a closure rule:
35
 *     Gate::define('post.update', fn ($user, Post $post) =>
36
 *         $user !== null && $user->id === $post->author_id);
37
 *
38
 *     // Or attach a policy class:
39
 *     Gate::policy(Post::class, PostPolicy::class);
40
 *     // → Gate::allows('update', $post) dispatches to PostPolicy::update()
41
 *
42
 *     // Check:
43
 *     if (Gate::allows('post.update', $post)) { ... }
44
 *     // Or fail-fast:
45
 *     Gate::authorize('post.delete', $post); // throws AuthorizationException
46
 */
47
class Gate
48
{
49
    /**
50
     * @var array<string, Closure>
51
     */
52
    private array $abilities = [];
53

54
    /**
55
     * @var array<class-string, class-string>
56
     */
57
    private array $policies = [];
58

59
    /**
60
     * Optional override for the user against whom checks resolve. When
61
     * null, falls back to the currently logged-in user.
62
     */
63
    private ?User $forUser = null;
64

65
    /**
66
     * Registers a closure-based ability rule.
67
     *
68
     * @param Closure(User|null, mixed...): (bool|PolicyResponse) $callback
69
     */
70
    public function define(string $ability, Closure $callback): self
11✔
71
    {
72
        $this->abilities[$ability] = $callback;
11✔
73

74
        return $this;
11✔
75
    }
76

77
    /**
78
     * Maps a resource class to a policy class. The policy method matched
79
     * against the ability name ("update", "delete", etc.) is invoked with
80
     * the user + the resource instance + any additional arguments.
81
     *
82
     * @param class-string $resource
83
     * @param class-string $policy
84
     */
85
    public function policy(string $resource, string $policy): self
3✔
86
    {
87
        $this->policies[$resource] = $policy;
3✔
88

89
        return $this;
3✔
90
    }
91

92
    /**
93
     * Returns a Gate instance scoped to the given user. Useful when you
94
     * need to authorize on behalf of a different user than the one
95
     * currently logged in (e.g. impersonation, admin operations).
96
     */
97
    public function forUser(User $user): self
4✔
98
    {
99
        $clone          = clone $this;
4✔
100
        $clone->forUser = $user;
4✔
101

102
        return $clone;
4✔
103
    }
104

105
    /**
106
     * @param mixed ...$arguments
107
     */
108
    public function allows(string $ability, ...$arguments): bool
10✔
109
    {
110
        return $this->resolve($ability, $arguments) === true;
10✔
111
    }
112

113
    /**
114
     * @param mixed ...$arguments
115
     */
116
    public function denies(string $ability, ...$arguments): bool
6✔
117
    {
118
        return ! $this->allows($ability, ...$arguments);
6✔
119
    }
120

121
    /**
122
     * Throws {@see AuthorizationException} when the check fails.
123
     *
124
     * @param mixed ...$arguments
125
     *
126
     * @throws AuthorizationException
127
     */
128
    public function authorize(string $ability, ...$arguments): PolicyResponse
5✔
129
    {
130
        $result = $this->dispatch($ability, $arguments);
5✔
131

132
        if ($result instanceof PolicyResponse) {
5✔
133
            return $result->authorize();
3✔
134
        }
135

136
        if ($result === true) {
2✔
137
            return PolicyResponse::allow();
1✔
138
        }
139

140
        throw new AuthorizationException();
1✔
141
    }
142

143
    /**
144
     * Returns true when the resolved value is strictly true.
145
     *
146
     * @param list<mixed> $arguments
147
     */
148
    private function resolve(string $ability, array $arguments): bool
10✔
149
    {
150
        $result = $this->dispatch($ability, $arguments);
10✔
151

152
        if ($result instanceof PolicyResponse) {
10✔
NEW
153
            return $result->allowed();
×
154
        }
155

156
        return $result === true;
10✔
157
    }
158

159
    /**
160
     * Runs the appropriate rule and returns its raw result.
161
     *
162
     * @param list<mixed> $arguments
163
     *
164
     * @return bool|PolicyResponse|null
165
     */
166
    private function dispatch(string $ability, array $arguments)
15✔
167
    {
168
        $user = $this->forUser ?? (auth()->loggedIn() ? auth()->user() : null);
15✔
169

170
        // 1. Closure-based ability registered via define().
171
        if (isset($this->abilities[$ability])) {
15✔
172
            return ($this->abilities[$ability])($user, ...$arguments);
10✔
173
        }
174

175
        // 2. Policy lookup by first argument's class.
176
        if ($arguments !== []) {
5✔
177
            $resource    = $arguments[0];
3✔
178
            $resourceCls = is_object($resource) ? $resource::class : (is_string($resource) ? $resource : null);
3✔
179

180
            if ($resourceCls !== null) {
3✔
181
                $policyCls = $this->policies[$resourceCls] ?? $this->autoDiscoverPolicy($resourceCls);
3✔
182

183
                if ($policyCls !== null && class_exists($policyCls)) {
3✔
184
                    return $this->callPolicy($policyCls, $ability, $user, $arguments);
3✔
185
                }
186
            }
187
        }
188

189
        return null; // unknown ability → deny
2✔
190
    }
191

192
    /**
193
     * @param class-string $policyCls
194
     * @param list<mixed>  $arguments
195
     *
196
     * @return bool|PolicyResponse|null
197
     */
198
    private function callPolicy(string $policyCls, string $ability, ?User $user, array $arguments)
3✔
199
    {
200
        $policy = new $policyCls();
3✔
201

202
        if ($policy instanceof Policy) {
3✔
203
            $before = $policy->before($user, $ability, $arguments);
3✔
204

205
            if ($before !== null) {
3✔
206
                return $before;
1✔
207
            }
208
        }
209

210
        // Ability names like "post.update" map to method "update";
211
        // bare names map directly.
212
        $method = str_contains($ability, '.') ? substr($ability, strrpos($ability, '.') + 1) : $ability;
2✔
213

214
        if (! method_exists($policy, $method)) {
2✔
NEW
215
            return null;
×
216
        }
217

218
        return $policy->{$method}($user, ...$arguments);
2✔
219
    }
220

221
    /**
222
     * Convention: `App\Models\Post` → `App\Policies\PostPolicy`.
223
     *
224
     * @param class-string $resourceCls
225
     *
226
     * @return class-string|null
227
     */
NEW
228
    private function autoDiscoverPolicy(string $resourceCls): ?string
×
229
    {
NEW
230
        $config = config('Auth');
×
231

NEW
232
        if (! (bool) $config->gateAutoDiscover) {
×
NEW
233
            return null;
×
234
        }
235

NEW
236
        $namespace = (string) $config->policyNamespace;
×
NEW
237
        $shortName = substr($resourceCls, strrpos($resourceCls, '\\') + 1);
×
NEW
238
        $candidate = $namespace . $shortName . 'Policy';
×
239

NEW
240
        return class_exists($candidate) ? $candidate : null;
×
241
    }
242

243
    /**
244
     * Returns true when an ability is registered as a closure (does not
245
     * include policy methods, which are discovered lazily on resolve).
246
     */
247
    public function has(string $ability): bool
1✔
248
    {
249
        return isset($this->abilities[$ability]);
1✔
250
    }
251
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc