smorken/service
A lightweight base service class for Laravel that gives you a clean place to put business logic outside controllers and models. Extend the provided Service, add your methods, and keep controllers thin while sharing reusable, testable operations across your app.
1. Verify it's installable. With zero stars and no dependents, first confirm the package is actually reachable. If it isn't on Packagist, add the Bitbucket repo as a VCS source in your root composer.json:
{
"repositories": [
{ "type": "vcs", "url": "https://bitbucket.org/scott_morken/laravel-service-package.git" }
],
"require": {
"smorken/service": "dev-master"
}
}
Bitbucket repos are usually private — you'll need an app password / SSH key locally, and COMPOSER_AUTH (or an SSH agent) in CI.
2. Read these four things before writing any code, in this order:
| File | Why |
|---|---|
composer.json |
PSR-4 namespace, extra.laravel.providers (auto-discovery), PHP/Laravel constraints |
src/*ServiceProvider.php |
What gets bound, singletons vs. bindings, config publishing |
The base service class (usually src/Service.php) |
The actual contract you're extending — constructor signature matters most |
config/*.php |
Any class-map / driver / option the package resolves from config |
The whole package is small; spend ten minutes reading src/ end to end rather than guessing from the README.
3. Register (if not auto-discovered). Older Laravel or a missing extra.laravel block means manual registration in config/app.php:
'providers' => [
Smorken\Service\ServiceProvider::class,
],
4. Publish config if the provider exposes it:
php artisan vendor:publish --provider="Smorken\Service\ServiceProvider"
5. First use case — extract one fat controller method. Create a service that extends the package base class and owns a single business operation:
namespace App\Services;
use Smorken\Service\Service;
class RegisterUser extends Service
{
public function __construct(
protected UserRepository $users,
protected Mailer $mailer,
) {}
public function handle(RegisterData $data): User
{
$user = $this->users->create($data->toArray());
$this->mailer->sendWelcome($user);
return $user;
}
}
Then inject it into the controller and keep the controller to validation + delegation. That's the smallest end-to-end slice that proves the package works in your app.
Thin controller, service owns the transaction. The day-to-day win is moving orchestration out of controllers and Eloquent models. Controllers validate and return; services coordinate repositories, events, mail, and jobs.
public function store(RegisterRequest $request, RegisterUser $service)
{
$user = $service->handle(RegisterData::fromRequest($request));
return new UserResource($user);
}
Bind interfaces, not concretes. If the base class is designed to be resolved from the container, bind an interface to your implementation in a dedicated App\Providers\ServiceProvider and type-hint the interface everywhere. This is what makes the pattern testable and swappable:
$this->app->bind(RegisterUserContract::class, RegisterUser::class);
Compose services by constructor injection, never app(). A checkout service that calls app(ChargeCard::class) mid-method is untestable and hides dependencies. Inject child services; the container resolves the graph for you.
Group services by domain, not by layer. App\Services\Billing\ChargeCard ages far better than App\Services\ChargeCardService. If the package offers a make:service-style command, point its namespace at the domain folder.
Use it inside queued jobs and commands too. Services are the natural unit for handle() in a job — the job becomes a serialization wrapper, the service holds the logic, and both HTTP and CLI paths share one implementation.
Testing pattern — swap the binding, assert the delegation. For controller tests, bind a fake so you're testing routing/validation, not business logic:
$this->mock(RegisterUserContract::class)
->shouldReceive('handle')->once()->andReturn($user);
Then unit-test the service directly with real collaborators and RefreshDatabase.
Config-driven resolution. If the package resolves class names from config (a common "driver" pattern), keep your own map in config/services.php and reference it — it gives you a single place to swap implementations per environment without touching code.
Modular monolith integration. Register each module's service bindings in a module-level provider registered after the package provider, so your overrides win.
"dev-master#abc1234") or fork it into your VCS. Budget an hour to read the source before it goes anywhere near production.COMPOSER_AUTH with bitbucket-oauth or use an SSH URL, and mirror that in CI.singleton, that instance survives for the whole request — and across jobs in a queue worker, and across requests under Octane. Any mutable property on a service becomes a cross-request bug. Prefer bind, and resolve fresh inside handle() if the worker is long-lived.register() or in a model's constructor — the container and config may not be ready, and you'll fight "target class does not exist" errors during config:cache.config:cache freezes your class-map config. Changing an implementation map in .env does nothing until php artisan config:clear. This bites everyone once.composer require succeeds but classes aren't found, check the package's composer.json PSR-4 prefix against the actual folder. Fix with a fork + composer dump-autoload, not by hand-editing vendor/.illuminate/* constraint against your framework version; a package written for Laravel 8 will often still install on 10 and then break on a changed container or provider signature.app()->bound(Foo::class), app()->getBindings() dumped in tinker, and php artisan about will tell you whether the provider loaded at all. Add Log::info() in the provider's register() as a first smoke test.vendor/. Override by re-binding in a provider registered later, or publish the config and override values. If neither works, fork — with a package this small, a fork you control is cheaper than a fragile override.How can I help you explore Laravel packages today?