Symfony Bundle for Poliris CSV Generation (V4.1.17)
Export real estate data to SeLoger, LeBonCoin and other French property portals
PolirisBundle is a Symfony bundle that simplifies the generation of CSV files in the Poliris format (V4.1.17), the standard used by major French real estate portals (SeLoger, LeBonCoin, etc.).
- ✅ Simplicity: No need to manually manage 333+ CSV columns
- ✅ Type-safe: Uses the Builder pattern with typed classes
- ✅ Performance: Optimized to process thousands of listings
- ✅ Maintainability: Easy evolution thanks to modular architecture
- Export your real estate properties to SeLoger, LeBonCoin
- Automate daily listing updates
- Manage multi-portal feeds with a single codebase
- Easily integrate photos and metadata
- PHP >= 8.2
- Symfony >= 5.4
composer require spiriitlabs/poliris-bundleThe bundle will be automatically registered thanks to Symfony Flex.
The bundle requires no configuration to get started. It works out-of-the-box!
If you wish to customize behavior, create a config/packages/poliris.yaml file:
# config/packages/poliris.yaml (optional)
poliris:
# Future configuration (currently no configuration needed)Let's create our first CSV file with 2 real estate listings.
<?php
namespace App\Service;
use Spiriit\PolirisBundle\Builders\AnnonceExportBuilder;
use Spiriit\PolirisBundle\Models\Annonce\Annonce;
class PolirisExportService
{
public function generateExport(): string
{
$builder = new AnnonceExportBuilder();
// Listing 1: 3-room apartment in Paris
$builder
->startLine()
->withIdentifiant(
agenceId: 'AGENCE001',
agencePropertyRef: 'PROP-12345',
annonceType: Annonce::ANNONCE_TYPE_VENTE,
annonceIdTechnique: 'TECH-001'
)
->withType(
type: 1, // 1 = Apartment
sousType: null
)
->withLocalisation(
cp: '75001',
ville: 'Paris',
pays: 'France',
adresse: '10 rue de Rivoli',
quartierProximite: 'Near Louvre metro',
situation: null,
procheLac: null,
procheTennis: null,
procheSki: null,
cpReel: '75001',
villeReelle: 'Paris',
idQuartier: null,
transportLigne: '1,14',
transportStation: 'Louvre-Rivoli',
latitude: 48.860611,
longitude: 2.342499,
precisionGps: null,
localisation: null
)
->withSurface(
surface: 65,
surfaceTerrain: null,
nbPieces: 3,
nbChambres: 2,
nbSdb: 1,
nbSalleEau: 0,
nbWc: 1,
nbBalcons: 1,
surfaceBalcon: 5,
nbParkings: 0,
nbBoxes: 0,
terrasse: false,
longueurFacade: null,
placesEnSalle: null,
nbBureaux: null,
surfaceSejour: 25,
nbTerrasses: 0,
surfaceCave: 0,
surfaceSalleManger: null,
surfaceMin: null,
surfaceMax: null,
nbPiecesMin: null,
nbPiecesMax: null,
nbChambresMin: null,
nbChambresMax: null,
comblesAmenageables: null,
surfaceTerrainNecessaire: null,
surfaceTerrasse: null
)
->withPrix(
prix: 450000,
loyerMoisMur: null,
loyerCC: null,
loyerHT: null,
depotGarantie: null,
prixMasque: false,
prixHT: null,
copropriete: true,
nbLots: 25,
syndicatCopro: null,
syndicatCoproDetails: 'Annual charges: €2400',
prixTerrain: null,
prixModeleMaison: null,
prixMin: null,
prixMax: null
)
->withDetail(
activitesCommerciales: null,
label: 'Bright 3-room apartment near Louvre',
description: 'Beautiful 65m² apartment with balcony, located in the heart of Paris. Unobstructed view, quiet, close to all amenities.',
datesDispo: new \DateTimeImmutable('2025-01-01'),
amenagementHandicapes: false,
animauxAcceptes: false,
duplex: false,
commPrives: null,
logementADisposition: null,
nomModele: null
)
->withDiagnostic(
recent: true,
travaux: false,
consoEnergie: 'C',
bilanConsoEnergie: '150',
ges: 'B',
bilanGes: '20',
dpeAt: new \DateTimeImmutable('2024-06-15'),
dpeVersion: '2021',
dpeMin: null,
dpeMax: null,
dpeAnneeRef: null,
dpeCoutConsoAnnuelle: 1200
)
->withContact(
tel: '0142000000',
fullName: 'John Smith',
email: 'contact@agency.com',
interCabinet: false,
interCabinetPrive: null,
codeNego: 'NEG001',
agenceTerrain: null
);
// Listing 2: House in Lyon
$builder
->startLine()
->withIdentifiant(
agenceId: 'AGENCE001',
agencePropertyRef: 'PROP-67890',
annonceType: Annonce::ANNONCE_TYPE_VENTE,
annonceIdTechnique: 'TECH-002'
)
->withType(
type: 2, // 2 = House
sousType: null
)
->withLocalisation(
cp: '69003',
ville: 'Lyon',
pays: 'France',
adresse: '25 avenue du Général Leclerc',
quartierProximite: 'Residential area',
situation: null,
procheLac: null,
procheTennis: null,
procheSki: null,
cpReel: '69003',
villeReelle: 'Lyon',
idQuartier: null,
transportLigne: 'B',
transportStation: 'Place Guichard',
latitude: 45.754610,
longitude: 4.856880,
precisionGps: null,
localisation: null
)
->withSurface(
surface: 120,
surfaceTerrain: 250,
nbPieces: 5,
nbChambres: 4,
nbSdb: 2,
nbSalleEau: 1,
nbWc: 2,
nbBalcons: 0,
surfaceBalcon: null,
nbParkings: 2,
nbBoxes: 0,
terrasse: true,
longueurFacade: null,
placesEnSalle: null,
nbBureaux: null,
surfaceSejour: 40,
nbTerrasses: 1,
surfaceCave: 15,
surfaceSalleManger: 18,
surfaceMin: null,
surfaceMax: null,
nbPiecesMin: null,
nbPiecesMax: null,
nbChambresMin: null,
nbChambresMax: null,
comblesAmenageables: null,
surfaceTerrainNecessaire: null,
surfaceTerrasse: 30
)
->withPrix(
prix: 650000,
loyerMoisMur: null,
loyerCC: null,
loyerHT: null,
depotGarantie: null,
prixMasque: false,
prixHT: null,
copropriete: false,
nbLots: null,
syndicatCopro: null,
syndicatCoproDetails: null,
prixTerrain: null,
prixModeleMaison: null,
prixMin: null,
prixMax: null
)
->withDetail(
activitesCommerciales: null,
label: 'Beautiful family house with garden',
description: '120m² house with 250m² garden, 4 bedrooms, terrace. Ideal for families.',
datesDispo: new \DateTimeImmutable('2025-02-01'),
amenagementHandicapes: false,
animauxAcceptes: true,
duplex: false,
commPrives: null,
logementADisposition: null,
nomModele: null
)
->withDiagnostic(
recent: true,
travaux: false,
consoEnergie: 'D',
bilanConsoEnergie: '220',
ges: 'D',
bilanGes: '35',
dpeAt: new \DateTimeImmutable('2024-09-10'),
dpeVersion: '2021',
dpeMin: null,
dpeMax: null,
dpeAnneeRef: null,
dpeCoutConsoAnnuelle: 1800
)
->withContact(
tel: '0478000000',
fullName: 'Mary Johnson',
email: 'lyon@agency.com',
interCabinet: false,
interCabinetPrive: null,
codeNego: 'NEG002',
agenceTerrain: null
);
// Generate CSV
$export = $builder->build();
return $export->toCSV();
}
}<?php
namespace App\Controller;
use App\Service\PolirisExportService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ExportController extends AbstractController
{
#[Route('/export/poliris', name: 'export_poliris')]
public function exportPoliris(PolirisExportService $exportService): Response
{
$csvContent = $exportService->generateExport();
$response = new Response($csvContent);
$response->headers->set('Content-Type', 'text/csv; charset=utf-8');
$response->headers->set('Content-Disposition', 'attachment; filename="export-poliris.csv"');
return $response;
}
}The generated CSV will contain 333+ columns. Here's a simplified excerpt of the first columns:
AGENCE001,PROP-12345,vente,1,,,,75001,Paris,...,65,3,2,...,450000,...
AGENCE001,PROP-67890,vente,2,,,,69003,Lyon,...,120,5,4,...,650000,...File structure:
| Col 1-4 | Col 5-10 | Col 11-40 | Col 41-100 | Col 101-200 | Col 201-333 |
|---|---|---|---|---|---|
| Identifier | Type | Photos | Location | Surface/Price | Miscellaneous |
| Column | Name | Example |
|---|---|---|
| 1 | Agency ID | AGENCE001 |
| 2 | Property Reference | PROP-12345 |
| 3 | Listing Type | vente or location |
| 4 | Property Type | 1 (Apt), 2 (House) |
| 5-10 | Photos | Image URLs |
| 11 | Postal Code | 75001 |
| 12 | City | Paris |
| 20 | Surface | 65 (m²) |
| 21 | Rooms | 3 |
| 22 | Bedrooms | 2 |
| 30 | Price | 450000 |
| ... | ... | ... |
SeLoger has specific requirements for Poliris data.
<?php
namespace App\Service;
class SeLogerMappingService
{
/**
* Property type mapping for SeLoger
*/
public const TYPE_MAPPING = [
'apartment' => 1,
'house' => 2,
'parking' => 3,
'land' => 4,
'shop' => 5,
'office' => 6,
'building' => 7,
'castle' => 8,
'block' => 9,
'loft' => 10,
'commercial_premises' => 11,
'new_development' => 13,
];
/**
* Subtypes for apartments
*/
public const SOUS_TYPE_APPARTEMENT = [
'duplex' => 1,
'triplex' => 2,
'loft' => 3,
'penthouse' => 4,
];
/**
* Subtypes for houses
*/
public const SOUS_TYPE_MAISON = [
'villa' => 1,
'farmhouse' => 2,
'chalet' => 3,
'farm' => 4,
'longhouse' => 5,
];
public function getTypePoliris(string $propertyType): int
{
return self::TYPE_MAPPING[$propertyType] ?? 1;
}
public function getSubType(string $propertyType, ?string $subType): ?int
{
if ($propertyType === 'apartment' && $subType) {
return self::SOUS_TYPE_APPARTEMENT[$subType] ?? null;
}
if ($propertyType === 'house' && $subType) {
return self::SOUS_TYPE_MAISON[$subType] ?? null;
}
return null;
}
}<?php
use Spiriit\PolirisBundle\Builders\AnnonceExportBuilder;
use App\Service\SeLogerMappingService;
$mapping = new SeLogerMappingService();
$builder = new AnnonceExportBuilder();
// For a duplex
$builder
->startLine()
->withIdentifiant('AGENCE001', 'REF-001', 'vente', 'TECH-001')
->withType(
type: $mapping->getTypePoliris('apartment'), // 1
sousType: $mapping->getSubType('apartment', 'duplex') // 1
)
->withDetail(
activitesCommerciales: null,
label: '4-room duplex',
description: 'Magnificent duplex...',
datesDispo: new \DateTimeImmutable(),
amenagementHandicapes: false,
animauxAcceptes: false,
duplex: true, // Important for SeLoger
commPrives: null,
logementADisposition: null,
nomModele: null
);$builder
->startLine()
->withIdentifiant(/* ... */)
->withPublication(
publications: 'seloger', // Target portals: seloger, leboncoin, etc.
coupDeCoeur: true, // Featured listing
versionFormat: '4.1.17' // Poliris version
);Real estate portals require quality photos. The bundle supports up to 30 photos per listing.
<?php
use Spiriit\PolirisBundle\Builders\AnnonceExportBuilder;
$builder = new AnnonceExportBuilder();
$builder
->startLine()
->withIdentifiant('AGENCE001', 'REF-001', 'vente', 'TECH-001')
->withType(1, null)
->withPhoto(
// Photo URLs (30 max)
photo1: 'https://www.example.com/photos/property-001/living-room.jpg',
photo2: 'https://www.example.com/photos/property-001/kitchen.jpg',
photo3: 'https://www.example.com/photos/property-001/bedroom1.jpg',
photo4: 'https://www.example.com/photos/property-001/bedroom2.jpg',
photo5: 'https://www.example.com/photos/property-001/bathroom.jpg',
photo6: 'https://www.example.com/photos/property-001/exterior.jpg',
photo7: null,
photo8: null,
photo9: null,
photo10: null,
photo11: null,
photo12: null,
photo13: null,
photo14: null,
photo15: null,
photo16: null,
photo17: null,
photo18: null,
photo19: null,
photo20: null,
photo21: null,
photo22: null,
photo23: null,
photo24: null,
photo25: null,
photo26: null,
photo27: null,
photo28: null,
photo29: null,
photo30: null,
// Photo titles (optional)
titre1: 'Bright living room',
titre2: 'Equipped kitchen',
titre3: 'Bedroom 1',
titre4: 'Bedroom 2',
titre5: 'Bathroom',
titre6: 'Exterior view',
titre7: null,
titre8: null,
titre9: null,
titre10: null,
titre11: null,
titre12: null,
titre13: null,
titre14: null,
titre15: null,
titre16: null,
titre17: null,
titre18: null,
titre19: null,
titre20: null,
titre21: null,
titre22: null,
titre23: null,
titre24: null,
titre25: null,
titre26: null,
titre27: null,
titre28: null,
titre29: null,
titre30: null,
// Panoramic photo and virtual tour
photoPanoramique: 'https://www.example.com/photos/property-001/panorama.jpg',
urlVisiteVirtuelle: 'https://www.example.com/virtual-tour/property-001'
);✅ Photo order: Start with main rooms (living room, kitchen)
✅ Quality: Minimum 1024x768, ideal 1920x1080
✅ Format: JPG or PNG
✅ Size: < 2 MB per photo
✅ Titles: Descriptive and informative
❌ Avoid: Blurry photos, too dark, with watermarks
Create a command to generate the export:
<?php
// src/Command/ExportPolirisCommand.php
namespace App\Command;
use App\Service\PolirisExportService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:export:poliris',
description: 'Generate Poliris CSV file for real estate export',
)]
class ExportPolirisCommand extends Command
{
public function __construct(
private PolirisExportService $exportService,
private string $exportDir
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('Poliris File Generation');
try {
// Generate CSV
$csvContent = $this->exportService->generateExport();
// Save file
$filename = sprintf(
'poliris-export-%s.csv',
(new \DateTime())->format('Y-m-d-His')
);
$filepath = $this->exportDir . '/' . $filename;
file_put_contents($filepath, $csvContent);
$io->success(sprintf('Export generated: %s', $filename));
return Command::SUCCESS;
} catch (\Exception $e) {
$io->error('Error during generation: ' . $e->getMessage());
return Command::FAILURE;
}
}
}Configuration (config/services.yaml):
# config/services.yaml
services:
App\Command\ExportPolirisCommand:
arguments:
$exportDir: '%kernel.project_dir%/var/exports'Execution:
php bin/console app:export:polirisAdd a cron task to run the export daily:
# Crontab: Export every day at 2am
0 2 * * * cd /var/www/myapp && php bin/console app:export:poliris >> /var/log/poliris-export.log 2>&1Trigger export automatically on certain events:
<?php
// src/EventSubscriber/PropertyUpdateSubscriber.php
namespace App\EventSubscriber;
use App\Event\PropertyUpdatedEvent;
use App\Service\PolirisExportService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class PropertyUpdateSubscriber implements EventSubscriberInterface
{
public function __construct(
private PolirisExportService $exportService
) {}
public static function getSubscribedEvents(): array
{
return [
PropertyUpdatedEvent::class => 'onPropertyUpdated',
];
}
public function onPropertyUpdated(PropertyUpdatedEvent $event): void
{
// Regenerate export after property update
$csvContent = $this->exportService->generateExport();
// Save or send the file
// ...
}
}For heavy exports, use Symfony Messenger:
<?php
// src/Message/GeneratePolirisExportMessage.php
namespace App\Message;
class GeneratePolirisExportMessage
{
public function __construct(
public readonly \DateTimeImmutable $requestedAt
) {}
}<?php
// src/MessageHandler/GeneratePolirisExportHandler.php
namespace App\MessageHandler;
use App\Message\GeneratePolirisExportMessage;
use App\Service\PolirisExportService;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
class GeneratePolirisExportHandler
{
public function __construct(
private PolirisExportService $exportService
) {}
public function __invoke(GeneratePolirisExportMessage $message): void
{
$csvContent = $this->exportService->generateExport();
// Save the file
file_put_contents(
'/var/exports/poliris-' . $message->requestedAt->format('YmdHis') . '.csv',
$csvContent
);
}
}Trigger the export:
$messageBus->dispatch(new GeneratePolirisExportMessage(new \DateTimeImmutable()));Symptom:
Portal rejects CSV with "Invalid format" message.
Possible causes:
- Incorrect encoding: CSV must be UTF-8
- Wrong separator: Poliris uses commas (
,) - Incorrect column count: 333 columns expected
- Missing values: Some required columns are empty
Solution 1: Check encoding
$csvContent = $builder->build()->toCSV();
// Force UTF-8 encoding
$csvContent = mb_convert_encoding($csvContent, 'UTF-8', 'auto');
// Add UTF-8 BOM (sometimes required)
$csvContent = "\xEF\xBB\xBF" . $csvContent;Solution 2: Check required columns
Ensure you fill in at minimum:
$builder
->startLine()
// REQUIRED
->withIdentifiant(
agenceId: 'AGENCE001', // ✅ Required
agencePropertyRef: 'REF-001', // ✅ Required
annonceType: 'vente', // ✅ Required
annonceIdTechnique: 'TECH-001' // ✅ Required
)
->withType(
type: 1, // ✅ Required
sousType: null
)
->withLocalisation(
cp: '75001', // ✅ Required
ville: 'Paris', // ✅ Required
pays: 'France', // ✅ Required
adresse: null, // Optional
// ... other optional parameters
)
->withSurface(
surface: 65, // ✅ Required
surfaceTerrain: null,
nbPieces: 3, // ✅ Required
nbChambres: 2, // ✅ Required (depending on type)
// ... other parameters
)
->withPrix(
prix: 450000, // ✅ Required
// ... other optional parameters
)
->withDetail(
activitesCommerciales: null,
label: 'My listing', // ✅ Required
description: 'Description...', // ✅ Required
// ... other parameters
);Solution 3: Enable debug logging
try {
$export = $builder->build();
$csvContent = $export->toCSV();
} catch (\Throwable $e) {
// Log the error
error_log('Poliris error: ' . $e->getMessage());
error_log('Trace: ' . $e->getTraceAsString());
throw $e;
}Symptom:
PHP Fatal error: Allowed memory size exhausted
Cause:
Too many listings in memory (several thousand).
Solution: Process in batches
<?php
namespace App\Service;
use Spiriit\PolirisBundle\Builders\AnnonceExportBuilder;
class PolirisExportService
{
private const BATCH_SIZE = 500;
public function generateExportInBatches(\Iterator $properties): \Generator
{
$builder = new AnnonceExportBuilder();
$count = 0;
foreach ($properties as $property) {
$this->addProperty($builder, $property);
$count++;
// Generate CSV every 500 listings
if ($count % self::BATCH_SIZE === 0) {
yield $builder->build()->toCSV();
// Reset builder
$builder = new AnnonceExportBuilder();
// Free memory
gc_collect_cycles();
}
}
// Last listings
if ($count % self::BATCH_SIZE !== 0) {
yield $builder->build()->toCSV();
}
}
private function addProperty(AnnonceExportBuilder $builder, $property): void
{
$builder
->startLine()
->withIdentifiant(/* ... */);
// etc.
}
}Usage:
// In a controller or command
$properties = $propertyRepository->findAllAsIterator();
foreach ($exportService->generateExportInBatches($properties) as $batch) {
// Write each batch to a file
file_put_contents('/tmp/export-batch-' . time() . '.csv', $batch, FILE_APPEND);
}The bundle provides 31 models to represent all aspects of a real estate property.
| Model | Description | CSV Columns |
|---|---|---|
| Identifiant | Agency ID, property reference | 1-3, 175 |
| Type | Property type (apt, house, etc.) | 4, 181 |
| Photo | Photo URLs (30 max) + virtual tour | 5-10, 96-125 |
| Localisation | Address, postal code, city, GPS | 11-19, 158-165 |
| Surface | Surface, rooms, bedrooms | 20-29, 182-188 |
| Prix | Price, rent, charges | 30-34, 126-137 |
| Detail | Title, description, availability | 35-40, 194-196 |
| Diagnostic | DPE, GES, diagnostics | 41-50, 198-205 |
| Contact | Phone, email, agent | 51-57, 300-305 |
| Exterieur | Elevator, pool, view | 58-64 |
| Interieur | Furnished, construction year | 65-85 |
| ChauffageClim | Heating type, air conditioning | 86-87 |
| PartieJour | Kitchen, living room, equipment | 88-95 |
| Etage | Floor, number of floors | 138-140 |
| Securite | Digicode, intercom, alarm | 141-144 |
| Garage | Garage, type | 145-146 |
| Parking | Number of vehicles, parking type | 147-149 |
| Bureau | Office property information | 150-167 |
| Boutique | Shop information | 168-169 |
| Terrain | Buildable land, agricultural | 170-176 |
| Location | Lease duration, lease type | 177-180, 206-209 |
| Viager | Lump sum, monthly annuity | 210-214 |
| Mandat | Exclusive mandate, number | 215-225 |
| HonoraireCharge | Fees, charges | 226-235 |
| Diagnostic | Property diagnostics | 236-250 |
| FondsCommerce | Revenue, results (business) | 251-258 |
| ProduitInvestissement | Purchase value, return | 259-260 |
| LocationVacances | Season prices, availability | 261-272 |
| Langue | Multilingual (3 languages max) | 273-284 |
| Publication | Distribution portals | 285-287 |
| ChampCustom | Custom fields (26 max) | 288-313 |
->withIdentifiant(
agenceId: 'AGENCE001', // Col 1: Agency ID
agencePropertyRef: 'REF-001', // Col 2: Property reference
annonceType: 'vente', // Col 3: vente/location
annonceIdTechnique: 'TECH-001' // Col 175: Technical ID
)Listing types:
Annonce::ANNONCE_TYPE_VENTE='vente'Annonce::ANNONCE_TYPE_LOCATION='location'Annonce::ANNONCE_TYPE_MODELE_MAISON='modèle de maison'
->withType(
type: 1, // Col 4: Main type
sousType: null // Col 181: Subtype
)Type codes:
1= Apartment2= House3= Parking/Box4= Land5= Shop6= Office7= Building8= Castle9= Block10= Loft11= Commercial premises13= New development
->withSurface(
surface: 65, // Col 20: Living area (m²)
surfaceTerrain: 200, // Col 21: Land area (m²)
nbPieces: 3, // Col 22: Number of rooms
nbChambres: 2, // Col 23: Number of bedrooms
nbSdb: 1, // Col 24: Bathrooms
nbSalleEau: 1, // Col 25: Shower rooms
nbWc: 1, // Col 26: WC
nbBalcons: 1, // Col 27: Number of balconies
surfaceBalcon: 5, // Col 28: Balcony surface (m²)
nbParkings: 1, // Col 29: Parkings
// ... 18 other parameters
)->withPrix(
prix: 450000, // Col 30: Sale price or rent
loyerMoisMur: null, // Col 31: Wall rent
loyerCC: 500, // Col 32: Rent including charges
loyerHT: null, // Col 33: Rent excl. tax (professional)
depotGarantie: 1000, // Col 34: Security deposit
prixMasque: false, // Col 126: Hide price
prixHT: null, // Col 127: Price excl. tax (professional)
copropriete: true, // Col 128: In co-ownership
nbLots: 25, // Col 129: Number of lots
syndicatCopro: null, // Col 130: Syndic
syndicatCoproDetails: 'Annual charges: €2400', // Col 131
// ... other parameters
)->withDetail(
activitesCommerciales: null, // Col 35
label: 'Bright 3-room apartment', // Col 36: Title
description: 'Beautiful description...', // Col 37: Description
datesDispo: new \DateTimeImmutable(), // Col 38: Availability date
amenagementHandicapes: false, // Col 39
animauxAcceptes: true, // Col 40
duplex: false, // Col 194
commPrives: null, // Col 195
logementADisposition: null, // Col 196
nomModele: null
)Q: Can I validate data before export?
A: No, the bundle does not offer built-in validation for performance reasons. Validate your data upstream with Symfony's Validator.
Q: How to handle multilingual fields?
A: Use the Langue model with up to 3 languages:
->withLangue(
code1: 'FR',
code2: 'EN',
code3: 'DE',
proximite1: 'Near metro',
proximite2: 'Near metro',
proximite3: 'Near U-Bahn',
label1: '3-room apartment',
label2: '3-room apartment',
label3: '3-Zimmer-Wohnung',
descriptif1: 'Beautiful description in French',
descriptif2: 'Beautiful description in English',
descriptif3: 'Beautiful description in German'
)Q: How to export only certain listings?
A: Filter your data upstream in your repository:
// In your service
public function generateExportForSeLoger(): string
{
$properties = $this->propertyRepository->findBy([
'status' => 'published',
'exportToSeLoger' => true
]);
$builder = new AnnonceExportBuilder();
foreach ($properties as $property) {
$this->addProperty($builder, $property);
}
return $builder->build()->toCSV();
}Q: Is the CSV valid for all portals?
A: Poliris V4.1.17 format is supported by most French portals (SeLoger, LeBonCoin, PAP, etc.). Check each portal's documentation for specifics.
Q: How to handle generation errors?
A: Use try-catch and log errors:
use Psr\Log\LoggerInterface;
public function generateExport(LoggerInterface $logger): ?string
{
try {
$builder = new AnnonceExportBuilder();
foreach ($this->properties as $property) {
try {
$this->addProperty($builder, $property);
} catch (\Throwable $e) {
// Log but continue
$logger->error('Listing error ' . $property->getId(), [
'exception' => $e->getMessage()
]);
}
}
return $builder->build()->toCSV();
} catch (\Throwable $e) {
$logger->critical('Poliris CSV generation error', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return null;
}
}- Required fields are filled (Identifiant, Type, Localisation, Surface, Prix, Detail)
- Photos are accessible via HTTPS
- CSV encoding is UTF-8
- Dates use
DateTimeImmutableorDateTime - Booleans are correctly converted (
true→'OUI',false→'NON') - CSV file is tested on a portal in test environment
- Logging is enabled to trace errors
- Monitoring system is in place (daily export success/failure)
- Performance is acceptable (< 30s for 1000 listings)
- Official Poliris documentation: https://www.poliris.com/documentation
- GitHub repository: https://github.com/SpiriitLabs/poliris-bundle
- Packagist: https://packagist.org/packages/spiriitlabs/poliris-bundle
┌─────────────────────────────────────────────────────────────┐
│ Your Application │
│ │
│ ┌────────────────┐ ┌─────────────────┐ │
│ │ Controller │─────▶│ ExportService │ │
│ └────────────────┘ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ AnnonceExportBuilder (Bundle) │ │
│ └──────────────┬──────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ AnnonceBuilder (Bundle) │ │
│ └──────────────┬──────────────┘ │
│ │ │
│ Uses 31 models │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ Identifiant Surface Photo │
│ Type Prix Detail │
│ Localisation Contact etc. │
│ │
└───────────────────────────┬───────────────────────────────┘
│
▼
┌────────────────┐
│ CSV File │
│ (333 columns) │
└────────┬───────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
SeLoger LeBonCoin PAP
| Use Case | Difficulty | Estimated Time | Required Models |
|---|---|---|---|
| Basic export | ⭐ Easy | 15 min | 5-7 models |
| Export with photos | ⭐⭐ Medium | 30 min | 8-10 models |
| Complete SeLoger export | ⭐⭐⭐ Advanced | 1-2h | 15-20 models |
| Cron automation | ⭐⭐ Medium | 45 min | Depends on case |
| Asynchronous export | ⭐⭐⭐ Advanced | 2-3h | Depends on case |
| Multi-portal | ⭐⭐⭐ Advanced | 3-4h | 20-25 models |
You're now ready to use PolirisBundle like a pro! 🚀
If you have questions, feel free to open an issue on GitHub or check the examples in the tests/ folder.
Happy exporting! 🏠✨