yeevy/centris-passerelle 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

yeevy/centris-passerelle

Composer 安装命令:

composer require yeevy/centris-passerelle

包简介

Unofficial PHP client for the Centris® Passerelle FTP feed — parse, sync & reconcile Quebec MLS listings. Not affiliated with or endorsed by Centris or QFREB.

README 文档

README

Latest Version on Packagist Tests Total Downloads

Non officiel. Aucune affiliation avec Centris ou l'APCIQ/QFREB, ni endossement de leur part. Requiert une entente de diffusion valide.

Unofficial. Not affiliated with or endorsed by Centris or QFREB. Requires a valid diffusion agreement.

Français | English

Français

Client PHP open source non officiel pour le flux FTP Centris® Passerelle (données d'inscriptions MLS du Québec distribuées aux courtiers autorisés). Analyse, synchronise et réconcilie les données d'inscriptions.

Noyau PHP pur, sans dépendance à un framework : utilisable depuis une extension WordPress, Laravel, Symfony ou un simple script cron.

Comment fonctionne le flux Passerelle

  • Aucune API publique. Le courtier signe une entente de diffusion avec Centris/APCIQ et reçoit des identifiants FTP limités à ses propres inscriptions.
  • Centris dépose un instantané complet une ou deux fois par jour (pas de deltas) : les retraits se détectent par différence — une inscription présente en base mais absente du nouveau fichier est vendue, expirée ou retirée.
  • Fichiers livrés : INSCRIPTIONS.TXT (inscriptions), REMARQUES.TXT (descriptions FR/EN), PHOTOS.TXT, ADDENDA.TXT, plus des fichiers de référence (courtiers, agences, caractéristiques, municipalités).
  • Format : CSV délimité par virgules, champs entre guillemets, encodage Windows-1252, sans ligne d'en-tête, colonnes positionnelles (~150), une inscription par ligne (CRLF).

Prérequis

  • PHP 8.2+ avec l'extension mbstring
  • Une entente de diffusion Passerelle valide (ce paquet ne fournit aucune donnée)

Installation

composer require yeevy/centris-passerelle

Utilisation

use Yeevy\CentrisPasserelle\Parser\ListingsParser;
use Yeevy\CentrisPasserelle\Enums\ListingStatus;

$parser = new ListingsParser();

foreach ($parser->parseFile('/chemin/vers/INSCRIPTIONS.TXT') as $listing) {
    $listing->mlsNumber;      // « 9999999 » — clé d'upsert
    $listing->salePrice;      // 975000 (null pour les locations)
    $listing->status;         // ListingStatus::Active | ListingStatus::Sold | null
    $listing->descriptionFr;  // contient du HTML <br/>
    $listing->descriptionEn;
    $listing->latitude;
    $listing->longitude;
    $listing->dirtyHash;      // sha256 de la ligne brute — ignorez les lignes inchangées
    $listing->row;            // ligne brute complète pour les colonnes non cartographiées
}

L'analyse est paresseuse (générateur) : les instantanés volumineux ne saturent pas la mémoire. La conversion Windows-1252 → UTF-8 est appliquée automatiquement.

Chaque fichier du dépôt a son analyseur. Les fichiers de détail se joignent aux inscriptions par numéro MLS ; les fichiers de référence par leurs propres codes :

Fichier Analyseur Contenu
REMARQUES.TXT RemarksParser Descriptions FR/EN
PHOTOS.TXT PhotosParser Photos ordonnées, URL media.ashx
ADDENDA.TXT AddendaParser Addenda en segments à réassembler
CARACTERISTIQUES.TXT FeaturesParser Caractéristiques codées
DEPENSES.TXT ExpensesParser Taxes et dépenses
RENOVATIONS.TXT RenovationsParser Rénovations déclarées
LIENS_ADDITIONNELS.TXT AdditionalLinksParser Visites virtuelles, vidéos
VISITES_LIBRES.TXT OpenHousesParser Visites libres planifiées
UNITES_DETAILLEES.TXT UnitsParser Unités (principale, logements, intergénération)
PIECES_UNITES.TXT RoomsParser Pièces par unité (dimensions, revêtements)
MEMBRES.TXT BrokersParser Courtiers (clé : code courtier)
FIRMES.TXT FirmsParser Agences (clé : code firme)
BUREAUX.TXT OfficesParser Bureaux (clé : code bureau)
use Yeevy\CentrisPasserelle\Parser\PhotosParser;

foreach ((new PhotosParser())->parseFile('/chemin/vers/PHOTOS.TXT') as $photo) {
    $photo->mlsNumber;      // clé de jointure
    $photo->sequence;       // ordre d'affichage
    $photo->categoryCode;   // FACA = façade, CUI = cuisine, SDB = salle de bain…
    $photo->url;            // https://mediaserver.centris.ca/media.ashx?id=…
}

Positions de colonnes

Les positions livrées avec le paquet sont observées par la communauté et peuvent varier selon la version de votre entente. Vérifiez-les contre la documentation PDF Passerelle fournie avec votre entente, puis surchargez-les au besoin :

use Yeevy\CentrisPasserelle\Config\ColumnMap;
use Yeevy\CentrisPasserelle\Parser\ListingsParser;

$columns = ColumnMap::listings()->with([
    'status_code' => 120,   // position vérifiée dans votre documentation
]);

$parser = new ListingsParser($columns);

Un logger PSR-3 peut être injecté ; les lignes sans numéro MLS sont journalisées puis ignorées au lieu d'interrompre l'instantané :

$parser = new ListingsParser(logger: $monLogger);

Si Centris introduit une nouvelle disposition de colonnes, elle sera publiée comme profil nommé plutôt qu'en écrasant la carte par défaut : ColumnMap::listings('2027') chargera config/listings-2027.php, et les profils existants continueront de fonctionner.

Détection de dérive

Un changement de structure du flux ne provoque aucune erreur par lui-même — il se manifeste par des données décalées importées silencieusement. Validez l'instantané avant l'import :

use Yeevy\CentrisPasserelle\Validation\SnapshotValidator;

$validator = new SnapshotValidator($columns);

// Échantillonne les lignes et vérifie les invariants (numéro MLS numérique,
// format des dates, coordonnées dans les bornes du Québec…).
// Lève ColumnMapMismatch si la structure ne correspond plus à la carte,
// ou si l'instantané est vide (ce qui dépublierait toutes les inscriptions).
$validator->validateFile('/chemin/vers/INSCRIPTIONS.TXT');

Les vérifications sont injectables — ajoutez des invariants propres à votre entente ou assouplissez ceux par défaut :

new SnapshotValidator($columns, checks: [
    ...SnapshotValidator::defaultChecks(),
    fn (array $row, ColumnMap $columns): ?string => /* votre invariant */ null,
]);

Cycle de vie des inscriptions

Signal Interprétation
EV (en vigueur) Publier
VE (vendue) Marquer vendue
Absente de l'instantané Dépublier (étape de réconciliation)

Synchronisation

Le moteur applique un instantané complet à votre stockage : validation de dérive d'abord (rien n'est écrit si elle échoue), upsert avec saut des lignes inchangées (dirty hash), puis réconciliation des retraits. Le paquet ne touche jamais à une base de données — implémentez ListingRepository contre votre propre schéma (Eloquent, PDO, WordPress…) :

