Add Sluggable to Rainlab Users / Extend Plugin and Swap Models

Continuing the discussion from Extend User Model, adding a trait:

I have the same problem as discussed in this thread:

I would like to extend the Rainlab User plugin and add a slug.

I know how to do all the other model modifications, but I cant implement the Sluggable trait to the Rainlab User model.

I found the thread above, but I couldnt find any further info on how to swap the models if I extend a plugin.

How can I do this?

The post you have mentioned is a proposed idea. We will add class resolving in a future build, so it is possible to replace model instances generally in the backend and other areas. However, it can create a black box, which we want to avoid, so it is best to only recommend it in rare cases.

In this case, you simply want to slug an attribute, which is a simple task, done using events.

// Slug 'slug' attribute from 'name' attribute
User::extend(function($model) {
    $model->bindEvent('model.saveInternal', function () use ($model) {
        if (!$model->slug) {
            $model->slug = Str::slug($model->name);
        }
    });
});

For a more detailed approach, see the ExtendUserPlugin class in the UserPlus plugin. Implemented in the boot() method with this call:

Event::subscribe(\RainLab\UserPlus\Classes\ExtendUserPlugin::class);

And here is the class definition:

When the extension logic becomes larger, you can roll your own behavior:

1 Like

The problem using Str::slug is that it can create doublette slugs, for example for “Name” and “Name_”. The sluggable trait did this a lot better with adding “name-2” for a second identical name.

So I tried to create sluggable as a Behavior, but I cant get it to work:

            $model->implement[] = 'Razen\OctoberUtils\Behaviors\Sluggable';
            $model->bindEvent('model.saveInternal', function () use ($model) {
                // $model->slug = Str::slug($model->name);
                $model->addDynamicProperty('slugs', ['slug' => ['id', 'name']]);
                $model->updateSlug();
                trace_log($model->slug);
            });
class Sluggable extends ModelBehavior
{
    use Sluggable;

    protected $model;

    public function __construct($model)
    {
        parent::__construct($model);
        $this->model = $model;
    }

    public function updateSlug(): void
    {
        $this->model->slugAttributes();
    }
}

But whatever I do, if I call updateSlug(), the slug attributes are missing and it does not work.

I tried several approaches already :see_no_evil:

Try this. I haven’t tested it but you get the idea:

// Slug 'slug' attribute from 'name' attribute
User::extend(function($model) {
    $slugFunc = function ($value) use ($model) {
        $counter = 1;
        $_value = $value;

        while ($model->newQuery()->where('slug', $_value)->count() > 0) {
            $_value = "{$value}-{$counter}";
            $counter++;
        }

        return $_value;
    };

    $model->bindEvent('model.saveInternal', function () use ($model, $slugFunc) {
        if (!$model->slug) {
            $model->slug = $slugFunc($model->name);
        }
    });
});
1 Like

I modified it a bit for my purpose, but basically it works. Thx ^^

            $model->bindEvent('model.saveInternal', function () use ($model) {
                if (!$model->slug) {
                    $model->slug = Helper::generateSlug($model, "name");
                }
            });
    public static function generateSlug(Model $model, string $value): string
    {
        $counter = 1;
        $slugCandidate = $slug = Str::slug($model->{$value});
        while ($model->newQuery()->where('slug', $slugCandidate)->count() > 0) {
            $counter++;
            $slugCandidate = $slug . '-' . $counter;
        }

        return $slugCandidate;
    }
1 Like