Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

cutups's avatar

PHP Stan (Larastan) throwing errors: Access to an undefined property in models

I recently updated to Laravel 8 and while I believe this was working after the update, I'm not sure if it was Laravel 8 or Larastan that changed between this passing and now not passing. When I run:

./vendor/bin/phpstan analyse -l 1

I'm getting numerous warnings for model classes such as:

  Line   Models/User.php                                          
 ------ --------------------------------------------------------- 
  216    Access to an undefined property App\Models\User::$name.  
  219    Access to an undefined property App\Models\User::$name.  

 ------ ----------------------------------------------------------------- 
  Line   Models/Thread.php                                                
 ------ ----------------------------------------------------------------- 
  116    Access to an undefined property App\Models\Thread::$created_at.  
  244    Access to an undefined property App\Models\Thread::$created_by.  
  259    Access to an undefined property App\Models\Thread::$created_at.  
  397    Access to an undefined property App\Models\Thread::$name.        
 ------ ----------------------------------------------------------------- 

Not sure how this normally checks, but I haven't changed much in these classes in a while. There isn't anything in the model that explicitly defines the property though.

Is there something that I'm missing here with the definition? Or is this just PHPStan doing a check that isn't quite compatible with the magic that Laravel uses? The code itself works fine.

Laravel code where the issue was found

User class for reference.


<?php

namespace App\Models;

use App\Models\UserStatus;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\MustVerifyEmail;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Contracts\Auth\MustVerifyEmail as MustVerifyEmailContract;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Notifications\Notifiable;

