Symfony Services

Symfony Services

Post Date : 2024-04-26T23:39:23+07:00

Modified Date : 2024-04-26T23:39:23+07:00

Category: symfony-tutorial

Tags: symfony

References

Understanding Services and Service Container in Symfony

Services in Symfony are central to the framework’s architecture and revolve around the service container, which is responsible for managing service objects. Services are PHP objects that perform specific tasks, such as sending emails, handling database interactions, or processing form data.

Service Container

Services help encapsulate reusable business logic. The service container in Symfony standardizes and centralizes the way objects are constructed, which helps manage dependencies effectively through a technique called dependency injection.

Registering a Service in Symfony

Services can be registered in Symfony in two main ways:

  1. Service Configuration: Services are defined in configuration files, typically config/services.yaml in modern Symfony applications.
  2. Autowiring: Symfony can automatically manage service creation based on the type hints in your constructor.

Here is how you might define a service in services.yaml:

services:
  App\Service\MessageGenerator:
    arguments: ["%app.special_key%"]

And the corresponding PHP class:

namespace App\Service;

class MessageGenerator
{
  private $key;

  public function __construct(string $key)
  {
    $this->key = $key;
  }

  public function generate()
  {
    return "Secret key: " . $this->key;
  }
}

Using Services

You can access services in controllers by type-hinting dependencies in the controller method or constructor. For instance:

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Service\MessageGenerator;

class MyController extends AbstractController
{
  public function index(MessageGenerator $generator)
  {
    $message = $generator->generate();

    return $this->render('index.html.twig', ['message' => $message]);
  }
}

Real-World Use Cases

1. User Management Service:

A service to handle user registration, authentication, and profile updates.

namespace App\Service;

use App\Entity\User;

class UserManager
{
  private $entityManager;

  public function __construct(EntityManagerInterface $entityManager)
  {
    $this->entityManager = $entityManager;
  }

  public function createUser($userData)
  {
    $user = new User();
    $user->setUsername($userData['username']);
    $user->setEmail($userData['email']);
    // More setters...

    $this->entityManager->persist($user);
    $this->entityManager->flush();

    return $user;
  }
}

2. Email Notification Service:

A service for sending emails, which could be used across different controllers or other services.

namespace App\Service;

use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

class EmailService
{
  private $mailer;

  public function __construct(MailerInterface $mailer)
  {
    $this->mailer = $mailer;
  }

  public function sendEmail($to, $subject, $content)
  {
    $email = (new Email())
      .from('hello@example.com')
      .to($to)
      .subject($subject)
      .text($content);

    $this->mailer->send($email);
  }
}

Symfony Lifecycle and Services

The lifecycle of a Symfony application typically involves the following steps, with services playing a crucial role throughout:

  • Request Handling: When a request is received, Symfony initializes the kernel and the service container.
  • Routing: The router decides which controller to execute based on the incoming request.
  • Controller Execution: The controller is instantiated, and its dependencies (services) are injected.
  • Response Generation: The controller uses services to process data and generate a response.
  • Response Return: The response object generated by the controller is sent back to the user.

Services are created and managed by the Symfony service container and are instantiated only when they are needed. If a service is configured as lazy, it won’t be instantiated until it’s actually used, saving resources.

Learn Symfony’s service by examples

Example 01: Fetch data from an external API and save data on your server

Eg: https://jsonplaceholder.typicode.com/posts

Requirements

  • Create a cronjob to fetch all posts data
  • Save posts data into your server: can be a file/a database/a database service

Solution

  • Create a FetchPostsService to fetch data via http request
  • Create a SavePostsService to save json data into a specific file
  • Setup a cronjob to invoke

Steps

  • Install httpClient
  • Register service dependencies
  • Create JsonPlaceholderService
  • Create DataSaverService
composer require symfony/http-client

services.yaml

parameters:
    uploads_dir: '%kernel.project_dir%/public/uploads/'
    json_placeholder_service.endpoints:
        posts: '/posts'
        comments: '/comments'
        albums: '/albums'
        photos: '/photos'
        todos: '/todos'
        users: '/users'
