Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Surveyor Laravel Package

laravel/surveyor

Laravel Surveyor is a mostly static analysis tool that parses PHP files to extract detailed metadata about classes, methods, properties, and return types, exposing it in a structured format other tools can consume. Currently in beta.

View on GitHub
Deep Wiki
Context7

Getting Started

Surveyor is a library, not a CLI. There is no php artisan survey:analyze to run — you install it, resolve Analyzer from the container, and build your own command, linter, doc generator, or IDE helper on top of the structured results. Install it as a dev dependency unless you are shipping a runtime tool:

composer require --dev laravel/surveyor

Where to look first, in this order:

  1. Laravel\Surveyor\Analyzer\Analyzer — the only entry point you need on day one (analyze($path) / analyzeClass(FQCN)).
  2. ClassLikeResult — name(), namespace(), extends(), implements(), hasMethod(), getMethod(), publicMethods(), publicProperties(), getConstant().
  3. Laravel\Surveyor\Types\Type — the factory plus Type::is() for the type objects coming back from returnType() and $property->type.
  4. src/Markers section of the README — you will hit "why is this method missing?" within an hour otherwise.

First use case — a throwaway artisan command that dumps a class contract. It exercises the whole happy path (analyzer, result, methods, types) and gives you something to iterate on:

use Laravel\Surveyor\Analyzer\Analyzer;

$result = app(Analyzer::class)->analyzeClass(App\Models\User::class)->result();

foreach ($result->publicMethods() as $method) {
    $this->line(sprintf(
        '%s(%s): %s',
        $method->name(),
        collect($method->parameters())->pluck('name')->implode(', '),
        $method->returnType()?->toString() ?? 'mixed',
    ));
}

If that prints sensible return types for your models (including casts()-driven attribute types), you have a working baseline. If it prints mixed everywhere, jump to the Gotchas section before writing more code.

Implementation Patterns

Analyze once, consume many times. The natural shape is a service that analyzes a set of classes and hands typed results to N consumers (route doc generation, OpenAPI stubs, a make:xyz generator, an architecture test). Do not call analyzeClass() inside a loop of format-specific code — wrap it:

final class ClassFacts
{
    public function __construct(private Analyzer $analyzer) {}

    public function for(string $class): ClassLikeResult
    {
        return $this->analyzer->analyzeClass($class)->result();
    }
}

Then inject ClassFacts and let the cache do its job across consumers.

Branch on type objects, never on strings. returnType() returns a Type, and a nullable return is a UnionType, not a StringType with a flag you can compare. Flatten before you branch:

use Laravel\Surveyor\Types\{Type, UnionType, StringType, ClassType};

$type = $method->returnType();
$members = $type instanceof UnionType ? $type->types() : [$type];

$isStringy = collect($members)->contains(
    fn ($t) => Type::is($t, StringType::class)
);

$type->isNullable() is the safe null check; toString() is for display only, not for control flow.

Register your marker vocabulary in a service provider. This is the extension point most day-to-day tools need — "hide this from generated output" is a product decision, and Surveyor deliberately makes you own the attribute. Implement Ignored on your own attribute, and if you need config-driven hiding, implement ConditionallyIgnored and register a resolver in boot():

use Laravel\Surveyor\Support\Markers;

Markers::registerConditionResolver(fn (string|array $condition) => is_string($condition)
    ? (bool) config($condition, false)
    : (bool) app()->call($condition));

Markers::registerTags('ignore'); // for array keys / config-shaped output

The resolver must be registered before you read results — registration is lazy relative to analysis but eager relative to getMethod(). Put it in a provider that is loaded in the same process that consumes results.

Batch through the cache, not through memory. For a whole-app scan, iterate class names, call analyzeClass(), and immediately serialize what you need out of each result before moving on. AnalyzedCache::clearMemory() between batches keeps peak memory down while the disk cache preserves the expensive parse:

AnalyzedCache::enableDiskCache(storage_path('surveyor-cache'));

