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' => [], ];
排查方向与建议
检查关联预加载导致的字段缺失
你的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'); } ], ]; }
- 临时注释掉
确认新增用户时
role_id已正确赋值
检查新增用户的代码,确保在保存前role_id已经被设置到模型实例中。如果role_id为空,在预加载role关联时会尝试访问不存在的属性,触发异常。调整关联加载时机
把$with中的role移除,改为在需要的时候手动预加载,避免在slug生成过程中不必要的关联加载。或者给role关联添加withDefault()方法,防止关联为空时抛出错误:public function role() { return $this->belongsTo(Role::class)->withDefault(); }查看sluggable查询逻辑
跟踪SlugService.php中的查询代码,确认在检查slug唯一性时,是否正确选择了role_id字段。默认情况下,sluggable可能只查询slug字段和主键,导致模型实例缺少role_id,触发预加载关联时的异常。
内容的提问来源于stack exchange,提问作者ryanpitts1
相关产品推荐
相关产品推荐

