homlity/sdk-softinm

SDK de Homlity para consultar el inventario de inmuebles de SoftInm (Zona Clientes) desde PHP, Laravel o WordPress.

Maintainers

Package info

github.com/homlity/softinm-sdk

Homepage

Documentation

pkg:composer/homlity/sdk-softinm

Transparency log

Statistics

Installs: 8

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v2.0.0 2026-08-24 21:22 UTC

This package is auto-updated.

Last update: 2026-08-24 21:23:33 UTC


README

Homlity

Homlity SDK para SoftInm

SDK oficial de Homlity en PHP para consultar el inventario de inmuebles de una inmobiliaria alojada en SoftInm (Zona Clientes) desde cualquier aplicación PHP, Laravel o WordPress.

homlity.com · Portal de desarrolladores · GitHub · Packagist

PHP 7.4+ Packagist Laravel auto-discovery 49 tests unitarios

Tabla de contenidos

  1. ¿Para qué sirve este SDK?
  2. El ecosistema de SDKs de Homlity
  3. Arquitectura
  4. Requisitos e instalación
  5. Configuración
  6. Quick start
  7. Referencia de filtros de búsqueda
  8. Paginación
  9. La respuesta: qué devuelve search()
  10. Manejo de errores
  11. Reintentos, timeouts y logging
  12. Caché: cómo no golpear la API en cada request
  13. Integración con Laravel
  14. Integración con WordPress
  15. Extender el SDK
  16. Ejemplos incluidos en el repositorio
  17. Testing
  18. Referencia de API del SDK
  19. Troubleshooting
  20. Soporte, contribución y seguridad

¿Vienes de la 1.x? Lee UPGRADE.md. El cambio importante: los errores ya no devuelven un array vacío, lanzan excepciones tipadas.

1. ¿Para qué sirve este SDK?

SoftInm es el software de gestión inmobiliaria donde muchas inmobiliarias colombianas mantienen su inventario. Su Zona Clientes expone una API HTTP que permite consultar los inmuebles publicados de una inmobiliaria concreta.

Este SDK es la capa PHP que Homlity —y cualquier portal, CRM, lonja o sitio web que se integre con Homlity— usa para leer ese inventario sin escribir HTTP, cabeceras de autenticación ni serialización a mano.

Qué te resuelve:

Sin el SDK Con el SDK
Construir la URL del endpoint y recordar el código de la inmobiliaria $config->setInmo('ElRo')
Montar el header Authorization: Bearer … y Content-Type: application/json Lo hace SearchPropertysRequest
Recordar los 21 parámetros del filtro y cuáles son obligatorios Todos vienen con valor por defecto
Serializar el body a JSON y decodificar la respuesta a array search() devuelve un array de PHP
Distinguir un token vencido de una búsqueda sin resultados Excepciones tipadas por causa
Reintentar un 500 con backoff, sin reintentar un 401 Incluido y configurable
Escribir el bucle de paginación en cada proyecto foreach ($softinm->iterate() as $inmueble)
Repetir ese código en cada proyecto composer require homlity/sdk-softinm

Qué NO hace (por diseño):

  • Es solo de lectura: consulta inmuebles. No publica, actualiza ni elimina.
  • Expone un único endpoint: consultar_inmuebles.
  • No transforma la respuesta a DTOs: te devuelve el JSON de SoftInm tal cual, como array asociativo.
  • No cachea por su cuenta: la política de caché es tuya (§12).

Caso de uso típico: una inmobiliaria usa SoftInm como CRM. Su sitio web (WordPress, Laravel o el portal de Homlity) necesita mostrar el inventario siempre actualizado, con filtros por municipio, barrio, precio, alcobas y baños. Este SDK es el puente.

2. El ecosistema de SDKs de Homlity

Homlity publica una familia de SDKs PHP en Packagist, uno por cada portal o CRM inmobiliario con el que se integra. Todos comparten el mismo espíritu: un paquete pequeño, instalable con Composer, que encapsula una integración concreta.

SDK Paquete Para qué
SoftInm homlity/sdk-softinm Este paquete. Leer inventario desde SoftInm
Proppit homlity/sdk-proppit Publicar y sincronizar anuncios en Proppit (LIFULL Connect)
Finca Raíz homlity/sdk-fincaraiz Integración con Finca Raíz
Ciencuadras homlity/sdk-ciencuadras Integración con Ciencuadras
Domus homlity/sdk-domus Integración con Domus
Mobilia homlity/sdk-mobilia Integración con Mobilia
Wasi homlity/sdk-wasi-php8 Integración con Wasi (PHP 8)
SmartHome homlity/sdk-smarthome Integración con dispositivos SmartHome
Chat homlity/chat-sdk Mensajería de Homlity

El catálogo completo y actualizado vive en https://homlity.com/desarrolladores/.

Cómo se combinan: un flujo habitual es leer el inventario de la inmobiliaria con sdk-softinm y publicarlo en los portales con sdk-proppit, sdk-fincaraiz o sdk-ciencuadras. Este SDK es la fuente; los otros son los destinos.

3. Arquitectura

Tu aplicación (Homlity / CRM / WordPress / Laravel)
        │
        ▼
  SoftInmSDKBuilder::build(Config)      ← punto de entrada
        │
        ▼
  SoftInmoFacade                        ← API pública: search(), iterate(),
        │                                  searchOrDefault(), extractItems()
        ▼
  SearchPropertysRequest                ← filtros por defecto, reintentos,
        │                                  traducción de errores a excepciones
        ▼
  CurlHttpClient (HttpClientInterface)  ← transporte sustituible
        │
        ▼
  POST {base_url}/inmuebles/consultar_inmuebles/{inmo}

