lorisleiva / laravel-actions

⚡️ Laravel components that take care of one specific task
https://laravelactions.com
MIT License
2.51k stars 123 forks source link

Creating an AsLog trait #91

Closed mbryne closed 3 years ago

mbryne commented 3 years ago

Hello and thank you very much for your efforts on this package. I'm interested in the approach you have taken using the Action decorators.

What I am hoping to achieve is to extend your ActionManager and add some an additional design pattern to automatically add an activity log for certain actions by adding a AsLog trait.

Am I correct in assuming that because of the decorator pattern you have used here, it should be possible to add a LoggingDecorator or similar that will fire off my own Log handler? That seems to be the case when I look at say:

Lorisleiva\Actions\Decorators\CommandDecorator

...
    public function handle()
    {
        if ($this->hasMethod('asCommand')) {
            return $this->resolveAndCallMethod('asCommand', ['command' => $this]);
        }

        if ($this->hasMethod('handle')) {
            return $this->resolveAndCallMethod('handle', ['command' => $this]);
        }
    }
...

So theoretically I should be able to put something together like:

MyActions\Decorators\LoggingDecorator

...
    public function handle()
    {
        if ($this->hasMethod('asLog')) {
            $this->resolveAndCallMethod('asLog', ['command' => $this]);
        } else {
            $this->resolveAndCallMethod('asDefaultLog', ['command' => $this]);
        }

        if ($this->hasMethod('handle')) {
            return $this->resolveAndCallMethod('handle', ['command' => $this]);
        }
    }

    protected function asDefaultLog() {
          Log::info('Default logging message goes here');
    }
...

Would you say my assumptions are correct and would you be able to provide a small amount of initial direction on extending the ActionManager to allow my custom Decorators and DesignPatterns?

Many thanks in advance for any assistance you can offer,

lorisleiva commented 3 years ago

Hi there, 👋

Yes you can absolutely extends Laravel Actions by creating your own DesignPattern and Decorator.

When creating a DesignPattern you'd need to implement recognizeFrame so Laravel Actions can tell if this decorator should be used when you're action is being resolved from the container.

Then you can add it to existing design patterns like this.

$currentDesignPatterns = Actions::getDesignPatterns();

Actions::setDesignPatterns(array_merge($currentDesignPatterns, [new MyDesignPattern()]));

// I need to add a `addDesignPattern` method tbf.

However, I'm not sure how you'll be using that AsLog trait. If it does not require to be identified when resolved from the container, you can add static methods similar to how the AsJob trait works.

I hope this helps.

mbryne commented 3 years ago

lorisleiva thank you for your speedy reply, I will look into extending those classes. I'm mindful of your time but do you mind if I ask an additional question...

My use case will be to add a semi-transparent Activity Logging layer to my actions, rather than having to call $this->log() within my handle method all the time.

So my thought process is:

  1. Create additional AsLog Action trait that retrieves default log message from transation files < done
  2. Create additional LoggingDesignPattern and LoggingDecorator < no worries
  3. Instantiate my Action via make or run to retrieve it from the service container < no worries
  4. As I've retrieved it from the service container, it will be wrapped in my LoggingDecorator which will call the handle method as outlined below
  5. This handle method will call LoggingDecorator.handle which fires off an Activity Log message in a manner of my choosing and then call MyCoolAction.handle method after that?
...
    public function handle()
    {
        if ($this->hasMethod('asLog')) {
            $this->resolveAndCallMethod('asLog', ['command' => $this]);
        } else {
            $this->resolveAndCallMethod('asDefaultLog', ['command' => $this]);
        }

        if ($this->hasMethod('handle')) {
            return $this->resolveAndCallMethod('handle', ['command' => $this]);
        }
    }
...

^ It's points 4 and 5 that I am keen to clarify with you if you don't mind, just to make sure I understand the reasoning behind the approach you have taken before I attempt to butcher it :)

Many thanks again

lorisleiva commented 3 years ago

So my only worry with this approach is that your LoggingDesignPattern will be in "competition" with other design patterns. Meaning this will only work if the action itself is not recognised as a ControllerDesignPattern, ListenerDesignPattern, etc.

It seems to me that this might be a slight overkill for what you're trying to achieve which, if I understand correctly, is a listener on the handle method that logs things before the handle method is being called.

The easiest way to achieve this would probably be to create a AsLog trait that simply contains a logAndHandle method which you can then use in your asJob, asController, etc. methods instead of using handle. Something like that:

trait AsLog
{
    public function logAndHandle(...$arguments)
    {
        // Log here...

        return $this->handle(...$arguments);
    }

    public static function logAndRun(...$arguments)
    {
        return static::make()->logAndHandle(...$arguments);
    }
}

Use it in your action like so:

class MyAction
{
    use AsAction;
    use AsLog;

    public function handle($foo)
    {
        // Your action logic here...
    }

    public function asController(ActionRequest $request)
    {
        // Don't forget to use logAndHandle in your "asPattern" methods if you need to.
        return $this->logAndHandle($request->get('foo'));
    }
}

And that's it. Now your action will be automatically logged when running as a controller. When running your action as an object, you can do:

// With logging.
MyAction::logAndRun('bar');

// Without logging.
MyAction::run('bar');

Or if you prefer dependency injection.

class MyOtherAction
{
    use AsAction;

    protected MyAction $myAction;

    public function __construct(MyAction $myAction)
    {
        $this->myAction = $myAction;
    }

    public function handle()
    {
        // With logging.
        $this->myAction->logAndHandle('bar');

        // Without logging.
        $this->myAction->handle('bar');
    }
}

I hope this helps. 🙂

mbryne commented 3 years ago

Yes @lorisleiva thank you very much!

I'll close the issue, cheers