Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number Litr A small, native web browser for macOS Notr A notebook for macOS e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Refr Local-first PDF workspace for macOS Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra
Laravel

Laravel

e ships with PHP/Laravel-aware features on top of the Intelephense language server.

Inspired by the official Laravel VS Code extension, e introspects your project (via php artisan and the filesystem) to provide completion, hover and navigation for Laravel's helpers. It is enabled automatically in any project with an artisan file; toggle it under Settings → Laravel features (or the laravel config key). The data refreshes itself: saving a route, config, lang or .env file in e reloads it at once, and a change on disk from outside — php artisan make:controller, a git checkout — is noticed within a couple of seconds. Laravel: Refresh Project Data (⌘⇧P) is still there for a forced reload.

Helper completion

When the caret is inside one of Laravel's helpers, e offers completions sourced from your project:

Helper Completes
route('…') named routes (with method + URI)
view('…') Blade view names
config('…') config keys (with resolved value)
env('…') environment variables (with value)
__('…'), trans('…'), @lang translation keys (with text)
<x-…> Blade components
<x-alert … the component's props as attributes (from @props([…]) or the class's constructor)
@include, @extends, @each, @component view names (with hover and go to definition)

These are read from your project, so they reflect your actual routes, views, config, .env, language files and components.

Hover & go to definition

  • Hover (F1) over a helper string shows the resolved value — a config value, a route's method/URI/action, an env value, or a translation's text.
  • Go to definition (F12) jumps to the target:
    • route('…') → the controller method
    • view('…') → the Blade file
    • config('…') → the config file (and the key's line)
    • env('…') → the .env line
    • __('…') → the language file
    • <x-…> → the component's Blade file

Blade templates

*.blade.php files are detected as Blade with full syntax highlighting: HTML tags, attributes and Tailwind utility classes, Blade directives (@php, @if, @foreach, @push, …), {{-- comments --}}, and the embedded PHP inside @php … @endphp blocks, {{ … }} and {!! … !!} expressions.

Query-builder completion

Inside a query builder, column names complete from the model's table and the live schema — where('…'), orderBy(), select(), pluck(), value(), groupBy(), having() — and relationship names complete inside with('…'), load(), whereHas(). The table is resolved from Model::, $model, or DB::table('…'). Columns that don't exist in the schema are underlined with a warning — a check PhpStorm can't do without the database.

Laravel lints

From the same project data, e underlines what would fail at runtime, as you type (300 ms after the last keystroke, off the UI thread):

In Check
view('…'), @include, @extends, @each, @component the view file exists (error)
route('…'), to_route('…') the route is defined (error); its required {parameters} are passed (warning)
__('file.key'), trans(), trans_choice(), @lang the key exists in lang/ (warning; sentence keys are JSON translations that fall back to themselves)
.env every key .env.example declares is set (warning at the top of the file)

Arguments that are expressions ('a.'.$b, $name) and namespaced names (pkg::view) are left alone. When the official laravel/lsp is running it reports missing views and routes itself, so these lints stay off then.

Two of them come with a fix in the code-action picker: Create resources/views/…blade.php for a missing view (the file opens once created), and Add key to lang//file.php for a missing translation key, where the locale is APP_LOCALE from .env (else app.locale, else en) and the file is created around the key when it doesn't exist yet.

Package completions (ide.json)

Packages that ship a Laravel Idea ide.json — and your own project's ide.json — teach e their string arguments: new Axis('…') completes the strings the package lists, ->rule('…') on its validation class completes rule names, a function it declares as taking a route name completes your routes. e reads ide.json from the project root and from every vendor/*/*/ on load, and honours the completions section: kinds routeName, viewName, configKey, translationKey, environmentVariable, bladeComponent, validationRule, staticStrings, gate/policy, inertiaPage, bound by function, method, constructor, parameter position and place (parameter or array value). Receiver types aren't inferred, so a ->method('…') rule matches by method name.

Refactorings (code actions)

Alongside the language servers' quick fixes, the code-action picker offers e's own Laravel refactorings at the caret:

  • Convert validation string to array — 'required|max:255' → ['required', 'max:255'].
  • Convert to [Controller::class, 'method'] — from the string 'UserController@index' form.
  • Convert {{ }} ↔ {!! !!} in Blade.
  • Convert scopeActive() to #[Scope] active() (Laravel 12.6+), adding the use Illuminate\Database\Eloquent\Attributes\Scope; import when missing.
  • Promote to FormRequest — with the caret in $request->validate([…]) (or $this->validate($request, […])): creates app/Http/Requests/StoreUserRequest.php (named from the method and controller) holding the rules, replaces the call with $request->validated(), retypes the method's Request parameter and adds the import. The new class opens.

Validation rules

Rule names complete inside validate([…]) and FormRequest rules(), and so do the tables and columns exists: and unique: name — exists:us offers your tables, exists:users, their columns, from the live schema; Rule::exists('…') and Rule::unique('users', '…') likewise. The command Laravel: Generate Validation Rules from Table writes 'field' => 'rules' lines from the live schema (nullable → nullable, varchar(255) → max:255, and so on) at the cursor.

Tests: run the one at the caret

⌘⌥⇧T runs only the test the caret is in — a Pest it('…') / test('…') description or a PHPUnit test_… / #[Test] method — as php artisan test <file> --filter="…", in the TDD panel, so a failure still has a line to jump to. The next ⌘⇧T runs the whole suite again.

The Laravel menu

⌘⇧, opens the command palette filtered to Laravel: — every Laravel command in one list, the way Laravel Idea's menu gathers them: code generation, the Artisan palette, route search, the Eloquent helper, migrations, the maps and panels. Type to narrow it further.

Code generation

Laravel: New Eloquent Model… (⌘⇧N) is a spec in an editor panel with a live preview of the files it will create. Fields have a type, ? for nullable and an optional default; foreignId Customer is a foreign key and a belongsTo. Relations, options (id timestamps softDeletes fillable, prefix - to turn one off) and what to generate are each one line:

model: Order
fields:
  customer_id   foreignId Customer
  status        string = 'pending'
  total         decimal(10,2)
  notes         text?
relations:
  hasMany OrderLine
options: id timestamps fillable
generate: migration factory seeder request controller api-controller resource policy

⌘↵ (or Create) writes the model with $fillable, casts() and the relation methods, plus whatever generate: lists: the create_orders_table migration; a factory with fakes chosen from the column types and names; a seeder; StoreOrderRequest/UpdateOrderRequest with rules derived from the fields (required|string|max:255, nullable|string, exists:customers,id, sometimes on update); a resource controller that uses those requests (or an API one under App\Http\Controllers\Api returning the JSON resource); OrderResource; OrderPolicy. Files that already exist are left alone and listed. The language servers are told about the new files at once, so they complete immediately.

Laravel: New Pivot Table… is the same panel with a pivot: Post Tag line: one migration for post_tag with both foreign keys, a composite primary key and (unless options: -timestamps) timestamps.

Laravel: New Eloquent Model from Database Table builds the model from the live schema instead (see below). Laravel: New Class… is the Artisan palette opened on make: — every generator the app and its packages declare (controller, job, event, listener, mail, notification, command, Livewire component, …), with Artisan's own usage line as the hint for the arguments.

Artisan

⌘⇧A opens the Artisan palette with every command the app declares (its own and its packages'), make: first. Pick one, and the usage line becomes the hint for an argument line: Order -mf for make:model, --seed for migrate. Enter runs it in a terminal tab, so prompts and output work as in a shell.

Eloquent helper

Laravel: Generate Eloquent Helper writes _ide_helper_models.php in the project root: for every model, @property per column with its PHP type from the live schema, @property-read for each relationship (a Collection<int, Post> plus posts_count on the many side), and @method static Builder for each scope (scopeActive() or #[Scope]). Intelephense reads it like any file, so $user->posts, $order->total and User::active() complete and type-check everywhere — in closures, on collections, in Blade. Once the file exists it is regenerated whenever the schema is reloaded, and the language servers are told about it at once. Add it to .gitignore.

Routes: controller actions

In a route file, [UserController::class, ' completes the controller's public methods, resolving the class through the file's use imports and Composer's PSR-4 map.

Route search

⌘⇧R lists every route — GET /users/{user} with its name and action — and filters as you type on method, URI, name or controller. Enter opens the controller action, or the routes-file line for a closure route.

Find unused views

Laravel: Find Unused Views scans app/, routes/, config/ and the views themselves for view(), @include/@extends/@each/@component, Route::view(), View::make(), mailables' ->view()/->markdown(), ->layout()/#[Layout], 'view' =>/'layout' => entries and <x-…> tags, and lists the Blade views nothing refers to. Livewire views and single-file components, errors.* and vendor.* are resolved by convention and left out. A view named from a variable can't be seen, so read the list as "worth a look"; Enter opens one.

Gates & policies

can(), authorize(), @can, and Gate::allows() complete ability names and jump (F12) to the policy method or Gate::define() that declares them.

Generate model from table

With a table open in the database panel, Laravel: Generate Model from Table creates an Eloquent model from the live schema — $fillable, $casts, and belongsTo/hasMany relationships inferred from the real foreign keys.

Event dispatch graph

⌘⌥G opens the event → listener graph, built from $listen, Event::listen(), and auto-discovered handle(EventType $event) listeners. F12 on a dispatched event class jumps to a listener.

Related files

⌘⌥E shows every file for the current resource — model, migration(s), factory, seeder, controller, policy, request, resource, and test — in a quick picker.

Livewire

e treats a Livewire component's class and Blade view as one unit:

  • wire:model="…" completes from the component class's public properties.
  • F12 on a property in the view jumps to its declaration in the class; ⌘⌥J switches between the view and the class.
  • Renaming a property with F2 updates both the class ($prop, $this->prop) and every wire: reference in the view.
  • wire:click="…", wire:submit, wire:change and the other event attributes complete from the class's action methods (public, minus lifecycle hooks).
  • <livewire:…> and @livewire('…') complete component names from app/Livewire.

Runtime insight

⌘⌥I opens a continuous, Telescope-style panel that captures every request against your dev app. No Telescope or Debugbar install required.

With Grove serving the project, the panel reads Grove's own request timeline — Grove is the proxy, so every request is there with nothing installed in the app: method, path, status and duration. Expanding a request fetches its causal chain from Grove: the SQL it issued (turn on SQL capture in the panel header, which runs grove sql-capture on; MySQL), the mail it sent, and the matching error-log entries. ✨ hands the agent Grove's whole explain bundle — the request with credentials redacted, its queries and mail, and the stacktrace from laravel.log — so it can go straight to the cause. The replay base URL also comes from Grove (the real host, and http:// for a site without HTTPS).

Without Grove, the panel polls Clockwork (/__clockwork/latest) as before: queries with N+1 warnings, cache hits/misses, sent mails, and events.

Verify the fix (below) takes its query counts from Grove's chain too when the app has no Clockwork — turn on SQL capture first.

Mail and webhooks (Grove)

Grove: Mail Catcher (command palette) lists every email the app sent to Grove's SMTP server — subject, recipients, time — and shows a message's text when you click it; ✨ hands it to the agent to check content and find where it is built. Grove: Webhooks lists the deliveries captured at /__grove/hooks/<bucket>; select one and ↻ Re-deliver sends the exact same request to your handler at that path on the app's own URL, so you can fix the handler and replay until it answers 200.

Verify the fix (✓)

Click the ✓ on a captured request to verify a change end to end. e checkpoints your working tree, replays the request and records a baseline (time, query count, N+1). Apply your fix — edit the code or ask the agent — then hit Measure again: e replays the request and shows a before/after verdict (Improved / No change / Regressed / Broke). Keep the change, or Discard it to revert to the checkpoint.

For Inertia/VILT projects, see Inertia & the VILT stack.

Tinker scratchpad

Press ⌘⌥T for a Tinker scratchpad: write PHP and press ⌘↵ to run it against your app via php artisan tinker, with the output shown below. Select code in the editor and run Tinker: Run Selection to evaluate it. The AI agent can also write and run Tinker snippets over the sync socket.

Architecture map

⌘⌥M opens an interactive map of your routes: each row shows route → controller → views as clickable cards. Click the controller to jump to its method, or a view to open the Blade file. Filter by route name, URI or action.

Eloquent completion (live schema)

When you type $user-> on a model instance, e suggests the model's real database columns — read from the live schema (via .env) at startup. It infers the model from $var = Model::… or a type hint, maps it to its table (protected $table or the snake_case + plural convention), and merges the columns alongside Intelephense — something Intelephense can't do on its own.

Relationship graph

⌘⌥R parses the relationships from your models (hasMany, belongsTo, belongsToMany, morph*) and cross-checks them against the live database's foreign keys. Each model is a node; click a relationship to jump to the related model or method. Relations that exist in code but have no backing foreign key are flagged ⚠ — so alongside the schema diff you see code, migrations, and the actual database in one place.

Security lens

In the architecture map (⌘⌥M) every route shows its middleware stack and a badge: 🔒 when it's authenticated, ⚠ when a state-changing route (POST/PUT/PATCH/ DELETE) has no authentication. The header counts unprotected routes, and clicking a ⚠ asks the agent to suggest the right middleware/policy.

Generate a test from a replay

After replaying a route (▶ in the map), the 🧪 Test button writes a Pest feature test to tests/Feature/ using the request path, the response status, and assertions inferred from the response (JSON structure or an HTML <title>). It opens the file, ready for the ⌘⇧T "fix to green" loop.

Migrations

Laravel: Migrations (command palette) lists every migration with its batch and whether it has run, from php artisan migrate:status. ▶ Migrate runs what's pending — behind a grove db snapshot when Grove serves the database, so a bad migration is one grove db restore from undone — and ↶ Rollback last batch runs migrate:rollback --step=1; both in a terminal tab, with the list re-read once they have had a moment to finish. Click a migration to open its file.

Schema diff

Laravel: Schema Diff (command palette) compares your migrations against the live database and lists discrepancies — columns that exist in the DB but no migration creates, and columns a migration adds that aren't in the DB yet.

Log tail

⌘⌥L opens a live tail of storage/logs/laravel.log: levels are coloured, stack-trace frames are clickable (jump to file:line), and Fix with AI hands the latest error to the agent.

Request replay

In the architecture map (⌘⌥M), click ▶ on a GET route to replay the request against your running app and see the response — plus the SQL queries it ran (if the app has laravel/clockwork), with N+1 duplicates flagged. The base URL is what Grove serves the project as when Grove is running (else https://<folder>.test); override it under Settings → Laravel → App URL. "Explain with agent" hands the analysis to the AI panel.

Working on a Laravel project

  1. Open the project root: e ~/code/my-laravel-app.
  2. Ensure intelephense is installed and on your PATH.
  3. Use ⌘P to jump between controllers, models, and views; ⌘⇧O to find classes and methods; and the Source Control panel for commits.

Tips

  • The AI agent panel (⌘L) is handy for Laravel scaffolding and refactors — point it at your project and let it work alongside you.
  • Use workspace search (⌘⇧F) to find usages across Blade views and PHP classes at once.

Pint and PHPStan

Both are picked up from vendor/bin — a project that doesn't use them gets nothing, with no configuration and no behaviour change.

Pint becomes the formatter for PHP files. When the project ships vendor/bin/pint it takes precedence over the language server, because a Laravel project's formatting is whatever Pint says it is — that's what CI enforces, and letting Intelephense format to its own taste would only produce a diff for Pint to undo. It respects your pint.json: the buffer is formatted through a temporary file beside the original, so a preset scoped to app/ sees the file as being in app/.

PHPStan runs on save, over the file you just saved rather than the whole project, and its findings appear as warnings alongside the language server's. Each carries PHPStan's rule identifier (variable.undefined) as the diagnostic code, so you can look it up or baseline it. Larastan works too — it ships the same binary.

It only runs when the project has both vendor/bin/phpstan and a config (phpstan.neon, phpstan.neon.dist or phpstan.dist.neon), since PHPStan needs the config to know its level and paths. Findings cover the whole line: PHPStan reports a line but no column, and guessing a span would put the squiggle under the wrong token.

If PHPStan itself fails — a broken config, a path that doesn't exist — the error is reported rather than swallowed. A run that never looked at your code must not be mistaken for a clean one.

Test results

Press ⌘⇧T for the test panel, or Run tests from the ship gate.

Where the runner can write JUnit XML — php artisan test, Pest, PHPUnit, Vitest — the panel lists each failing test with its first assertion line, and clicking one opens the file at the failing line. The toolbar shows 12 passed · 2 failed · 1 skipped, because the suite failed and two of ninety tests failed are different news.

Runners that can't produce a report are run exactly as before, with their plain output. Nothing is passed a flag it doesn't take.

PHPUnit reports a failure's location only in the message text, not as an attribute, so a failure it can't describe that way is listed without a link rather than pointed somewhere wrong.