| name | magic-framework |
| description | Write correct, idiomatic code in a Flutter app that depends on the `magic` framework (Laravel-inspired: IoC container, 18 facades, Eloquent-style ORM, service providers, reactive controllers, GoRouter routing, validation, auth, broadcasting). Use whenever code imports `package:magic/magic.dart` or `package:magic/testing.dart`, or the work touches Magic.init, MagicApp, a facade (Auth/Http/Cache/DB/Echo/Event/Gate/Config/Lang/Launch/Log/Pick/MagicRoute/Schema/Session/Storage/Vault/Crypt), a Model, MagicController, a MagicView, MagicFormData, FormRequest, a ServiceProvider, a migration, or the artisan make:* CLI. UI styling is Wind (separate wind-ui skill). Do NOT use for plain Flutter or Wind-only work with no magic import. |
| when_to_use | Use proactively when editing or scaffolding a magic app: Magic.init / a facade / a Model / a MagicController or MagicView / a form (MagicFormData, FormRequest, Validator) / a ServiceProvider / a route or MagicMiddleware / a migration / MagicStateMixin + RxStatus + fetchList / Session flash + old() + trans() / testing with MagicTest + Http.fake/Auth.fake / the artisan make:* CLI / the magic_deeplink, magic_notifications, magic_social_auth, magic_starter, magic_payments, or magic_devtools plugins. Trigger even when the user does not say the word 'magic'. Do NOT trigger for plain Flutter or Wind-only UI with no package:magic import. |
| version | 0.1.11 |
Magic Framework
Laravel-inspired Flutter framework: IoC container, 18 facades, Eloquent-style ORM, service providers, reactive controllers, and GoRouter-backed routing. This skill makes an agent write code that an experienced magic developer would write: facade-first, IoC-resolved, reactive, and verified against the real API in lib/src. All visual styling is handled by Wind (load the wind-ui skill for className work); this skill owns architecture, data, navigation, auth, and testing.
The host app already depends on package:magic/magic.dart. The accuracy contract for this skill: every API you write must exist in lib/src. When unsure of a signature, open the source or the matching doc/** page rather than guessing; magic is pre-1.0 and the surface is exact, not approximate.
0. Before writing code in this project
Three checks, each pays off across the whole session.
- Read
lib/main.dart and lib/config/app.dart. Note the providers list and its ORDER (AppServiceProvider must precede AuthServiceProvider so setUserFactory is set before auth restore runs), and whether configFactories or configs is used.
- Scan one existing controller + view pair in
lib/app/ for the project's idioms: the singleton accessor shape, how views resolve controllers, how forms are wired. Match the surrounding code, do not invent a dialect.
- CLI invocation. Magic ships an
artisan executable, so every command runs as dart run magic:artisan <cmd> from any app that depends on magic (no package-name placeholder, no global activate).
1. Core Laws
Hard constraints for every line of magic code.
await Magic.init() first. It must be awaited in main() before any facade call and before runApp(). Never .then(); providers are not booted until the future completes.
- Facade-first. Reach for
Auth, Http, Config, Cache, DB, Schema, Log, Event, Echo, Lang, MagicRoute, Gate, Session, Vault, Storage, Pick, Crypt, Launch. Resolve from the container manually (Magic.make<T>('key')) only when extending the framework.
- Controllers are singletons.
static X get instance => Magic.findOrPut(X.new); is the canonical accessor. Views resolve controllers via Magic.find<T>() (automatic in MagicView), never through constructors.
- IoC over
new for services. Bind in a provider's register(), resolve via the facade or Magic.make<T>('key'). Do not scatter Service() construction across the app.
- Provider discipline.
register() is synchronous and is where routes and bindings go. boot() is async and may resolve other services; set Auth.manager.setUserFactory(...) here.
- Reactive state, not setState. Controllers extend
MagicController (a ChangeNotifier); state flows through MagicStateMixin + RxStatus. Use refreshUI() (guarded notifyListeners, and the single seam every controller notification goes through, including validation), setLoading/setSuccess/setError/setEmpty, and MagicBuilder for sections. MagicController.onRefreshUI is a null-by-default static debug tooling sets to observe those notifications. Local setState belongs only to genuine widget-local UI state inside a MagicStatefulView.
- Typed attribute access. Models use
get<T>('key') and set('key', v), never raw getAttribute. Declare fillable; use fill(validated, strict: true) after validation so schema drift throws MassAssignmentException.
- Context-free navigation and feedback.
MagicRoute.to/back/replace, Magic.snackbar/toast/dialog/confirm/loading. Never depend on a BuildContext for navigation or feedback. Never navigate or fetch inside build().
- Validate at the boundary.
MagicFormData for forms, FormRequest for complex payloads, Validator for ad hoc checks. Surface server errors with handleApiError(response) (from the ValidatesRequests mixin).
- Trailing commas, multi-line collections. Always. Match the project's existing style.
2. Bootstrap
import 'package:magic/magic.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Magic.init(
configFactories: [
() => appConfig, // factories: evaluated AFTER Env.load(), so env() works inside them
() => authConfig,
() => networkConfig,
],
);
runApp(MagicApplication(title: 'My App'));
}
Real lifecycle (from lib/src/foundation/magic.dart): Env.load() then configFactories evaluate, then MagicApp.init (config merge), then the web URL strategy is applied if routing.url_strategy == 'path', then core bindings, then providers register() (sync), then await boot() (async), then the router pre-builds, then ready.
Use configFactories (not configs) whenever a config value reads Env.get(): configs is evaluated before Env is loaded. MagicApplication accepts title, titleSuffix, windTheme, themeMode, locale, localizationsDelegates, onThemeChanged, onInit, initialRoute.
3. Mental model: Laravel to magic (and where it diverges)
Magic mirrors Laravel's vocabulary; it diverges wherever Dart lacks PHP's runtime reflection or where the target is a Flutter client, not an HTTP server. Internalize the divergences; they fail silently (null, not an exception).
| Laravel | magic | Note |
|---|
Container autowiring, __callStatic facades | string-keyed factory closures + explicit static facade stubs | No reflection, no autowiring; an unregistered key throws at runtime |
ServiceProvider::boot() (sync) | boot() is async | await it; dropped futures leave a half-booted provider |
Router::resource returns Responses | routes resolve to WIDGETS; middleware runs on NAVIGATION | not an HTTP request cycle |
| controllers = per-request handlers | controllers = reactive ChangeNotifier singletons | live for the session, drive UI via RxStatus |
Eloquent lazy load + with() eager load | relations cast from nested API Maps, cached on first access | NO lazy load, NO with(), NO query-builder relations: if the payload did not nest it, it is null |
Gate/Policy server-authoritative | Gate/Policy run CLIENT-side, advisory only | always re-authorize on the backend |
| server sessions | tokens in Vault (secure storage), cache-first restore | Auth.restore() on cold start |
Encrypter AES + HMAC/AEAD JSON envelope | AES-256-CBC iv:ciphertext (base64), no MAC | not cross-decryptable with Laravel's Crypt |
The five assumptions a Laravel developer gets wrong most: (1) the container autowires (it does not, register explicitly); (2) user.posts lazy-loads (it does not, embed in the payload); (3) Gate.allows() is real security (advisory only); (4) with() exists (it does not); (5) boot() is sync (it is async). Full mapping with Laravel source citations: ${CLAUDE_SKILL_DIR}/references/bootstrap-lifecycle.md.
4. Facades and the container
IoC container (the methods an app uses)
| Call | Purpose |
|---|
Magic.bind('key', () => Svc(), {shared}) | factory binding (new instance per resolve; shared: true caches) |
Magic.singleton('key', () => Svc()) | lazy shared singleton |
Magic.make<T>('key') | resolve a service (throws if unbound) |
Magic.bound('key') | is the key registered |
Magic.put<T>(ctrl) / Magic.find<T>() / Magic.findOrPut<T>(T.new) | controller register / resolve / get-or-create |
Magic.delete<T>() / Magic.isRegistered<T>() | controller remove / check |
Magic.flush() / MagicApp.reset() | clear controllers / full container reset (testing) |
The 18 facades
Config and Gate resolve through their managers (no plain IoC key); the rest bind to the key shown.
| Facade | Key | Surface you reach for (all verified in lib/src/facades/) |
|---|
Auth | auth | login(data, user), logout(), check(), guest (getter), user<T>(), id(), getToken(), refreshToken(), restore(), registerModel<T>(factory), guard([name]), stateNotifier, manager, fake({user}) |
Http | network | get/post/put/delete, upload, RESTful index/show/store/update/destroy, fake([stubs]), response([data, code]), unfake(). NO patch |
Config | (manager) | get<T>, getOrFail<T>, set, has, all, merge, prepend, push, forget, flush, repository |
Cache | cache | put(key, value, {ttl}), get, has, forget, flush, remember<T>(key, ttl, cb), fake() |
DB | (lazy) | table(name) (query builder), select/statement/insert/update/delete (raw SQL), transaction(cb), beginTransaction/commit/rollback |
Schema | (manager) | create(table, (b){}), table, drop, dropIfExists, hasTable, hasColumn, getColumns, rename |
Log | log | info/error/warning/debug/notice/critical/alert/emergency, log(level, msg), channel(name), fake() |
Event | (dispatcher) | dispatch(MagicEvent); register listeners with EventDispatcher.register(Type, [() => Listener()]) |
Echo | broadcasting | channel/private/join, listen, leave, connect/disconnect, socketId, connectionState, onReconnect, addInterceptor, manager, fake() |
MagicRoute | (router) | page, group, layout, resource(name, ctrl, {only, except}), to, toNamed, push, back({fallback}), replace, setTitle, currentTitle, config |
Gate | (manager) | define, before, allows, denies, allowsAny(list), allowsAll(list), has, abilities, flush |
Session | (store) | flash(map), flashErrors(map), old(field, [fallback]), oldRaw, error(field), errors(field), hasError, hasFlash, tick() |
Lang | (translator) | get(key, [replace]), has, current, isLoaded, supportedLocales, setLocale, detectLocale, detectAndSetLocale, setSupportedLocales, addListener/removeListener, delegate |
Vault | vault | put(key, value), get, delete, flush, fake([initial]) |
Storage | (manager) | disk([name]), put, get, getFile, exists, delete, url, download, setManager, flush |
Pick | (static) | image, images, camera, media, video, recordVideo, file, files, directory, saveFile |
Crypt | encrypter | encrypt, decrypt, encryptWithDeviceKey, decryptWithDeviceKey, hasDeviceKey, generateDeviceKey, clearDeviceKey |
Launch | launch | url(u, {mode}), email, phone, sms, canLaunch |
Global helper functions exist and are idiomatic: env<T>(key, [default]), trans(key, [replace]), old(field, [fallback]), error(field), carbonNow(), carbonToday(), carbonParse(s). Full per-facade signatures: ${CLAUDE_SKILL_DIR}/references/facades-api.md.
5. Canonical patterns