/**
 * @property mixed $id
 */
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, MustVerifyEmailContract
{
    use Authenticatable;
    use Authorizable;
    use CanResetPassword;
    use MustVerifyEmail;
    use Notifiable;
    use HasFactory;

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'users';

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name', 'email', 'password', 'user_status_id'];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = ['password', 'remember_token'];

    /**
     * A how many events the user created.
     */
    public function getEventCountAttribute()
    {
        return $this->hasMany(Event::class, 'created_by')->count();
    }

    /**
     * A user can have many series.
     */
    public function series()
    {
        return $this->hasMany(Series::class);
    }

    /**
     * A user can have much activity.
     */
    public function activity()
    {
        return $this->hasMany(Activity::class);
    }

    /**
     * A user can have many comments.
     */
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }

    /**
     * A user can have one profile().
     */
    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class);
    }

    /**
     * A user has a status.
     */
    public function status(): HasOne
    {
        return $this->hasOne(UserStatus::class, 'id', 'user_status_id');
    }

    /**
     * Return the primary photo for this user.
     *
     * @return Photo $photo
     *
     **/
    public function getPrimaryPhoto()
    {
        // get a list of events that start on the passed date
        $primary = $this->photos()->where('photos.is_primary', '=', '1')->first();

        return $primary;
    }

    /**
     * Get all of the events photos.
     */
    public function photos()
    {
        return $this->belongsToMany(Photo::class)->withTimestamps();
    }

    /**
     * Return the count of events the user is attending.
     */
    public function getAttendingCountAttribute()
    {
        $responses = $this->eventResponses()->get();
        $responses->filter(function ($e) {
            return 'Attending' == $e->responseType->name;
        });

        return count($responses);
    }

    /**
     * A user can have many event responses.
     */
    public function eventResponses()
    {
        return $this->hasMany(EventResponse::class);
    }

    /**
     * Return the count of entities the user is following.
     */
    public function getEntitiesFollowingCountAttribute()
    {
        $responses = $this->follows()->get();
        $responses->filter(function ($e) {
            return 'entity' == $e->object_type;
        });

        return count($responses);
    }

    /**
     * Return the count of tags the user is following.
     */
    public function getTagsFollowingCountAttribute()
    {
        $responses = $this->follows()->get();
        $responses->filter(function ($e) {
            return 'tag' == $e->object_type;
        });

        return count($responses);
    }

    /**
     * Return the count of series the user is following.
     */
    public function getSeriesFollowingCountAttribute()
    {
        $responses = $this->follows()->get();
        $responses->filter(function ($e) {
            return 'series' == $e->object_type;
        });

        return count($responses);
    }

    /**
     * Return the count of threads the user is following.
     */
    public function getThreadsFollowingCountAttribute()
    {
        $responses = $this->follows()->get();
        $responses->filter(function ($e) {
            return 'thread' == $e->object_type;
        });

        return count($responses);
    }

    /**
     * A user can follow many objects.
     */
    public function follows()
    {
        return $this->hasMany(Follow::class);
    }

    /**
     * An profile is owned by a user.
     *
     * @ return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function getFullNameAttribute()
    {
        if ($profile = $this->profile) {
            $full = $profile->first_name . ' ' . $profile->last_name;

            return strlen($full) > 1 ? $full : $this->name; //$profile->first_name.' '.$profile->last_name;
        }

        return $this->name;
    }

    /**
     * Return a list of events the user is attending in the future.
     */
    public function getAttendingFuture()
    {
        $events = Event::join('event_responses', 'events.id', '=', 'event_responses.event_id')
            ->join('response_types', 'event_responses.response_type_id', '=', 'response_types.id')
            ->where('response_types.name', '=', 'Attending')
            ->where('event_responses.user_id', '=', $this->id)
            ->where('start_at', '>=', Carbon::today()->startOfDay())
            ->orderBy('events.start_at', 'asc')
            ->select('events.*')
            ->get();

        return $events;
    }

    /**
     * Return a list of events the user is attending in the future.
     */
    public function getAttendingToday()
    {
        $events = Event::join('event_responses', 'events.id', '=', 'event_responses.event_id')
            ->join('response_types', 'event_responses.response_type_id', '=', 'response_types.id')
            ->where('response_types.name', '=', 'Attending')
            ->where('event_responses.user_id', '=', $this->id)
            ->where('start_at', '>=', Carbon::today()->startOfDay())
            ->where('start_at', '<', Carbon::tomorrow()->startOfDay())
            ->orderBy('events.start_at', 'asc')
            ->select('events.*')
            ->get();

        return $events;
    }

    /**
     * Return a list of events the user is attending.
     */
    public function getAttending()
    {
        $events = Event::join('event_responses', 'events.id', '=', 'event_responses.event_id')
            ->join('response_types', 'event_responses.response_type_id', '=', 'response_types.id')
            ->where('response_types.name', '=', 'Attending')
            ->where('event_responses.user_id', '=', $this->id)
            ->orderBy('events.start_at', 'desc')
            ->select('events.*');

        return $events;
    }

    /**
     * Return a list of entities the user is following.
     */
    public function getEntitiesFollowing()
    {
        $entities = Entity::join('follows', 'entities.id', '=', 'follows.object_id')
            ->where('follows.object_type', '=', 'entity')
            ->where('follows.user_id', '=', $this->id)
            ->orderBy('follows.created_at', 'desc')
            ->select('entities.*')
            ->get();

        return $entities;
    }

    /**
     * Return a list of tags the user is following.
     */
    public function getTagsFollowing()
    {
        $tags = Tag::join('follows', 'tags.id', '=', 'follows.object_id')
            ->where('follows.object_type', '=', 'tag')
            ->where('follows.user_id', '=', $this->id)
            ->orderBy('tags.name', 'asc')
            ->select('tags.*')
            ->get();

        return $tags;
    }

    /**
     * Return a list of series the user is following.
     */
    public function getSeriesFollowing()
    {
        $series = Series::join('follows', 'series.id', '=', 'follows.object_id')
            ->where('follows.object_type', '=', 'series')
            ->where('follows.user_id', '=', $this->id)
            ->orderBy('follows.created_at', 'desc')
            ->select('series.*')
            ->get();

        return $series;
    }

    /**
     * Return a list of threads the user is following.
     */
    public function getThreadsFollowing()
    {
        $threads = Thread::join('follows', 'threads.id', '=', 'follows.object_id')
            ->where('follows.object_type', '=', 'thread')
            ->where('follows.user_id', '=', $this->id)
            ->orderBy('follows.created_at', 'desc')
            ->select('threads.*')
            ->get();

        return $threads;
    }

    /**
     * Events that were created by the user.
     *
     * @return BelongsToMany
     */
    public function createdEvents()
    {
        $events = $this->events()->where('created_at', '=', Auth::user())->orderBy('start_at', 'ASC')->get();

        return $events;
    }

    /**
     * A user can have many events.
     */
    public function events()
    {
        return $this->hasMany(Event::class, 'created_by')->orderBy('start_at', 'DESC');
    }

    public function addPhoto(Photo $photo)
    {
        return $this->photos()->attach($photo->id);
    }

    public function hasGroup($group)
    {
        if (is_string($group)) {
            return $this->groups->contains('name', $group);
        }

        return (bool) $group->intersect($this->groups)->count();
    }

    public function assignGroup($group)
    {
        return $this->groups()->save(
            Group::whereName($group)->firstOrFail()
        );
    }

    /**
     * @return BelongsToMany
     */
    public function groups()
    {
        return $this->belongsToMany(Group::class);
    }

    /**
     * Fetch the last published post for the user.
     *
     * @return HasOne
     */
    public function lastPost()
    {
        return $this->hasOne(Post::class, 'created_by')->latest();
    }

    /**
     * Fetch the login date for the user.
     *
     * @return HasOne
     */
    public function lastActivity()
    {
        return $this->hasOne(Activity::class, 'user_id')->latest();
    }

    /**
     * Check that the user is active.
     *
     * @ return boolean
     */
    public function getIsActiveAttribute()
    {
        if ($this->status && 'Active' === $this->status->name) {
            return 1;
        }

        return 0;
    }

    /**
     * Return the feed of user activity.
     *
     * @param $user
     * @param int $take
     *
     * @return array
     */
    public function feed($user, $take = 50)
    {
        return static::where('user_id', $user->id)
            ->latest()
            ->with('object')
            ->take($take)
            ->get()
            ->groupBy(function ($activity) {
                return $activity->created_at->format('Y-m-d');
            });
    }

    /**
     * Get a list of group ids associated with the user.
     *
     * @ return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function getGroupListAttribute()
    {
        return $this->groups->pluck('id')->all();
    }

}


0 likes
12 replies
Tippin's avatar
Tippin
Best Answer
Level 13

@cutups Since they are magic methods, etc, it probably does not see them, and now expects you to declare it as a property in the class docblock.

This package does a good job of auto generating them for you: https://github.com/barryvdh/laravel-ide-helper

It would generate the following in your User model, etc

<?php

namespace App\Models;

/**
 * App\Models\User
 *
 * @property mixed $id
 * @property string $name
 */
