You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel eloquent-sluggable生成唯一slug时报错求助

排查eloquent-sluggable生成重复slug时抛出MissingAttributeException异常

我之前解决了eloquent-sluggable包中slug按父模型作用域生成的问题,现在遇到新异常。

我的User模型属性包括:

id
role_id
first_name
last_name
name
slug

...其他属性

当User模型无slug冲突时,保存可正常生成slug;但已存在用户:

role_id = 1
first_name = John
last_name = Smith
name = John Smith
slug = john-smith

新增另一个John Smith用户时,本应生成john-smith-2,却抛出错误:

Illuminate\Database\Eloquent\MissingAttributeException^ {#2420
  #message: "The attribute [role_id] either does not exist or was not retrieved for model [App\Models\User]."
  #code: 0
  #file: "/var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php"
  #line: 475
  trace: {
    /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php:475 { …}
    /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php:455 { …}
    /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:2226 { …}
    /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php:378 { …}
    /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php:133 { …}
    /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php:116 { …}
    /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php:776 { …}
    /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php:754 { …}
    /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php:722 { …}
    /var/www/html/vendor/cviebrock/eloquent-sluggable/src/Services/SlugService.php:379 { …}
    /var/www/html/vendor/cviebrock/eloquent-sluggable/src/Services/SlugService.php:278 { …}
    /var/www/html/vendor/cviebrock/eloquent-sluggable/src/Services/SlugService.php:89 { …}
    /var/www/html/vendor/cviebrock/eloquent-sluggable/src/Services/SlugService.php:44 { …}
    /var/www/html/vendor/cviebrock/eloquent-sluggable/src/SluggableObserver.php:81 { …}
    /var/www/html/vendor/cviebrock/eloquent-sluggable/src/SluggableObserver.php:53 { …}
    /var/www/html/vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php:478 { …}
    /var/www/html/vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php:286 { …}
    /var/www/html/vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php:266 { …}
    /var/www/html/vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php:232 { …}
    /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php:188 { …}
}

User模型代码

<?php

namespace App\Models;

use Cviebrock\EloquentSluggable\Sluggable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use function Illuminate\Events\queueable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Jetstream\HasProfilePhoto;

class User extends Authenticatable implements JWTSubject
{
    use HasFactory;
    use HasProfilePhoto;
    use Notifiable;
    use TwoFactorAuthenticatable;
    use SoftDeletes;
    use Sluggable;

    protected $guarded = ['id'];

    protected $with = ['role'];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
        'two_factor_recovery_codes',
        'two_factor_secret',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    /**
     * The accessors to append to the model's array form.
     *
     * @var array
     */
    protected $appends = [
        'profile_photo_url',
        'name',
    ];

    /**
     * Return the sluggable configuration array for this model.
     */
    public function sluggable(): array
    {
        return [
            'slug' => [
                'source' => 'name',
            ],
        ];
    }

    /**
     * Get the route key for the model.
     *
     * @return string
     */
    public function getRouteKeyName(): string
    {
        return 'slug';
    }

    public function role()
    {
        return $this->belongsTo(Role::class);
    }

    .....
}

sluggable配置文件

<?php

return [

    /**
     * What attributes do we use to build the slug?
     * This can be a single field, like "name" which will build a slug from:
     *
     *     $model->name;
     *
     * Or it can be an array of fields, like ["name", "company"], which builds a slug from:
     *
     *     $model->name . ' ' . $model->company;
     *
     * If you've defined custom getters in your model, you can use those too,
     * since Eloquent will call them when you request a custom attribute.
     *
     * Defaults to null, which uses the toString() method on your model.
     */
    'source' => null,

    /**
     * The maximum length of a generated slug.  Defaults to "null", which means
     * no length restrictions are enforced.  Set it to a positive integer if you
     * want to make sure your slugs aren't too long.
     */
    'maxLength' => null,

    /**
     * If you are setting a maximum length on your slugs, you may not want the
     * truncated string to split a word in half.  The default setting of "true"
     * will ensure this, e.g. with a maxLength of 12:
     *
     *   "my source string" -> "my-source"
     *
     * Setting it to "false" will simply truncate the generated slug at the
     * desired length, e.g.:
     *
     *   "my source string" -> "my-source-st"
     */
    'maxLengthKeepWords' => true,

    /**
     * If left to "null", then use the cocur/slugify package to generate the slug
     * (with the separator defined below).
     *
     * Set this to a closure that accepts two parameters (string and separator)
     * to define a custom slugger.  e.g.:
     *
     *    'method' => function( $string, $sep ) {
     *       return preg_replace('/[^a-z]+/i', $sep, $string);
     *    },
     *
     * Otherwise, this will be treated as a callable to be used.  e.g.:
     *
     *    'method' => array('Str','slug'),
     */
    'method' => null,

    /**
     * Separator to use when generating slugs.  Defaults to a hyphen.
     */
    'separator' => '-',

    /**
     * Enforce uniqueness of slugs?  Defaults to true.
     * If a generated slug already exists, an incremental numeric
     * value will be appended to the end until a unique slug is found.  e.g.:
     *
     *     my-slug
     *     my-slug-1
     *     my-slug-2
     */
    'unique' => true,

    /**
     * If you are enforcing unique slugs, the default is to add an
     * incremental value to the end of the base slug.  Alternatively, you
     * can change this value to a closure that accepts three parameters:
     * the base slug, the separator, and a Collection of the other
     * "similar" slugs.  The closure should return the new unique
     * suffix to append to the slug.
     */
    'uniqueSuffix' => null,

    /**
     * What is the first suffix to add to a slug to make it unique?
     * For the default method of adding incremental integers, we start
     * counting at 2, so the list of slugs would be, e.g.:
     *
     *   - my-post
     *   - my-post-2
     *   - my-post-3
     */
    'firstUniqueSuffix' => 2,

    /**
     * Should we include the trashed items when generating a unique slug?
     * This only applies if the softDelete property is set for the Eloquent model.
     * If set to "false", then a new slug could duplicate one that exists on a trashed model.
     * If set to "true", then uniqueness is enforced across trashed and existing models.
     */
    'includeTrashed' => false,

    /**
     * An array of slug names that can never be used for this model,
     * e.g. to prevent collisions with existing routes or controller methods, etc..
     * Defaults to null (i.e. no reserved names).
     * Can be a static array, e.g.:
     *
     *    'reserved' => array('add', 'delete'),
     *
     * or a closure that returns an array of reserved names.
     * If using a closure, it will accept one parameter: the model itself, and should
     * return an array of reserved names, or null. e.g.
     *
     *    'reserved' => function( Model $model) {
     *      return $model->some_method_that_returns_an_array();
     *    }
     *
     * In the case of a slug that gets generated with one of these reserved names,
     * we will do:
     *
     *    $slug .= $separator + "1"
     *
     * and continue from there.
     */
    'reserved' => null,

    /**
     * Whether to update the slug value when a model is being
     * re-saved (i.e. already exists).  Defaults to false, which
     * means slugs are not updated.
     *
     * Be careful! If you are using slugs to generate URLs, then
     * updating your slug automatically might change your URLs which
     * is probably not a good idea from an SEO point of view.
     * Only set this to true if you understand the possible consequences.
     */
    'onUpdate' => true,

    /**
     * If the default slug engine of cocur/slugify is used, this array of
     * configuration options will be used when instantiating the engine.
     */
    'slugEngineOptions' => [],

];

排查方向与建议

  1. 检查关联预加载导致的字段缺失
    你的User模型设置了protected $with = ['role'];,强制预加载role关联。当eloquent-sluggable查询现有slug时,生成的查询可能没有包含role_id字段,导致模型实例尝试访问role_id时抛出异常。

    • 临时注释掉$with数组,测试新增用户是否正常生成slug,验证是否是预加载导致的问题。
    • 如果确认是这个问题,可以在sluggable配置中添加scope方法,确保查询时包含role_id字段:
      public function sluggable(): array
      {
          return [
              'slug' => [
                  'source' => 'name',
                  'scope' => function ($query) {
                      $query->select('id', 'slug', 'role_id', 'name');
                  }
              ],
          ];
      }
      
  2. 确认新增用户时role_id已正确赋值
    检查新增用户的代码,确保在保存前role_id已经被设置到模型实例中。如果role_id为空,在预加载role关联时会尝试访问不存在的属性,触发异常。

  3. 调整关联加载时机
    把$with中的role移除,改为在需要的时候手动预加载,避免在slug生成过程中不必要的关联加载。或者给role关联添加withDefault()方法,防止关联为空时抛出错误:

    public function role()
    {
        return $this->belongsTo(Role::class)->withDefault();
    }
    
  4. 查看sluggable查询逻辑
    跟踪SlugService.php中的查询代码,确认在检查slug唯一性时,是否正确选择了role_id字段。默认情况下,sluggable可能只查询slug字段和主键,导致模型实例缺少role_id,触发预加载关联时的异常。

内容的提问来源于stack exchange,提问作者ryanpitts1

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 23:03:09