A comprehensive guide to best practices when using the Service Layer architecture.
- Architecture Principles
- Service Registration
- Dependency Management
- Configuration
- Testing
- Performance
- Security
- Monitoring
- Common Pitfalls
Each service should have one clear purpose:
// ✅ Good - Focused responsibility
class CacheService {
// Only handles caching
}
class StorageService {
// Only handles storage
}
// ❌ Bad - Mixed responsibilities
class DataService {
// Handles both caching AND storage AND database
}Let the ServiceManager inject dependencies:
// ✅ Good - Dependencies injected
class MyService implements ServiceInterface
{
public function __construct(
private SettingsService $settings,
private CacheService $cache
) {}
}
// ❌ Bad - Manual instantiation
class MyService implements ServiceInterface
{
private $settings;
public function __construct()
{
$this->settings = new SettingsService(); // Don't do this!
}
}Depend on interfaces, not implementations:
// ✅ Good - Depends on interface
class MyAgent
{
public function __construct(
private ServiceInterface $cache
) {}
}
// ❌ Bad - Depends on concrete class
class MyAgent
{
public function __construct(
private ArrayCache $cache // Too specific
) {}
}Register all services during application bootstrap:
// bootstrap.php
$manager = ServiceManager::getInstance();
// Register all services at startup
$manager
->registerFactory(new SettingsServiceFactory())
->registerFactory(new CacheServiceFactory())
->registerFactory(new StorageServiceFactory())
->registerFactory(new VariableServiceFactory())
->registerFactory(new TracingServiceFactory())
->registerFactory(new TelemetryServiceFactory())
->registerFactory(new SessionServiceFactory());
return $manager;Don't register services you won't use:
// ✅ Good - Only needed services
$manager
->registerFactory(new CacheServiceFactory())
->registerFactory(new StorageServiceFactory());
// ❌ Bad - Registering everything
$manager->registerFactory(new TracingServiceFactory()); // Not using tracing// config/services.php
return [
'enabled_services' => [
'cache',
'storage',
'telemetry',
],
];
// bootstrap.php
$settings = $manager->get(ServiceType::SETTINGS);
$enabledServices = $settings->get('enabled_services', []);
$serviceFactories = [
'cache' => new CacheServiceFactory(),
'storage' => new StorageServiceFactory(),
'telemetry' => new TelemetryServiceFactory(),
// ...
];
foreach ($enabledServices as $service) {
if (isset($serviceFactories[$service])) {
$manager->registerFactory($serviceFactories[$service]);
}
}// ❌ Bad - Circular dependency
class ServiceA {
public function __construct(ServiceB $b) {}
}
class ServiceB {
public function __construct(ServiceA $a) {} // Creates cycle!
}
// ✅ Good - Break the cycle
class ServiceA {
public function __construct(SettingsService $settings) {}
}
class ServiceB {
public function __construct(SettingsService $settings) {}
}// ✅ Good - Minimal dependencies
class MyService implements ServiceInterface
{
public function __construct(
private SettingsService $settings
) {}
}
// ❌ Bad - Too many dependencies
class MyService implements ServiceInterface
{
public function __construct(
private SettingsService $settings,
private CacheService $cache,
private StorageService $storage,
private VariableService $variables,
private TracingService $tracing,
private TelemetryService $telemetry
) {}
}Use optional parameters for non-critical dependencies:
class MyService implements ServiceInterface
{
public function __construct(
private SettingsService $settings,
private ?TracingService $tracing = null // Optional
) {}
public function doWork(): void
{
if ($this->tracing !== null) {
$this->tracing->startTrace('work', 'operation');
}
// Do work...
if ($this->tracing !== null) {
$this->tracing->endTrace('work');
}
}
}// config/services.dev.php
return [
'cache' => ['driver' => 'array'],
'tracing' => ['enabled' => false],
];
// config/services.prod.php
return [
'cache' => ['driver' => 'redis'],
'tracing' => ['enabled' => true],
];
// bootstrap.php
$env = getenv('APP_ENV') ?: 'dev';
$configFile = __DIR__ . "/config/services.{$env}.php";
$manager->registerFactory(
new SettingsServiceFactory(configFile: $configFile)
);class SettingsValidator
{
public static function validate(array $config): void
{
$required = ['cache.driver', 'storage.directory'];
foreach ($required as $key) {
if (!isset($config[$key])) {
throw new \RuntimeException("Missing required config: {$key}");
}
}
}
}
// Use after loading config
$settings = $manager->get(ServiceType::SETTINGS);
SettingsValidator::validate($settings->all());class SettingsService implements ServiceInterface
{
private array $defaults = [
'cache.driver' => 'array',
'cache.ttl' => 3600,
'storage.directory' => './storage',
'tracing.enabled' => false,
];
public function get(string $key, mixed $default = null): mixed
{
// Check config, then defaults, then parameter
return $this->config[$key]
?? $this->defaults[$key]
?? $default;
}
}class MyFeatureTest extends TestCase
{
private ServiceManager $manager;
protected function setUp(): void
{
$this->manager = ServiceManager::getInstance();
$this->manager->reset();
// Mock cache with in-memory implementation
$settings = new SettingsService();
$settings->initialize();
$mockCache = new CacheService($settings, new ArrayCache());
$mockCache->initialize();
$this->manager->mock(ServiceType::CACHE, $mockCache);
}
public function testFeature(): void
{
$feature = new MyFeature();
// Test uses mocked cache
}
}class CacheServiceTest extends TestCase
{
public function testCacheOperations(): void
{
// Test service in complete isolation
$settings = new SettingsService();
$settings->initialize();
$cache = new CacheService($settings, new ArrayCache());
$cache->initialize();
// Test without ServiceManager
$cache->set('key', 'value');
$this->assertSame('value', $cache->get('key'));
}
}class ServiceIntegrationTest extends TestCase
{
public function testServicesWorkTogether(): void
{
$manager = ServiceManager::getInstance();
$manager->reset();
// Register real factories
$manager
->registerFactory(new SettingsServiceFactory())
->registerFactory(new CacheServiceFactory())
->registerFactory(new StorageServiceFactory());
// Test actual integration
$cache = $manager->get(ServiceType::CACHE);
$storage = $manager->get(ServiceType::STORAGE);
// Both services should work together
$cache->set('key', 'value');
$storage->saveFile('test', 'file.txt', 'data');
$this->assertSame('value', $cache->get('key'));
$this->assertSame('data', $storage->getFile('test', 'file.txt'));
}
}Services are created only when needed:
// ✅ Good - Services created on demand
$manager->registerFactory(new CacheServiceFactory());
// No service created yet
$cache = $manager->get(ServiceType::CACHE);
// Now created and initialized// ✅ Good - Reuse singleton
$cache1 = $manager->get(ServiceType::CACHE);
$cache2 = $manager->get(ServiceType::CACHE);
// $cache1 === $cache2 (same instance)
// ❌ Bad - Creating new instances
$cache = new CacheService($settings, new ArrayCache());
// Don't create services manuallyclass ExpensiveService
{
private CacheService $cache;
public function getExpensiveData(): array
{
return $this->cache->remember('expensive_data', function() {
// Only computed once, then cached
return $this->computeExpensiveData();
}, 3600);
}
}// ✅ Good - Use CREDENTIAL type
$variables->setVariable(
'user-123',
'api_key',
'sk-secret',
VariableType::CREDENTIAL // Encrypted
);
// ❌ Bad - Storing secrets as plain text
$cache->set('api_key', 'sk-secret'); // Not encrypted!// TracingService automatically removes sensitive fields
$tracing->startTrace($id, 'operation', [
'user_id' => '123',
'api_key' => 'sk-xxx', // Automatically removed
'password' => 'secret', // Automatically removed
'data' => 'safe data', // Kept
]);class StorageService
{
private function sanitizePath(string $path): string
{
// Prevent directory traversal
$path = str_replace(['..', '\\'], ['', '/'], $path);
return trim($path, '/');
}
}// ❌ Bad - Hardcoded secrets
return [
'cache' => [
'redis' => [
'password' => 'my-redis-password',
],
],
];
// ✅ Good - From environment
return [
'cache' => [
'redis' => [
'password' => getenv('REDIS_PASSWORD'),
],
],
];class MonitoredAgent
{
private TelemetryService $telemetry;
public function run(string $input): string
{
$startTime = microtime(true);
try {
$result = $this->execute($input);
// Record success
$duration = (microtime(true) - $startTime) * 1000;
$this->telemetry->recordAgentRequest(
success: true,
tokensInput: 100,
tokensOutput: 50,
duration: $duration
);
return $result;
} catch (\Exception $e) {
// Record failure
$this->telemetry->recordCounter('agent.errors');
throw $e;
}
}
}class ServiceManager
{
public function get(ServiceType $type): ServiceInterface
{
$this->logger->debug("Creating service: {$type->value}");
$service = $this->createService($type);
$this->logger->info("Service ready: {$type->value}");
return $service;
}
}class ServiceHealthCheck
{
public function check(): array
{
$manager = ServiceManager::getInstance();
$status = [];
foreach (ServiceType::cases() as $type) {
if ($manager->has($type)) {
$service = $manager->get($type);
$status[$type->value] = $service->isReady();
}
}
return $status;
}
}// ❌ Bad - No cleanup
function runApp() {
$manager = ServiceManager::getInstance();
// ... use services ...
} // Services never cleaned up!
// ✅ Good - Proper cleanup
function runApp() {
$manager = ServiceManager::getInstance();
try {
// ... use services ...
} finally {
$manager->teardownAll();
}
}// ❌ Bad - Manual creation
$cache = new CacheService($settings, new ArrayCache());
// ✅ Good - Via ServiceManager
$cache = ServiceManager::getInstance()->get(ServiceType::CACHE);// ❌ Bad - Assuming service exists
$cache = $manager->get(ServiceType::CACHE); // Throws if not registered
// ✅ Good - Check first
if ($manager->has(ServiceType::CACHE)) {
$cache = $manager->get(ServiceType::CACHE);
} else {
// Handle missing service
}// ❌ Bad - Multiple ServiceManager instances
$manager1 = new ServiceManager(); // Don't do this!
$manager2 = new ServiceManager();
// ✅ Good - Use singleton
$manager = ServiceManager::getInstance();// ❌ Bad - Swallowing errors
try {
$service->initialize();
} catch (\Exception $e) {
// Silent failure
}
// ✅ Good - Log and handle
try {
$service->initialize();
} catch (\Exception $e) {
$logger->error("Service initialization failed", [
'service' => $service->getName(),
'error' => $e->getMessage(),
]);
throw $e;
}Follow these best practices for production-ready services:
✅ Single responsibility per service
✅ Use dependency injection
✅ Register services early
✅ Avoid circular dependencies
✅ Environment-specific configuration
✅ Mock services in tests
✅ Lazy initialization
✅ Encrypt sensitive data
✅ Monitor with telemetry
✅ Always teardown services
Your service layer will be robust, maintainable, and production-ready!