class User extends Model
{

////

Thread:

<?php

namespaceApp\Models;

/**
 * App\Models\Thread.
 * @property string $name
 * @property \Illuminate\Support\Carbon|null $created_at
 * @property mixed $created_by
 */
class Thread extends Model
{

///

And many more attributes of course.

4 likes
cutups's avatar

Thanks for that, should help. I was a bit confused that PHP Stan level 1 would have changed something so basic.

MariusJP's avatar

We just stumbled upon the same issue, but we hate the ide:helper @property outputs on our models.

We had subdirectories in database/migrations these interfered with larastans ability to discover the table contents.

1 like
Kobayakawa's avatar

Larastan recursively scans for php files under the database/migrations (or whatever your database path is. its configurable) folder. So that shouldn't be a problem. You can open an issue in Larastan repo if you still have problems.

loureirorg's avatar

What worked for me was to add a type hint for the return.

public function series(): HasMany
{
    return $this->hasMany(Series::class);
}

And the Access to an undefined property error was gone.

In this example, the type hint is "HasMany".

No need for laravel-ide-helper, just Larastan.

7 likes
PinTend's avatar

@loureirorg That would work for $model->series()->first() what about $model->series->first()

The ->first() method on first example is a method on query builder.

The ->first() method on second example is a method on eloquent collection. which returns null | Series

does this work correctly for you by just type hinting the relationship?

loureirorg's avatar

@RawSlugs

does this work correctly for you by just type hinting the relationship?

Yes, both ways work: $model->series()->first() and $model->series->first().

1 like
MikePageDev's avatar

Hi, I am still having issues with the ID property (I am using UUIDs so I don't know if that makes a difference ). I have added the doc blocks but getting the error in PHPStan. I have declared the ID as a string.

valentin_vranic's avatar

Hi there! I'm facing something similar, and can't figure out how to make is work

61     Access to an undefined property                                           
         Illuminate\Contracts\Auth\Authenticatable::$client_id.                    
         💡 Learn more:                                                            
            https://phpstan.org/blog/solving-phpstan-access-to-undefined-property 

on this auth()->user()->client_id

However, my User class looks like this

/**
 * @property string $email
 * @property int $client_id
 * @property string $password_hash
 * @property string $password_salt
 * @property Clients $client
 */
class User extends Authenticatable

The only way I make it work was adding annotation before like this:

/** @var \App\Models\User $user */
$user = auth()->user();

and then accessing like $user->client_id

valst's avatar

@valentin_vranic From the error I think the package cannot figure out that auth()->user() is of type User not Authenticatable. I think one of the ways in which you can fix it indeed by using /** @var and pointing out that the var should be of type User to sorts of "cast" it. Or something like if (auth()->user() instanceof User) Otherwise try either $request->user() or Auth::user() you might have better luck with them

Please or to participate in this conversation.