This tutorial will guide you through the Services System, a powerful enterprise-grade service management layer with automatic dependency injection, lifecycle management, and type safety for the Claude PHP Agent framework.
By the end of this tutorial, you'll be able to:
- Use ServiceManager for centralized service management
- Register and use built-in services (Cache, Settings)
- Create custom services with dependency injection
- Build service factories
- Manage service lifecycle (initialize, teardown)
- Test services with mocking
- PHP 8.1 or higher
- Composer
- Basic understanding of dependency injection
- Familiarity with design patterns (Factory, Singleton)
- Understanding the Services System
- Setup and Installation
- Tutorial 1: ServiceManager Basics
- Tutorial 2: Using Built-in Services
- Tutorial 3: Service Dependencies
- Tutorial 4: Custom Services
- Tutorial 5: Service Factories
- Tutorial 6: Lifecycle Management
- Tutorial 7: Testing with Services
- Common Patterns
- Troubleshooting
- Next Steps
The Services System provides:
- Centralized Management - Single ServiceManager for all services
- Automatic DI - Services resolve dependencies automatically
- Type Safety - ServiceType enum prevents errors
- Lazy Loading - Services created only when needed
- Lifecycle Control - Proper initialization and cleanup
- Testability - Easy mocking for unit tests
┌─────────────────────────┐
│ ServiceManager │ ← Singleton
│ (Central Registry) │
└────────┬────────────────┘
│
├─→ Cache Service
├─→ Settings Service
├─→ Logging Service
└─→ Custom Services
│
├─→ Auto-resolved dependencies
└─→ Lifecycle management
- ServiceManager - Central registry and service orchestrator
- ServiceInterface - Contract all services implement
- ServiceFactory - Base class for service factories
- ServiceType - Enum for type-safe service access
The Services System is included in claude-php/agent v0.7.0+.
<?php
require_once 'vendor/autoload.php';
use ClaudeAgents\Services\ServiceManager;
use ClaudeAgents\Services\ServiceType;
// Get singleton instance
$manager = ServiceManager::getInstance();
// Verify setup
if ($manager instanceof ServiceManager) {
echo "✓ ServiceManager ready\n";
}Learn the fundamentals of ServiceManager.
<?php
require_once 'vendor/autoload.php';
use ClaudeAgents\Services\ServiceManager;
use ClaudeAgents\Services\Settings\SettingsServiceFactory;
use ClaudeAgents\Services\Cache\CacheServiceFactory;
// Get singleton instance
$serviceManager = ServiceManager::getInstance();
// Register service factories
$serviceManager
->registerFactory(new SettingsServiceFactory())
->registerFactory(new CacheServiceFactory());
echo "✓ Factories registered\n";use ClaudeAgents\Services\ServiceType;
// Get cache service (created on first access)
$cache = $serviceManager->get(ServiceType::CACHE);
echo "✓ Cache service retrieved\n";
echo " Type: " . $cache->getName() . "\n";
echo " Ready: " . ($cache->isReady() ? 'Yes' : 'No') . "\n";// Check if service is registered
if ($serviceManager->has(ServiceType::CACHE)) {
echo "✓ Cache service is available\n";
}
// List all registered services
$registered = $serviceManager->getRegisteredTypes();
echo "Registered services: " . implode(', ', array_map(
fn($type) => $type->value,
$registered
)) . "\n";Work with Cache and Settings services.
<?php
require_once 'vendor/autoload.php';
use ClaudeAgents\Services\ServiceManager;
use ClaudeAgents\Services\ServiceType;
use ClaudeAgents\Services\Cache\CacheServiceFactory;
$manager = ServiceManager::getInstance();
$manager->registerFactory(new CacheServiceFactory());
// Get cache service
$cache = $manager->get(ServiceType::CACHE);
// Store data
$cache->set('user:123', ['name' => 'John Doe', 'email' => 'john@example.com']);
$cache->set('config:api', 'https://api.example.com');
// Retrieve data
$user = $cache->get('user:123');
echo "User: " . $user['name'] . "\n";
// Check existence
if ($cache->has('config:api')) {
echo "✓ API config exists\n";
}
// Delete
$cache->delete('config:api');
// Clear all
// $cache->clear();use ClaudeAgents\Services\Settings\SettingsServiceFactory;
$manager->registerFactory(new SettingsServiceFactory());
// Get settings service
$settings = $manager->get(ServiceType::SETTINGS);
// Set values
$settings->set('app.name', 'My AI App');
$settings->set('app.debug', true);
$settings->set('api.timeout', 30);
// Get values
$appName = $settings->get('app.name');
$timeout = $settings->get('api.timeout', 60); // Default: 60
echo "App: $appName\n";
echo "Timeout: {$timeout}s\n";
// Check existence
if ($settings->has('app.debug')) {
echo "✓ Debug setting configured\n";
}
// Get all settings
$allSettings = $settings->all();
print_r($allSettings);// Set nested configuration
$settings->set('database', [
'host' => 'localhost',
'port' => 3306,
'database' => 'myapp',
'username' => 'root',
]);
// Get nested value using dot notation
$dbHost = $settings->get('database.host');
echo "DB Host: $dbHost\n";
// Update nested value
$settings->set('database.port', 5432);Services can depend on other services.
// Some services depend on others
// Example: Logging might depend on Settings
use ClaudeAgents\Services\ServiceInterface;
class LoggingService implements ServiceInterface
{
private SettingsService $settings;
// Dependency injected via factory
public function __construct(SettingsService $settings)
{
$this->settings = $settings;
}
public function log(string $message): void
{
$level = $this->settings->get('logging.level', 'info');
$format = $this->settings->get('logging.format', 'json');
// Log with configured settings
echo "[$level] $message\n";
}
}use ClaudeAgents\Services\ServiceFactory;
class LoggingServiceFactory extends ServiceFactory
{
protected ServiceType $serviceType = ServiceType::LOGGING;
protected string $serviceClass = LoggingService::class;
public function create(array $dependencies = []): ServiceInterface
{
// Get required dependency
$settings = $dependencies[ServiceType::SETTINGS->value]
?? throw new \RuntimeException('Settings service required');
return new LoggingService($settings);
}
public function getDependencies(): array
{
return [ServiceType::SETTINGS];
}
}// ServiceManager automatically resolves dependencies!
$manager = ServiceManager::getInstance();
$manager
->registerFactory(new SettingsServiceFactory())
->registerFactory(new LoggingServiceFactory());
// When you get LoggingService, Settings is auto-created
$logging = $manager->get(ServiceType::LOGGING);
// Settings was automatically created and injected
$logging->log('Application started');Create your own service.
<?php
use ClaudeAgents\Services\ServiceInterface;
class EmailService implements ServiceInterface
{
private bool $initialized = false;
private array $config;
public function __construct(array $config = [])
{
$this->config = array_merge([
'smtp_host' => 'localhost',
'smtp_port' => 25,
'from' => 'noreply@example.com',
], $config);
}
public function getName(): string
{
return 'email';
}
public function initialize(): void
{
// Setup SMTP connection, verify config, etc.
if (empty($this->config['smtp_host'])) {
throw new \RuntimeException('SMTP host required');
}
$this->initialized = true;
}
public function teardown(): void
{
// Close connections, cleanup resources
$this->initialized = false;
}
public function isReady(): bool
{
return $this->initialized;
}
public function getSchema(): array
{
return [
'name' => 'email',
'type' => 'notification',
'config_schema' => [
'smtp_host' => 'string',
'smtp_port' => 'integer',
'from' => 'string',
],
];
}
// Custom methods
public function send(string $to, string $subject, string $body): bool
{
if (!$this->isReady()) {
throw new \RuntimeException('Service not initialized');
}
// Send email logic
error_log("Sending email to $to: $subject");
return true;
}
}// In your application, extend ServiceType or create custom registry
// For this example, we'll use a string identifier
class CustomServiceType
{
public const EMAIL = 'email';
public const SMS = 'sms';
public const NOTIFICATION = 'notification';
}// Register the service
$manager->register(
CustomServiceType::EMAIL,
new EmailService([
'smtp_host' => 'smtp.gmail.com',
'smtp_port' => 587,
'from' => 'app@example.com',
])
);
// Initialize
$manager->initialize(CustomServiceType::EMAIL);
// Use the service
$email = $manager->get(CustomServiceType::EMAIL);
$email->send('user@example.com', 'Welcome', 'Thanks for signing up!');Build custom factories for your services.
<?php
use ClaudeAgents\Services\ServiceFactory;
use ClaudeAgents\Services\ServiceInterface;
class EmailServiceFactory extends ServiceFactory
{
protected ServiceType $serviceType = ServiceType::EMAIL;
protected string $serviceClass = EmailService::class;
public function create(array $dependencies = []): ServiceInterface
{
// Get settings dependency if available
$settings = $dependencies[ServiceType::SETTINGS->value] ?? null;
// Load config from settings or use defaults
$config = [];
if ($settings !== null) {
$config = [
'smtp_host' => $settings->get('email.smtp_host', 'localhost'),
'smtp_port' => $settings->get('email.smtp_port', 25),
'from' => $settings->get('email.from', 'noreply@example.com'),
];
}
return new EmailService($config);
}
public function getDependencies(): array
{
return [ServiceType::SETTINGS]; // Optional dependency
}
public function getSchema(): array
{
return [
'service_type' => $this->serviceType->value,
'class' => $this->serviceClass,
'dependencies' => ['settings'],
'config_keys' => [
'email.smtp_host',
'email.smtp_port',
'email.from',
],
];
}
}$manager = ServiceManager::getInstance();
// Register factory
$manager->registerFactory(new EmailServiceFactory());
// Service is created on-demand
$email = $manager->get(ServiceType::EMAIL);class NotificationServiceFactory extends ServiceFactory
{
protected ServiceType $serviceType = ServiceType::NOTIFICATION;
protected string $serviceClass = NotificationService::class;
public function create(array $dependencies = []): ServiceInterface
{
// Get multiple dependencies
$email = $dependencies[ServiceType::EMAIL->value] ?? null;
$sms = $dependencies[CustomServiceType::SMS] ?? null;
$cache = $dependencies[ServiceType::CACHE->value] ?? null;
return new NotificationService(
emailService: $email,
smsService: $sms,
cache: $cache
);
}
public function getDependencies(): array
{
return [
ServiceType::EMAIL,
CustomServiceType::SMS,
ServiceType::CACHE,
];
}
}Manage service initialization and cleanup.
<?php
require_once 'vendor/autoload.php';
use ClaudeAgents\Services\ServiceManager;
use ClaudeAgents\Services\ServiceType;
$manager = ServiceManager::getInstance();
// Register services
$manager->registerFactory(new CacheServiceFactory());
// Service is lazy-loaded (not initialized until accessed)
$cache = $manager->get(ServiceType::CACHE);
// Check if ready
if (!$cache->isReady()) {
// Initialize manually if needed
$manager->initialize(ServiceType::CACHE);
}
echo "Cache ready: " . ($cache->isReady() ? 'Yes' : 'No') . "\n";// Initialize all registered services at once
$manager->initializeAll();
echo "All services initialized\n";
// List initialized services
$services = $manager->getInitializedServices();
foreach ($services as $type => $service) {
echo "- $type: " . $service->getName() . "\n";
}// Cleanup at application end
register_shutdown_function(function () use ($manager) {
echo "Shutting down services...\n";
$manager->teardownAll();
});
// Or manually
try {
// Your application code
$cache->set('key', 'value');
} finally {
// Always cleanup
$manager->teardownAll();
}Mock services for testing.
<?php
use PHPUnit\Framework\TestCase;
use ClaudeAgents\Services\ServiceManager;
use ClaudeAgents\Services\ServiceType;
class MyFeatureTest extends TestCase
{
private ServiceManager $manager;
protected function setUp(): void
{
$this->manager = ServiceManager::getInstance();
// Mock the cache service
$mockCache = $this->createMock(CacheService::class);
$mockCache->method('get')->willReturn('mocked_value');
$mockCache->method('set')->willReturn(true);
$this->manager->mock(ServiceType::CACHE, $mockCache);
}
public function test_feature_uses_cache(): void
{
$cache = $this->manager->get(ServiceType::CACHE);
// This uses the mock
$value = $cache->get('key');
$this->assertSame('mocked_value', $value);
}
protected function tearDown(): void
{
// Clear mocks
$this->manager->clearMocks();
}
}public function test_service_initializes_correctly(): void
{
$service = new EmailService([
'smtp_host' => 'test.smtp.com',
'smtp_port' => 587,
]);
$this->assertFalse($service->isReady());
$service->initialize();
$this->assertTrue($service->isReady());
}public function test_service_resolves_dependencies(): void
{
$manager = ServiceManager::getInstance();
$manager
->registerFactory(new SettingsServiceFactory())
->registerFactory(new EmailServiceFactory());
// Getting EmailService should auto-create Settings
$email = $manager->get(ServiceType::EMAIL);
$this->assertTrue($manager->has(ServiceType::SETTINGS));
$this->assertInstanceOf(EmailService::class, $email);
}class Application
{
private ServiceManager $services;
public function __construct()
{
$this->services = ServiceManager::getInstance();
$this->registerServices();
}
private function registerServices(): void
{
$this->services
->registerFactory(new CacheServiceFactory())
->registerFactory(new SettingsServiceFactory())
->registerFactory(new LoggingServiceFactory());
}
public function getCache(): CacheService
{
return $this->services->get(ServiceType::CACHE);
}
public function getSettings(): SettingsService
{
return $this->services->get(ServiceType::SETTINGS);
}
}class ServiceBootstrapper
{
public static function bootstrap(array $config): void
{
$manager = ServiceManager::getInstance();
// Register cache
if ($config['cache']['enabled'] ?? false) {
$manager->registerFactory(new CacheServiceFactory());
// Configure cache
$cache = $manager->get(ServiceType::CACHE);
// Apply config...
}
// Register settings
$manager->registerFactory(new SettingsServiceFactory());
$settings = $manager->get(ServiceType::SETTINGS);
// Load settings from config
foreach ($config['settings'] ?? [] as $key => $value) {
$settings->set($key, $value);
}
}
}
// Use it
ServiceBootstrapper::bootstrap([
'cache' => ['enabled' => true],
'settings' => [
'app.name' => 'My App',
'app.env' => 'production',
],
]);class Cache
{
private static ?CacheService $instance = null;
public static function instance(): CacheService
{
if (self::$instance === null) {
$manager = ServiceManager::getInstance();
self::$instance = $manager->get(ServiceType::CACHE);
}
return self::$instance;
}
public static function get(string $key, mixed $default = null): mixed
{
return self::instance()->get($key, $default);
}
public static function set(string $key, mixed $value, ?int $ttl = null): bool
{
return self::instance()->set($key, $value, $ttl);
}
}
// Use anywhere in your app
Cache::set('user:123', $user);
$user = Cache::get('user:123');Cause: Factory not registered for the service type.
Solution:
// Check if registered
if (!$manager->has(ServiceType::CACHE)) {
// Register the factory
$manager->registerFactory(new CacheServiceFactory());
}
// Then get the service
$cache = $manager->get(ServiceType::CACHE);Cause: Service A depends on B, which depends on A.
Solution:
// Break the circle by using setter injection instead of constructor injection
class ServiceA implements ServiceInterface
{
private ?ServiceB $serviceB = null;
public function setServiceB(ServiceB $b): void
{
$this->serviceB = $b;
}
}
// Or redesign to remove circular dependencyCause: Trying to register a factory for a type that's already registered.
Solution:
// Check first
if (!$manager->has(ServiceType::CACHE)) {
$manager->registerFactory(new CacheServiceFactory());
}
// Or use replace mode (if available)
$manager->registerFactory(new CacheServiceFactory(), replace: true);- Production Patterns Tutorial - Deploy services in production
- Testing Strategies Tutorial - Test service-based applications
- MCP Server Tutorial - Expose services via MCP
All examples from this tutorial are available in:
examples/tutorials/services-system/examples/Services/
✓ Use ServiceManager singleton ✓ Register and use services ✓ Handle service dependencies ✓ Create custom services ✓ Build service factories ✓ Manage service lifecycle ✓ Test with service mocking ✓ Implement common service patterns
Ready for more? Continue with the Production Patterns Tutorial to learn deployment strategies!
Tutorial Version: 1.0 Framework Version: v0.7.0+ Last Updated: February 2026