Services
Services are the core building blocks of your application’s business logic and external integrations. They encapsulate reusable functionality managed by the framework’s service container. The framework only loads the configs/services.php
file if it exists; otherwise it uses its default service registrations.
IMPORTANT
Be cautious when modifying service configurations: incorrect bindings or misconfigured factories can lead to runtime errors, unmet dependencies, or unexpected behavior across your application.
Creating services.php
Inside your configs
directory, create a file named services.php
. This file should return an associative array mapping service identifiers (aliases) to either:
- Fully qualified class names (for auto‑resolved, constructor‑injected services).
- Callables (for inline factories or complex instantiation logic).
<?php
return [
// Example: simple mailer service
'mailer' => MyProject\Services\MailService::class,
// Example: simple cache service
'cache' => MyProject\Services\CacheService::class,
// Example: inline factory for a custom logger
'custom_logger' => function ($container) {
$config = $container['config']['logger'];
return new \Monolog\Logger($config['name']);
},
// Example: payment gateway with constructor arguments
'payment' => function ($container) {
$settings = $container['config']['payment'];
return new MyProject\Services\StripeGateway(
$settings['api_key'],
$settings['webhook_secret']
);
},
];
- Key: the alias used when retrieving the service (
$container['mailer']
). - Value: a class name or a callable factory receiving the container.
- Only custom or overridden services need to be listed; the framework merges your array with its defaults.
How the Framework Container Works
When the application boots, the framework container:
Registers core (default) services provided by the framework (e.g., router, request, database).
Merges in your custom definitions from
configs/services.php
.Resolves dependencies on demand:
- Strings (class names) are treated as services instantiated when first requested.
- Callables (factories) are invoked when the corresponding service is requested and their return value is stored for reuse.
Provides each service instance to your application code whenever you access
$container['service_key']
.
Important: If you define a service with the same key as a default, your definition overrides the framework’s. If you add a new key, it is made available in addition to the defaults.
Writing Your Own Service Classes
Create standalone PHP classes—commonly under src/Services
or app/Services
—that encapsulate a single responsibility. To enable constructor injection (with an autowiring extension), type‑hint dependencies:
namespace MyProject\Services;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
class UserService
{
private CacheInterface $cache;
private LoggerInterface $logger;
public function __construct(CacheInterface $cache, LoggerInterface $logger)
{
$this->cache = $cache;
$this->logger = $logger;
}
public function findUser(int $id): User
{
$key = "user:{$id}";
if ($this->cache->has($key)) {
return $this->cache->get($key);
}
$user = UserRepository::find($id);
$this->cache->set($key, $user, 3600);
$this->logger->info("Loaded user {$id}");
return $user;
}
}
1. Simple Service Example
A trivial configuration service providing app metadata:
namespace MyProject\Services;
class AppInfoService
{
public function getName(): string
{
return 'MyProject App';
}
public function getVersion(): string
{
return '1.0.0';
}
}
Register it in configs/services.php
:
return [
'app_info' => MyProject\Services\AppInfoService::class,
];
2. Advanced Service Example
A more complex service integrating a third‑party API with retry logic and logging:
namespace MyProject\Services;
use GuzzleHttp\Client;
use Psr\Log\LoggerInterface;
class ExternalApiService
{
private Client $http;
private LoggerInterface $logger;
private int $maxRetries;
public function __construct(Client $http, LoggerInterface $logger, int $maxRetries = 3)
{
$this->http = $http;
$this->logger = $logger;
$this->maxRetries = $maxRetries;
}
public function fetchData(string $endpoint): array
{
$attempt = 0;
retry:
try {
$response = $this->http->get($endpoint);
return json_decode((string)$response->getBody(), true);
} catch (\Exception $e) {
if (++$attempt <= $this->maxRetries) {
$this->logger->warning("Retry {$attempt} for {$endpoint}");
goto retry;
}
$this->logger->error("Failed to fetch data: " . $e->getMessage());
throw $e;
}
}
}
And register in configs/services.php
:
return [
'external_api' => function ($c) {
$config = $c['config']['api'];
$client = new \GuzzleHttp\Client(['base_uri' => $config['base_uri']]);
return new MyProject\Services\ExternalApiService(
$client,
$c['logger'],
$config['max_retries'] ?? 3
);
},
];
Overriding & Disabling Services
Override a default service by using the same key:
phpreturn [ 'database' => \App\Services\CustomDatabaseService::class, ];
Disable a service (if supported) by setting its value to
null
:phpreturn [ 'legacy_logger' => null, ];
Disabling core services can break dependencies. Only disable when you understand the impact.
Using Environment Variables
Service configuration files can reference environment variables directly:
return [
'mailer' => getenv('USE_SENDGRID') === 'true'
? \App\Services\SendGridMailer::class
: \App\Services\SmtpMailer::class,
];
Ensure your environment is loaded before the container instantiates services.
Tips & Best Practices
- Separation of Concerns: Services should each focus on a single responsibility.
- Constructor Injection: Enables clear, testable contracts.
- Interface Contracts: Bind to interfaces for swappable implementations.
- Container Behavior: The container resolves and caches service instances automatically—focus on defining contracts, not implementation details.
- Testing: Mock dependencies in unit tests; integration-test container registrations.
- Performance: Avoid heavy work in service factories—leverage lazy loading and cached instances.
By following this structure and relying on the framework container’s high‑level behavior, you’ll maintain flexibility to swap or remove the underlying implementation without rewriting your documentation.