Extend User Model, adding a trait

Hi,

I’m looking to extend and add a trait (Revisionable) to \RainLab\User\Models\User;

I tried this in my boot() method :

\RainLab\User\Models\User::extend(function($model) {

      $model->morphMany['revision_history'] = [
          \System\Models\Revision::class, 'name' => 'revisionable'
      ];

      $model->revisionable = [
          'name',
          'surname'
      ];

      $model->implement[] = '\October\Rain\Database\Traits\Revisionable';

});

But this is giving an error :

Cannot instantiate trait October\Rain\Database\Traits\Revisionable

How can we extend a plugin and add any trait to it ?

Thank you.

Tried this GitHub - CptMeatball/notifiable-user: Adds the Notifiable trait as a behavior to the user model

<?php namespace Larnet\Osm\Behaviors;

use \October\Rain\Database\Traits\Revisionable as RevisionableTrait;

class Revisionable extends \October\Rain\Database\ModelBehavior
{
    use RevisionableTrait;

    public function __call($name, $params = null)
    {
        if (!method_exists($this, $name) || !is_callable($this, $name)) {
            return call_user_func_array([$this->model, $name], $params);
        }
    }
}

I added this to my boot function :

\RainLab\User\Models\User::extend(function ($model) {
  $model->morphMany['revision_history'] = [
                  \System\Models\Revision::class, 'name' => 'revisionable'
              ];
  
  $model->revisionable = ['name', 'surname', 'email'];
  
  $model->implement[] = 'Larnet.Osm.Behaviors.Revisionable';
});

but it’s not working.

Hi @apinard

You’re close.

We don’t support this because maintaining it is extremely difficult. You can write your own implementation; you should do it to make things predictable and the code robust.

Don’t be afraid to duplicate code and ship it with your plugin to gain the flexibility benefits. Something like this:

<?php namespace Acme\MyPlugin\Behaviors;

use System\Classes\ModelBehavior;

/**
 * RevisionableModel implementation of \October\Rain\Database\Traits\Revisionable
 *
 * Usage:
 *
 * In the model class definition:
 *
 *   public $implement = [\Acme\MyPlugin\Behaviors\RevisionableModel::class];
 *
 */
class RevisionableModel extends ModelBehavior
{
    use \October\Rain\Database\Traits\Revisionable;

    /**
     * __construct the behavior
     */
    public function __construct($model)
    {
        parent::__construct($model);

        if (!is_array($this->model->revisionable)) {
            throw new Exception(sprintf(
                'The $revisionable property in %s must be an array to use the Revisionable behavior.',
                get_class($this->model)
            ));
        }

        $this->model->bindEvent('model.afterUpdate', function () {
            $this->revisionableAfterUpdate();
        });

        $this->model->bindEvent('model.afterDelete', function () {
            $this->revisionableAfterDelete();
        });
    }

    /**
     * revisionableAfterUpdate event
     */
    public function revisionableAfterUpdate()
    {
        // every time $this is mentioned, use $this->model
    }
}
1 Like

If you did do this how would you access protected properties on the extended class? Trait allow you access to protected stuff where as a behaviour does not as far as I can tell.

This is true, although it shouldn’t be a problem if the model is well designed.

Generally speaking, using behaviors in models can cause performance problems at scale since each model object requires several companion objects, and it eats up memory fast. This is why traits are preferable.

So doing what you have suggested above would chew up memory then, but needs to be done if if you want to extend a third party class? So traits for your owns classes and extends for ones you cant access is the best approach?

1 Like

The best approach is extending the model you can’t access with your own model.

class MyModel extends OtherModel
{
    use Trait1;
    use Trait2;
}

Then extend the plugin to swap the model with yours.

This could create a mess, for example take a plugin with somewhat “ecosystem” or in other words plugins that rely on other plugins. You should extend every plugin if you want your changes to apply into them.

Shopaholic for example.

I’m not sure what you mean. Decorating models is how the Translate plugin handles its injection, and is also used by Laravel’s service container.

Shopaholic could resolve its models through the container to make this extension approach easier.