Cada capa está detrás de una interfaz en src/Contracts/, así que cualquier pieza es reemplazable — ver §15.

Clase Archivo Responsabilidad
SoftInmSDKBuilder src/SoftInmSDKBuilder.php Construir la fachada a partir de una Config
App\Config src/App/Config.php Credenciales, endpoint, red, logging y política de errores
App\SoftInmoFacade src/App/SoftInmoFacade.php API pública
InfraStructure\SearchPropertysRequest src/InfraStructure/SearchPropertysRequest.php Petición, reintentos y errores tipados
InfraStructure\CurlHttpClient src/InfraStructure/CurlHttpClient.php Transporte HTTP por defecto
Support\HttpResponse src/Support/HttpResponse.php Respuesta cruda: status, cuerpo, error
Laravel\SoftInmServiceProvider src/Laravel/SoftInmServiceProvider.php Registro automático en Laravel

Namespace raíz: Homlity\SoftInm\SDK\ (PSR-4 sobre src/).

El contrato HTTP

POST /api/inmuebles/consultar_inmuebles/ElRo HTTP/1.1
Host: zonaclientes.softinm.com
Authorization: Bearer eyJhbGciOi…
Content-Type: application/json

{"cantidadporpagina":20,"pagina":0,"destinacion":null,"municipio":"Medellín", }

El SDK envía las 21 claves del filtro aunque valgan null. Las claves adicionales que añadas también viajan al servidor.

4. Requisitos e instalación

Requisito Versión Nota
PHP ≥ 7.4 Probado también en PHP 8.0–8.3
ext-curl * Transporte HTTP
ext-json * Serialización del filtro y decodificación de la respuesta
ext-mbstring * Requerido por la dependencia de cURL
Composer 2.x
composer require homlity/sdk-softinm

Eso instala el SDK y su única dependencia de runtime, php-curl-class/php-curl-class ^11.1. Laravel es opcional; si lo usas, el service provider se registra solo por auto-discovery.

Si prefieres seguir la rama de desarrollo:

composer require homlity/sdk-softinm:dev-main

…y si tu proyecto usa minimum-stability: stable, añade a tu composer.json:

{ "minimum-stability": "dev", "prefer-stable": true }

Instalar desde GitHub (sin Packagist)

{
  "repositories": [
    { "type": "vcs", "url": "https://github.com/homlity/softinm-sdk" }
  ],
  "require": {
    "homlity/sdk-softinm": "dev-main"
  }
}

Nota sobre el nombre del paquete: el repositorio se llama homlity/softinm-sdk en GitHub, pero el paquete de Composer es homlity/sdk-softinm. Instala siempre por el nombre de Packagist.

Verificar la instalación

<?php
require __DIR__ . '/vendor/autoload.php';

var_dump(class_exists(\Homlity\SoftInm\SDK\SoftInmSDKBuilder::class)); // true

5. Configuración

Credenciales: inmo y token

Dato Qué es Dónde va Ejemplo
inmo Código de la inmobiliaria en SoftInm. Identifica de quién es el inventario. Va en la URL. Config::setInmo() ElRo
token Token de acceso emitido por SoftInm para esa inmobiliaria. Viaja como Authorization: Bearer {token}. Config::setToken() eyJhbGciOi…

Ambos los entrega SoftInm al administrador de la inmobiliaria desde su Zona Clientes. Si estás integrando una inmobiliaria nueva, pídeselos a ella; Homlity no los genera.

Un inmo = una inmobiliaria. Si tu aplicación sirve a varias inmobiliarias, guarda el par (inmo, token) por inmobiliaria en tu base de datos y construye una instancia del SDK por cada una.

Todas las opciones

use Homlity\SoftInm\SDK\App\Config;

$config = new Config();
$config->setInmo(getenv('SOFTINM_INMO'))       // obligatorio
       ->setToken(getenv('SOFTINM_TOKEN'))     // obligatorio
       ->setBaseUrl('https://zonaclientes.softinm.com/api')
       ->setTimeout(15)          // segundos por intento
       ->setConnectTimeout(5)    // segundos para establecer la conexión
       ->setRetries(2)           // reintentos ante fallos transitorios
       ->setRetryDelayMs(200)    // backoff base: 200 ms, 400 ms…
       ->setThrowOnError(true)   // false = comportamiento 1.x
       ->setLogger($psrLogger);  // logger PSR-3 o callable
Opción Por defecto Para qué
setInmo() Código de la inmobiliaria
setToken() Token Bearer
setBaseUrl() https://zonaclientes.softinm.com/api Apuntar a staging o a un mock server
setTimeout() 15 s Espera total por intento
setConnectTimeout() 5 s Espera para conectar
setRetries() 2 Reintentos ante 5xx, 429 y errores de red
setRetryDelayMs() 200 ms Backoff lineal entre reintentos
setThrowOnError() true false devuelve [] ante cualquier error
setLogger() null Logger PSR-3 o fn($nivel, $mensaje, $contexto)

Todos los setters son encadenables.

Desde un array

$config = Config::fromArray([
    'inmo'           => getenv('SOFTINM_INMO'),
    'token'          => getenv('SOFTINM_TOKEN'),
    'base_url'       => getenv('SOFTINM_BASE_URL'),
    'timeout'        => 15,
    'retries'        => 2,
    'throw_on_error' => true,
]);

Las claves con valor null se ignoran, así que un .env incompleto no pisa los valores por defecto.

Validar antes de llamar