services:
    json_placeholder_http_client:
        class: Symfony\Contracts\HttpClient\HttpClientInterface
        factory: ['Symfony\Component\HttpClient\HttpClient', 'create']
        arguments:
            $defaultOptions:
                base_uri: 'https://jsonplaceholder.typicode.com'
                headers:
                    # Authorization: 'Bearer YOUR_API_TOKEN'
                    Accept: 'application/json'
                timeout: 20
    App\Service\JsonPlaceholderService:
        public: true,
        arguments:
            $httpClient: '@json_placeholder_http_client'
            $endpoints: '%json_placeholder_service.endpoints%'

// Service\EndpointKeys.php

namespace App\Service;

class EndpointKeys
{
    public const POSTS = 'posts';
    public const COMMENTS = 'comments';
    public const ALBUMS = 'albums';
    public const PHOTOS = 'photos';
    public const TODOS = 'todos';
    public const USERS = 'users';
}
// Service\JsonPlaceholderService.php

namespace  App\Service;

use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class JsonPlaceholderService {
    private $httpClient;
    private $endpoints;

    public function __construct(HttpClientInterface $httpClient, array $endpoints)
    {
        $this->httpClient = $httpClient;
        $this->endpoints = $endpoints;
    }

    public function getEndpoint(string $key)
    {
        if (!isset($this->endpoints[$key])) {
            throw new \InvalidArgumentException("No endpoint configured for key: {$key}");
        }

        return $this->endpoints[$key];
    }

    /**
     * @throws \Exception
     */
    public function getPosts(){
        try {
            $endpoint = $this->getEndpoint(EndpointKeys::POSTS);
            $response = $this->httpClient->request('GET', $endpoint);
            foreach ($this->httpClient->stream($response) as $chunk) {
                if ($chunk->isLast()) {
                    // This is the last chunk of the response
                    $content = $response->getContent();
                    return json_decode($content, true);
                }
            }
        } catch (TransportExceptionInterface $e) {
            // This exception is thrown on a network error
            throw new \Exception('Network error occurred.');
        } catch (HttpExceptionInterface  $e) {
            throw new \Exception('Server returned an error response.');
        } catch(DecodingExceptionInterface $e){
            throw new \Exception('Failed to decode response.');
        } catch(\Throwable $e){
            // Catch other exceptions
            throw new \Exception('An unexpected error occurred: ' . $e->getMessage());
        }
    }
}

Let’s write tests:

  • Unit Test with Mock
  • Integration Test

Unit Test

// tests/Service/JsonPlaceholderServiceTest.php

namespace App\Tests\Service;

use App\Service\JsonPlaceholderService;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;

class JsonPlaceholderServiceTest extends TestCase
{
    private $jsonPlaceholderService;

    protected function setUp(): void
    {
        $endpoints = [
            'posts' => 'https://jsonplaceholder.typicode.com/posts'
        ];

        $responses = [
            new MockResponse(json_encode([
                ['id' => 1, 'title' => 'Test Post', 'body' => 'This is a test post', 'userId' => 1]
            ])),
        ];

        $httpClient = new MockHttpClient($responses, $endpoints['posts']);
        $this->jsonPlaceholderService = new JsonPlaceholderService($httpClient, $endpoints);
    }

    /**
     * @throws \Exception
     */
    public function testFetchPosts()
    {
        $posts = $this->jsonPlaceholderService->getPosts();
        $this->assertCount(1, $posts);
        $this->assertEquals('Test Post', $posts[0]['title']);
    }
}

Integration Test

// tests/Integration/Service/JsonPlaceholderServiceIntegrationTest.php

namespace App\Tests\Integration\Service;

use App\Service\JsonPlaceholderService;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class JsonPlaceholderServiceIntegrationTest extends WebTestCase
{
    private $jsonPlaceholderService;

    protected function setUp(): void
    {
        self::bootKernel();
        $container = static::getContainer();
        $this->jsonPlaceholderService = $container->get(JsonPlaceholderService::class);
    }

    public function testFetchPosts()
    {
        $posts = $this->jsonPlaceholderService->getPosts();
        $this->assertIsArray($posts);
        $this->assertNotEmpty($posts);
        // Add more specific assertions here, such as checking the structure of the posts
    }
}

DataSaverService

# services.yaml
params:
  data_saver.directory: "%kernel.project_dir%/data"
services:
  Symfony\Component\Filesystem\Filesystem: ~

  App\Service\DataSaverService:
    arguments:
      $filesystem: '@Symfony\Component\Filesystem\Filesystem'
      $targetDirectory: "%data_saver.directory%"