use Yeevy\CentrisPasserelle\Sync\ListingsSynchronizer;

$synchronizer = new ListingsSynchronizer(
    repository: $monRepository,   // implémente Contracts\ListingRepository
    events: $dispatcherPsr14,     // optionnel — ListingCreated / ListingUpdated / ListingRemoved
);

$result = $synchronizer->sync('/chemin/vers/instantane'); // dossier ou fichier
// $result->created, $result->updated, $result->skipped, $result->removed

Pour récupérer l'instantané depuis le compte FTP Passerelle, utilisez FlysystemFeedSource (installez league/flysystem et league/flysystem-ftp ou -sftp-v3) :

use Yeevy\CentrisPasserelle\Feed\FlysystemFeedSource;

$source = new FlysystemFeedSource($filesystem, localDirectory: '/tmp/centris');

$result = $synchronizer->sync($source);

Si votre entente livre l'instantané en archive ZIP, décorez la source — l'extraction se fait sur place (requiert ext-zip) :

use Yeevy\CentrisPasserelle\Feed\ZipExtractingSource;

$result = $synchronizer->sync(new ZipExtractingSource($source));

Téléchargement des photos

PhotoDownloader télécharge les photos via n'importe quel client PSR-18 (p. ex. Guzzle) dans des fichiers adressés par contenu ({sha256}.jpg) — les octets identiques ne sont stockés qu'une seule fois :

use Yeevy\CentrisPasserelle\Photo\PhotoDownloader;

$downloader = new PhotoDownloader($clientPsr18, $requestFactory, '/chemin/vers/photos');

foreach ($downloader->downloadAll($photosParser->parseFile('/chemin/vers/PHOTOS.TXT')) as $photo) {
    $photo->path;             // /chemin/vers/photos/{sha256}.jpg
    $photo->wasDeduplicated;  // true si les octets existaient déjà
}

download() lève PhotoDownloadFailed ; downloadAll() journalise et ignore les échecs pour qu'une URL brisée n'interrompe jamais le lot. La conversion WebP reste une préoccupation de l'application consommatrice.