use Homlity\SoftInm\SDK\Exceptions\ConfigurationException;

try {
    $config->validate();
} catch (ConfigurationException $e) {
    // Falta inmo, falta token o la URL base no es válida
}

validate() se ejecuta automáticamente en cada consulta, así que una configuración incompleta falla antes de tocar la red.

Nunca pongas las credenciales en el código

// ❌ mal
$config->setToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');

// ✅ bien
$config->setToken(getenv('SOFTINM_TOKEN'));

.env sugerido:

SOFTINM_INMO=ElRo
SOFTINM_TOKEN=tu-token-de-softinm

Para volcar la configuración en un log o en un endpoint de diagnóstico, usa $config->redacted(): devuelve el mismo array con el token enmascarado.

6. Quick start

<?php
require __DIR__ . '/vendor/autoload.php';

use Homlity\SoftInm\SDK\App\Config;
use Homlity\SoftInm\SDK\Exceptions\AuthException;
use Homlity\SoftInm\SDK\Exceptions\SoftInmException;
use Homlity\SoftInm\SDK\SoftInmSDKBuilder;

// 1. Configuración
$config = new Config();
$config->setInmo(getenv('SOFTINM_INMO'))
       ->setToken(getenv('SOFTINM_TOKEN'));

// 2. Fachada
$softinm = SoftInmSDKBuilder::build($config);

// 3. Consulta
try {
    $inmuebles = $softinm->search([
        'cantidadporpagina' => 20,
        'pagina'            => 0,
        'municipio'         => 'Medellín',
        'destinacion'       => 'Venta',
        'alcobas'           => 3,
        'preciohasta'       => 500000000,
    ]);
} catch (AuthException $e) {
    // El token caducó: avisa a quien pueda renovarlo
    throw $e;
} catch (SoftInmException $e) {
    // Red, timeout o error de la API
    $inmuebles = [];
}

// 4. Resultado: array asociativo con el JSON de SoftInm
print_r($inmuebles);

Atajo

$softinm = SoftInmSDKBuilder::make(getenv('SOFTINM_INMO'), getenv('SOFTINM_TOKEN'));

Sin filtros (primeros 50 inmuebles)

$inmuebles = $softinm->search();

Reutilizar la instancia

SoftInmoFacade no guarda estado entre llamadas más allá de la configuración, así que puedes construirla una vez y reutilizarla:

$venta    = $softinm->search(['destinacion' => 'Venta']);
$arriendo = $softinm->search(['destinacion' => 'Arriendo']);

Cada llamada a search() abre una conexión HTTP nueva.

7. Referencia de filtros de búsqueda

search() acepta un array asociativo. Las claves que envíes se combinan con los valores por defecto, así que solo necesitas pasar lo que quieras cambiar.

Clave Tipo Por defecto Para qué sirve
cantidadporpagina int 50 Cuántos inmuebles devuelve por página
pagina int 0 Página solicitada, base 0
destinacion string null Destinación del inmueble (venta / arriendo)
municipio string null Municipio o ciudad
barrio string null Barrio
clase string null Clase o tipo de inmueble (apartamento, casa, local…)
alcobas int null Número de alcobas
banos int null Número de baños
preciodesde number null Precio mínimo
preciohasta number null Precio máximo
areadesde number null Área mínima
areahasta number null Área máxima
ascensor bool/int null Filtra inmuebles con ascensor
piscina bool/int null Filtra inmuebles con piscina
unidadcerrada bool/int null Filtra inmuebles en unidad cerrada
parqueadero bool/int null Filtra inmuebles con parqueadero
destacado bool/int null Solo inmuebles destacados
inmueble_lujo bool/int null Solo inmuebles de lujo
fecha_modificacion string null Inmuebles modificados desde una fecha — clave para sincronizaciones incrementales
order string null Campo por el que ordenar
type_order string null Sentido del orden (asc / desc)

La lista vive en la constante pública SearchPropertysRequest::DEFAULT_FILTERS, por si necesitas reutilizarla.

Importante: los valores admitidos por destinacion, clase, order, type_order y el formato exacto de fecha_modificacion los define SoftInm, no el SDK. El SDK no valida ni normaliza: envía lo que le pases. Confirma los valores válidos con el soporte de SoftInm o inspeccionando una respuesta real (examples/06-inspeccionar-respuesta.php).

Filtros desde un formulario web

Descarta los campos vacíos antes de enviarlos: una cadena vacía puede interpretarse como un filtro real.

$filtros = array_filter($request->only([
    'municipio', 'barrio', 'destinacion', 'alcobas',
    'banos', 'preciodesde', 'preciohasta',
]), fn($v) => $v !== '' && $v !== null);

$filtros['cantidadporpagina'] = 12;
$filtros['pagina'] = (int) $request->get('pagina', 0);

$resultado = $softinm->search($filtros);

8. Paginación

La paginación es base 0: la primera página es pagina => 0.

$pagina1 = $softinm->search(['pagina' => 0, 'cantidadporpagina' => 50]); // 1–50
$pagina2 = $softinm->search(['pagina' => 1, 'cantidadporpagina' => 50]); // 51–100

Recorrer todo el inventario

iterate() es un generador: entrega los inmuebles de todas las páginas con memoria constante, sin acumular el inventario completo en un array.

foreach ($softinm->iterate([], 50) as $inmueble) {
    // Tu upsert a base de datos, indexado o export
}

Firma completa:

