Skip to content

Adding an Integration ​

This guide covers connecting Logidav to a new external service. It follows the established pattern used across all integrations: an API client for HTTP communication, a service for business logic, and commands for scheduled operations.

Pattern

The DPD integration is the canonical example to study:

  • src/CoreBundle/Api/DpdApi.php -- API client
  • src/CoreBundle/Services/DpdService.php -- business logic
  • src/AppBundle/Controller/DpdController.php -- HTTP endpoints

Follow this same layered structure for any new integration.

1. Create the API Client ​

API clients live in src/CoreBundle/Api/ and handle all HTTP communication with the external service.

php
<?php
// src/CoreBundle/Api/AcmeApi.php

namespace CoreBundle\Api;

class AcmeApi
{
    private $baseUrl;
    private $apiKey;
    private $logger;

    public function __construct(array $config, $logger)
    {
        $this->baseUrl = $config['base_url'];
        $this->apiKey = $config['api_key'];
        $this->logger = $logger;
    }

    /**
     * Fetch products from the Acme API.
     *
     * @param array $filters
     * @return array
     * @throws \RuntimeException on API failure
     */
    public function getProducts(array $filters = [])
    {
        $response = $this->request('GET', '/products', ['query' => $filters]);

        return json_decode($response, true);
    }

    /**
     * Send an order to the Acme API.
     *
     * @param array $orderData
     * @return array
     */
    public function createOrder(array $orderData)
    {
        return $this->request('POST', '/orders', ['json' => $orderData]);
    }

    private function request($method, $endpoint, array $options = [])
    {
        $url = $this->baseUrl . $endpoint;
        $options['headers']['Authorization'] = 'Bearer ' . $this->apiKey;

        $this->logger->info('Acme API request', [
            'method' => $method,
            'endpoint' => $endpoint,
        ]);

        // HTTP request logic here (cURL or Guzzle)
        // Return response body or throw on failure
    }
}

Naming convention: {Service}Api.php -- e.g., DpdApi.php, ColissimoApi.php, BigbuyApi.php.

2. Create the Service ​

Services live in src/CoreBundle/Services/ (or the relevant bundle) and contain all business logic: data transformation, persistence, and error handling.

php
<?php
// src/CoreBundle/Services/AcmeService.php

namespace CoreBundle\Services;

use CoreBundle\Api\AcmeApi;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;

class AcmeService
{
    private $api;
    private $em;
    private $logger;

    public function __construct(AcmeApi $api, EntityManagerInterface $em, LoggerInterface $logger)
    {
        $this->api = $api;
        $this->em = $em;
        $this->logger = $logger;
    }

    /**
     * Sync products from Acme into local catalog.
     */
    public function syncProducts()
    {
        $products = $this->api->getProducts();
        $processed = 0;
        $errors = 0;

        foreach ($products as $productData) {
            try {
                $this->processProduct($productData);
                $processed++;
            } catch (\Exception $e) {
                $this->logger->error('Failed to process Acme product', [
                    'product_id' => $productData['id'] ?? 'unknown',
                    'error' => $e->getMessage(),
                ]);
                $errors++;
            }
        }

        $this->em->flush();

        return ['processed' => $processed, 'errors' => $errors];
    }

    private function processProduct(array $data)
    {
        // Transform external data to internal entity
        // Persist via EntityManager
    }
}

Naming convention: {Service}Service.php -- e.g., DpdService.php, ColissimoService.php.

3. Add Configuration ​

Add the new service credentials to app/config/parameters.yml.dist so other developers know what is required:

yaml
parameters:
    # Acme Integration
    acme_api_base_url: 'https://api.acme.example.com/v1'
    acme_api_key: 'your-api-key-here'
    acme_api_timeout: 30

Then add the actual values to app/config/parameters.yml (not committed to git).

DANGER

Never commit real API credentials to the repository. Only parameters.yml.dist (with placeholder values) is tracked in version control.

4. Register Services ​

Wire everything up in src/CoreBundle/Resources/config/services.yml:

yaml
services:
    core.api.acme:
        class: CoreBundle\Api\AcmeApi
        arguments:
            - base_url: '%acme_api_base_url%'
              api_key: '%acme_api_key%'
              timeout: '%acme_api_timeout%'
            - '@logger'

    core.service.acme:
        class: CoreBundle\Services\AcmeService
        arguments:
            - '@core.api.acme'
            - '@doctrine.orm.entity_manager'
            - '@logger'

5. Create Commands (If Needed) ​

If the integration requires scheduled sync operations, create commands following the Adding a Cronjob guide:

php
<?php
// src/AppBundle/Command/Product/SyncAcmeProductsCommand.php

namespace AppBundle\Command\Product;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Command\LockableTrait;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class SyncAcmeProductsCommand extends ContainerAwareCommand
{
    use LockableTrait;

    protected function configure()
    {
        $this
            ->setName('app:acme:sync-products')
            ->setDescription('Sync products from Acme catalog');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        if (!$this->lock()) {
            $output->writeln('Command is already running.');
            return 0;
        }

        try {
            $service = $this->getContainer()->get('core.service.acme');
            $result = $service->syncProducts();
            $output->writeln(sprintf('Done: %d processed, %d errors',
                $result['processed'], $result['errors']));
        } finally {
            $this->release();
        }

        return 0;
    }
}

6. Add Integration Documentation ​

Create a documentation page under content/projects/logidav/integrations/ describing the new integration, its purpose, API reference, and configuration. See the existing integration docs for the expected format.

Integration Architecture ​

View diagram source
graph LR
    A[Command / Controller] --> B[Service Layer]
    B --> C[API Client]
    C --> D[External API]
    B --> E[Entity Manager]
    E --> F[(Database)]
    B --> G[Logger]

Diagram

Checklist ​

  • [ ] API client created in src/CoreBundle/Api/
  • [ ] Service created with business logic
  • [ ] Parameters added to parameters.yml.dist
  • [ ] Services registered in services.yml
  • [ ] Commands created for scheduled operations (if needed)
  • [ ] Integration documented under content/projects/logidav/integrations/
  • [ ] Tests written for service logic (mock the API client)

See Also ​

Internal developer documentation for Menzzo systems.