Services in BowPHP
Introductionβ
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:
namespace App\Services;
class UserService
{
//
}
Practical exampleβ
Let's create a complete service for managing users:
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β
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:
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:
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;
}
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).
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():
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:
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β
- 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.