class

Analyzer::Php::Laravel

Inherits Analyzer::Php::PhpEngine < FileScanEngine < Analyzer < FileHelper < Reference < Object

Constants

ACCESSOR_NAME_RE = /\A(?:#{ILLUMINATE_ACCESSORS})\z/
ACTION_FILTER_RES = {"only" => Regex.new("->\\s*only\\s*\\((.*?)\\)", Regex::Options::IGNORE_CASE | Regex::Options::MULTILINE), "except" => Regex.new("->\\s*except\\s*\\((.*?)\\)", Regex::Options::IGNORE_CASE | Regex::Options::MULTILINE)}
ALIAS_ENTRY_RE = /['"]([\w.-]+)['"]\s*=>\s*(\\?[A-Za-z_][\w\\]*)::class/
ANY_CALL_RE = /([\\\w$]*)\s*\(/
ANY_REGEX = /Route::any\s*\(\s*#{PATH_ARG}\s*,/im

Route::any('/x', …): capture 1 is the path; the verbs are implied.

API_PREFIX_RE = /\bapiPrefix\s*:\s*['"]([^'"]*)['"]/
API_RESOURCE_ACTIONS = ["index", "store", "show", "update", "destroy"]
ARRAY_ENTRY_RE = /\A(?:(['"])([^'"$\\]*)\1\s*=>\s*)?(.+)\z/m
ARRAY_MERGE_RE = /\barray_merge\s*\(/
ARRAY_SPLIT_RULES = Noir::TopLevelSplit::Rules.new
ARRAY_VALUE_RE = /\A(?:'[^'\\]*'|"[^"$\\]*"|\\?[A-Za-z_][\w\\]*::class|\[\s*\\?[A-Za-z_][\w\\]*::class\s*,\s*['"]\w+['"]\s*\])\z/
ASSIGNED_ARRAY_RE = /(?<![\w$>:])\$(\w+)\s*=\s*(\[|array\s*\()/
ASSIGNMENT_RE = /(?<![\w$>:])\$(\w+)\s*(?:\[[^\]]*\]\s*)*(?:[.+\-*\/%|&^]|\?\?|<<|>>)?=(?![=>])/

Every write to a variable: assignment (also through a subscript or a compound operator), a foreach / destructuring target, a reference, a mutating builtin or unset.

AUTH_ALIAS_RE = /\Aauth(?:\.basic)?(?::|\z)/

The built-in auth / auth:sanctum / auth.basic aliases.

BLOCK_REFUSAL_RE = /\A(?:abort\s*\(|throw\b|return\s+(?:redirect\b|Redirect::)|return\s+response\s*\([^;]*?\b40[13]\b|return\s+response\s*\(\s*\)\s*->\s*json\s*\([^;]*?\b40[13]\b)/

What an if (!user) block does to refuse: abort, throw, redirect, or answer 401/403.

CHAIN_CALL_RE = /(?:->|::)\s*(\w+)\s*\(/
CHAINED_MATCH_REGEX = /Route::((?:\w+\s*\([^;]*?\)\s*->\s*)+)(?:match|addRoute)\s*\(\s*(\[[^\]]+\]|['"][^'"]+['"])\s*,\s*#{PATH_ARG}\s*,/im

The match/addRoute routes behind a fluent prelude. Capture 1 is the prelude, capture 2 the verb specification, capture 3 the path.

CHAINED_STATIC_ROUTE_REGEX = /Route::((?:\w+\s*\([^;]*?\)\s*->\s*)+)(view|redirect|permanentRedirect)\s*\(\s*#{PATH_ARG}\s*,/im

The static routes behind a fluent prelude. Capture 1 is the prelude, capture 2 the kind, capture 3 the path.

CHAINED_VERB_REGEX = /Route::((?:\w+\s*\([^;]*?\)\s*->\s*)+)(get|post|put|patch|delete|options|head|query)\s*\(\s*#{PATH_ARG}\s*,/im

The same, behind a fluent prelude — Route::middleware('auth')->prefix('v1')->get('/x', …). Capture 1 is the whole prelude (a ->-separated chain of calls; the inner [^;]*? keeps each call's argument list inside one statement), capture 2 the verb, capture 3 the path.

CLASS_DECL_RE = /\b(?:abstract\s+|final\s+)*class\s+\w+[^{]*\{/
CLASS_EXTENDS_RE = /\bclass\s+\w+\s+extends\s+(\\?[A-Za-z_][\w\\]*)/
CLOSURE_ARG_RE = /\A\s*(?:static\s+)?(?:function|fn)\b/
COMPOUND_RE = /\A(?:if|else|try|foreach|for|while|switch|do|match)\b/
CONDITION_NOTE = "a condition decides whether it applies"
CONFIG_CALL_RE = /\bconfig\s*\(\s*['"]([\w-]+(?:\.[\w-]+)+)['"]\s*(?:,\s*(['"])([^'"]*)\2\s*)?\)/

config('portfolio.path') / config('x.y', 'default').

CONFIG_VALUE_RE = /\G\s*(?:(['"])([^'"\\$]*)\1|env\s*\(\s*['"][\w.-]+['"]\s*,\s*(['"])([^'"\\$]*)\3\s*\)|explode\s*\(\s*['"],['"]\s*,\s*env\s*\(\s*['"][\w.-]+['"]\s*,\s*(['"])([^'"\\$,]*)\5\s*\)\s*\))\s*[,\]]/

A config value the scan can read: a literal, or env('X', 'literal') (what the app serves when the variable is unset). explode(',', env('X', 'api')) (a one-name list's default) reads as that name. ponytail: a comma-separated default stays unread (middleware names such as throttle:60,1 hold commas).

CREDENTIAL_CONDITION_RE = /\buser\s*\(|\bauth(?:::|\s*\()|->\s*(?:check|guest|tokenCan|can|cannot|hasValid\w*Signature)\s*\(|token|password|api_?key|signature|credential/i
CTOR_FILTER_CALL_RE = /->\s*(only|except)\s*\(/
CTOR_MIDDLEWARE_RE = /\$this\s*->\s*middleware\s*\(/
CTOR_OPTION_RE = /\A\[\s*['"](only|except)['"]\s*=>\s*(\[[^\[\]$]*\]|['"][^'"$]*['"])\s*,?\s*\]\z/
DESTRUCTURE_RE = /(?:\blist\s*\(|\[)([^\[\]();=]*)[\])]\s*=(?![=>])/
DYNAMIC_VARS_RE = /\$\$|\$\{|\b(?:extract|parse_str|compact)\s*\(|\$GLOBALS\b/
EMPTY_ARRAYS = {} of String => LiteralArray
EMPTY_IMPORTS = {} of String => String
EMPTY_RESOURCE_PARAMS = {} of String => String
ENV_CONDITION_RE = /\b(?:app\s*\(\s*\)\s*->|App::|\$this->app\s*->)\s*(?:environment|isLocal|isProduction|runningUnitTests|runningInConsole|isDownForMaintenance)\s*\(|\bconfig\s*\(|\bConfig::get\s*\(|\benv\s*\(/
FILTER_NOTE = "its only/except list is not readable, so it may not apply to this action; a condition decides whether it applies"
FOREACH_BIND_RE = /\bas\s+&?\s*\$(\w+)(?:\s*=>\s*&?\s*\$(\w+))?/
FOREACH_HEAD_RE = /\A\s*(\$\w+|\[.*\]|array\s*\(.*\))\s+as\s+(?:\$(\w+)\s*=>\s*)?\$(\w+)\s*\z/m
FOREACH_RE = /\bforeach\s*\(/
FORM_REQUEST_HINT_RE = /([A-Za-z_\\][\w\\]*)\s+\$/

Any class type hint: a FormRequest need not be named *Request (login(LoginUser $request)), form_request_hint? decides.

GETTER_CALL_RE = /(?:\$this->|parent::|self::|static::)(?!\w*(?:Or|And)[A-Z])get[A-Z]\w*\(\s*\)/
GROUP_CONTROLLER_RE = /(?:(?:->|::)controller\s*\(\s*|['"]controller['"]\s*=>\s*)([A-Za-z_\\][\w\\]*)::class/i

Route::controller(X::class) in a group's chain, or the 'controller' => X::class group attribute.

GROUP_EDIT_RE = /->\s*(?:appendToGroup|prependToGroup|group)\s*\(\s*['"]([\w-]+)['"]\s*,\s*(\[[^\]]*\]|\\?[\w\\]+::class)/
GROUP_NAMESPACE_RE = /(?:(?:->|::)namespace\s*\(\s*|['"]namespace['"]\s*=>\s*)['"]([\w\\]+)['"]/i

Route::namespace('X') in a group's chain, or the 'namespace' => 'X' attribute.

GROUP_SHORTHAND_RE = /->\s*(web|api)\s*\(/
GUARD_ARG = (/(?:['"]\w+['"]|\$\w+)?/).source
HEALTH_ROUTE_RE = /\bhealth\s*:\s*['"]([^'"]+)['"]/
IDENTITY_REFUSAL_RE = /authenticate|authenticates|authentication|authenticated|authenticating|authenticator|\bguest\s*\(|\bAuth\w*Exception|Unauthori[sz]edHttpException/i

A refusal that is about the caller's identity by its own words.

KNOWN_AUTH_MIDDLEWARE = Set {"Illuminate\\Auth\\Middleware\\Authenticate", "Illuminate\\Auth\\Middleware\\AuthenticateWithBasicAuth"}

Framework classes whose definition lives in vendor/.

LOOKUP_ASSIGN_RE = /\A\$\w+\s*=(?!=)/

A lookup before the check ($post = Post::find($id);) answers nothing. Only calls known to read count: anything else may have side effects.

LOOP_CONTROL_RE = /\b(?:continue|break|goto)\b/
LOOP_VAR_USE_RE = /\{\$(\w+)\}|\$(\w+)\b/
MATCH_REGEX = /Route::(?:match|addRoute)\s*\(\s*(\[[^\]]+\]|['"][^'"]+['"])\s*,\s*#{PATH_ARG}\s*,/im

Route::match(['get', 'post'], '/x', …) or Route::addRoute('QUERY', '/x', …): capture 1 is the verb specification (array literal or string literal), capture 2 the path.

METHOD_DEF_RE = /(?:public|protected|private)\s+(?:static\s+)?function\s+([A-Za-z_]\w*)\s*\(/
MIDDLEWARE_ARG_RE = /(?:(?:->|::)middleware\s*\(\s*|['"]middleware['"]\s*=>\s*)(\[[^\]]*\]|#{MIDDLEWARE_NAME_ARG}(?:\s*[,.]\s*#{MIDDLEWARE_NAME_ARG})*)/
MIDDLEWARE_ENTRY_RE = /['"]([^'"]+)['"]|(\\?[A-Za-z_][\w\\]*)::class/
MIDDLEWARE_NAME_ARG = "(?:['\"][^'\"]+['\"]|\\\\?[A-Za-z_][\\w\\\\]*::class)"

The names a ->middleware(...) call or 'middleware' => attribute gives: an array, or one or more strings / Foo::class arguments.

MIDDLEWARE_REFUSAL_RE = /\babort(?:_if|_unless)?\s*\([^;]*?\b40[13]\b|\bthrow\s+new\s+\\?(?:[\w\\]*\\)?\w*(?:Auth|Unauthori[sz]ed|AccessDenied|Forbidden)\w*\s*\(|\bthrow\s+new\s+[^;]*?\b40[13]\b|->\s*authenticate\s*\(|\bAuth::authenticate\s*\(|\bredirect\(\s*\)\s*->\s*guest\s*\(|\bRedirect::guest\s*\(|\broute\(\s*['"]login['"]|\bredirect\s*\(\s*['"]\/?login\b|\b(?:response\s*\(|->\s*json\s*\()[^;]*?\b40[13]\b|HTTP_(?:UNAUTHORIZED|FORBIDDEN)\b/

What a handle() that can refuse the caller does: abort / throw / answer with 401 or 403, throw an auth exception, authenticate (which throws AuthenticationException), or send the guest to the login page.

MIDDLEWARE_VAR_RE = /((?:->|::)middleware\s*\(\s*|['"]middleware['"]\s*=>\s*)\$(\w+)\b(?!\s*(?:\[|->|\(|::))/

->middleware($middleware) / 'middleware' => $middleware: a variable handed whole as the list.

MUTATING_CALL_RE = /\b(?:array_(?:push|unshift|splice|pop|shift|walk)|[uk]?sort|r?sort|u?[ak]r?sort|shuffle|unset|settype|global)\b\s*\(?\s*\$(\w+)/
NAMESPACE_DECL_RE = /\bnamespace\s+([\w\\]+)\s*;/
NEXT_CALL_RE = /\$next\(/
NO_CTOR_NAMES = [] of CtorName
ORIGIN_CONDITION_RE = /['"](?:origin|referer|host)['"]|->\s*(?:isMethod|getMethod|method|getHost)\s*\(/i

A request-shape check (CORS origin, preflight method, host), not a check on who the caller is.

PARAM_LIST_RE = /\b(?:function|fn)\s*&?\s*\w*\s*\(([^)]*)\)/
PARAMETERS_RE = /(?:->\s*parameters\s*\(\s*|['"]parameters['"]\s*=>\s*)\[([^\]]+)\]/im
PARENT_CTOR_RE = /\bparent::__construct\s*\(/
PATH_ARG = "(['\"][^'\"]*['\"](?:\\s*\\.\\s*(?:['\"][^'\"]*['\"]|\\$\\w+(?:->\\w+)*|[\\w\\\\]+(?:::\\w+)?\\s*\\((?:[^()'\"]++|['\"][^'\"]*['\"])*+\\)))*)"

A route's path argument, quotes included (laravel_path_arg folds it): a string literal, or literals and simple expressions joined with . ('/a/' . config('x.y'), '/c/' . $prefix). One capture.

PATH_PIECE_RE = /\G\s*\.?\s*(?:(['"])([^'"]*)\1|\$(?:\w+->)*(\w+)|(?:[\w\\]+::|\\?(?:\w+\\)*)?(\w+)\s*\((?:[^()'"]++|['"][^'"]*['"])*+\))/

One piece of a PATH_ARG: a literal, a variable or a call.

POLICY_CHECK_RE = /\G(?:\$this\s*->\s*authorize|Gate::authorize)\s*\(/

$this->authorize('view', $post) / Gate::authorize(...): the gate answers 403 for a signed-out caller unless the policy takes a nullable user, so it stands in front of the handler like a user check, but a policy can open it.

POLICY_NOTE = "the policy decides whether a guest passes; a condition decides whether it applies"
READ_BARE_CALLS = Set {"array", "isset", "empty", "collect", "intval", "trim", "auth", "request", "route"}
READ_CALL_RE = /\A(?:find|findOrFail|findMany|first|firstOrFail|firstWhere|get|getKey|load|loadMissing|loadCount|where|whereKey|whereIn|whereNull|whereNotNull|with|withCount|withTrashed|onlyTrashed|query|all|latest|oldest|orderBy|pluck|count|input|route|string|integer|boolean|only|header|has|filled|user|exists|value)\z/
REDIRECT_RE = /\breturn\s+(?:redirect\b|Redirect::)/
REFERENCE_RE = /&\s*\$(\w+)/
RESOURCE_ACTIONS = ["index", "create", "store", "show", "edit", "update", "destroy"]
RESOURCE_CALL_RES = {"resource" => Regex.new("Route::resource\\s*\\(\\s*['\"]([^'\"]+)['\"]", Regex::Options::IGNORE_CASE | Regex::Options::MULTILINE), "apiResource" => Regex.new("Route::apiResource\\s*\\(\\s*['\"]([^'\"]+)['\"]", Regex::Options::IGNORE_CASE | Regex::Options::MULTILINE), "resources" => /Route::(resources)\s*\(\s*(?:\[|array\s*\()/i, "apiResources" => /Route::(apiResources)\s*\(\s*(?:\[|array\s*\()/i, "singleton" => /Route::singleton\s*\(\s*['"]([^'"]+)['"]/i, "apiSingleton" => /Route::apiSingleton\s*\(\s*['"]([^'"]+)['"]/i}

Hoisted patterns for extract_resource_route_calls / extract_resource_action_filter — both call sites pass literal names ("resource"/"apiResource", "only"/"except"), and the previous Regex.new interpolation recompiled the pattern on every call (per file, and per resource statement respectively).

RESOURCE_STRING_CONTROLLER_RE = /\A\w+::\w+\s*\(\s*['"][^'"]*['"]\s*,\s*['"]([A-Za-z_\\][\w\\]*)['"]/

Route::resource('photos', 'PhotoController'): the quoted controller second argument, a string action like 'PhotoController@index'.

RESOURCES_ENTRY_RE = /['"]([^'"]+)['"]\s*=>\s*(\\?[A-Za-z_][\w\\]*::class|['"][^'"]*['"])/
ROUTE_FILE_REF_RE = /(['"])([^'"\n]*routes\/[^'"\n]*\.php)\1/

A route file's URL prefix comes from where the app loads it, not from the file: Route::prefix('api')->group(base_path('routes/api.php')) in a RouteServiceProvider (Laravel <= 10, including the older Route::group(['prefix' => 'api'], function () { require ... })), or ->withRouting(api: __DIR__.'/../routes/api.php', apiPrefix: 'v2') in bootstrap/app.php (Laravel 11+, apiPrefix defaulting to api), whose then: closure registers further groups the same way.

ROUTE_REGISTRATION_RE = /Route::(?:\w+\s*\([^;]*?\)\s*->\s*)*\w+\s*\(\s*(?:(?:\[[^\]]*\]|['"][^'"]*['"])\s*,\s*)?#{PATH_ARG}\s*,/im

Any Route::…('path', … registration, whatever verb or chain precedes it. Used only to locate handler-closure bodies up front; the six scans above are what actually decide which registrations become endpoints.

RULES_METHOD_RE = /(?:public|protected|private)\s+function\s+rules\s*\(/
SCOPE_RE = /\b(?:function|class|trait|interface|enum)\b/
SETTING_NOTE = "an environment or config check decides; a setting can make it public"

Why a refusal is not unconditional: it only happens under an environment or config check.

SHALLOW_RE = /->\s*shallow\s*\(/
SIGNATURE_RE = /\G[^)]*/
SPREAD_RULES_RE = /\.\.\.\s*\$this\s*->\s*(\w+)\s*\(\s*\)\s*(?=[,\]])/

rules() composed from a helper's array, ...$this->folderRules() or array_merge([...], $this->folderRules()): the helper's keys, from the class or a trait it uses.

STATIC_REQUEST_CALL_RE = /(?<![\w\\$>])(\\?[A-Z][\w\\]*)::(\w+)\s*\(\s*\$request\s*[,)]/

Resolver::resolve($request): a static method of an app class (one the project's PSR-4 autoload maps, never a vendor class) handed the request as its first argument reads it under its own parameter name.

STATIC_ROUTE_REGEX = /Route::(view|redirect|permanentRedirect)\s*\(\s*#{PATH_ARG}\s*,/im

Route::view / redirect / permanentRedirect: capture 1 is the kind and capture 2 the path. Laravel registers a view with match(['GET', 'HEAD']) and a redirect with any.

STRING_LITERAL_RE = /\A(['"])(.*)\1\z/m
THIS_CALL_RE = /\$this->(\w+)\s*\(/
THIS_LITERAL_CALL_RE = /\$this\s*->\s*(\w+)\s*\(\s*['"]([\w-]+)['"]/
THIS_READ_RE = /\$this\s*->\s*(#{ILLUMINATE_ACCESSORS})\s*\(\s*['"]([^'"]+)['"]/

A FormRequest is the request: $this->input('x') in its own methods reads input, and so does $this->helper('x') when the helper (on the class or a trait it uses) hands its first parameter to such a read.

TRAIT_ALIAS_RE = /(?:([\w\\]+)::)?(\w+)\s+as\s+(?:(?:public|protected|private)\s+)?(\w+)\s*;/
TRAIT_INSTEADOF_RE = /(?:([\w\\]+)::)?(\w+)\s+insteadof\s+[\w\\\s,]+;/
TRAIT_USE_RE = /(?:^|[;{}])\s*use\s+([\w\\]+(?:\s*,\s*[\w\\]+)*)\s*[;{]/m
UNRESOLVED_ROUTE_ARG_RE = /(?:\bRoute::|->)\s*\w+\s*\(\s*(?:[^;()"]*?,\s*)?"[^"]*\$[^"]*"/

A route call (or a call chained on one) whose string argument still interpolates a variable after substitution: an inner loop that was not expanded, or a value the body computes.

USER_ASSIGN_ANY_RE = /\$(\w+)\s*=\s*#{USER_SOURCE}\s*;/
USER_ASSIGN_RE = /\G\$(\w+)\s*=\s*#{USER_SOURCE}\s*;/
USER_CHECK_RE = /\G(?:abort_(if|unless)|if)\s*\(/

A handler that refuses an anonymous caller before doing anything: a top-level abort_if(!$request->user(), 403) / abort_unless(auth()->check(), 404), or if (!Auth::check()) { abort(403); } (also on a $user = $request->user() it read first), before it does anything else.

USER_GUEST_RE = /\A(?:auth\s*\(\s*\)\s*->\s*guest|Auth::guest)\s*\(\s*\)\z/
USER_SOURCE = (/(?:\$request\s*->\s*user\s*\(\s*#{GUARD_ARG}\s*\)|(?:auth\s*\(\s*#{GUARD_ARG}\s*\)|Auth::guard\s*\(\s*#{GUARD_ARG}\s*\)|auth\s*\(\s*\)\s*->\s*guard\s*\(\s*#{GUARD_ARG}\s*\))\s*->\s*(?:user|check)\s*\(\s*\)|Auth::(?:user|check)\s*\(\s*\))/).source
USER_SOURCE_RE = /\A#{USER_SOURCE}\z/
USES_ACTION_RE = /\G(?:\[|array\s*\()[^\[\];]*?['"]uses['"]\s*=>\s*['"]([\w\\]+)@([A-Za-z_]\w*)['"]/i
VALIDATE_STATEMENT_RE = /\G(?:\$\w+\s*=\s*)?(?:\$this|\$request)\s*->\s*validate(?:WithBag)?\s*\(/

Validating the input first refuses rather than serves (422).

VERB_REGEX = /Route::(get|post|put|patch|delete|options|head|query)\s*\(\s*#{PATH_ARG}\s*,/im

Route::get('/x', …) and friends: the verb is capture 1, the path capture 2.

VISIBILITY_WORDS = {"public", "protected", "private"}
VOLT_ROUTE_REGEX = /\bVolt::route\s*\(\s*['"]([^'"]+)['"]\s*,/im

Livewire Volt full-page components: Volt::route('/users', 'users.index') registers a GET route. Capture 1 is the path.

WHERE_CALL_RE = /->\s*(where|whereNumber|whereAlpha|whereAlphaNumeric)\s*\(/

->where('path', '.*') and its shorthands, chained after the route. Laravel matches the placeholder against the regex, so .* lets one placeholder span slashes and [0-9]+ makes /x/new fall through to the next route; the URL keeps the constraint as {path:.*} (Laravel is in URLPath::REGEX_PLACEHOLDER_TECHS).

WHERE_FIXED = {"whereNumber" => "[0-9]+", "whereAlpha" => "[a-zA-Z]+", "whereAlphaNumeric" => "[a-zA-Z0-9]+"}
WHOLE_THIS_CALL_RE = /\A\$this\s*->\s*(\w+)\s*\(\s*\)\z/
WITH_ROUTING_API_RE = /\bapi\s*:\s*(?:\[[^\]]*\]|[^,)]*)/
WITH_ROUTING_WEB_RE = /\bweb\s*:\s*(?:\[[^\]]*\]|[^,)]*)/
WITHOUT_MIDDLEWARE_RE = /->\s*withoutMiddleware\s*\(\s*(\[[^\]]*\]|['"][^'"]+['"])?/

Class methods

tech_name
Source

Instance methods

analyze
Source
analyze_file(path : String) : Array(Endpoint)
Source
tech

Instance-side view of the same declaration. The per-file rescues live on this base class, which has no way to name the analyzer that is running inside them, so a skipped file could not be attributed to a tech. Deriving it from analyzer_for keeps the name written exactly once.

Source

Nested types