// service\DataSaverService.php

namespace App\Service;

use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;

class DataSaverService
{
    private $filesystem;
    private $targetDirectory;

    public function __construct(Filesystem $filesystem, string $targetDirectory)
    {
        $this->filesystem = $filesystem;
        $this->targetDirectory = $targetDirectory;
    }

    public function saveDataToFile(array $data, string $filename): void
    {
        $jsonData = json_encode($data);

        try {
            // Ensure the target directory exists
            $this->filesystem->mkdir($this->targetDirectory);

            // Save the file
            $this->filesystem->dumpFile($this->targetDirectory . '/' . $filename, $jsonData);
        } catch (IOExceptionInterface $exception) {
            throw new \Exception("An error occurred while writing to the file at " . $exception->getPath());
        }
    }
}
// tests\service\DataSaverServiceTest.php

namespace App\Tests\Service;

use App\Service\DataSaverService;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Filesystem\Filesystem;

class DataSaverServiceTest extends TestCase
{
    private $targetDirectory;

    protected function setUp(): void
    {
        parent::setUp();
        $this->targetDirectory = sys_get_temp_dir(); // Use system temp directory for testing
        $this->service = new DataSaverService(new Filesystem(), $this->targetDirectory);
    }

    protected function tearDown(): void
    {
        // Cleanup: Remove any files created during tests
        $filesystem = new Filesystem();
        $filesystem->remove($this->targetDirectory . '/test_posts.json');
    }

    public function testSaveDataToFile()
    {
        $data = [
            ['id' => 1, 'title' => 'Test Post', 'content' => 'This is a test post']
        ];
        $filename = 'test_posts.json';
        $this->service->saveDataToFile($data, $filename);

        $expectedPath = $this->targetDirectory . '/' . $filename;
        $this->assertFileExists($expectedPath);
        $content = file_get_contents($expectedPath);
        $this->assertEquals(json_encode($data), $content);
    }

    public function testWriteFailure()
    {
        $filesystemMock = $this->createMock(Filesystem::class);
        $filesystemMock->method('dumpFile')
            ->will($this->throwException(new \Exception("Failed to write file")));

        $service = new DataSaverService($filesystemMock, $this->targetDirectory);
        $this->expectException(\Exception::class);
        $service->saveDataToFile([], 'test_fail.json');
    }

}

Create command and config crontab

// src/Command/FetchJsonPlaceholderPostsCommand.php

namespace App\Command;

use App\Service\JsonPlaceholderService;
use App\Service\DataSaverService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class FetchJsonPlaceholderPostsCommand extends Command
{
    protected static $defaultName = 'app:fetch-json-placeholder-posts';

    private $jsonPlaceholderService;
    private $dataSaverService;

    public function __construct(JsonPlaceholderService $jsonPlaceholderService, DataSaverService $dataSaverService)
    {
        parent::__construct();
        $this->jsonPlaceholderService = $jsonPlaceholderService;
        $this->dataSaverService = $dataSaverService;
    }

    protected function configure()
    {
        $this
            ->setDescription('Fetches data from JsonPlaceholder and saves it locally.')
            ->setHelp('This command allows you to fetch posts from JsonPlaceholder API and save them to a local file');
    }

    /**
     * @throws \Exception
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln('Starting data fetch...');
        $data = $this->jsonPlaceholderService->getPosts(); // Assuming fetchData returns an array of data

        $output->writeln('Saving data...');
        try {
            $this->dataSaverService->saveDataToFile($data, 'posts.json');
        } catch (\Exception $e) {
            throw new \Exception($e->getMessage());
        }

        $output->writeln('Data fetched and saved successfully.');

        return Command::SUCCESS;
    }
}
services:
  # commands
  App\Command\FetchJsonPlaceholderPostsCommand:
    arguments:
      $jsonPlaceholderService: '@App\Service\JsonPlaceholderService'
      $dataSaverService: '@App\Service\DataSaverService'
    tags:
      - { name: "console.command" }
php bin/console list app
php bin/console app:fetch-json-placeholder-posts
php bin/console help app:fetch-json-placeholder-posts # run

Create crontab

crontab -e
0 0 * * * /usr/bin/php /path/to/your/symfony/app/bin/console app:fetch-json-placeholder-posts
You Might Also Like