Feuille de route

  • Enveloppe Laravel : yeevy/laravel-centris — commande centris:sync, événements Laravel, jobs de photos en file d'attente à venir

Gestion des versions

Le paquet suit SemVer via les étiquettes git.

  • 0.x : les positions de colonnes sont observées par la communauté et l'API se stabilise — des ruptures peuvent survenir dans les versions mineures.
  • À partir de 1.0 : correctifs = patch ; nouveaux champs et analyseurs = mineure ; changement d'API = majeure.
  • Cartes de colonnes : corriger une position par défaut est publié au minimum en version mineure avec une entrée de changelog explicite — le code compile mais les données changent. Une nouvelle disposition Centris devient un nouveau profil nommé, jamais un écrasement du profil existant.

Tests

composer test      # Pest
composer analyse   # PHPStan niveau 8
composer format    # Pint

Important : ne commettez jamais de données réelles du flux. Les tests utilisent exclusivement des fixtures synthétiques.

Licence

MIT — © Digital Unity Inc. (Yeevy)

English

Unofficial open-source PHP client for the Centris® Passerelle FTP feed (Quebec MLS listing data distributed to authorized brokers). Parses, syncs, and reconciles listing data.

Pure PHP core with no framework dependency: consumable from a WordPress plugin, Laravel, Symfony, or a bare cron script.

How the Passerelle feed works

  • No public API. The broker signs a diffusion agreement with Centris/QFREB and receives FTP credentials scoped to their own listings.
  • Centris drops a full snapshot once or twice daily (no deltas): removals are detected by diffing — a listing present in your database but absent from the new file is sold, expired, or withdrawn.
  • Delivered files: INSCRIPTIONS.TXT (listings master), REMARQUES.TXT (FR/EN descriptions), PHOTOS.TXT, ADDENDA.TXT, plus reference files (brokers, agencies, features, municipalities).
  • Format: comma-delimited CSV, quoted fields, Windows-1252 encoding, no header row, positional columns (~150), one listing per CRLF line.

Requirements

  • PHP 8.2+ with the mbstring extension
  • A valid Passerelle diffusion agreement (this package ships no data)

Installation

composer require yeevy/centris-passerelle

Usage

use Yeevy\CentrisPasserelle\Parser\ListingsParser;
use Yeevy\CentrisPasserelle\Enums\ListingStatus;

$parser = new ListingsParser();

foreach ($parser->parseFile('/path/to/INSCRIPTIONS.TXT') as $listing) {
    $listing->mlsNumber;      // "9999999" — upsert key
    $listing->salePrice;      // 975000 (null for rentals)
    $listing->status;         // ListingStatus::Active | ListingStatus::Sold | null
    $listing->descriptionFr;  // contains HTML <br/>
    $listing->descriptionEn;
    $listing->latitude;
    $listing->longitude;
    $listing->dirtyHash;      // sha256 of the raw row — skip unchanged rows on upsert
    $listing->row;            // full raw row for unmapped columns
}

Parsing is lazy (generator-based), so large snapshots don't exhaust memory. Windows-1252 → UTF-8 conversion is applied automatically.

Every file in the drop has its own parser. Detail files join to listings by MLS number; reference files by their own codes:

File Parser Content
REMARQUES.TXT RemarksParser FR/EN descriptions
PHOTOS.TXT PhotosParser Ordered photos, media.ashx URLs
ADDENDA.TXT AddendaParser Chunked addenda to reassemble
CARACTERISTIQUES.TXT FeaturesParser Coded features
DEPENSES.TXT ExpensesParser Taxes and expenses
RENOVATIONS.TXT RenovationsParser Declared renovations
LIENS_ADDITIONNELS.TXT AdditionalLinksParser Virtual tours, videos
VISITES_LIBRES.TXT OpenHousesParser Scheduled open houses
UNITES_DETAILLEES.TXT UnitsParser Units (main, rental, intergenerational)
PIECES_UNITES.TXT RoomsParser Rooms per unit (dimensions, flooring)
MEMBRES.TXT BrokersParser Brokers (key: broker code)
FIRMES.TXT FirmsParser Firms (key: firm code)
BUREAUX.TXT OfficesParser Offices (key: office code)
use Yeevy\CentrisPasserelle\Parser\PhotosParser;

foreach ((new PhotosParser())->parseFile('/path/to/PHOTOS.TXT') as $photo) {
    $photo->mlsNumber;      // join key
    $photo->sequence;       // display order
    $photo->categoryCode;   // FACA = façade, CUI = kitchen, SDB = bathroom…
    $photo->url;            // https://mediaserver.centris.ca/media.ashx?id=…
}

