vincent15000's avatar

Is a Service Provider really useful in this case ?

Hello,

I have written my first service provider and I have a question : is a Service Provider really useful in this case ?

I have read that several developers should really use much more the service container and service providers.

Imagine a very simple application with a projects table and a controller with an index method to retrieve all the projects.

Version 1 simply with a controller

// ProjectController
public function index()
{
	$projects = Project::all();

	return view('projects.index', compact('projects'));
}

Version 2 with a ProjectService

// ProjectService
public function index()
{
	$projects = Project::all();

	return view('projects.index', compact('projects'));
}

// ProjectController
public function index()
{
	$projects = (new ProjectService)->index();

return view('projects.index', compact('projects'));
}

Version 3 with a service provider

// Contract
abstract class ProjectProvider()
{
	abstract function index();
}

// ProjectService (the same as the previous one, but now it implements the ProjectProvider class)

// ProjectServiceProvider
public function register()
{
	$this->app->bind(ProjectProvider::class, function (Application $app) {
    	return new ProjectService;
	});
}

// ProjectController
public function __construct(protected ProjectProvider $projectProvider)
{
}

public function index()
{
	$projects = $this->projectProvider->index();

	return view('projects.index', compact('projects'));
}

What's the real difference between the three versions ?

Is a Service Provider really useful in this case ?

I think that I understand well the difference between versions 1 and 2, but it's more difficult to understand the difference between versions 2 and 3.

With version 2 I have a better abstraction level, with version 3 too, but the controller depends on the ProjectService or on the ProjectProvider class, isn't it the same dependency level ? It's easy to add other methods to the ProjectService class and it doesn't really change anything using the ProjectService directlu of passing via the ProjectProvider class. Or ?

Can you help me understand the difference between versions 2 and 3 please ?

Furthermore is a Service Provider really useful in this case ?

Thanks for your help.

V

0 likes
14 replies
tisuchi's avatar

@vincent15000 For your situation, I see you simply fetch data from database. If you don't have any other logic/operation/calculation, I personally prefer Version 1 simply with a controller because:

  • It's clean
  • It's much more readable compared with other versions.
  • Not having unnecessary complexity
2 likes
vincent15000's avatar

@tisuchi Do you have an example of a more complex situation which would justify to use version 2 or version 3 ?

1 like
tisuchi's avatar
tisuchi
Best Answer
Level 70

@vincent15000 Here could be my idea:

Version 2

app/Services/ProjectService.php

namespace App\Services;

use App\Models\Project;
use App\Models\User;
use Illuminate\Support\Facades\Cache;

class ProjectService
{
    public function index(User $user)
    {
        // Here, you could implement logic to filter projects based on user permissions.
        if($user->hasRole('admin')) {
            return Project::all();
        }
        
        return $user->projects;
    }

    public function findWithCache($projectId)
    {
        return Cache::remember("projects.{$projectId}", 60, function () use ($projectId) {
            return Project::find($projectId);
        });
    }

    // ... more methods for creating, updating, deleting projects
}

In the controller:

class ProjectController extends Controller
{
    protected $projectService;

    public function __construct(ProjectService $projectService)
    {
        $this->projectService = $projectService;
    }

    public function index(Request $request)
    {
        $user = Auth::user();
        $projects = $this->projectService->index($user);
        
        return view('projects.index', compact('projects'));
    }

    public function show($id)
    {
        $project = $this->projectService->findWithCache($id);
        
        return view('projects.show', compact('project'));
    }

    // ... more methods for creating, updating, deleting projects
}

Version 3 (Multi-Tenant Project Example)

app/Contracts/ProjectProvider.php

namespace App\Contracts;

interface ProjectProvider
{
    public function index();
    // ... other methods like show, create, update, delete
}

Implementing the Interface:

// app/Services/SqlProjectService.php

use App\Contracts\ProjectProvider;
use App\Models\Project;

class SqlProjectService implements ProjectProvider
{
    public function index()
    {
        // Fetch from a relational database
        return Project::all();
    }
}

// app/Services/NoSqlProjectService.php

use App\Contracts\ProjectProvider;

class NoSqlProjectService implements ProjectProvider
{
    public function index()
    {
        // Fetch from a NoSQL database
        // ...implementation...
    }
}

Binding in the service provider:

app/Providers/ProjectServiceProvider.php

use App\Contracts\ProjectProvider;
use App\Services\SqlProjectService;
use App\Services\NoSqlProjectService;

public function register()
{
    $this->app->bind(ProjectProvider::class, function ($app) {
        if (/* Condition to determine the storage type, perhaps from tenant config */) {
            return new SqlProjectService();
        } else {
            return new NoSqlProjectService();
        }
    });
}

Using the contract in the controller:

app/Http/Controllers/ProjectController.php

use App\Contracts\ProjectProvider;

public function __construct(protected ProjectProvider $projectProvider)
{
}

public function index()
{
    $projects = $this->projectProvider->index();
    return view('projects.index', compact('projects'));
}

⚠️ Please take note, all of them are pseudocode. At least you will get some idea from there.

2 likes
vincent15000's avatar

@tisuchi Ok thank you.

That let me think at another situation.

Take the example of a library with books, CDs, DVDs, ... all these items are medias.

According to you, would it be a situation where a service provider coud be useful ? For example : a media provider which will automatically bind (via contextual binding) to a book service, a CD service or a DVD service.

If would even be possible to customize the service to retrieve all borrowed books.

What do you think about it ?

1 like
tisuchi's avatar

@vincent15000 Of course you can follow version 3 for your scenario.

But I feel you can achieve it easily with the factory pattern. Here I would love to do:

  • Create an interface. Book, CD, DVD, each of these classes would implement the MediaItemInterface and provide their specific details:
interface MediaItemInterface {
    public function checkout(User $user);
}
  • Create & dissolve Factory
class MediaItemFactory {
    public static function create($type) {
        switch ($type) {
            case 'book':
                return new Book();
            case 'cd':
                return new CD();
            case 'dvd':
                return new DVD();
        }
    }
}
  • In you controller/class, you can simply utilize them:
public function borrowMedia(Request $request, $mediaType)
{
    $user = Auth::user();
    $mediaItem = MediaItemFactory::create($mediaType);

    $mediaItem->checkout($user);

    // Some other code
}

Why do I prefer to use the factory pattern over using a service provider?: I feel (arguable) service provider is magical! If you are working in a team, some developers (who did not work with a service provider before) might be confused to read your code.

2 likes
tisuchi's avatar

@vincent15000 It's a design pattern. Getting from wikipedia:

In class-based programming, the factory method pattern is a creational pattern that uses factory methods to deal with the problem of creating objects without having to specify the exact class of the object that will be created. This is done by creating objects by calling a factory method—either specified in an interface and implemented by child classes, or implemented in a base class and optionally overridden by derived classes—rather than by calling a constructor.

Take a look this one also: https://www.youtube.com/watch?v=cCRZGBQH9o4

ℹ️ Surely there are a lot more resources about it. Just check some more resources.

2 likes
vincent15000's avatar

@tisuchi Thank you for your answer ;). Oh ok ... I already coded like this, but I didn't know the name factory methods.

2 likes
krisi_gjika's avatar

"Premature optimization is the root of all evil" - start with version 1, upgrade to version 2 as the project grows (but put the service into the controllers constructor), and latter only if really needed go to version 3.

2 likes

Please or to participate in this conversation.