iterate(array $filtros = [], int $porPagina = 50, int $maxPaginas = 200): \Generator
  • Corta cuando una página llega incompleta: si pediste 50 y llegaron menos, esa era la última.
  • $maxPaginas es un tope de seguridad contra un bucle infinito si la API ignorase el parámetro pagina.
  • Los filtros se conservan en todas las páginas; pagina y cantidadporpagina los gestiona el generador.
// Solo Medellín, 100 por página, máximo 20 páginas
foreach ($softinm->iterate(['municipio' => 'Medellín'], 100, 20) as $inmueble) {
    // …
}

Página a página, cuando necesitas los metadatos

iterate() entrega inmuebles sueltos. Si necesitas la respuesta completa de cada página (totales, paginación), recorre con search():

$pagina = 0;

do {
    $respuesta = $softinm->search(['pagina' => $pagina, 'cantidadporpagina' => 50]);
    $lote = SoftInmoFacade::extractItems($respuesta);

    // … procesa $respuesta completa

    $pagina++;
} while (count($lote) === 50 && $pagina < 200);

Sincronización incremental

Para no descargar todo el inventario cada vez, guarda la marca de tiempo de la última sincronización y usa fecha_modificacion:

foreach ($softinm->iterate(['fecha_modificacion' => $ultimaSync], 100) as $inmueble) {
    // Solo lo que cambió
}

9. La respuesta: qué devuelve search()

SoftInmoFacade::search() devuelve siempre un array: el JSON de SoftInm decodificado de forma recursiva como array asociativo (json_decode(..., true); no hay objetos stdClass).

El SDK no reestructura ni renombra nada: la forma exacta del payload (nombres de campos, si vienen envueltos en data/inmuebles, si hay metadatos de paginación) la define la API de SoftInm y puede variar entre versiones o inmobiliarias.

Inspecciona la respuesta la primera vez que integres

php examples/06-inspeccionar-respuesta.php

Imprime las claves de primer nivel, los campos de un inmueble con sus tipos y un esqueleto de mapeo listo para copiar.

Extraer la lista de inmuebles

El SDK trae el adaptador incluido:

use Homlity\SoftInm\SDK\App\SoftInmoFacade;

$lista = SoftInmoFacade::extractItems($respuesta);

Cubre los envoltorios habituales (data, inmuebles, result, results, items, rows) y las listas planas. Si tu instancia usa otro envoltorio, envuelve esa llamada en un único adaptador de tu aplicación:

final class InventarioSoftInm
{
    public function __construct(private SoftInmoFacadeInterface $softinm) {}

    public function buscar(array $filtros): array
    {
        return SoftInmoFacade::extractItems($this->softinm->search($filtros));
    }
}

Regla práctica: nunca accedas a $respuesta['data'][0]['precio'] disperso por tu código. Pasa siempre por tu adaptador y, si puedes, mapea a un DTO/modelo propio. Es la diferencia entre una integración que sobrevive a un cambio de la API y una que se rompe en producción.

10. Manejo de errores

Desde la versión 2.0, cada fallo tiene su excepción. En la 1.x, un token vencido, un inmo inexistente, un timeout y una búsqueda sin resultados devolvían todos [].

Jerarquía

SoftInmException                 ← captúrala para tratarlos todos igual
  ├── ConfigurationException     ← falta inmo/token, URL base inválida
  ├── AuthException              ← HTTP 401 / 403: credenciales rechazadas
  ├── ApiException               ← otros errores HTTP de SoftInm
  └── UnavailableException       ← timeout, DNS, conexión rechazada

Todas heredan de \RuntimeException y llevan:

  • getMessage() — qué pasó, en lenguaje claro.
  • getCode() — el código HTTP cuando aplica.
  • getContext() — array de diagnóstico: inmo, url, status, intentos. Nunca incluye el token.

Mapa de situaciones

Escenario Respuesta de SoftInm Excepción
Consulta con resultados 200 + JSON — (devuelve los datos)
Filtro sin resultados 200 + vacío — (devuelve [])
Token inválido o vencido 401 AuthException
Sin permiso sobre ese inmo 403 AuthException
Filtro rechazado 4xx ApiException
Error del servidor 5xx ApiException tras reintentar
Demasiadas peticiones 429 ApiException tras reintentar
Timeout, DNS, conexión rechazada UnavailableException tras reintentar
Falta inmo o token ConfigurationException, sin tocar la red

Captura por tipo

use Homlity\SoftInm\SDK\Exceptions\ApiException;
use Homlity\SoftInm\SDK\Exceptions\AuthException;
use Homlity\SoftInm\SDK\Exceptions\ConfigurationException;
use Homlity\SoftInm\SDK\Exceptions\UnavailableException;

try {
    $inmuebles = $softinm->search($filtros);

} catch (ConfigurationException $e) {
    // Error de despliegue: falta una variable de entorno
    throw $e;

} catch (AuthException $e) {
    // El token de esta inmobiliaria caducó. No es culpa del usuario final:
    // alerta a quien pueda renovarlo.
    Log::error('SoftInm rechazó las credenciales', $e->getContext());
    $inmuebles = [];

} catch (UnavailableException $e) {
    // Red o timeout. El SDK ya reintentó; sirve caché si la tienes.
    Log::warning('SoftInm no respondió', $e->getContext());
    $inmuebles = $cache ?? [];

} catch (ApiException $e) {
    // SoftInm respondió, pero con error. Revisa los filtros enviados.
    Log::warning('SoftInm devolvió error', $e->getContext());
    $inmuebles = [];
}

Captura genérica

use Homlity\SoftInm\SDK\Exceptions\SoftInmException;