Column positions

The positions shipped with the package are community-observed and may vary by agreement version. Verify them against the Passerelle PDF documentation that came with your agreement, then override as needed:

use Yeevy\CentrisPasserelle\Config\ColumnMap;
use Yeevy\CentrisPasserelle\Parser\ListingsParser;

$columns = ColumnMap::listings()->with([
    'status_code' => 120,   // position verified in your documentation
]);

$parser = new ListingsParser($columns);

A PSR-3 logger can be injected; rows without an MLS number are logged and skipped instead of aborting the snapshot:

$parser = new ListingsParser(logger: $myLogger);

If Centris introduces a new column layout, it will ship as a named profile rather than overwriting the default map: ColumnMap::listings('2027') loads config/listings-2027.php, and existing profiles keep working.

Drift detection

A feed structure change raises no error by itself — it shows up as shifted data imported silently. Validate the snapshot before importing:

use Yeevy\CentrisPasserelle\Validation\SnapshotValidator;

$validator = new SnapshotValidator($columns);

// Samples rows and checks invariants (numeric MLS number, date format,
// coordinates within Quebec bounds…). Throws ColumnMapMismatch when the
// structure no longer lines up with the map, or when the snapshot is
// empty (which would unpublish every listing).
$validator->validateFile('/path/to/INSCRIPTIONS.TXT');

Checks are injectable — add per-agreement invariants or relax the defaults:

new SnapshotValidator($columns, checks: [
    ...SnapshotValidator::defaultChecks(),
    fn (array $row, ColumnMap $columns): ?string => /* your invariant */ null,
]);

Listing lifecycle

Signal Interpretation
EV (en vigueur) Publish
VE (vendue) Mark sold
Absent from snapshot Unpublish (reconciliation step)

Synchronization

The engine applies a full snapshot to your storage: drift validation first (nothing is written if it fails), upserts with unchanged-row skipping (dirty hash), then removal reconciliation. The package never touches a database — implement ListingRepository against your own schema (Eloquent, PDO, WordPress…):

use Yeevy\CentrisPasserelle\Sync\ListingsSynchronizer;

$synchronizer = new ListingsSynchronizer(
    repository: $myRepository,    // implements Contracts\ListingRepository
    events: $psr14Dispatcher,     // optional — ListingCreated / ListingUpdated / ListingRemoved
);

$result = $synchronizer->sync('/path/to/snapshot'); // directory or file
// $result->created, $result->updated, $result->skipped, $result->removed

To fetch the snapshot from the Passerelle FTP account, use FlysystemFeedSource (install league/flysystem plus league/flysystem-ftp or -sftp-v3):

use Yeevy\CentrisPasserelle\Feed\FlysystemFeedSource;

$source = new FlysystemFeedSource($filesystem, localDirectory: '/tmp/centris');

$result = $synchronizer->sync($source);

If your agreement delivers the snapshot as a ZIP archive, decorate the source — extraction happens in place (requires ext-zip):

use Yeevy\CentrisPasserelle\Feed\ZipExtractingSource;

$result = $synchronizer->sync(new ZipExtractingSource($source));

Photo downloads

PhotoDownloader fetches photos through any PSR-18 client (e.g. Guzzle) into content-addressed files ({sha256}.jpg) — identical bytes are stored exactly once:

use Yeevy\CentrisPasserelle\Photo\PhotoDownloader;

$downloader = new PhotoDownloader($psr18Client, $requestFactory, '/path/to/photos');

foreach ($downloader->downloadAll($photosParser->parseFile('/path/to/PHOTOS.TXT')) as $photo) {
    $photo->path;             // /path/to/photos/{sha256}.jpg
    $photo->wasDeduplicated;  // true when the bytes already existed
}

download() throws PhotoDownloadFailed; downloadAll() logs and skips failures so one broken URL never aborts the batch. WebP conversion stays a consumer-application concern.

Roadmap

  • Laravel wrapper: yeevy/laravel-centriscentris:sync command, Laravel events, queued photo jobs upcoming

Versioning

The package follows SemVer via git tags.

  • 0.x: column positions are community-observed and the API is still settling — breaking changes may land in minor versions.
  • From 1.0 on: fixes = patch; new fields and parsers = minor; API changes = major.
  • Column maps: correcting a shipped default position is released as at least a minor version with an explicit changelog entry — code still compiles, but data shifts. A new Centris layout becomes a new named profile, never an overwrite of an existing one.

Testing

composer test      # Pest
composer analyse   # PHPStan level 8
composer format    # Pint

Important: never commit real feed data. Tests use synthetic fixtures only.

License

MIT — © Digital Unity Inc. (Yeevy)

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 3
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-07-05

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固