ichinya / laramago
Laravel presets for the native Mago CLI, configured automatically by Composer.
Requires
- php: ^8.2
- composer-plugin-api: ^2.0
- carthage-software/mago: ^1.48.1
- doctrine/inflector: ^2.1
- nikic/php-parser: ^5.8
- revolt/event-loop: ^1.0.9
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-master
- 0.1.5
- 0.1.4
- 0.1.3
- 0.1.2
- 0.1.1
- 0.1.0
- 0.0.27
- 0.0.26
- 0.0.25
- 0.0.24
- 0.0.23
- 0.0.22
- 0.0.21
- 0.0.20
- 0.0.19
- 0.0.18
- 0.0.17
- 0.0.16
- 0.0.15
- 0.0.14
- 0.0.13
- 0.0.12
- 0.0.11
- 0.0.10
- 0.0.9
- 0.0.8
- 0.0.7
- 0.0.6
- 0.0.5
- 0.0.4
- 0.0.3
- 0.0.2
- 0.0.1
- dev-docs/anonymous-case-01-benchmark
- dev-feat/issue-1-runtime-schema
- dev-feat/fix-request-query-array-contract
This package is auto-updated.
Last update: 2026-10-15 15:21:19 UTC
README
A Composer package with a Laravel preset for the native Mago CLI.
Available on Packagist. Install it in your Laravel application:
composer require --dev ichinya/laramago vendor/bin/mago lint
Composer uses Packagist by default; no custom repository is required. See the installation guide for requirements, updates, CI setup, and migration from the earlier VCS installation.
During installation, Composer asks for permission to run the ichinya/laramago
plugin. Once allowed, the plugin creates mago.dist.json in the application root.
The carthage-software/mago dependency provides vendor/bin/mago; this package
uses that executable directly, without a wrapper or Laravel service provider.
For editor integrations, vendor/bin/laramago-metadata exports source-only
configuration, route and translation metadata as versioned JSON.
Source scans reuse bounded, isolated syntax and extraction results across plugins. Configuration write indexes also share one in-flight construction per generation, so concurrent SDK callbacks do not repeat the full metadata traversal. Factory, model refresh and defensive boundary scanners share source work while keeping their own path policy, scan lifecycle and native contracts. See the source query cache contract for limits and controls.
Version 0.1.1 preserves native diagnostics when runtime writes can change
configuration values and corrects Eloquent key contracts. Configuration scanning
stops once an unknown write invalidates all keys, avoiding unnecessary metadata
requests while retaining conservative results for unreadable source files.
Configuration refinements also check the contributing config file, manifest and
explicit binding sources against their prepared snapshots. A final revision check
also checks composer.lock and rejects changed files or config directory membership with
ichinya/laramago/configuration-source-changed; rerun analysis with unchanged
sources. Native declarations and PHPDoc retain priority. See
metadata reliability checks.
Version 0.1.0 expanded source-bound model, collection, container, callback and
configuration type contracts. It adds guarded compatibility checks for array,
string, null-flow, date, header, session, XML and factory/Faker patterns. Unknown
dispatch and code outside the proven contracts retain native diagnostics.
It also refines installed framework argument contracts, string predicates,
backed enum value columns, structural XPath results, native never calls and
process descriptor redirects, with LF/CRLF source regression checks.
It includes the Laravel integrations from earlier releases.
For local package development, see the path repository instructions below.
How it works
The plugin runs on Composer's post-install-cmd and post-update-cmd events.
If the application has no Mago configuration, it creates a file such as:
{
"extends": "vendor/ichinya/laramago/presets/laravel.toml",
"source": {
"paths": ["app", "bootstrap", "config", "database", "routes", "tests"],
"includes": ["vendor"]
},
"extension-hosts": {
"laramago": {
"command": ["php", "vendor/ichinya/laramago/bin/laramago-worker.php", "vendor/autoload.php"]
}
}
}
Only existing standard directories are added to paths. If none exist, the
plugin uses .. Add custom directories such as src or Modules yourself.
Dependency paths respect Composer's vendor-dir setting.
Mago loads mago.dist.json automatically and inherits the package settings through
extends. Updating the package updates the shared preset, while application
settings stay in the root configuration. Commit the generated configuration.
Set the application's PHP version with the root php-version option; the preset
does not pin it.
Settings
- Laravel linter integration, optional
strict_types, and support for?:. - More permissive thresholds for method counts, parameter counts, and complexity.
- Formatting based on Mago's built-in Pint preset.
- Exclusions for caches, Blade templates, node_modules, and IDE helper files.
- Vendor dependencies available for type information, outside lint/format targets.
- Type errors and missing methods remain visible in the analyzer. Unused definition checks are disabled because Laravel often invokes definitions indirectly.
- A bounded defensive-check policy permits a fresh local array guard protecting a literal-key read followed immediately by a throwing string check. Native array types and argument/return errors remain active; other redundant checks retain their diagnostics. See the compatibility policy.
- Direct script assignments of an
includeorrequireresult permit untyped variable storage. The result staysmixed; unsafe uses and stronger variable annotations retain native analysis. - A local object
@varon a plain assignment or ordinaryforeachheader may repeat the inferred type. Laramago permits this documentation when native Mago certifies exact equality and current source and class metadata confirm the binding. Types and usage errors remain active; stronger or uncertain annotations retain diagnostics. - An object
@varbefore the first method guard in a by-valueforeachmay repeat its existing type when native Mago certifies exact equality. The current loop binding, enclosing scope and concrete class must agree; usage errors and stronger annotations retain native analysis. - The same native equality policy permits a local object tag on an immediate
argument to
$thisafter a matchinginstanceofguard. Intervening statements, references, compound conditions and uncertain bindings retain diagnostics. - Exhaustive string checks on a fresh decoded JSON list can establish an
optional
list<string>field in a yielded record. The declared iterable contract remains authoritative; unchecked elements and incompatible fields retain errors. See the compatibility policy.
Linter diagnostic levels
The default profile supports adoption in existing Laravel applications. Recommendations stay visible without making every recommendation fail the command. A successful lint run does not mean that every issue in the application is resolved.
| Rules | Level and rationale |
|---|---|
cyclomatic-complexity, kan-defect, halstead, too-many-methods, excessive-parameter-list, too-many-enum-cases |
Warning: design metrics require context |
final-controller, no-empty |
Warning: application style choices |
no-literal-password |
Warning: Mago 1.48.1 also flags 'password' => 'hashed' in casts and 'token' => 'required' in validation rules |
sensitive-parameter |
Warning: parameter name heuristics need review; the attribute remains useful for actual secrets |
no-error-control-operator |
Warning: review error handling after @, as well as the operator itself |
no-unsafe-finally |
Warning: Mago 1.48.1 also flags continue inside a loop contained in finally; actual return/throw statements still need review |
no-eval |
Error in application code; only tests/** is excluded to allow checks of generated PHP configuration |
Review security warnings: the default profile is not a standalone security gate. Security checks remain enabled outside the documented exclusions. Syntax errors still fail lint. To fail on warnings as well:
vendor/bin/mago lint --minimum-fail-level=warning
You can raise an individual rule's severity in the application configuration:
[linter.rules] no-literal-password = { level = "error" }
vendor/bin/mago config --show linter vendor/bin/mago list-files vendor/bin/mago lint vendor/bin/mago format --check vendor/bin/mago analyze
Use vendor/bin/mago.bat in PowerShell. The analyzer extension currently supports
magic where, orWhere, whereNot, and orWhereNot calls on Eloquent models.
It reads signatures from the installed Laravel metadata and returns
Builder<YourModel>, including the builder passed to a where callback. Native
argument count, argument type, and named argument checks remain active.
Declared methods keep their native behavior. Custom query factories and magic
dispatchers defer when their behavior is not statically known. Explicit custom
builders, local scopes, and additional bounded integrations are described below.
Laravel contracts and PHPStan-compatible analysis policies
run directly on Mago. PHPStan and Larastan are used to compare behavior; they are
not required by the extension. No source overlays are generated.
Source-certified native float callable boundaries accept numeric strings in
weak PHP mode; strict files and stronger PHPDoc retain their checks. Own physical
methods may coexist with source-certified named @mixin declarations; callable
contracts are taken from those physical methods.
Defensive CLI normalization of $_SERVER['argv'] into a string list permits
the source-proven redundant array check while preserving all native types and
errors. Conditional, referenced or dynamically shared bindings retain their checks.
Ordinary unannotated local assignments and by-value foreach bindings may store
mixed, matching PHPStan's advisory policy. Native types and all unsafe-use
errors remain active; references, captures and stronger contracts retain their checks.
examples/compatibility.toml remains an optional fragment with targeted
suppressions for your [analyzer] section.
The worker uses Mago's bundled PHP SDK and loads the application's Composer
autoloader. The default analyzer does not itself bootstrap Laravel or query a
database. Loading the autoloader can execute Composer autoload.files entries;
source inspection does not execute the inspected files. Runtime evaluation is
separately opt-in, as described below. The php executable must be on PATH; a
project may override the worker command with a specific executable.
Local scopes without a database
Local scopeName() methods are available through models and the standard
Builder, including inherited and trait declarations. The analyzer removes the
implicit query parameter and checks the remaining native/PHPDoc parameter types,
named arguments, defaults, references and variadic parameters.
// public function scopeActive(Builder $query, bool $enabled = true): void User::active(enabled: true)->findOrFail(1); // User User::query()->active(); // Builder<User>
Scopes returning void or null retain Builder<YourModel>. Other declared
results are preserved; int|null, for example, becomes int|Builder<YourModel>.
An untyped scope retains its builder only when its single return expression
provably preserves the query: the query itself, or a native where/orWhere
chain with safe arguments. Native Query Builder forwarding is also recognized
for the supported column, membership, null, range, and date predicates, as well
as orderBy and orderByDesc, provided
the installed method returns itself and no model scope, declaration, or PHPDoc
contract shadows the forwarded operation. Safe arguments include recursively
checked arrays without references or unpacking, literals, scope parameters, and known
enum case name or backed enum case value properties. Other untyped scope
results stay unknown, except for a bounded enum conversion: an
if ($value instanceof SomeEnum) containing one query-preserving return, followed by an
unconditional query-preserving return. Within that branch, the enum parameter's
name (or backed enum value) is safe; the guard does not apply to the fallback.
Additional statements, side effects, and other branch layouts remain unknown.
Generic scope methods and custom query or
scope dispatchers defer to native analysis. Explicit methods and PHPDoc contracts
retain priority. No scope body, model constructor or application bootstrap runs.
Methods marked with #[Scope] are supported through User::query()->active().
Direct calls such as User::active() to a protected attributed method still
produce Mago's native visibility/static-call diagnostics: the SDK signature
provider cannot change access to a declared method. Use an explicit builder call
for these scopes; Laramago does not suppress access diagnostics.
php tests/scopes.php verifies scope behavior through the real Mago worker in
an isolated workspace with spaces in its path, without an environment file or
database. Fixtures include execution traps and negative argument/access cases.
Model properties without a database
vendor/bin/mago analyze also resolves Eloquent properties from static source.
There is no Artisan step, migration execution, model instantiation, or database
inspection. The worker still requires the application's Composer autoloader.
The linter command and its preset remain independent of model analysis.
Supported information includes:
- Migration
up()methods with literalSchema::create()andSchema::table()calls, column types, nullability, timestamps, foreign IDs, morph columns, column/table renames, drops, andchange().down()methods are ignored. - Explicit or inherited
$table, conventional English table names, and model keys. - Native
Model::getKey()reads a proven primary-key attribute without becomingmixed; the result conservatively retainsint|string|nullfor unsaved models and driver differences. Custom key readers and unknown attributes defer. $castsand literalcasts()arrays, including inherited and trait methods: scalar, decimal, array/JSON, collection, object, date, immutable date, and enum casts.- Custom
CastsAttributesclasses with concrete native/PHPDoc contracts onget()and the value parameter ofset(), including inherited/trait methods and array shapes. Read and write types are independent. CastsInboundAttributes: the write contract comes fromset()and the read contract from a known migration column. Without that column, analysis defers.- Typed
getNameAttribute()/setNameAttribute()methods andAttribute<TGet, TSet>contracts, with separate read and write types. - Standard relations with PHPDoc type arguments or a direct
return $this->belongsTo(Related::class)-style declaration. Many relations returnCollection<int, Related>; singular relations includenullunless a statically recognizedwithDefault()call provides a default.
Native PHP properties, typed accessors and explicit @property-read contracts
retain priority. A known cast determines the read type ahead of a general
@property tag, including supported PHPStan and Psalm tags. Compatible array
casts preserve declared list items, keys and shapes. Explicit @property-write
tags retain priority. A virtual general float tag with a known decimal cast
uses the cast's int|float|string input contract; other general write contracts
retain their types. Casts
preserve schema and documented nullability; without a known column,
cast-derived attributes include null. Unresolved casts retain native analysis.
Decimal casts read as numeric-string, while numeric writes are accepted. Uncast
decimal and boolean columns retain driver-dependent numeric/string and boolean/flag
alternatives; explicit casts give them precise PHP types. Ordinary date
casts use CarbonInterface to allow Laravel's configurable date implementation;
explicit immutable casts use CarbonImmutable.
Custom casts run on null values in Laravel, so their method contracts determine
nullability independently of the database column. A nullable column can produce
a non-null object; a non-null column can still have a nullable cast result.
Laramago never constructs a caster or invokes get(), set() or castUsing().
For Castable, a factory consisting solely of return ConcreteCaster::class
or return new ConcreteCaster is resolved statically, including inherited factories.
Conditional factories, constructor arguments, contextual class names, anonymous
casters and recursive Castable targets remain unresolved.
Literal caster strings with constructor arguments are supported when the static
model metadata resolves the string. Constructor arguments are not executed.
Generic interface bindings alone are not used to narrow a mixed method
parameter; use an explicit method PHPDoc contract for that case.
Inherited template contracts can be resolved through verified direct
@extends/@implements bindings. Scalar, named-class and nullable union arguments,
nested named generics, lists, array maps and explicit array shapes are supported
across multiple inheritance levels. Named generic existence and arity are checked;
template constraints remain subject to native Mago declaration validation.
Explicit concrete or mixed method contracts retain priority. Unsupported type
expressions, trait/method templates, ambiguous inheritance paths and unresolved
mappings defer to native analysis.
The same resolver can handle one literal Castable factory hop.
php tests/casts.php checks custom casts through the real worker, including
negative assignments, nullable behavior, inheritance, traits and execution traps.
php tests/generic-casts.php adds template substitution and unresolved-binding
regressions through the real analyzer.
php tests/cast-contracts.php checks nested ancestry arguments and malformed,
overflowing or unresolved type-expression boundaries.
The worker finds the application root through Composer's installed-package metadata,
including custom vendor directories. It recursively reads database/migrations
and processes files in filename order. Add other migration directories in the
application's composer.json:
{
"extra": {
"laramago": {
"migration-paths": ["Modules/Billing/database/migrations"]
}
}
}
These paths supplement database/migrations and are relative to the application
root; absolute directory paths are also accepted. For isolated analysis workspaces,
the worker accepts an optional third command argument specifying the project root:
[extension-hosts.laramago] command = ["php", "vendor/ichinya/laramago/bin/laramago-worker.php", "vendor/autoload.php", "."]
Migrations are read even when Mago analyzes only one application file. Metadata is
cached only within a worker's analysis generation; subsequent runs read changed
files. A missing default migration directory is supported. Read/parse failures or
invalid migration configuration emit ichinya/laramago/metadata-unavailable warnings
with the affected path and disable unreliable schema inference.
Within Schema::create() and Schema::table() callbacks, a fresh local variable
may prepare a scalar expression without making the whole table unknown:
Schema::create('entries', function (Blueprint $table) { $table->id(); $table->string('code'); $table->string('label'); $expression = DB::getDriverName() === 'sqlite' ? 'code || label' : 'concat(code, label)'; $table->string('display')->virtualAs($expression); $table->unsignedInteger('quantity')->nullable(); });
Literal scalars, scalar operators, ternaries, string interpolation and earlier
scalar locals are recognized. The only recognized call in these expressions is
the resolved Laravel DB::getDriverName() facade call without arguments. Laramago
does not invoke it, select a database driver, or evaluate the SQL expression.
Column types and nullability still come from their explicit Blueprint declarations;
virtualAs() and storedAs() do not supply a type themselves.
Assignments must introduce a previously unmentioned local, excluding callback
parameters, captures and superglobals. Reassignments, references, objects,
arbitrary calls and conditional schema changes leave the table uncertain. Proven
literal scalars and flat lists, including fresh copies, can supply column names,
nullable modifiers, rename targets and drop lists. Computed expressions are never
folded into values. Literal knowledge expires when a non-preparation statement
mentions a local, protecting against references passed through column arguments.
Reusing an expired local, using captured values or mutating arguments leaves the
affected schema uncertain. These rules apply only inside Blueprint callbacks;
arbitrary migration setup remains unsupported. php tests/migration-literals.php
covers the literal substitutions and their negative cases.
Simple foreach declarations can use a flat literal list or a fresh list local:
foreach (['title', 'summary'] as $column) { $table->string($column)->nullable(); }
The target must be a fresh by-value variable and the body must contain direct
Blueprint calls. Key targets, references, nested loops, branching, helper calls
and mutations defer. Expansion is bounded to 64 elements, 64 body statements and
256 expanded statements. Loop bindings do not establish types for later uses.
php tests/migration-contracts.php covers these boundaries without executing PHP.
Literal-string DB::raw(...) wrappers in Blueprint arguments remain supported,
including a fresh proven string local. Their SQL is never interpreted or executed.
Dynamic raw arguments and arbitrary nested calls leave the schema uncertain.
This is a declarative schema reader, not a PHP interpreter. SQL dumps, arbitrary
SQL or helper calls, conditional/dynamic schema changes, custom connections,
dynamic Castable::castUsing() factories, unresolved generic cast contracts,
runtime table/cast changes, and untyped accessors are not
inferred. Uncertain metadata stays unresolved; $fillable alone does not establish
a type. Unknown properties and invalid assignments remain visible. Properties on
collections, unbound base Model values, and request input properties remain
outside this property provider's coverage.
Factory results
The analyzer also tracks factory result types without creating models or executing factory definitions:
User::factory()->create(); // User User::factory()->active()->make(); // User, for a simple declared state User::factory(3)->create(); // Collection<int, User> User::factory()->count(1)->create(); // Collection<int, User> User::factory(3)->count(null)->create(); // User User::factory(3)->createOne(); // User
Factory discovery reads HasFactory<YourFactory>, UseFactory, a static $factory
property, or a concrete non-null newFactory() return contract. The default
App / App\Models and Database\Factories naming convention is also supported.
Model identity comes from the factory's $model, Factory<YourModel> declaration,
or the default naming convention. These declarations must describe the actual
factory association; application classes and custom resolvers are never executed.
Count information survives assignments and standard fluent state methods. A
single-return custom state chaining standard factory methods is also understood.
forEachSequence() selects a collection; sequence() preserves the current count.
Quiet creation, makeOne(), createMany(), makeMany(), new() and times() are
covered. An integer count always selects a collection, including zero and one.
Unknown counts, unpacked arguments, arbitrary custom states, and merged single
and multiple branches retain Model|Collection<int, Model>. Core factory overrides
and subclasses that directly mutate count/model state defer to native analysis.
Runtime naming callbacks remain unresolved. Counted create(), createQuietly()
and make() preserve a statically resolved model collection class. Laravel's
createMany(), createManyQuietly() and makeMany() construct the base Eloquent
collection, so those retain the base collection type.
Native argument checks, protected property access, missing methods and invalid
collection property access remain active.
Custom Eloquent builders
Native query entry points (query(), newQuery(), newModelQuery(),
newQueryWithoutScopes() and newQueryWithoutRelationships()) preserve a known,
non-generic Eloquent builder subclass declared through $builder,
#[UseEloquentBuilder] or a concrete newEloquentBuilder() return contract.
Inherited declarations and positional/named attribute arguments are supported.
// User declares a UserBuilder with an active() method. User::query()->active(); // UserBuilder; native argument checks stay active
The custom builder's own signatures and @extends Builder<User> declaration
control subsequent calls. Explicit factory PHPDoc such as
@return CustomBuilder<User> preserves concrete generic arguments; class-string
selectors without arguments remain unresolved for generic builders.
Public custom methods also support direct calls such as User::active(), with
native argument checks and builder-aware fluent results. Explicit class template
arguments are substituted through parameters, results, lists and array shapes;
the defining class and template constraints are verified. Fluent static/$this
returns keep the bound builder, and explicit factory CustomBuilder<static>
contracts follow inherited model receivers. Verified parent-class @extends arguments also propagate through reordered and multi-level ancestry, with each template constraint checked. Method templates, generic traits, unresolved callable containers and reference signatures defer.
php tests/builder-generics.php and php tests/builder-ancestry.php check these
specializations and their negative cases.
Existing query overrides remain native; dynamic builder resolvers and
custom construction chains defer to native analysis. Constructors and factory
bodies are never executed. Tests run with php tests/builders.php.
Custom Eloquent collections
Model reads and primary-key lookups preserve an explicitly known, non-generic Eloquent collection subclass declared through:
- A concrete
newCollection()return type, including inherited declarations. - A literal
$collectionClassclass name. - A positional
#[CollectedBy(CollectionClass::class)]attribute, including inherited attributes. The nearest class attribute takes priority.
For example, User::get(), User::query()->get() and User::findMany([1])
retain a declared UserCollection, making its custom methods available for
analysis. A custom newCollection() return contract takes priority over class
attributes and properties. No collection constructor or factory is executed.
Explicit newCollection() PHPDoc such as @return CustomCollection<int, User>
preserves concrete generic arguments. Generic class-string selectors without
arguments, unresolved return types and custom
resolveCollectionFromAttribute() implementations still defer to native analysis.
Collection subclasses can declare a fixed element type with @extends; Laramago
does not invent template arguments from their names or parameter count. This
support also covers counted factory results as described above.
Calling the verified native newCollection() with no arguments or an empty
array also preserves the receiver model's collection contract and integer keys.
Populated arguments and custom factory implementations retain native analysis.
Additional static integrations
These integrations inspect declarations and syntax without booting the application. Unknown dynamic behavior keeps native Mago diagnostics and types. The coverage matrix lists the implemented and deferred portions of these integrations.
| Area | Supported subset | Boundaries |
|---|---|---|
| Relationship methods | Standard factories, trait declarations and explicit native pivot modifiers retain related/declaring/intermediate model types | Explicit PHPDoc wins; unresolved model/trait generics and custom factories defer; MorphTo properties retain nullable Model bounds or explicit model unions |
| Relation results | Standard relation forwarding for results, sorting, aggregates and 24 key/membership/null/range/date predicates | Native declarations win; custom relations, MorphTo and custom related builders defer |
| Selected query fields | Direct model value/pluck, including literal keys and exact-table qualification; terminal queries preserve literal aliases on their own result |
Stateful builder projections, expressions, withCount/withSum and unknown schema defer; sum keeps its installed contract |
| Literal query columns | Fresh literal Model::query() terminal chains check filters and ordering against an explicit effective-source and native-semantics contract |
Joins, source/projection/raw mutations, scopes, saved builders, dynamic columns, custom dispatch, direct static forwarding and nonterminal chains defer |
| Related projection columns | Closed native eager-load colon projections checked against explicit owner-relation source contracts | Pivot/through relations, modifiers, dynamic projections, mutable builders and unasserted relation sources defer |
| Search attribute arrays | Three direct fresh-query search/create methods check literal nonnumeric first-argument keys against the query-source contract while leaving creation/update values untouched | Saved or modified builders, direct static magic, scopes, custom dispatch, dynamic/numeric-key arrays, write acceptance, createOrFirst() and findOrNew() |
| Relationship callbacks | Literal dotted paths for the four whereHas/whereDoesntHave variants receive Builder<Related>; withWhereHas receives a Builder/Relation union |
Uses authoritative PHPDoc or supported relation bodies; dynamic paths and custom dispatch defer |
| Relation validation | Warns when a referenced existing method explicitly returns a known non-relation class, including nested paths | Missing names are checked only with explicit complete model contracts |
| Model field-list validation | Warns for missing literal names in directly declared $fillable, $guarded, $hidden, $visible and $appends arrays and their native Laravel class attributes, distinguishing filterable relationship keys from appendable legacy/Attribute accessor and class-cast keys |
Requires explicit complete exact-model field, serialization-key and appendable-key catalogs; schema-only, inherited, older/custom attribute, runtime-discovered and custom-dispatch definitions defer |
| Authentication | Nullable default models from literal config, explicit Composer auth contracts, or — under the opt-in runtime evaluation below — the local application's resolved auth configuration; standard guards and explicitly declared custom guard classes | Without the opt-in flag environment values are never evaluated and standard selected guard users retain native `Authenticatable |
| Collection operations | Standard collection null filtering, higher-order map/filter/reject, known model/shape/union items and literal-property aggregates | Custom subclasses, unsafe branches, callback/key filtering and union method calls with arguments defer; see the mapping contract below |
| HTTP test assertions | Typed Laravel and optional Laratesto callbacks, nested fluent scopes, standard Inertia page envelopes and flash assertions | Known installed declarations required; custom contracts/macros win; selected prop and JSON values remain unknown |
| Facades and container helpers | Concrete roots and public service signatures from class-string accessors, installed framework core service aliases or explicit static binding catalogs, including literal typed factories | Uncataloged aliases, runtime binding discovery, generic/reference contracts and custom dispatch defer; declared methods and PHPDoc win |
| Console command closures | Native Artisan::command() callbacks receive ClosureCommand as $this when installed Laravel constructs and binds that command |
Custom Kernel bindings, changed facade or callback binding, static closures and unrelated command methods retain native analysis |
| Console command inputs | Literal entries in an unchanged native command $signature refine $this->option('name') (--flag to bool, --name= to `string |
null); finite option-name unions combine every member's declared type. $this->argument('name')supports single literal names (required tostring, optional to string |
| JSON decoding | json_decode(..., true) with a literal true assoc argument yields list<mixed>|array<string,mixed>|bool|int|float|string|null, matching the PHPStan/Larastan value space |
Absent, false, null, dynamic or truthy-int assoc arguments retain native mixed; depth and flags are ignored because json_decode never returns false and null stays reachable |
| Container contract methods | Method calls on receivers typed exactly as an Illuminate\Contracts\* interface bound by the installed framework's core alias table resolve to the root concrete class: a false non-existent-method is not reported and declared concrete return types apply |
Methods missing on both the contract and the concrete keep their diagnostics; unmapped or foreign interfaces, union/generic receivers and methods the contract declares retain native analysis |
| Class aliases | Global-namespace calls through Laravel's boot aliases (e.g. \Str::random()) no longer report a false non-existent-method when the literal boot chain — Facade::defaultAliases() merged by the framework base config, without project overrides or colliding package aliases — maps the name to a class declaring the method |
A project config/app.php with any aliases key, dontMergeFrameworkConfiguration(), unresolvable chain shapes and alias names claimed by installed packages disable the map; a declared global class of the same name and methods the target lacks keep their diagnostics; the call expression itself remains mixed |
| Configuration | Literal helper and native Config reads from static configuration arrays, including shapes, known defaults, getMany, typed getters and exact native #[Config] injection attributes; verified native literal env() calls with an explicit scalar default use its generalized declared type; calls without a default and environment-derived configuration values retain the broader native possibilities; missing literal keys under an explicit complete runtime contract |
Arbitrary repository instances, dynamic env calls/defaults, machine-specific values without runtime opt-in, runtime mutations, package-merged defaults and unasserted missing-key warnings defer |
| Storage disks | Literal native Storage::disk and #[Storage] names under an explicit complete runtime contract |
Dynamic names, custom facades/managers/attributes, adapters and unasserted runtime mutations defer |
| Translation strings | Known PHP/JSON strings with explicit locales or literal app.locale, respecting JSON precedence |
Missing literal views/translations can be checked with complete catalogs; dynamic locales/loaders defer; native view() typing is retained |
| Route parameters | Duplicate placeholders in literal native Router, lexically resolved native Route facade and Route setUri() calls; missing required URI keys for closed named URL-parameter arrays under an explicit effective-default contract |
Complete named-route contracts check native URL/redirect generators; positional/domain parameters, runtime aliases, middleware and dynamic routing defer |
| Macros | Typed closures, static callable arrays and invokable objects in explicit files or active provider boot sources, including known boolean/ordered hasMacro guards | Catalog activation/order/completeness are user guarantees; unknown conditions, dynamic, generic and reference contracts defer; see macro catalogs |
Static configuration types describe the source defaults; applications that replace
configuration, authentication resolvers or facade bindings at runtime need explicit
contracts. app(Foo::class), resolve(Foo::class) and standard container make()
already receive native Mago generic inference and require no extra provider.
Opt-in container binding catalogs additionally resolve
known interface implementations and literal aliases for helpers, native make
and facade accessors. Listed registrations are an explicit application contract;
Laramago does not execute or activate them.
A catalog that replaces or makes a core service uncertain also disables related
standard-service inference: auth for authentication, validator for validated
fields, and config for static configuration reads and the default locale.
Standard Inertia inertiaPage() results expose the page envelope, including
component, props, url, version and flash. inertiaProps() without a
selector exposes an array of unknown values; selecting a prop does not establish
its type. Flash assertions retain the response type. The optional Laratesto
wrapper refines only a zero-argument inertiaPage() call; its forwarded optional
argument is not interpreted as a field selector.
Envelope refinement additionally checks the installed page source and property
contracts. Incompatible implementations retain native types.
Verified Laratesto PhpUnitCompatibility::assertIsArray, assertIsString,
assertIsInt, assertIsBool, assertIsObject and assertNotFalse narrow a
simple local variable after the assertion returns. Laramago checks the installed
Laratesto and Testo implementations before applying this rule. Property chains
and other expressions retain native analysis because their values can change.
Consecutive verified array assertions additionally support literal nested offsets
when each prefix is proven to be a PHP array. Assignments, opaque calls, dynamic
keys and ArrayAccess receivers break this proof.
A native throwing guard for an optional string key can also preserve a nested array fact in the immediately following matching ternary branch. This requires fresh associative JSON, a native root-array check, and checks for the literal field's presence, array type and key. Laramago verifies the source, native helper metadata and current file bytes before correcting only the proven access. Named function locals must have no references or hidden access to local variables; intervening assignments must have fresh or known nonobject values. At top level, the final root-array check must follow all assignments: replacing an older global object could invoke a destructor that changes the root. Unsupported control flow, opaque calls, dynamic consumers and incompatible annotations retain diagnostics.
A fresh local array rebuilt immediately after a throwing discriminator guard
can preserve the integer field required by one of two literal variants. The
guard must exhaustively validate both tags and check the dependent field with
native is_int(). Laramago derives the rebuilt shape from the current source
and checks it against the declaring class's constructor contract, including
declared type aliases. A copied primitive field remains independent of later
changes to its original input. The local must remain unexposed until its single
by-value constructor argument. References, captures, mutations, implicit local
access, undefined guard locals, unknown native calls and incompatible annotations
retain diagnostics. Source and metadata identities must agree. The SDK cannot
restore this discriminator correlation directly; the extension corrects only
the exact constructor diagnostic for the independently proven argument.
php tests/reconstructed-array-shapes.php --integrated verifies these boundaries.
Native class_uses() returns an array with string keys and trait-string values
for an object, or an unconditionally declared class name with autoloading enabled.
Unknown names, conditional or incomplete declarations and class names with
uncertain autoloading retain the possible false result.
A documented array<string, string> return can also be verified from a direct
return self::MAP and its literal class constant initializer. Explicit keys must
resolve to nonnumeric strings, and values must be string literals. Dynamic maps,
spreads, duplicate keys, incompatible declarations and late binding defer.
A direct array return can retain its element contract after every element passes
a native is_string(), is_int(), is_float() or is_bool() check in foreach.
The supported loop throws on rejection and immediately returns the unchanged
by-value parameter. Array/list guards preserve key constraints; empty arrays do
not establish a non-empty return. References, callbacks, mutations, early exits
and user-defined predicate functions retain native diagnostics.
A local result initialized to null can also be proven nonnullable after an
exhaustive foreach over a literal list. The selector must occur with the same
scalar type and value, and its branch must assign a fresh producer result.
Supported producers have a verified native nonnullable object return or use the
standard uncounted model factory contract. References, early exits, mutable locals,
nullable producers and changed factory implementations retain diagnostics.
Factory proofs verify installed native methods and reject custom result-mapping
changes. Available analyzed-file snapshots must match the source read from disk.
The SDK does not supply snapshots for source.includes; those dependencies are
verified against their installed files.
Fresh native SimpleXMLElement roots and guarded, literal element-only XPath
results can establish the parent of a child read. A missing first-level child is
an empty element proxy; a further read through an unproven empty proxy remains
nullable. A throwing count($element->child) !== 1 guard establishes an actual
child for subsequent reads. Unguarded parse failures, opaque XML parameters,
custom classes, writes, unsupported aliases, references and escaped nodes
retain native analysis. Proven XML construction uses omitted options, 0 or
the native LIBXML_NONET constant. Other options and constructor URL mode defer.
Mago's PHP SDK cannot restore child types after its native count reconciliation
has replaced them with mixed. Laramago therefore also corrects exact native
cardinality and cached child-access diagnostics when current source, builtin
dispatch and the same node provenance independently prove the operation. This
does not suppress application argument or return errors. Cardinality reporting
corrections defer for known or constrained XML payloads and unaudited calls.
Unsupported cardinality requirements and stale or ambiguous source spans also
defer. The native XPath contract
retains null and false on failure; XPath iteration requires an array guard.
php tests/simplexml-provenance.php --integrated verifies these boundaries.
Native Eloquent fresh() and refresh() preserve the exact callable-owned model
template when called without arguments directly in the first statement of a named
function or method. The receiver must be its original by-value parameter with a
template bounded by Model. fresh() remains nullable. Aliases, captures, later
calls, different bounds and changed native contracts defer.
Native Eloquent refresh() may also invalidate a previously observed literal
value of a virtual attribute. Laramago checks the effective refresh, raw-attribute
replacement and read implementations against their installed source, then
checks supported native Testo\Assert::same(), true() and false() calls
against an independently established read contract.
Explicit @property-read PHPDoc retains priority; known casts refine general
@property reads while keeping explicit directional write types. This
corrects exact stale comparison diagnostics without claiming that persistence or
the assertion succeeds. Known unsaved receivers, no-op overrides, constant
accessors, real PHP property shadows, custom casts, incompatible read types and
stale or incomplete source retain diagnostics. Other flow errors, including
never cascades, require their own proof. The SDK cannot forget a cached property
literal directly, so this correction is limited to the proven comparison.
php tests/refreshed-model-properties.php --integrated verifies these boundaries.
Model attribute source freshness also checks that an already certified unscanned dependency remains unchanged after its AST is evicted; changed source defers until exact restoration. Run php tests/model-attribute-source-freshness.php for the genuine source mutation and restoration regression.
A successful native Testo\Assert::notNull() can preserve a nullable string
getter's result at its immediate repeated call. The receiver must be a final
source-verified class, and the public getter must return one ordinary physical
field without callbacks or side effects. Laramago also verifies the installed
assertion's successful logging path. Nullable declarations stay unchanged;
intervening operations, changed dependencies, magic storage and stronger PHPDoc
defer. Missing analyzed declarations and ambiguous call spans defer as well.
php tests/asserted-pure-getter.php --integrated verifies these boundaries.
A direct native array_map() over a fresh list of nonempty string literals can
preserve those finite input values at the first constructor argument, even when
the inline callback declares a general string parameter. Laramago verifies
the actual callback and constructor metadata against their current source and
checks every literal against the declared non-empty-string parameter. Included
dependency declarations are read independently. Empty, computed or unknown inputs,
escaped callbacks, references and stronger conflicting annotations retain native
diagnostics. This correction leaves the general callback declaration unchanged.
php tests/finite-map-arguments.php --integrated verifies these boundaries.
Native Eloquent chunk callbacks with a bare Collection parameter can lose the
queried model's generic type inside their body. For an unchanged foreach item,
Laramago can restore an independently documented public property read when the
query, hydration, collection and iterator contracts are verified from source.
Explicit generic annotations, real base-model properties and unsupported query
effects retain priority. This correction does not infer unknown fields or model
methods. Ambiguous property spans, aliases, references, writes and custom query
or collection implementations defer.
php tests/contextual-collection-members.php --integrated verifies these boundaries.
Read-only helpers may read an item property in a dictionary key. Actual field
writes, indexed writes, destructuring and reference escapes still defer.
When an ambiguous base-model property warning survives the native provider,
Laramago can use the exact analyzed file and a verified concrete model declaration
to resolve that advisory. The shared source index keeps property-provider span
collisions conservative. Unknown fields and unsafe uses retain their diagnostics.
php tests/contextual-documented-property.php checks the full registry and native controls.
An exhaustive by-value validation loop can establish scalar row fields before
an inline native usort() comparator and a direct list return. Laramago verifies
the current guards, callback, caller and documented return contract. References,
missing checks, altered rows and incompatible returns retain errors. A bounded
top-level getopt() array guard follows the same lexical compatibility policy.
Native expression types stay unchanged.
php tests/validated-array-contracts.php checks these boundaries.
Two guarded string cast policies preserve native
types while matching PHPStan's declared callable-method guard and Larastan's
benevolent literal Request route conversion. Exact source spans, native caller
and builtin contracts, and unchanged variables are required. Concrete bad objects,
unprotected casts, references, custom implementations and unsupported defaults
retain their diagnostics. php tests/guarded-string-casts.php verifies the exact
native delta and one/three-worker agreement.
A directly invoked closure may change a fresh boolean local captured by reference.
Laramago verifies the analyzed caller, final callee and private forwarding helpers,
including native Closure::fromCallable() normalization and caught exceptions.
Bounded source constructors preserve physical fields, promotions and the actual
signatures of stored closures and method callbacks. Invalid arguments, inaccessible
fields and unsupported initialization retain diagnostics.
It reconstructs the values reaching a later native Testo\Assert::true() or
false() comparison; a transient write followed by a reset does not establish
the earlier value. Unknown effects, escaped callbacks or references, stronger
conflicting annotations, incomplete snapshots and stale source retain diagnostics.
This correction is limited to the proven comparison. Deferred listeners and
downstream unreachable-flow diagnostics require separate evidence.
php tests/direct-callback-reference-effects.php --integrated verifies these boundaries.
The installed Internal\Container\Container::get() and make() contracts accept
unions of class strings while retaining their corresponding object union. Bounded
and generic class strings preserve their return contract. Unknown class strings
do not establish a particular service type, and invalid selectors or constructor
argument arrays retain diagnostics. Changed interface contracts defer.
An immediate is_int($value) guard after a fresh local filter_var() assignment
preserves literal FILTER_VALIDATE_INT minimum and maximum bounds. Only the
successful integer branch gains the range. Defaults, dynamic options, references,
intervening statements and unsupported flags retain native analysis.
Inside a direct positive instanceof branch, HTTP and console kernel unions use
the selected native interface's argument and return contracts. This avoids
combining a console status code with an HTTP response. Calls outside such branches
and changed framework contracts retain native analysis.
Omitted, null or falsy translation locales use only a literal config/app.php
locale and one conventional language root. This models the configured initial
locale; runtime locale changes and custom translation-loader paths require
additional application contracts.
Literal trans_choice() references can also receive missing-translation warnings
under complete effective-locale catalogs.
The helper and app() forwarding must match the installed native implementation;
custom helpers, uncertain loaders and unasserted locales retain native behavior.
Tests for these integrations run through composer check, including invalid
arguments/results, nullable access and execution traps.
Relationship method types
A parameterless model method with a native relationship return type can supply its missing generic arguments from a single direct factory call:
class Author extends Model { public function articles(): HasMany { return $this->hasMany(Article::class); } } $author->articles(); // HasMany<Article, Author> $author->articles()->first(); // Article|null $author->articles()->get(); // Collection<int, Article>
Laramago reads the method body without executing it. Class aliases, inherited
methods, self::class, static::class, named arguments and literal key names are
supported. The declaring model argument follows the receiver; an inherited
self::class target continues to refer to the method's declaring class.
Factories covered are hasMany, hasOne, belongsTo, belongsToMany,
hasManyThrough, hasOneThrough, morphOne, morphMany, morphToMany and
morphedByMany. Through relations preserve the intermediate model. Supported
many-to-many template layouts retain their installed Pivot/MorphPivot defaults.
Native withTimestamps() and withTrashedParents() chains are recognized after
checking their installed declarations; timestamp names must be literal strings
or null.
Native withDefault() chains preserve relationship generics for boolean values
and static attribute arrays, including methods without a return declaration.
The installed SupportsDefaultModels declaration and its default argument must
match the supported contract; callbacks and dynamic values defer.
Explicit return PHPDoc keeps priority, including an intentionally broad contract. The installed relationship templates and factory declarations must match the supported Laravel contracts. Custom factories, relation constructors and related model resolvers defer. Native visibility and argument checks remain active, as do unknown-property, invalid-assignment and nullable-result diagnostics.
Relationship properties also defer for private methods and custom
isRelation, getRelationValue or getRelationshipFromMethod dispatch.
Protected relation methods remain accessible to the base model. Empty
withDefault([]) attributes preserve nullability; the last default modifier wins.
Callback defaults and potentially callable arrays defer because they can replace
the related model with an arbitrary value.
To-many relationship properties use the related model's collection contract,
including explicit newCollection() return types, CollectedBy attributes and
static collection-class declarations. Unknown collection factories defer;
explicit property PHPDoc retains priority.
Source-derived targets also defer when a model overrides relationship factories,
constructors or related-instance resolvers; explicit relation PHPDoc is preserved.
Explicit unions such as HasOne<FirstModel|SecondModel> preserve every model
branch in the property type. To-many unions resolve each model's collection
contract. Invalid documented related types defer instead of being replaced by
a model found in the method body.
Application-trait declarations are supported, including nested imports, aliases
and inheritance. A trait's self::class refers to its importing class, while
static::class follows the actual receiver. Native using(Pivot::class) and
as('accessor') modifiers preserve explicit pivot/accessor arguments for verified
four-template relation layouts. Morph relations require a MorphPivot subclass.
The same lexical trait resolution applies to relationship properties, including
inherited and reimported traits, while explicit return PHPDoc remains authoritative.
Methods without a return declaration use the same bounded factory inference,
including their relationship properties. Explicit native and PHPDoc return
contracts retain priority, including mixed.
Unresolved model/trait generics, dynamic targets or keys, branches, locals,
arbitrary chained modifiers, nullable method declarations and dynamic
MorphTo targets retain native types. Passing a custom pivot as the factory table
argument is outside this inference. php tests/relation-contracts.php covers
trait dispatch, pivot contracts and declaration-priority regressions.
Relationship callbacks reuse these inferred types when no authoritative return
PHPDoc is present. Literal dotted paths can combine documented and inferred
relations. whereHas, orWhereHas, whereDoesntHave and orWhereDoesntHave
receive Builder<Related>. withWhereHas also uses the callback for eager loading,
so its argument is Builder<Related>|ConcreteRelation<...>. Both cases must be
handled, for example through an explicit union and instanceof narrowing.
Arbitrary with callbacks, dynamic paths, parameterized relation methods and
custom related builders remain outside this inference.
Explicit authentication contracts
When authentication configuration depends on environment values, an application
can declare its analysis contract in composer.json:
{
"extra": {
"laramago": {
"auth": {
"default-guard": "web",
"guards": {
"web": {"model": "App\\Models\\User"},
"api": {"class": "App\\Auth\\ApiGuard", "model": "App\\Models\\User"}
}
}
}
}
}
These entries are explicit assertions about the analyzed application's runtime
configuration. Laramago reads JSON only; it does not evaluate env() or boot a
service provider. Model classes must implement Authenticatable; custom guard
classes must implement Guard. A model declaration that contradicts a custom
guard's declared user() return type is not used. Native and PHPDoc contracts
retain priority, and user results retain nullability.
Custom guard chains use that class's own user() contract. Standard framework
guards are not generic, so selecting one does not carry a model argument into
later user() calls. Missing or invalid metadata remains conservative.
php tests/auth-contracts.php covers these declarations and their negative cases.
Runtime evaluation
Instead of declaring contracts by hand, an application can let Laramago read its
resolved authentication configuration from a local application load. The mode is
strictly opt-in: set LARAMAGO_EVALUATE_RUNTIME=1 in the environment or add
--evaluate-runtime to the worker command in extension-hosts. Without the flag,
the runtime loader does not require bootstrap/app.php or evaluate configuration
files; the required Composer autoloader still runs. Verified native env() calls
with an explicit scalar default use its generalized type under Larastan's policy; calls without
a default retain string|bool|null. Static configuration arrays retain the broader
native environment possibilities.
[extension-hosts.laramago] command = ["php", "-d", "display_errors=stderr", "vendor/ichinya/laramago/bin/laramago-worker.php", "vendor/autoload.php", ".", "--evaluate-runtime"]
When enabled, the worker requires the project's bootstrap/app.php and explicitly
runs only three configuration bootstrappers: environment variables,
configuration files and facade registration. It does not invoke the service-provider
registration or boot phases. Project bootstrap and configuration files still
execute PHP and may register providers, access a database or cause other side
effects. The worker does not prevent those project-defined effects. It discards
the loaded application after extracting the values. Authentication keeps only
literal string entries: the default guard,
each guard's provider and driver, and each provider's driver and model,
validated against the live autoloader. Literal config(), Config::get() and
env() reads additionally resolve to the machine's actual values — the general
scalar or array type, never a machine-dependent literal — after the static
source, with unchanged missing-key semantics and the exact framework
env() value conversion. Explicit composer.json contracts keep priority, any
load or extraction failure defers to the flag-off behavior, and the resolved
values describe the machine running the analysis — the same trade-off Larastan
makes when it boots the application. The local run must therefore match the
environment you want analyzed.
php tests/evaluated-runtime.php and php tests/evaluated-config.php cover
the flag semantics, deferral paths and contract priority.
For actual database schema metadata, use the separate opt-in
laramago-runtime collector. It prepares an explicit
model snapshot from file-backed SQLite in a child process; enable it with
LARAMAGO_RUNTIME_METADATA or the worker's --runtime-metadata=... option.
Named connections, schema revisions and separate property read/write contracts
are checked. Failed or stale metadata prevents a clean analysis result. The
default analyzer and native lint do not run the collector or require a database.
Selected query fields
Direct model calls can refine known physical columns:
User::value('email'); // string|null, for a required string column User::pluck('email'); // Support Collection<int, string> User::firstOrFail(['email as contact']); // User&object{contact: string}
value and pluck use the model's known read type, including supported
casts and accessors, but require a physical column. value retains null for a
missing row. Literal aliases in terminal first, firstOrFail, sole and get
calls belong only to that query result. They use raw schema types; renaming a
selected column does not apply the source attribute's Eloquent cast to the alias.
Aliases that collide with declared properties, casts, dates or accessors defer.
Literal pluck key columns use raw schema values and PHP array-key coercion;
casts and accessors affect the selected values, not the keys. Nullable keys retain
string because null becomes an empty-string key. Physical columns may be qualified
by the model's exact table name, including terminal aliases. Unknown or unrelated
tables and fields defer. php tests/query-chains.php checks these contracts.
Builder chains, SQL expressions and mutable
select/addSelect/withCount/withSum state remain outside this inference.
The extension does not validate SQL or invent a numeric type for sum.
Primary-key lookups
Laramago resolves find(), findOrFail(), findOrNew(),
findMany() and findSole() on models and the standard Eloquent Builder:
User::find(1); // User|null User::findOrFail(1); // User, when the call returns User::findOrNew(1); // User User::find([1, 2]); // Collection<int, User> User::query()->findOrFail([1, 2]); // Collection<int, User> User::where('active', true)->find(1); // User|null
Arrays and Arrayable identifiers select a collection, including empty arrays.
findMany() always returns a collection; findSole() returns one model when it
succeeds. Unknown identifiers, scalar/array unions and unpacked argument lists
retain all possible result branches. find() keeps null in its scalar branch.
No query is executed, and the analyzer does not assume that a database row exists.
Magic model calls use parameter names, types and defaults from the installed
Laravel builder, so named arguments, argument counts and invalid arguments remain
checked. Declared methods and @method contracts retain priority, including
inherited declarations. Custom builders, query factories, magic dispatchers and
unresolved collection factories defer to native analysis. Relation forwarding and findOr()
callbacks remain separate work. First-class callable expressions stay callable;
their later invocation is left to native analysis.
Creating models
Laramago resolves create(), createQuietly(), forceCreate(),
forceCreateQuietly(), firstOrNew(), firstOrCreate(), createOrFirst() and
updateOrCreate() on models:
User::create(['name' => 'Ada']); // User User::firstOrNew(['email' => 'ada@example.com']); // User User::firstOrCreate(['email' => 'ada@example.com']); // User User::updateOrCreate(['id' => 1], ['name' => 'Ada']); // User
Results retain the concrete model class, including inherited and instance calls.
These methods return one model when they return successfully. This type does not
prove that a row was persisted: firstOrNew() may return an unsaved model, and
model events may cancel a save. No model constructor, event, callback, application
bootstrap or database query is executed during analysis.
Parameter names, defaults and types come from the installed Laravel builder.
This includes version-specific support for closure values; methods absent from
that builder remain unknown. Native analysis handles declared builder calls such
as User::query()->create(). Explicit methods, trait methods and PHPDoc contracts
take priority. Custom builders, query dispatch, instance factories, hydration and
event/guard wrappers defer to native analysis. Custom collections do not prevent
inferring a single model result.
Invalid arguments, unknown properties and methods, and invalid property writes remain visible. Attribute arrays are checked against the builder signature; per-column mass-assignment validation and relationship creation remain separate work. First-class method references remain callable.
Reading and sorting models
Laramago resolves first(), firstOrFail(), sole(), get(), latest(),
oldest(), orderBy(), orderByDesc(), count(), sum(), exists() and
doesntExist() on models:
User::first(); // User|null User::firstOrFail(); // User, when the call returns User::sole(); // User, when the call returns User::get(); // Collection<int, User> User::latest()->first(); // User|null User::orderBy('name')->orderByDesc('id')->get(); // Collection<int, User> User::where('active', true)->orderBy('id'); // Builder<User> User::count(); // installed integer contract User::exists(); // bool
Inherited, instance and class-string calls retain the model class. Forwarded
orderBy() and orderByDesc() also preserve the model type on standard Eloquent
builders. Builder reads use Laravel's generic contracts and the explicitly
resolved custom collection types described above.
Parameter names, defaults and types come from the installed Eloquent or Query
Builder, including version-specific sorting directions. Aggregate return types
also use installed metadata: count() can retain int<0, max>, while sum()
stays mixed when that is Laravel's contract. Missing methods stay unknown.
Declared methods and PHPDoc retain priority. The standard query provider defers for custom builders, query factories and magic dispatchers. Reads preserve known collection contracts and defer for custom hydration, instance factories and unresolved collections. A named scope that shadows a forwarded Query Builder method prevents assigning the standard result. Scope, relation and explicit macro support have the boundaries described above; runtime macro discovery and other higher-order operations remain separate work.
No query, model constructor or application bootstrap runs during inference.
first() remains nullable, and the analyzer does not assume a row exists.
Unknown properties, collection property access, invalid writes and argument errors
remain visible. First-class method references remain callable.
Query predicates
Standard model filters retain Builder<Model> through static, instance, inherited
and class-string calls. Query Builder predicates forwarded through an exact standard
Eloquent builder retain its model type as well:
User::whereIn('id', [1, 2])->get(); // Collection<int, User> User::whereColumn('created_at', '<', 'updated_at'); // Builder<User> User::whereDate('created_at', '2026-01-01')->first(); // User|null User::whereMonth('created_at', 1)->firstOrFail(); // User User::whereKey(1)->exists(); // bool
| Family | Supported methods |
|---|---|
| Primary key | whereKey, whereKeyNot |
| Column comparisons | whereColumn, orWhereColumn |
| Membership | whereIn, whereNotIn, orWhereIn, orWhereNotIn |
| Null checks | whereNull, whereNotNull, orWhereNull, orWhereNotNull |
| Ranges | whereBetween, whereNotBetween, orWhereBetween, orWhereNotBetween |
| Dates and times | whereDate, whereTime, whereDay, whereMonth, whereYear, and their orWhere variants |
Parameter types, names, defaults and arity come from the installed framework.
For example, whereColumn() retains its two-operand shortcut and array form, and
whereIn() retains Laravel's mixed values contract; the extension does not invent
stricter argument types. Existing Mago checks on generic subquery arguments remain
visible. Direct Query Builder calls retain their native result.
Declared model methods and PHPDoc keep priority. Public Eloquent methods such as
whereKey() precede named scopes; scopes precede forwarding to Query Builder.
Mixin calls dispatched by Mago to Query Builder still use the original Eloquent
receiver when resolving a scope. Custom builders, query factories and dispatchers
retain the boundaries described above; unknown predicate names stay unknown.
Filters do not prove that rows exist or narrow model property types. In particular,
whereNotNull('label')->firstOrFail()->label retains the declared property contract,
and whereKey(1)->first() remains nullable. Queries and callbacks are never executed.
Higher-order mapping over models
For standard Support and Eloquent collections of a single concrete model type,
->map->method() preserves the collection around the model method's result:
User::get()->map->getRawOriginal()->all(); // array<int, array<string, mixed>> $users->map->label(); // Support Collection<TKey, string> $users->map->replicate(); // Eloquent Collection<TKey, User>
The latter two examples assume label(): string on the model and a standard
Eloquent collection of users. Keys are preserved. Eloquent keeps its collection
type when every returned value is a model; otherwise the result uses the common
Support collection type, which also accommodates an empty Eloquent collection.
Nullable method results remain nullable collection values. A completed void
method maps to null; a never method can only produce an empty collection.
Concrete native/PHPDoc return contracts, array shapes, and top-level static /
$this results are supported. Parameter-dependent PHPDoc branches use argument
types and declared defaults; uncertain or unpacked arguments retain both branches.
For example, getRawOriginal() returns attribute arrays, while a non-null or
unknown key retains Laravel's mixed value contract. Mago still checks argument
types, names, counts, visibility and unknown methods. Direct model calls and
first-class method references retain native behavior.
Support is limited to methods declared on model classes, their model ancestors,
or Laravel's Eloquent Concerns traits. Mago dispatches mixin calls to the method's
declaring class or trait, so unrelated application traits are currently deferred.
Custom collection/proxy classes, non-model or class-string items, mixed item types,
generic methods/classes, unresolved nested contextual types and higher-order
operations other than map defer to native analysis.
Existing explicit method/PHPDoc contracts retain priority. No model method,
collection callback, constructor, application bootstrap or database query executes.
php tests/higher-order-map.php runs real Mago scenarios with isolated declarations,
execution traps and concurrent workers in a path containing spaces. A comparison
with analyzer plugins disabled reproduces the native proxy-chain errors.
Property mapping such as $users->map->name also preserves keys and known model
property types. Physical properties and explicit read contracts retain priority;
known casts refine general model property reads. Scalar or nullable results produce a Support
collection; proven model results retain the Eloquent collection. Unknown or
inaccessible properties still produce diagnostics. Required array-shape and tuple
keys can also be mapped; all branches of an item union must have a known readable
value. Nullable input items, optional direct array keys and custom collection/proxy
classes defer. Model unions support higher-order methods when every branch has a
public zero-argument-compatible method; calls with arguments retain native behavior.
php tests/collection-properties.php covers the original property variant.
Standard collections also refine min, max and sum when item generics and a
literal selected property are known. Min/max retain null for an empty collection.
Sum uses int|float for proven numeric values, accounting for integer overflow and
the empty zero seed; arbitrary strings are not assumed numeric. This applies to
in-memory collection operations, independently of database aggregate semantics.
php tests/collection-contracts.php verifies these contracts and negative cases.
An immediate $records->isEmpty() ? $fallback : $records->first() branch can
preserve the nonnullable item type of a fresh native eager collection; last()
is also supported. The receiver must come from an exact native new Collection
or new EloquentCollection with a literal array, and the selected getter must
be evaluated first with no arguments. Installed constructor, item conversion,
predicate and getter contracts are verified. Nullable items, callbacks, defaults,
custom or lazy collections, hidden aliases and opaque or query-produced origins
defer. A base collection parameter can hold a subclass and is insufficient
evidence. The supported native implementation uses PHP 8.5 array helpers;
earlier PHP targets retain native diagnostics.
php tests/nonempty-collection-results.php --integrated compares native,
isolated and integrated analysis, including unguarded reads after guarded reads.
Higher-order filter and reject support known item properties and concrete model
methods on standard collections. They preserve input items, keys and the
Support/Eloquent collection class. Results may be empty; predicates do not narrow
individual item properties. Native argument diagnostics remain active, while
custom dispatch and unresolved contracts defer. tests/higher-order-predicates.php
compares these contracts against native Mago.
Validated form input
Laramago resolves the complete result of
Illuminate\Foundation\Http\FormRequest::validated() as an array. A single
unconditional literal rules() array can refine known fields:
// rules(): ['title' => 'required|string', 'note' => 'sometimes|nullable|string'] $request->validated(); // open shape with required title and optional note $request->validated('title'); // string $request->validated('note'); // string|null $request->validated('note', 'Untitled'); // string|null: present null stays null
Omitted keys and keys known to be null return the entire validated array.
When rules() is a literal map of string field names but its rule values are
unsupported, the complete result retains array<string, mixed>: the top-level
keys are known to be strings, while their values remain unknown. Dynamic rules
or mutating/unknown validation hooks retain array<array-key, mixed>. A literal map can
follow a native $this->user() read and a throwing type or strict-null guard; those
statements do not alter its field names. An after() hook that only checks
literal fields through native request readers and adds a validation error also
preserves the rule keys. Named arguments
and inherited form requests are supported. Unknown selected fields and unpacked
argument lists defer to native analysis. Native parameter checking remains active;
first-class method references remain callable.
Supported rules include required, present, sometimes, nullable, filled,
string, array, numeric and boolean. Numeric values retain
int|float|string; boolean values retain bool|0|1|'0'|'1'. The integer rule
does not establish an integer PHP representation and therefore stays mixed.
Optional array/boolean fields also retain string when blank strings can skip
ordinary validators. bail, email and numeric min/max/size
constraints are accepted without supplying a type themselves.
Literal dotted fields form nested open shapes when every intermediate parent has
an explicit array rule. Wildcards describe arrays with integer or string keys;
they do not guarantee a list, fixed index or nonempty result. Optional parents
remain optional. Laravel can omit even a required array parent when its optional
children produce no validated fields, so that possibility is retained.
Literal Rule::in([...]) and string in: rules are recognized without executing
them. They do not cast the returned value or prove string type on their own.
date_format:Y-m-d, url, uuid, ulid and alpha establish string values;
other supported date formats, alpha_num and alpha_dash retain numeric input
representations. php tests/request-nested.php covers these rules and boundaries.
Explicit overrides, trait methods, PHPDoc contracts and more precise framework return types retain priority. Requests that redeclare the validator property also defer to native analysis.
This provider does not execute the request, validation or application bootstrap,
and never treats validation as a type cast. Dynamic rules, arbitrary rule objects,
unsupported rules, overlapping wildcard/literal paths and custom validation/preparation hooks
disable field inference. Shapes stay open; optional fields are not guaranteed to
exist. Scalar and array defaults are supported for optional selected fields;
callable/object defaults defer. Ordinary input() calls and magic request
properties retain native behavior.
Existing configuration
The plugin preserves any mago.{toml,yaml,yml,json} or
mago.dist.{toml,yaml,yml,json} file and prints the preset path.
For an existing TOML configuration, add this at the top level, before any sections:
extends = "vendor/ichinya/laramago/presets/laravel.toml"
For JSON, add an extends property; for YAML, add an extends key.
If the configuration already inherits other files, add the preset to its parent list.
For a custom vendor-dir, use the path printed by the plugin.
Keep your source paths and includes. Application settings override scalar values
from the preset; Mago concatenates arrays, so inherited exclusions remain active.
To enable analyzer support in an existing configuration, also add:
[extension-hosts.laramago] command = ["php", "vendor/ichinya/laramago/bin/laramago-worker.php", "vendor/autoload.php"]
For JSON or YAML, add the equivalent extension-hosts mapping. Use the worker and
autoload paths printed by the plugin when the package or vendor directory is
elsewhere. Existing configurations are preserved during updates, so applications
installed before the analyzer extension was added need this one-time setup.
Use this manual setup when automatic configuration creation is skipped, such as
when Composer runs with --no-plugins.
Removing the package leaves the application configuration in place. Remove its
extends reference and extension-hosts.laramago entry, or replace them with your
own settings before running Mago again.
Local development installation
For a published release, use the Packagist installation. To test changes from a local checkout, use a path repository instead.
Add a path repository to the test Laravel application's composer.json:
{
"repositories": [
{
"type": "path",
"url": "C:/projects/laramago",
"options": {"symlink": false}
}
]
}
Then run:
composer config allow-plugins.ichinya/laramago true
composer require --dev ichinya/laramago:@dev
vendor/bin/mago lint
Use symlink: false to test a regular package copy inside vendor.
For CI, store plugin permission in the application's config.allow-plugins.
Development
Comparing with Larastan
Larastan is an independent reference for Laravel behavior. Its code and PHPStan extensions are not copied into this package. Our providers use Mago's native PHP extension SDK; comparison results guide their development and verification.
php scripts/compare.php --project=C:/projects/laravel-app
The test application must have Mago and Larastan installed, a phpstan.neon
configuration (or a dist variant), and a working Laravel bootstrap. The script
runs the tools through the current PHP executable using the application's
dependencies. It leaves application configuration, source files, and baselines
unchanged. Analyzers may write their own caches; Larastan boots the Laravel container.
The runs execute sequentially:
- Larastan with the application configuration.
- Larastan at the same level, with configuration-level
ignoreErrorscleared. - Larastan at the maximum level, with those suppressions cleared.
- Mago on the source paths from the PHPStan configuration.
- Both analyzers on
tests/fixtures/analysis/eloquent.php: explicitquery(), magicwhere(), a misspelled method, a missing argument, and an invalid argument.
Reports are stored in var/comparisons/<project-name>/<UTC-timestamp>/, which Git
ignores. Override the directory with --output. comparison.md contains the
overview; summary.json records versions, paths, levels, diagnostic codes, and
exit statuses. Raw JSON and normalized diagnostics are saved separately for each run.
mago-only-location-candidates.json lists Mago diagnostics on lines without a
Larastan max diagnostic. These are candidates for investigation: matching line
numbers do not establish equivalent meaning, and the absence of a Larastan
diagnostic does not prove a Mago false positive. Each tool's own exclusions and
source annotations remain in effect.
Bootstrap errors and internal PHPStan failures abort the comparison; an incomplete
run cannot count as a successful check with zero errors. On the first Windows run,
php artisan package:discover may be needed to prevent parallel Larastan processes
from attempting to create a missing package manifest at the same time.
Package checks
PHP 8.2+, Composer 2, Mago ^1.48.1, PHP-Parser ^5.8, Doctrine Inflector ^2.1.
composer check
This validates Composer metadata and runs every integration script registered in
composer.json. Individual scripts can also be run directly while developing.
composer test:framework-native-contracts runs the focused
native framework and PHP contract checks,
including LF/CRLF source, changed framework declarations and retained diagnostics.
Some integration scripts leave generated workspaces in var/compatibility-*
for inspection. Completed workspaces can be deleted when their detailed reports
are no longer needed; subsequent runs create fresh directories. Other files in
var, including comparison reports and research scripts, should be assessed
separately. var and the local .repowise index are excluded from Git and package
archives.
Installer tests cover configuration creation, custom vendor directories, repeated
installation, preservation of all eight user configuration variants, and fallback
source paths.
tests/preset.php runs the real Mago executable and checks Laravel casts and
validation rules, visible secret warnings, a nested loop in finally, eval in tests,
and failures for eval in application code and invalid syntax. Install dependencies
first. To use an external executable, set MAGO_BINARY to a native executable on
Windows or to the Composer PHP script at vendor/bin/mago.
tests/analyzer.php runs the real Mago analyzer and extension worker against
isolated framework declarations. It checks magic calls, chained and callback model
types, named arguments, invalid calls, and fallback for custom behavior. Use the
comparison script with a Laravel application to verify the installed framework.
tests/properties.php exercises the real analyzer with migrations outside the host
file set, without .env or application bootstrap. It covers property contracts,
schema changes, relationships, inherited/trait metadata, invalid accesses and writes,
additional migration directories, paths with spaces, concurrent worker requests,
fresh metadata on subsequent runs, and visible parse failures. Fixture PHP remains
in .stub files in the package so it cannot shadow the installed Laravel framework.
tests/migration-locals.php checks scalar migration preparation, generated columns,
schema changes, nullable types, unknown properties and invalid writes through real
Mago. It also verifies conservative handling of references, captured variables,
dynamic calls and schema branches, without executing migrations or loading .env.
tests/predicates.php checks predicate chains, native argument validation, scope
precedence, installed signature changes and declaration priority through real Mago.
It includes a provider-disabled comparison and preserves errors for nullable results,
unknown properties and invalid writes, without application bootstrap or a database.
tests/relation-methods.php checks relationship generic inference, inherited and
late-static model targets, PHPDoc priority, factory overrides and installed contract
changes through real Mago. It compares native behavior with the provider disabled,
rereads changed source on a new run, and keeps bootstrap and constructor execution
traps in an isolated workspace without .env or a database.
tests/factories.php checks concrete factory discovery, count state through chains
and variables, single and collection results, named arguments, custom declarations,
unknown state and branch unions, protected properties, and native negative cases.
Factory fixtures contain execution traps and require no environment file or database.
tests/find.php exercises model and builder lookups, nullable and collection
branches, Arrayable, union and unpacked identifiers, first-class callables,
native argument validation, inheritance, PHPDoc priority and custom behavior.
The real worker analyzes isolated declarations without loading an application.
tests/create.php checks model creation and native builder calls, inherited and
late-static results, argument errors, fresh property types, custom declarations
and callable references. A second worker run changes the fixture's builder
signature to verify version-specific callback support and unavailable methods.
The isolated project has no environment file or database and contains execution
traps for model construction and application bootstrap.
tests/validation.php checks complete validated arrays, null and named keys,
unknown field values, native argument errors, inheritance, overrides and PHPDoc
priority. A fresh worker run verifies a more precise framework return contract.
The isolated project contains execution traps and needs no environment file or
database; only the scenario file is analyzed, with request declarations included
as dependencies.
tests/queries.php checks model reads, sorting chains, aggregates, concrete model
and collection types, nullable results, invalid returns and arguments, overrides
and scope collisions. A fresh worker run verifies changed sorting direction and
count contracts, plus unavailable methods. Only the scenario file is analyzed;
framework and model declarations are dependencies. The workspace has a path with
spaces, concurrent workers, execution traps, and no environment file or database.
A real Composer path repository installation was also checked in an isolated
Windows project: automatic configuration creation, native vendor/bin/mago.bat lint,
Laravel integration, an application rule override, configuration preservation on
repeated composer install, and a nonzero exit status for invalid PHP.
That check used Mago 1.48.1; a full Laravel CI run was not performed.
Standard service helpers such as app('cache') and resolve('session') use installed framework aliases. See framework helper contracts for supported calls and override boundaries.
str() preserves the installed anonymous proxy and known Str method contracts; explicit str(null) returns Stringable. Standard DB::transaction() calls retain analyzed Closure results, including retry-aware nullability. Both are covered by framework helper contracts.
Permitted middleware references provide an optional exact literal-reference policy for native Route middleware calls.
Final controller call contracts optionally check missing arguments and incompatible native parameter types for independently asserted final controller calls after route binding and dependency resolution. They preserve positional, nullable, default and variadic semantics without bootstrapping Laravel or inferring effective routes and container state.
Permitted assertViewIs identities provide an optional literal-reference policy for native stored-view identity assertions.
Eloquent factory $model declarations accept a general @var string when the
literal model class agrees with the declared factory generic and the installed
parent contract. Current source and native metadata are checked without loading
the application. Conflicting models and unsafe storage changes retain diagnostics.
Bounded false operand compatibility follows PHPStan's handling of PHP ordered comparisons and string concatenation. False plus an integer is accepted at source-certified ordered comparisons with an integer peer; false plus a string is accepted in concatenation with literal string peers. Other domains and all unsafe arithmetic diagnostics retain their native behavior. Inferred types stay unchanged, including the false alternatives.
Closed configuration arguments and framework default date arguments adapt source-bound Laravel call contracts without loading the application. Dedicated native Mago regression matrices cover receiving declarations, one and three workers, and unsafe source or metadata replacements.
Declared framework compatibility supports Eloquent coalesce probes, certified nullable collection offsets and native Request, route and console postconditions. Native Mago regression matrices retain unsafe property, element and receiving-contract diagnostics.
Repeated refreshed model values also recognize a native no-value argument after a second unconditional refresh on the same model receiver. Physical typed producer fields, primitive attribute readers, casts, schema and native declarations must establish the changed literal domain; aliases, references, unsaved receivers and genuine unreachable expressions keep their diagnostics. Run php tests/repeated-refresh.php for the native one/three-worker matrix and selected SDK cache controls.
Bounded defensive compatibility covers native array caller contracts, captured-state guards, configuration classification, quoted route keys and Inertia response checks. Installed Fortify defaults use verified static package binding declarations with explicit application bindings taking priority. All regression fixtures are analyzed through the production worker.
Laramago retains the selected model type in standard Eloquent chunkById() callbacks. See selected chunk collection arguments and driver extension forwarding for the supported contracts and regression checks.
Collection offset guards cover defensive index reads and model checks after a standard Eloquent query and literal keyBy('id'). Native value types and unrelated diagnostics remain available.
Literal Request header arguments use the physical HeaderBag contract after a stable successful guard; see the policy.
Declared methods on standard Eloquent chunk models keep their concrete receiver contract through a read-only collection helper. See selected chunk model methods.
Declared and captured postconditions preserve closed assertion and callback evidence without changing native types.
Defensive configuration member checks retain classified validation checks while preserving business-operation Errors.
XML cardinality guards keep defensive singleton-node validation while preserving native value types and unrelated Errors.
Session array-key copies follow the physical Laravel key normalization while retaining invalid aliases, refinements and receiving contracts.
Model property arguments use current model declarations and primitive migration columns at verified receiving contracts, without loading the application or connecting to a database.
Factory Faker contracts certify selected literal formatter values through current default declarations and registered-source override checks, without executing providers or the application.
Possible callback tuples preserve closed positional writes from literal argument closures and physically typed captured inputs. Run php -d memory_limit=512M tests/possible-tuples.php for native one/three-worker and SDK restoration checks.
Guarded local model tuples preserve a guarded concrete model from a standard Eloquent query when it is stored in a literal callback tuple. Run php -d memory_limit=512M -d opcache.enable_cli=0 tests/guarded-local-model-tuples.php for the native regression matrix.