try {
    $inmuebles = $softinm->search($filtros);
} catch (SoftInmException $e) {
    Log::warning('Fallo consultando SoftInm', $e->getContext());
    $inmuebles = [];
}

searchOrDefault(): para listados públicos

Cuando una web sin inmuebles es preferible a un error 500:

$inmuebles = $softinm->searchOrDefault($filtros);          // [] si falla
$inmuebles = $softinm->searchOrDefault($filtros, $cache);  // o tu copia cacheada

No lanza nunca. Úsalo con logging: un listado vacío inesperado sigue siendo la señal temprana de un token vencido.

Modo compatible con la 1.x

$config->setThrowOnError(false); // cualquier error devuelve []

Válido como paso intermedio en una migración, pero recuerda por qué existía el problema: con esta opción, un token vencido vuelve a ser indistinguible de “no hay inmuebles”.

11. Reintentos, timeouts y logging

Reintentos

Se reintentan los fallos transitorios: 5xx, 429 y errores de red. Un 401 o un 403 nunca se reintentan — una credencial rechazada no mejora sola, y reintentarla solo suma latencia.

$config->setRetries(2)        // 1 intento + 2 reintentos
       ->setRetryDelayMs(200); // backoff lineal: 200 ms, 400 ms

Con setRetries(0) se desactivan.

Timeouts

$config->setTimeout(15)       // espera total por intento
       ->setConnectTimeout(5); // espera para establecer la conexión

Cuidado con el peor caso: el tiempo máximo total es timeout × (retries + 1) más los backoffs. Con los valores por defecto son unos 45 s en el escenario más lento. Ajusta ambos números en conjunto si estás en un request web con límite de tiempo.

Logging

El SDK no depende de psr/log. setLogger() acepta las dos formas habituales:

// Un logger PSR-3 (Monolog, el de Laravel, cualquiera con log())
$config->setLogger($psrLogger);

// O un callable
$config->setLogger(function (string $nivel, string $mensaje, array $contexto) {
    error_log("[softinm][$nivel] $mensaje " . json_encode($contexto));
});

Eventos que emite:

Nivel Cuándo
debug Consulta correcta
warning Reintentando tras un fallo transitorio
error Credenciales rechazadas, error de la API o sin respuesta

El token nunca se escribe en los logs. Si vuelcas la configuración, usa $config->redacted(). Un logger que lance una excepción no tumba la consulta: el SDK lo aísla.

12. Caché: cómo no golpear la API en cada request

Un listado de inmuebles cambia pocas veces al día; una página de resultados puede recibir miles de visitas. Cachea siempre.

use Homlity\SoftInm\SDK\Exceptions\SoftInmException;

function buscarConCache(SoftInmoFacadeInterface $softinm, array $filtros, int $ttl = 900): array
{
    $archivo = sys_get_temp_dir() . '/softinm-' . md5(json_encode($filtros)) . '.json';

    if (is_file($archivo) && (time() - filemtime($archivo)) < $ttl) {
        return json_decode(file_get_contents($archivo), true) ?: [];
    }

    try {
        $datos = $softinm->search($filtros);
    } catch (SoftInmException $e) {
        // stale-if-error: datos viejos antes que ninguno
        if (is_file($archivo)) {
            error_log('[softinm] ' . $e->getMessage() . ' — sirviendo caché obsoleta');
            return json_decode(file_get_contents($archivo), true) ?: [];
        }

        throw $e;
    }

    file_put_contents($archivo, json_encode($datos), LOCK_EX);

    return $datos;
}

Dos ideas que valen oro en producción:

  1. La clave de caché es el hash del filtro. Ordena las claves (ksort) antes de hashear para no duplicar entradas equivalentes.
  2. Stale-if-error. Ahora que el SDK lanza excepciones, la estrategia es explícita: si la consulta falla y hay copia previa, sirve la copia. Un token vencido deja de vaciar la web.

TTL sugerido: 15 minutos para listados públicos, 1 hora para páginas de detalle, 5 minutos si la inmobiliaria publica varias veces al día.

13. Integración con Laravel

El SDK incluye service provider con auto-discovery: no hay que tocar config/app.php.

Publicar la configuración

php artisan vendor:publish --tag=softinm-config
# → config/softinm.php

Variables de entorno

# ── Obligatorias ──────────────────────────────────────────────
SOFTINM_INMO=ElRo
SOFTINM_TOKEN=tu-token-de-softinm

# ── Opcionales ────────────────────────────────────────────────
SOFTINM_BASE_URL=https://zonaclientes.softinm.com/api
SOFTINM_TIMEOUT=15
SOFTINM_CONNECT_TIMEOUT=5
SOFTINM_RETRIES=2
SOFTINM_RETRY_DELAY_MS=200
SOFTINM_THROW_ON_ERROR=true
SOFTINM_LOGGING=true
SOFTINM_LOG_CHANNEL=stack
SOFTINM_CACHE_TTL=900

El provider enchufa el logger de Laravel automáticamente cuando SOFTINM_LOGGING=true.

Qué queda registrado

Enlace Devuelve
SoftInmoFacadeInterface::class La fachada — inyecta esta, es mockeable
SoftInmoFacade::class La misma instancia (singleton)
Config::class La configuración construida desde config/softinm.php
'softinm' Alias de la fachada

Servicio con caché y logging

namespace App\Services;

