Skip to content

Commit 999be24

Browse files
committed
md for dev team
1 parent ae62c19 commit 999be24

1 file changed

Lines changed: 250 additions & 0 deletions

File tree

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
# Moox Scope
2+
3+
This guide is **minimal and practical**.
4+
5+
Rule of thumb:
6+
**A model is scopable if (and only if) it has a nullable `scope` column (`string|null`).**
7+
8+
---
9+
10+
## 1) Scope string format
11+
12+
Records store a scope as a string:
13+
14+
`origin:source:context:boundary`
15+
16+
Example:
17+
18+
- `media:draft:jobapplications:private`
19+
20+
Meaning:
21+
22+
- **origin**: which record type stores the scope (e.g. `media`, `category`, `tag`)
23+
- **source**: which “parent context type” (e.g. `draft`, later `career`)
24+
- **context**: concrete bucket inside that source (e.g. `jobapplications`)
25+
- **boundary**: boundary bucket (`private`, `public`, `group`, `user`, `user_type`)
26+
27+
### Global / unassigned
28+
29+
Global is **not** a scope key. Global means:
30+
31+
- `scope IS NULL` (or `scope = ''`)
32+
33+
Global resource views show **only** global records by default.
34+
35+
---
36+
37+
## 2) Runtime truth (DB) vs registry (config)
38+
39+
### DB (`scopes` table) = runtime truth
40+
41+
The DB controls what is active/visible at runtime:
42+
43+
- `is_active` controls:
44+
- scoped child navigation visibility (hidden when inactive)
45+
- scoped query guards (fail-closed: only active scopes/contexts return records)
46+
- `label` is UI naming.
47+
48+
### Config = registry / whitelist / mapping
49+
50+
Config does **not** decide runtime visibility. Config defines what the codebase supports.
51+
52+
#### `packages/core/config/core.php`
53+
54+
This is the registry for translating scope keys ↔ model classes:
55+
56+
```php
57+
'scopes' => [
58+
'origins' => [
59+
'media' => \Moox\Media\Models\Media::class,
60+
'category' => \Moox\Category\Models\Category::class,
61+
// ...
62+
],
63+
'sources' => [
64+
'draft' => \Moox\Draft\Models\Draft::class,
65+
// ...
66+
],
67+
],
68+
```
69+
70+
Why we need this mapping:
71+
72+
- **Reverse lookup (write validation)**: record model → expected origin key
73+
- **Whitelist**: only known keys are considered supported by the project
74+
- **Bootstrapping**: UI/dev tools need keys even when DB is empty
75+
76+
---
77+
78+
## 3) Make a model scopable (copy/paste checklist)
79+
80+
### 3.1 Add the `scope` column
81+
82+
Migration snippet:
83+
84+
```php
85+
$table->string('scope')->nullable()->index();
86+
```
87+
88+
Convention:
89+
90+
- `NULL`/`''` = global/unassigned
91+
92+
### 3.2 Use Moox base resources (query scoping)
93+
94+
Resources that extend `Moox\Core\Entities\BaseResource` automatically call:
95+
96+
- `ScopedResourceContext::applyScope($query, static::class)`
97+
98+
Effect:
99+
100+
- **scoped list view** → filtered by `exact` or `context`
101+
- **global list view** → only `scope IS NULL OR scope=''`
102+
103+
### 3.3 Ensure “Create” applies defaults in scoped contexts
104+
105+
Moox base create pages call:
106+
107+
- `ScopedResourceContext::applyDefaults($record, static::getResource())`
108+
109+
So creating a record inside a scoped child resource automatically writes the correct 4-part scope string.
110+
111+
### 3.4 (Optional) enable bulk “Assign scope”
112+
113+
If a resource uses:
114+
115+
- `Moox\Core\Support\Resources\Concerns\HasScopedChildResource`
116+
117+
Then it can provide a bulk action that moves records between scopes by writing the `scope` column.
118+
119+
---
120+
121+
## 4) Global resource registration (example: Categories)
122+
123+
If you want a **global** admin resource (unscoped), register it explicitly via `ResourceNavigationRegistrar`.
124+
125+
### `packages/category/src/Moox/Plugins/CategoryPlugin.php`
126+
127+
```php
128+
use Moox\Core\Support\Resources\ResourceNavigationRegistrar;
129+
use Moox\Category\Moox\Entities\Categories\Category\CategoryResource;
130+
131+
public function register(Panel $panel): void
132+
{
133+
ResourceNavigationRegistrar::register($panel, [
134+
CategoryResource::class,
135+
]);
136+
}
137+
```
138+
139+
What this does:
140+
141+
- `$panel->resources([...])` → registers pages/routes for the resource
142+
- `$panel->navigationItems([...])` → forces a global navigation item using the resource’s own navigation methods
143+
144+
This is useful when scoped child navigation is also present, so global resources don’t “disappear” due to navigation composition.
145+
146+
---
147+
148+
## 5) Scoped child resources under a parent (example: Draft)
149+
150+
You define child resources in the parent feature config, then register them in the parent plugin using `ChildResourceRegistrar`.
151+
152+
### 5.1 Define scoped children in config
153+
154+
Location:
155+
156+
- `packages/draft/config/draft.php`
157+
158+
Example (real, from our repo):
159+
160+
```php
161+
'resources' => [
162+
'draft' => [
163+
'scopes' => [
164+
'media' => [
165+
'enabled' => true,
166+
'resource' => \Moox\Media\Resources\MediaResource::class,
167+
'origin' => 'media',
168+
'boundary' => 'private',
169+
'label' => 'Media Private',
170+
],
171+
'media_public' => [
172+
'enabled' => true,
173+
'resource' => \Moox\Media\Resources\MediaResource::class,
174+
'origin' => 'media',
175+
'boundary' => 'public',
176+
'label' => 'Media Public',
177+
],
178+
'tag' => [
179+
'enabled' => true,
180+
'resource' => \Moox\Tag\Resources\TagResource::class,
181+
],
182+
'category' => [
183+
'enabled' => false,
184+
'resource' => \Moox\Category\Moox\Entities\Categories\Category\CategoryResource::class,
185+
'origin' => 'category',
186+
'boundary' => 'private',
187+
'label' => 'Category Private',
188+
],
189+
],
190+
],
191+
],
192+
```
193+
194+
Notes:
195+
196+
- `resource` is the Filament resource class that will be registered for this scoped child.
197+
- `enabled` is informational only (runtime activation is controlled by DB `scopes.is_active`).
198+
- `origin`/`boundary`/`label` are optional overrides. If omitted, the system derives defaults from keys and the parent context.
199+
200+
### 5.2 Register the parent definition in the plugin
201+
202+
Location:
203+
204+
- `packages/draft/src/Moox/Plugins/DraftPlugin.php`
205+
206+
The key call:
207+
208+
```php
209+
ChildResourceRegistrar::registerFromParentDefinition(
210+
$panel,
211+
DraftResource::class,
212+
'draft',
213+
config('draft.resources.draft', []),
214+
);
215+
```
216+
217+
What happens:
218+
219+
1. The parent resource is registered in the panel.
220+
2. Each child resource is registered as a **resource configuration** (same PHP class, different configuration key).
221+
3. A navigation item is added **only if** the corresponding DB scope exists and `is_active=true` (fail-closed).
222+
223+
### 5.3 Sync config → DB (`scopes` table)
224+
225+
```bash
226+
php artisan scopes:sync
227+
```
228+
229+
Then activate/deactivate via the Scopes UI.
230+
231+
---
232+
233+
## 6) What “is_active” affects
234+
235+
- **Navigation**: scoped child nav item appears only when its scope is present and active.
236+
- **Queries**: `ScopeQuery` applies DB guards so inactive scopes do not return data.
237+
- **Bulk Assign options**: only active scopes are offered for assignment.
238+
239+
---
240+
241+
## 7) exact vs context (scope_match)
242+
243+
- `exact` → matches the full `origin:source:context:boundary`
244+
- `context` → matches `origin:source:context:%` (boundary ignored)
245+
246+
Default behavior (when not explicitly set) is derived from the DB:
247+
248+
- if there is **more than one active boundary** for the same `origin/source/context` → default is `exact`
249+
- else → default is `context`
250+

0 commit comments

Comments
 (0)