homlity / sdk-softinm
SDK de Homlity para consultar el inventario de inmuebles de SoftInm (Zona Clientes) desde PHP, Laravel o WordPress.
Requires
- php: >=7.4
- ext-curl: *
- ext-json: *
- ext-mbstring: *
- php-curl-class/php-curl-class: ^11.1
Requires (Dev)
- phpunit/phpunit: ^9.0
Suggests
- illuminate/support: Para el service provider de Laravel (ya incluido en cualquier app Laravel)
- psr/log: Para inyectar un logger PSR-3 con Config::setLogger()
README
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
Tabla de contenidos
- ¿Para qué sirve este SDK?
- El ecosistema de SDKs de Homlity
- Arquitectura
- Requisitos e instalación
- Configuración
- Quick start
- Referencia de filtros de búsqueda
- Paginación
- La respuesta: qué devuelve
search() - Manejo de errores
- Reintentos, timeouts y logging
- Caché: cómo no golpear la API en cada request
- Integración con Laravel
- Integración con WordPress
- Extender el SDK
- Ejemplos incluidos en el repositorio
- Testing
- Referencia de API del SDK
- Troubleshooting
- 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-softinmy publicarlo en los portales consdk-proppit,sdk-fincaraizosdk-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-sdken GitHub, pero el paquete de Composer eshomlity/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_ordery el formato exacto defecha_modificacionlos 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.
$maxPaginases un tope de seguridad contra un bucle infinito si la API ignorase el parámetropagina.- Los filtros se conservan en todas las páginas;
paginaycantidadporpaginalos 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:
- La clave de caché es el hash del filtro. Ordena las claves (
ksort) antes de hashear para no duplicar entradas equivalentes. - 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_INMOySOFTINM_TOKENenwp-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()yesc_url(). Los datos vienen de una API externa. - Si el listado es pesado, considera un cron (
wp_schedule_event) que sincronice a un CPT coniterate()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 | Sí |
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
- Crea una rama desde
main. - Mantén la compatibilidad con PHP 7.4 (es el mínimo declarado).
- Añade tests en
tests/Unit— sin red — y, si aplica, entests/Integration. vendor/bin/phpuniten verde.- 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
tokende 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.phpestá en.gitignorepor 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/.
Hecho por Homlity · homlity.com/desarrolladores