use Homlity\SoftInm\SDK\App\SoftInmoFacade;
use Homlity\SoftInm\SDK\Contracts\SoftInmoFacadeInterface;
use Homlity\SoftInm\SDK\Exceptions\AuthException;
use Homlity\SoftInm\SDK\Exceptions\SoftInmException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class SoftInmService
{
    public function __construct(private SoftInmoFacadeInterface $softinm) {}

    /**
     * @return array<int, array> Lista plana de inmuebles
     */
    public function buscar(array $filtros = []): array
    {
        ksort($filtros);
        $clave = 'softinm:' . md5(json_encode($filtros));

        return Cache::remember($clave, config('softinm.cache_ttl'), function () use ($filtros) {
            try {
                return SoftInmoFacade::extractItems($this->softinm->search($filtros));
            } catch (AuthException $e) {
                // Esto necesita intervención humana: alerta, no lo escondas
                Log::critical('Token de SoftInm rechazado', $e->getContext());
                throw $e;
            } catch (SoftInmException $e) {
                Log::warning('SoftInm no disponible', $e->getContext());
                return [];
            }
        });
    }
}

Controlador

public function index(Request $request, SoftInmService $softinm)
{
    return response()->json($softinm->buscar($request->only([
        'municipio', 'barrio', 'destinacion', 'alcobas', 'banos',
        'preciodesde', 'preciohasta', 'pagina', 'cantidadporpagina',
    ])));
}

Comando de sincronización

public function handle(SoftInmoFacadeInterface $softinm): int
{
    $procesados = 0;

    foreach ($softinm->iterate([], 100) as $item) {
        // Inmueble::updateOrCreate(['softinm_id' => $item['id']], $this->mapear($item));
        $procesados++;
    }

    $this->info("Sincronizados {$procesados} inmuebles");

    return self::SUCCESS;
}

Multi-inmobiliaria: si cada tenant tiene su propio inmo/token, no uses el singleton del provider. Crea una factoría que reciba el modelo de la inmobiliaria y devuelva una fachada configurada con sus credenciales:

public function paraInmobiliaria(Inmobiliaria $i): SoftInmoFacadeInterface
{
    return SoftInmSDKBuilder::build(
        Config::fromArray(['inmo' => $i->softinm_inmo, 'token' => $i->softinm_token])
    );
}

14. Integración con WordPress

Homlity y muchas inmobiliarias corren sobre WordPress. El patrón es: Composer en el tema o plugin, transients para la caché y un shortcode para pintar.

<?php
/**
 * Plugin Name: Inventario SoftInm
 * Description: Muestra el inventario de la inmobiliaria desde SoftInm.
 */

require_once __DIR__ . '/vendor/autoload.php';

use Homlity\SoftInm\SDK\App\Config;
use Homlity\SoftInm\SDK\App\SoftInmoFacade;
use Homlity\SoftInm\SDK\Exceptions\SoftInmException;
use Homlity\SoftInm\SDK\SoftInmSDKBuilder;

function softinm_client() {
    static $facade = null;

    if ($facade === null) {
        $config = Config::fromArray([
            'inmo'    => defined('SOFTINM_INMO') ? SOFTINM_INMO : '',
            'token'   => defined('SOFTINM_TOKEN') ? SOFTINM_TOKEN : '',
            'timeout' => 10, // WordPress no perdona los requests lentos
            'logger'  => function ($nivel, $mensaje, $contexto) {
                error_log("[softinm][$nivel] $mensaje " . wp_json_encode($contexto));
            },
        ]);

        $facade = SoftInmSDKBuilder::build($config);
    }

    return $facade;
}

/**
 * Uso: [softinm_inmuebles municipio="Medellín" destinacion="Venta" cantidad="12"]
 */
function softinm_shortcode($atts) {
    $atts = shortcode_atts([
        'municipio'   => '',
        'barrio'      => '',
        'destinacion' => '',
        'cantidad'    => 12,
        'pagina'      => 0,
    ], $atts, 'softinm_inmuebles');

    $filtros = array_filter([
        'municipio'         => $atts['municipio'] ?: null,
        'barrio'            => $atts['barrio'] ?: null,
        'destinacion'       => $atts['destinacion'] ?: null,
        'cantidadporpagina' => (int) $atts['cantidad'],
        'pagina'            => (int) $atts['pagina'],
    ], fn($v) => $v !== null);

    $clave = 'softinm_' . md5(wp_json_encode($filtros));
    $datos = get_transient($clave);

    if ($datos === false) {
        try {
            $datos = SoftInmoFacade::extractItems(softinm_client()->search($filtros));
            set_transient($clave, $datos, 15 * MINUTE_IN_SECONDS);
        } catch (SoftInmException $e) {
            error_log('[softinm] ' . $e->getMessage());
            return '<!-- inventario no disponible -->';
        }
    }

    ob_start();
    // Pinta $datos con tu plantilla. Escapa SIEMPRE con esc_html/esc_url.
    include __DIR__ . '/templates/listado.php';
    return ob_get_clean();
}
add_shortcode('softinm_inmuebles', 'softinm_shortcode');

Reglas de oro en WordPress:

  • Define SOFTINM_INMO y SOFTINM_TOKEN en wp-config.php, nunca en el tema ni en la base de datos en texto plano.
  • Usa transients: sin caché, cada visita es una llamada HTTP.
  • Baja el timeout: 10 s es más razonable que 15 en una petición web.
  • Escapa toda salida con esc_html(), esc_attr() y esc_url(). Los datos vienen de una API externa.
  • Si el listado es pesado, considera un cron (wp_schedule_event) que sincronice a un CPT con iterate() en lugar de consultar en tiempo real.

15. Extender el SDK

Todo lo público está detrás de una interfaz en src/Contracts/.