foreach ($classes as $class) {
    $this->facts[$class] = summarize(app(Analyzer::class)->analyzeClass($class)->result());
    AnalyzedCache::clearMemory();
}

Treat "missing" as a three-state answer. hasMethod() returning false can mean does not exist or exists but is marker-hidden. If your tool needs to distinguish, use method($name) (raw, past the filter) alongside getMethod($name) (filtered), and isIgnored() on the class. This matters for anything that reports coverage or diffs contracts.

Model analysis is opt-in by value. You get database attribute types, isModelRelation() flags on relationship methods, accessors/mutators, and casts for free — but only for classes that are actually Eloquent models and reachable through a booted app. Use it for form-request/scaffold generators where column types matter; skip it for tooling that only cares about method signatures, and you avoid the DB entirely.

Gotchas and Tips

  • Beta API. The README says the API is "subject (and likely) to change prior to v1.0.0." Pin the version in composer.json and keep every Surveyor touchpoint behind your own small wrapper class so a breaking rename is a one-file fix, not a grep across the codebase.
  • Not purely static. It boots the app, inspects bindings, and hits the database for models. That means: your CI job needs a working DB (or DB_CONNECTION=sqlite + :memory:), and it must not run in contexts where booting the app has side effects. Never call the analyzer from inside a request handler.
  • Performance is explicitly acknowledged as unsolved. Slower and hungrier than you will want. Consequences: enable the disk cache before you benchmark anything, chunk large scans, and consider running whole-app analysis in a queued job or separate process rather than inline in a command that developers run on every save.
  • Cache invalidation is mtime + dependency based. Edits to a parent class or trait invalidate dependents — good — but a git checkout/rebuild that restores timestamps, or a stale disk cache on a shared CI volume, can serve you yesterday's answer. AnalyzedCache::clear() in CI before a full run, or scope SURVEYOR_CACHE_DIR per pipeline.
  • Env config is read at the point the cache is consulted. SURVEYOR_CACHE_ENABLED / SURVEYOR_CACHE_DIR are convenient but invisible in code review; if your tool depends on caching being on, call AnalyzedCache::enableDiskCache(...) explicitly rather than trusting an env var to be set in prod.
  • Conditional markers fail closed and resolve late. With no resolver registered, every conditional marker hides its member. Conditions are stored in the cached analysis and evaluated when the member is read — so the same cached file can yield different member sets depending on runtime config. If your output is generated at build time, resolve conditions in the same environment as the build.
  • Marker parameter names are load-bearing. ConditionallyIgnored readers look for constructor parameters literally named unless and when. Name them anything else and the file-based reader treats the marker as unconditional while instantiation honors the condition — a discrepancy that will only show up in one code path.
  • Comment tags are registered, not discovered. // @ignore does nothing until Markers::registerTags('ignore'). Keys are matched positionally, so duplicated keys or heavily formatted arrays can shift what gets marked.
  • Members inherited via traits or parents bypass file-based marker reading. Use Markers::fromReflection($reflection) to get an IgnoreMarker or null for those.
  • Useful escape hatches to remember: Type::isSame() for exact comparison (strict) vs Type::is() for class checks (loose), $type->required() / optional() when generating parameter lists, and $classResult->method() / property() when you need to read a member your own marker just hid — which is easy to do accidentally while developing the marker itself.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
bernskioldmedia/laravel-matomo
bernskioldmedia/laravel-harvest
bernskiold/laravel-data-scrubber
bernskiold/laravel-currency-converter
bernskiold/laravel-campaign-monitor
bernskioldmedia/laravel-autotranslate
siberfx/telegram-bot
roy-sandip/laravel-view-extended
gregsphotography/site-builder-core
roy-sandip/laravel-view
anodyne/blade-fluentui-icons
webx-ui/module-catalog-collections
webx-ui/module-catalog-feeds
makidizajnerica/laravel-dto
matviib/scheduler
webx-ui/templates
anodyne/blade-tabler-icons
bref/laravel-websockets
anodyne/blade-hugeicons
1994/flatpack-statamic