Skip to content

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:

  1. Fully qualified class names (for auto‑resolved, constructor‑injected services).
  2. Callables (for inline factories or complex instantiation logic).
php
<?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:

  1. Registers core (default) services provided by the framework (e.g., router, request, database).

  2. Merges in your custom definitions from configs/services.php.

  3. 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.
  4. 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:

php
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:

php
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:

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:

php
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:

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:

    php
    return [
        'database' => \App\Services\CustomDatabaseService::class,
    ];
  • Disable a service (if supported) by setting its value to null:

    php
    return [
        '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:

php
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

  1. Separation of Concerns: Services should each focus on a single responsibility.
  2. Constructor Injection: Enables clear, testable contracts.
  3. Interface Contracts: Bind to interfaces for swappable implementations.
  4. Container Behavior: The container resolves and caches service instances automatically—focus on defining contracts, not implementation details.
  5. Testing: Mock dependencies in unit tests; integration-test container registrations.
  6. 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.