I asked a question earlier related to the https://github.com/cviebrock/eloquent-sluggable package surrounding the ability to scope slugs to parent models. Got that all working. Now I am running into a different issue.
I have a User
model that has, among other attributes, the following attributes:
id
role_id
first_name
last_name
name
slug
...and others
I am trying to generate a slug for this object. As long as no conflicting slug already exists for the User
model, it works just fine and will generate one for me upon save.
However, say i already have a user with these attributes:
role_id = 1
first_name = John
last_name = Smith
name = John Smith
slug = john-smith
If i try and save another John Smith
, i get this error:
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 { …}
It SHOULD be generating the slug john-smith-2
per my setup. Where can i start to diagnose this?
Here are the relevant parts of my User
model:
<?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);
}
.....
}
And here is my sluggalbe config:
<?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' => [],
];
Any advice is greatly appreciated!
Based on the discussion, and checking the query of sluggalbe package, the issue you are facing is a bug related to the https://github.com/cviebrock/eloquent-sluggable package.
It seem you cannot use the package in combination to $with
relationships.
The query used by the package is:
$results = $query->select([$attribute, $this->model->getQualifiedKeyName()])
->get()
->toBase();
// key the results and return
return $results->pluck($attribute, $this->model->getKeyName());
As you can see its selecting 2 columns, in your case the users.name column and the users.id column when returning a collection of Eloquent models.
However laravel internally also tries to append the $with relationship when retrieving the models, but since there are only 2 columns it cannot create the right query since it needs the missing role_id
column for the role
relationship and hence the Exception.
You can file a bug on eloquent-sluggable regarding the issue.
I suppose they used a selective select in the query to reduce memory by avoiding returning a lot of data, however this brakes the usage of $with
relationships.