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 clientsrc/CoreBundle/Services/DpdService.php-- business logicsrc/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).
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
- Architecture: Bundle Responsibilities for service layer conventions
- Adding a Cronjob for scheduled command setup
- Adding a Queue Processor if the integration uses async processing
- Integrations reference for existing integration documentation