Interfaz Para qué
SoftInmoFacadeInterface La API pública. Inyéctala en tu código en lugar de la clase concreta
SearchRequestInterface La petición de búsqueda
HttpClientInterface El transporte HTTP

Sustituir el transporte

use Homlity\SoftInm\SDK\Contracts\HttpClientInterface;
use Homlity\SoftInm\SDK\Support\HttpResponse;

class GuzzleSoftInmClient implements HttpClientInterface
{
    public function __construct(private \GuzzleHttp\Client $guzzle) {}

    public function post(string $url, array $headers, array $body, int $timeout): HttpResponse
    {
        try {
            $r = $this->guzzle->post($url, [
                'headers'     => $headers,
                'json'        => $body,
                'timeout'     => $timeout,
                'http_errors' => false,
            ]);

            return new HttpResponse(
                $r->getStatusCode(),
                json_decode((string) $r->getBody(), true)
            );
        } catch (\GuzzleHttp\Exception\ConnectException $e) {
            return new HttpResponse(0, null, true, $e->getMessage());
        }
    }
}

$softinm = SoftInmSDKBuilder::build($config, new GuzzleSoftInmClient($guzzle));

Un HttpClientInterface no debe lanzar por errores HTTP ni de red: los reporta en el HttpResponse y deja que el SDK decida si reintenta o traduce a excepción.

En el contenedor de Laravel

$this->app->bind(HttpClientInterface::class, GuzzleSoftInmClient::class);

16. Ejemplos incluidos en el repositorio

Ejemplo Qué demuestra
examples/01-busqueda-basica.php Configurar el SDK y hacer la primera consulta
examples/02-filtros.php Todos los filtros disponibles, con casos de uso reales
examples/03-paginacion.php iterate(), recorrido manual y sincronización incremental
examples/04-cache.php Caché en disco con TTL y stale-if-error
examples/05-manejo-errores.php Las cinco excepciones, reintentos, logging y modo 1.x
examples/06-inspeccionar-respuesta.php Descubrir la estructura real que devuelve tu instancia
composer install
export SOFTINM_INMO="ElRo"
export SOFTINM_TOKEN="tu-token"
php examples/01-busqueda-basica.php

17. Testing

Tests del SDK

composer install
vendor/bin/phpunit                          # todo
vendor/bin/phpunit --testsuite Unitarios    # solo los que no tocan la red
Suite Qué cubre Necesita credenciales
Unitarios (tests/Unit) Config, petición, reintentos, excepciones, fachada, iterate() — con un HttpClientInterface falso No
Integración (tests/Integration) La API real de SoftInm

Para los de integración:

cp tests/testing.env.php.example tests/testing.env.php
# edita el archivo con un inmo y un token válidos
vendor/bin/phpunit --testsuite Integración

Sin ese archivo, los tests de integración se marcan como omitidos (no fallan), así que vendor/bin/phpunit queda en verde en CI sin exponer credenciales. tests/testing.env.php está en .gitignore: nunca lo commitees.

Mockear el SDK en tu aplicación

Depende de la interfaz:

$fake = $this->createMock(SoftInmoFacadeInterface::class);
$fake->method('search')->willReturn(['data' => [['id' => 1]]]);

O prueba el SDK completo sin red inyectando un transporte falso — el mismo que usa la suite del paquete:

use Homlity\SoftInm\SDK\Tests\Doubles\FakeHttpClient;

$cliente = new FakeHttpClient([
    FakeHttpClient::status(500),                    // primer intento falla
    FakeHttpClient::ok(['data' => [['id' => 7]]]),  // el reintento funciona
]);

$sdk = SoftInmSDKBuilder::build($config, $cliente);

$this->assertSame(['data' => [['id' => 7]]], $sdk->search());
$this->assertSame(2, $cliente->contadorPeticiones());

FakeHttpClient ofrece ok(), status() y transportError(), y guarda todas las peticiones recibidas en $cliente->peticiones para que puedas afirmar sobre URL, cabeceras, filtros y timeout.

18. Referencia de API del SDK

Homlity\SoftInm\SDK\SoftInmSDKBuilder

public static function build(Config $config, ?HttpClientInterface $httpClient = null): SoftInmoFacade
public static function make(string $inmo, string $token): SoftInmoFacade

Homlity\SoftInm\SDK\App\Config

public static function fromArray(array $valores): self

// Todos encadenables
public function setToken($token): self
public function setInmo($inmo): self
public function setBaseUrl($baseUrl): self
public function setTimeout($segundos): self
public function setConnectTimeout($segundos): self
public function setRetries($reintentos): self
public function setRetryDelayMs($milisegundos): self
public function setThrowOnError($lanzar): self
public function setLogger($logger): self

public function getToken()
public function getInmo()
public function getBaseUrl(): string
public function getTimeout(): int
public function getConnectTimeout(): int
public function getRetries(): int
public function getRetryDelayMs(): int
public function shouldThrowOnError(): bool
public function getLogger()

public function getSearchUrl(): string   // URL completa del endpoint
public function validate(): void         // lanza ConfigurationException
public function redacted(): array        // configuración con el token enmascarado

Homlity\SoftInm\SDK\App\SoftInmoFacade

Implementa Contracts\SoftInmoFacadeInterface.

public function setConfig(Config $config)
public function getConfig(): ?Config
public function setRequest(SearchRequestInterface $request)

public function search($filters = []): array
public function searchOrDefault($filters = [], array $default = []): array
public function iterate($filters = [], int $porPagina = 50, int $maxPaginas = 200): \Generator

public static function extractItems(array $respuesta): array

Homlity\SoftInm\SDK\InfraStructure\SearchPropertysRequest

