7

I'm trying to apply Clean Architecture in my PHP application. At the moment I have my business logic entity User and service UsersService. UsersService collects all of the use cases related to the User entity.

UsersService->createUser(someData) takes Users repository and stores it in the database.

I call createUser use case in my Controller and in my Cli Task. And I want to integrate logging system in my project. I want to log something inside use case and inside controller/task.

Where I need to place my Loggers factory/Logger interfaces?

Jesse
  • 3,243
  • 1
  • 22
  • 29
Mark Yeltsin
  • 111
  • 2
  • 6
  • Just use a decorator: https://stackoverflow.com/a/18682856/727208 (kinda old-ish post). You apply at whatever level you need using DIC. – tereško Nov 26 '18 at 00:14
  • @tereško What about class explosion? I would need one decorator for each class I want to log, wouldn't I? – IamDOM May 07 '22 at 09:44
  • No, you just abuse the`__call()` magic method and have a single "wrapper" for an entire namespace of classes. The only real issue is that this does not play well with DIC and typehints, – tereško May 09 '22 at 08:27

3 Answers3

3

Well the premise here is that in your internal layers know nothing from externals ones, so any interfaces you use in your domain should lays in the domain, that gives you the hability to change how you log later without making a single change in your business logic, so just create a Log interface in your domain, implement it from outside and voila.

In your domain it could looks like this:

interface ILogger {
    log(level: string, message: string): void
}

class MyUseCase {
    public constructor(private readonly logger: ILogger){}
    public execute(input: Input): void {
        // some stuff
        this.ILogger.log('level', 'message')
        // some other stuff
    }
}

and in your framework and drivers layers you could:

class MyLogger implements ILogger {
   log(level: string, message: string): void {
      myChosenLoggerTech.log(level, message) // ie winston, console.log, etc
   }
}

So now when MyUseCaseis instantiated can use:

new MyUseCase(new MyLogger())

But wait, nothing is that simple, generally speaking there are two log use cases:

  1. You want to log business info
  2. You want to log app status info

All right, if your case is number 2 go with this solution, but if your case is number 1 maybe you should make the action of logging more explicit by wrapping it into a dedicated entity, or exception or whatever truly represents your business. You can check here for a more detailed explanation.

Angel Tabares
  • 333
  • 4
  • 10
3

In Clean Architecture it is not allowed for Inner Layers to depend on outer Layers.

e.g. Domain Layer must not Depend on Infrastructure Layer. You could therefore place an interface ILogger in the Domain Layer.

However to place ILogger in the Domain project, would suggest that logging is relevant for the business domain.

To me a Logger is general utility in nature and therefore i would place an ILogger interface in a 'Common' project which is referred ubiquitously throughout the solution. With an implementation of ILogger in the Infrastructure Layer.

2

According to Uncle Bob's tweet:

"Logging is a cross-cutting concern. It’s best done in plugin components."

and..

"The basic approach is to have a base class that does something you want to log. You out that behavior alone in a method of the base. Then in a derivative you override that function to log and then invoke the base function."

So far, as I understand he is suggesting to use the Template pattern, but a Decorator should work too.

Anyway, if you dig, you find this other tweet:

"You can, of course, just log. You can just construct SQL. You can just call the database from the GUI. You can. Sometimes, in fact, you should. But you should always know what the cost is, and what your options are."

Christian H
  • 3,779
  • 2
  • 7
  • 17