Skip to main 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.

:::info 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
// 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
// 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:

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:

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
// 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

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