Implementa Contracts\SearchRequestInterface.

const DEFAULT_FILTERS = [ /* los 21 filtros */ ];

public function setConfig(Config $config)
public function setHttpClient(HttpClientInterface $httpClient)
public function getLastResponse(): ?HttpResponse
public function run($filter = [])

Homlity\SoftInm\SDK\Support\HttpResponse

public function getStatusCode(): int
public function getBody()                 // mixed
public function getArrayBody(): array
public function isTransportError(): bool
public function getErrorMessage(): string
public function isSuccessful(): bool
public function isAuthError(): bool       // 401 / 403
public function isRetryable(): bool       // 5xx, 429 o error de red

Excepciones

Homlity\SoftInm\SDK\Exceptions\SoftInmException        // base
Homlity\SoftInm\SDK\Exceptions\ConfigurationException
Homlity\SoftInm\SDK\Exceptions\AuthException
Homlity\SoftInm\SDK\Exceptions\ApiException
Homlity\SoftInm\SDK\Exceptions\UnavailableException

public function getContext(): array   // inmo, url, status, intentos

19. Troubleshooting

Síntoma Causa probable Solución
AuthException con HTTP 401 Token vencido o inválido Renuévalo en la Zona Clientes de SoftInm
AuthException con HTTP 403 El token no tiene permiso sobre ese inmo Confirma el código de la inmobiliaria
ConfigurationException Falta inmo o token, o la URL base es inválida Revisa tus variables de entorno
UnavailableException Timeout, DNS o firewall de salida Sube setTimeout(); confirma con SoftInm si hay restricción por IP
ApiException con 5xx persistente SoftInm caído Sirve caché obsoleta; reintenta más tarde
search() devuelve [] sin excepción Realmente no hay resultados para ese filtro Prueba sin filtros para confirmar
Class "Curl\Curl" not found Falta composer install o el autoload Requiere vendor/autoload.php
cURL library is not loaded ext-curl no habilitada Instálala/habilítala en tu php.ini
Caracteres raros en municipios y barrios Doble codificación de UTF-8 Sirve tus páginas como UTF-8; no apliques utf8_encode() sobre datos que ya lo son
Los filtros no parecen aplicarse Valor no reconocido por SoftInm Consulta los valores válidos; el SDK no valida
Composer se queja del nombre del paquete Estás requiriendo homlity/softinm-sdk (nombre del repo) Requiere homlity/sdk-softinm
El request tarda demasiado timeout × (retries + 1) en el peor caso Baja ambos: setTimeout(8)->setRetries(1)

Depurar la petición cruda

$request = new SearchPropertysRequest();
$request->setConfig($config);

try {
    $request->run(['cantidadporpagina' => 1]);
} catch (SoftInmException $e) {
    $r = $request->getLastResponse();

    echo 'HTTP:    ' . $r->getStatusCode() . PHP_EOL;
    echo 'Red:     ' . var_export($r->isTransportError(), true) . PHP_EOL;
    echo 'Mensaje: ' . $r->getErrorMessage() . PHP_EOL;
    echo 'Cuerpo:  ' . json_encode($r->getBody()) . PHP_EOL;
}

O sin PHP:

curl -s -o /dev/null -w "%{http_code}\n" -X POST \
  "https://zonaclientes.softinm.com/api/inmuebles/consultar_inmuebles/$SOFTINM_INMO" \
  -H "Authorization: Bearer $SOFTINM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"cantidadporpagina":1,"pagina":0}'

200 → token válido. 401 → token rechazado.

20. Soporte, contribución y seguridad

Recurso Enlace
Sitio de Homlity https://homlity.com/
Portal de desarrolladores https://homlity.com/desarrolladores/
Repositorio https://github.com/homlity/softinm-sdk
Packagist https://packagist.org/packages/homlity/sdk-softinm
Organización en GitHub https://github.com/homlity
SoftInm — Zona Clientes https://zonaclientes.softinm.com/

Documentación adicional en este repositorio

Documento Contenido
UPGRADE.md Migrar de 1.x a 2.0
CHANGELOG.md Historial de cambios
docs/api-softinm.md Referencia del endpoint, contrato HTTP y comportamiento observado
docs/desarrolladores.html Guía web para desarrolladores, lista para publicar en homlity.com/desarrolladores/
examples/ Seis ejemplos ejecutables

Contribuir

  1. Crea una rama desde main.
  2. Mantén la compatibilidad con PHP 7.4 (es el mínimo declarado).
  3. Añade tests en tests/Unit — sin red — y, si aplica, en tests/Integration.
  4. vendor/bin/phpunit en verde.
  5. Abre un PR describiendo el cambio y su impacto en la API pública.

Convenciones: todo lo público detrás de una interfaz en src/Contracts/, los errores como excepciones tipadas y ningún dato sensible en los logs.

Incidencias y propuestas: https://github.com/homlity/softinm-sdk/issues.

Seguridad

  • El token de SoftInm da acceso al inventario completo de una inmobiliaria. Trátalo como una contraseña.
  • Nunca lo commitees: usa variables de entorno o un gestor de secretos. tests/testing.env.php está en .gitignore por esa razón.
  • El SDK nunca escribe el token en los logs ni en el contexto de las excepciones. Para volcar la configuración, usa $config->redacted().
  • No lo expongas en JavaScript ni en el HTML: todas las llamadas al SDK deben ocurrir en el servidor.
  • Reporta vulnerabilidades de forma privada a través de https://homlity.com/desarrolladores/.

Homlity
Hecho por Homlity · homlity.com/desarrolladores