Skip to main content
Version: 5.x

Services in BowPHP

Introduction​

About services

Services are classes that encapsulate your application's business logic. They allow you to decouple code from controllers and make your application more testable and maintainable.

Services are automatically injected by BowPHP's dependency injection container.

Creating a service​

To create a service, use the add:service command:

php bow add:service UserService

The service will be created in the app/Services folder:

app/Services/UserService.php
namespace App\Services;

class UserService
{
//
}

Practical example​

Let's create a complete service for managing users:

app/Services/UserService.php
namespace App\Services;

use App\Models\User;
use Bow\Database\Collection;

class UserService
{
/**
* Retrieves all users
*/
public function getAllUsers(): Collection
{
return User::all();
}

/**
* Retrieves a user by their ID
*/
public function findById(int $id): ?User
{
return User::retrieve($id);
}

/**
* Creates a new user
*/
public function create(array $data): User
{
$user = User::create([
"name" => $data["name"],
"email" => $data["email"],
"password" => app_hash($data["password"]),
]);

$user->persist();

return $user;
}

/**
* Updates a user
*/
public function update(int $id, array $data): bool
{
$user = User::retrieve($id);

if (!$user) {
return false;
}

return (bool) $user->update($data);
}

/**
* Deletes a user
*/
public function delete(int $id): bool
{
return User::deleteBy('id', $id) > 0;
}
}

Injecting into a controller​

The service can be injected via the constructor or directly into methods:

Via the constructor​

app/Controllers/UserController.php
namespace App\Controllers;

use App\Controllers\Controller;
use App\Services\UserService;
use Bow\Http\Request;

class UserController extends Controller
{
public function __construct(
private UserService $userService
) {}

public function index()
{
$users = $this->userService->getAllUsers();

return view("users.index", ["users" => $users]);
}

public function show(int $id)
{
$user = $this->userService->findById($id);

if (!$user) {
return app_abort(404, "User not found");
}

return view("users.show", ["user" => $user]);
}

public function store(Request $request)
{
$user = $this->userService->create($request->all());

return redirect("/users/" . $user->id);
}
}

Via method parameters​

public function index(UserService $userService)
{
$users = $userService->getAllUsers();

return view("users.index", ["users" => $users]);
}

Dependency injection within a service​

Services can themselves have dependencies injected:

app/Services/NotificationService.php
namespace App\Services;

class NotificationService
{
public function __construct(
private UserService $userService
) {}

public function notifyAllUsers(string $message): void
{
$users = $this->userService->getAllUsers();

foreach ($users as $user) {
email("emails.notification", ["message" => $message], function ($mail) use ($user) {
$mail->to($user->email)->subject("Notification");
});
}
}
}

Services with an interface (Repository Pattern)​

For a more flexible architecture, use interfaces:

app/Contracts/UserRepositoryInterface.php
namespace App\Contracts;

use App\Models\User;
use Bow\Support\Collection;

interface UserRepositoryInterface
{
public function all(): Collection;
public function find(int $id): ?User;
public function create(array $data): User;
public function update(int $id, array $data): bool;
public function delete(int $id): bool;
}
app/Services/UserRepository.php
namespace App\Services;

use App\Contracts\UserRepositoryInterface;
use App\Models\User;
use Bow\Database\Collection;

class UserRepository implements UserRepositoryInterface
{
public function all(): Collection
{
return User::all();
}

public function find(int $id): ?User
{
return User::retrieve($id);
}

public function create(array $data): User
{
$user = User::create($data);
$user->persist();
return $user;
}

public function update(int $id, array $data): bool
{
return (bool) (User::retrieve($id)?->update($data) ?? false);
}

public function delete(int $id): bool
{
return User::deleteBy('id', $id) > 0;
}
}

Registering the binding in a provider​

Bindings are declared in a provider that extends Bow\Configuration\Configuration. The Kernel's boot() method is reserved for the framework β€” do not override it (you would short-circuit the loading of the other configurations).

app/Configurations/AppServiceProvider.php
namespace App\Configurations;

use App\Contracts\UserRepositoryInterface;
use App\Services\UserRepository;
use Bow\Configuration\Configuration;
use Bow\Configuration\Loader;

class AppServiceProvider extends Configuration
{
public function create(Loader $config): void
{
$this->container->bind(
UserRepositoryInterface::class,
UserRepository::class
);
}

public function run(): void
{
//
}
}

Then register the provider in Kernel::configurations():

app/Kernel.php
public function configurations(): array
{
return [
// ... other providers
\App\Configurations\AppServiceProvider::class,
];
}

Usage​

class UserController extends Controller
{
public function __construct(
private UserRepositoryInterface $users
) {}

public function index()
{
return view("users.index", ["users" => $this->users->all()]);
}
}

Testing a service​

Services are easy to unit test:

tests/Services/UserServiceTest.php
namespace Tests\Services;

use App\Services\UserService;
use App\Models\User;
use PHPUnit\Framework\TestCase;

class UserServiceTest extends TestCase
{
private UserService $service;

protected function setUp(): void
{
parent::setUp();
$this->service = new UserService();
}

public function test_create_user(): void
{
$data = [
"name" => "John Doe",
"email" => "john@example.com",
"password" => "secret123"
];

$user = $this->service->create($data);

$this->assertInstanceOf(User::class, $user);
$this->assertEquals("John Doe", $user->name);
}

public function test_find_user_by_id(): void
{
$user = $this->service->findById(1);

$this->assertInstanceOf(User::class, $user);
}
}

Best practices​

Recommendations
  • One service = one responsibility: Each service should have a single area of responsibility.
  • Dependency injection: Prefer constructor injection for required dependencies.
  • Interfaces: Use interfaces for complex services to make testing and changes easier.
  • Naming: Use descriptive names such as UserService, PaymentService, NotificationService.
  • Tests: Services let you isolate business logic and test it independently.

Is something missing?

If you run into problems with the documentation or have suggestions to improve the documentation or the project in general, please open an issue for us, or send a tweet mentioning the Twitter account @bowframework or directly on github.