# Request Lifecycle

Understanding the Magic request lifecycle helps you know exactly where your service providers, middleware, and controllers run relative to each bootstrap step.

- [Introduction](#introduction)
- [Lifecycle Overview](#lifecycle-overview)
- [Application Bootstrap](#application-bootstrap)
- [Service Providers](#service-providers)
- [Routing](#routing)
- [Middleware](#middleware)
- [Controller Dispatch](#controller-dispatch)
- [View Rendering](#view-rendering)

## Introduction

Understanding the Magic request lifecycle will help you build better applications. This document covers how a Magic application starts up, handles navigation requests, and renders views.

## Lifecycle Overview

```
┌─────────────────────────────────────────────────────────┐
│                    main.dart                            │
│                       │                                 │
│                  Magic.init()                           │
│                       │                                 │
│                  Env.load()                             │
│                       │                                 │
│               configFactories evaluated                 │
│                       │                                 │
│             URL strategy applied (web)                  │
│                       │                                 │
│      ┌────────────────┴────────────────┐                │
│      ▼                                 ▼                │
│  providers register() (sync)    (all providers)        │
│                       │                                 │
│      providers boot() (async, all registered)          │
│                       │                                 │
│              router pre-build                           │
│                       │                                 │
│                 runApp(MagicApplication)                │
│                       │                                 │
│              Route Matched (GoRouter)                   │
│                       │                                 │
│         ┌─────────────┼─────────────┐                   │
│         ▼             ▼             ▼                   │
│   Run Middleware   Resolve Layout   Get Controller      │
│                                                         │
│                       │                                 │
│              Controller.method()                        │
│                       │                                 │
│                 Render View                             │
└─────────────────────────────────────────────────────────┘
```

## Application Bootstrap

The application starts in `main.dart`:

```dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 1. Initialize Magic
  await Magic.init(
    envFileName: '.env',
    configFactories: [
      () => appConfig,
      () => networkConfig,
      () => authConfig,
    ],
  );
  
  // 2. Restore authentication (optional)
  await Auth.restore();
  
  // 3. Run migrations (development)
  if (kDebugMode) {
    await Migrator().run([...migrations]);
  }
  
  // 4. Start the application
  runApp(MagicApplication(
    title: 'My App',
    initialRoute: Auth.check() ? '/dashboard' : '/login',
  ));
}
```

### Magic.init() Steps

1. **Load Environment** - Reads `.env` file into memory via `Env.load()`
2. **Evaluate Config Factories** - Runs all `configFactories` now that `Env` is populated
3. **Apply URL Strategy** - Configures path vs hash URL strategy for web (before any widget tree exists)
4. **Register Service Providers** - Calls `register()` on each provider synchronously
5. **Boot Service Providers** - Calls `boot()` on each provider asynchronously
6. **Pre-build Router** - Resolves `MagicRouter.instance.routerConfig` so GoRouter is ready before `runApp`

## Service Providers

Providers are registered in `config/app.dart`:

```dart
'providers': [
  (app) => NetworkServiceProvider(app),
  (app) => AuthServiceProvider(app),
  (app) => DatabaseServiceProvider(app),
  (app) => CacheServiceProvider(app),
  (app) => LocalizationServiceProvider(app),
  (app) => EventServiceProvider(app),
  (app) => AppServiceProvider(app),
],
```

### Provider Lifecycle

1. **register()** - Bind services to container (no dependencies)
2. **boot()** - Initialize services (can use other services)

```dart
class AppServiceProvider extends ServiceProvider {
  @override
  void register() {
    // Runs first - just bind services
    app.bind('api', () => ApiService());
  }

  @override
  Future<void> boot() async {
    // Runs after ALL providers register
    // Safe to use other services here
    Gate.before((user, ability) {
      if ((user as User).isAdmin) return true;
      return null;
    });
  }
}
```

### Registering a provider after boot

A provider registered once the app has already booted is booted on the spot,
rather than waiting for a boot phase that has been and gone. `register()`
returns a `Future` so a caller that needs the provider wired before its next
line can wait for it:

```dart
// A plugin installing itself lazily, after the app is running.
await Magic.app.register(NotificationsServiceProvider(Magic.app));
Notify.startPolling(); // the provider has finished booting
```

Ignoring the future is fine when you do not need that guarantee. Before the boot
phase it completes immediately, since nothing asynchronous has happened yet.

Registering the same provider **instance** twice is a no-op the second time, so
its `register()` and `boot()` each run once. Two different instances of the same
provider class both run, which is deliberate: a provider parameterised per
plugin is a legitimate shape.

## Routing

Routes are defined in `lib/routes/`:

```dart
// lib/routes/web.dart
void registerRoutes() {
  // Guest routes
  MagicRoute.group(
    layoutId: 'guest',
    layout: (child) => GuestLayout(child: child),
    routes: () {
      MagicRoute.page('/login', () => AuthController.instance.login());
      MagicRoute.page('/register', () => AuthController.instance.register());
    },
  );

  // Authenticated routes
  MagicRoute.group(
    layoutId: 'app',
    middleware: ['auth'],
    layout: (child) => AppLayout(child: child),
    routes: () {
      MagicRoute.page('/dashboard', () => DashboardView());
      MagicRoute.page('/settings', () => SettingsView());
    },
  );
}
```

Each group with a `layout:` is its own go_router `ShellRoute`, so the guest and authenticated groups are separate shells. Groups that share a `layoutId` share one persistent shell, and shells are emitted in registration order. See [Layouts](https://anilcancakir.com/magic/docs/v0.0/basics/routing.md#layouts-shell-routes).

### Route Resolution

1. URL is matched against defined routes
2. Layout is wrapped around the view
3. Middleware is executed in order
4. Controller/View is resolved

## Middleware

Middleware intercepts navigation before the view renders:

```dart
class EnsureAuthenticated extends MagicMiddleware {
  @override
  Future<void> handle(void Function() next) async {
    if (Auth.check()) {
      next();  // Allow navigation
    } else {
      MagicRoute.to('/login');  // Redirect
    }
  }
}
```

### Middleware Execution Order

1. Global middleware (registered in provider)
2. Route group middleware
3. Route-specific middleware

## Controller Dispatch

Controllers are resolved using the `findOrPut` pattern:

```dart
// Route definition
MagicRoute.page('/tasks', () => TaskController.instance.index());

// Controller
class TaskController extends MagicController {
  static TaskController get instance => Magic.findOrPut(TaskController.new);
  
  Widget index() {
    if (isEmpty) _loadTasks();
    return const TaskListView();
  }
}
```

### Controller Lifecycle

1. **findOrPut** - Get existing or create new controller
2. **onInit()** - Called when controller is created
3. **Method execution** - Returns view widget
4. **State updates** - `notifyListeners()` triggers rebuilds
5. **onClose()** - Called when controller is disposed

## View Rendering

Views render using controller state:

```dart
class TaskListView extends MagicView {
  const TaskListView({super.key});

  @override
  Widget build(BuildContext context) {
    final controller = TaskController.instance;
    
    return controller.renderState(
      (tasks) => _buildList(tasks),
      onLoading: CircularProgressIndicator(),
      onError: (msg) => ErrorWidget(message: msg),
      onEmpty: EmptyState(),
    );
  }
}
```

### State Flow

1. Controller calls `setLoading()` → View shows loading
2. Controller calls `setSuccess(data)` → View renders data
3. Controller calls `setError(msg)` → View shows error
4. Controller calls `notifyListeners()` → View rebuilds

> [!TIP]
> Understanding this lifecycle helps you place logic in the right location: configuration in providers, authorization in middleware, business logic in controllers, and UI in views.
