homlity / sdk-mobilia
SDK PHP oficial de Homlity para la API de Mobilia Gestión: autenticación OAuth2, inmuebles, paginación y agentes.
Requires
- php: ^7.4|^8.0
- ext-json: *
- guzzlehttp/guzzle: ^7.0
Requires (Dev)
- phpunit/phpunit: ^9.5
README
Mobilia SDK para PHP
Integra el CRM inmobiliario Mobilia Gestión en tu aplicación PHP en menos de 10 líneas de código.
🏠 homlity.com · 👩💻 Portal de Desarrolladores · 📦 GitHub Homlity · 🐘 Packagist
Tabla de contenidos
| Sección | Descripción |
|---|---|
| ¿Qué es este SDK? | Para qué sirve y a quién está dirigido |
| Casos de uso | Qué se construye con esto |
| Instalación | Composer, requisitos, autoload |
| Configuración | Credenciales, SSL, timeouts |
| Inicio rápido | Tu primera consulta |
| Guía de uso | Autenticación, inmuebles, paginación, agentes |
| Filtrado y helpers | Filtros en memoria y toSimpleArray() |
| Referencia de la API | Todas las clases y métodos |
| Estructura de datos | El JSON que devuelve Mobilia |
| Recetas | Laravel, WordPress, caché, CLI |
| Estado del SDK | Qué se corrigió en la v2.0.0 y qué sigue vigente |
| Arquitectura | Cómo está construido |
| Contribuir | Cómo colaborar |
Documentación extendida en la carpeta docs/ y ejemplos ejecutables en examples/.
🎯 ¿Qué es este SDK?
homlity/sdk-mobilia es un cliente PHP oficial de Homlity para la API REST de Mobilia Gestión (https://api.mobiliagestion.es/api/v1/), el CRM inmobiliario usado por agencias en España.
El SDK se encarga de la parte aburrida y propensa a errores de hablar con la API:
- 🔐 Autenticación OAuth2 (
client_credentials) con renovación automática del token — te olvidas de gestionaraccess_tokeny su caducidad. - 📄 Paginación manual (
getProperties) o automática (getAllProperties, recorre todas las páginas por ti). - 🧱 Objetos de respuesta tipados en lugar de arrays crudos:
AuthResponse,PropertiesResponse,GetAgentResponse. - 🧹 Normalización defensiva del payload: la API puede devolver los inmuebles en
elementos,data,inmuebles,propertiesoresults— el SDK los detecta todos. - 🔎 Helpers de filtrado y transformación en memoria: por familia, tipo, operación, provincia, precio, habitaciones, fotos…
- 🔁 Reintentos automáticos con backoff exponencial y jitter ante 5xx, 429 y fallos de conexión.
- 🧯 Excepciones tipadas:
ConfigurationException,AuthenticationException,ApiException,InvalidResponseException. - 🌐 Cliente HTTP configurable (Guzzle) mediante un builder: base URI, cabeceras, timeout, verificación SSL, middleware.
¿Para quién es?
| Perfil | Qué gana |
|---|---|
| Agencias inmobiliarias con Mobilia | Publicar su cartera en su propia web sin exportaciones manuales |
| Agencias de desarrollo / freelance | Montar portales, landings y buscadores de inmuebles rápido |
| Portales inmobiliarios | Ingesta de cartera de agencias que usan Mobilia |
| Plugins WordPress / módulos CMS | Widgets, shortcodes y bloques de inmuebles |
| ERPs y SaaS PropTech | Sincronización de catálogo y datos de agentes |
Este SDK forma parte del ecosistema de SDKs de Homlity para desarrolladores inmobiliarios, junto a Wasi PHP 8 SDK, Domus SDK, Finca Raíz SDK, Ciencuadras SDK, Proppit SDK, Softinm SDK, SmartHome SDK y Chat SDK. Todos ellos y su documentación están en homlity.com/desarrolladores.
💡 Casos de uso reales
📦 Instalación
Requisitos
| Requisito | Versión |
|---|---|
| PHP | 7.4 o superior (compatible con 8.0–8.4) |
| Extensión | ext-json (activada por defecto) |
| Extensión recomendada | ext-curl (Guzzle la usa como handler preferente) |
| Composer | 2.x |
| Dependencias | guzzlehttp/guzzle ^7.0 (única en producción) |
Vía Composer
composer require homlity/sdk-mobilia:^2.0
⚠️ Si Composer no encuentra el paquete, es porque el webhook de Packagist no está sincronizando los tags del repositorio. Instálalo entonces vía VCS, que lee los tags directamente de GitHub:
{ "repositories": [ { "type": "vcs", "url": "https://github.com/homlity/sdk-mobilia" } ], "require": { "homlity/sdk-mobilia": "^2.0" } }
Instalación directa desde GitHub
Útil si trabajas contra un fork o una rama concreta:
{
"repositories": [
{ "type": "vcs", "url": "https://github.com/homlity/sdk-mobilia" }
],
"require": {
"homlity/sdk-mobilia": "dev-main"
}
}
composer update homlity/sdk-mobilia
Fija siempre un tag (
^2.0) o un commit en producción.dev-maines una rama en movimiento.
Instalación para desarrollo del propio SDK
git clone https://github.com/homlity/sdk-mobilia.git cd sdk-mobilia composer install cp tests/testing.env.php.example tests/testing.env.php # rellena tus credenciales composer test # 88 tests, sin red
Autoload
El paquete usa PSR-4. Tras instalar, incluye el autoloader de Composer:
require __DIR__ . '/vendor/autoload.php';
| Namespace | Ruta |
|---|---|
Homlity\Mobilia\SDK\ |
src/ |
Homlity\Mobilia\SDK\Tests\ |
tests/ (solo dev) |
⚙️ Configuración
Toda la configuración vive en la clase estática Homlity\Mobilia\SDK\Config\Config.
Parámetros disponibles
| Propiedad | Tipo | Por defecto | Descripción |
|---|---|---|---|
$clientId |
string |
'' |
Client ID que te entrega Mobilia Gestión |
$clientSecret |
string |
'' |
Client Secret de Mobilia Gestión |
$baseUri |
string |
https://api.mobiliagestion.es/api/v1/ |
URL base de la API |
$verifySSL |
bool |
true |
Verificación del certificado TLS |
$timeout |
int |
30 |
Timeout de cada petición, en segundos |
$maxRetries |
int |
2 |
Reintentos ante 5xx, 429 y fallos de conexión |
$retryDelayMs |
int |
250 |
Espera base entre reintentos (exponencial + jitter) |
Los defaults son seguros por diseño: credenciales vacías, para que nunca viajen en el código, y TLS verificado.
🔴 Configuración segura (recomendada)
Nunca escribas tus credenciales dentro de src/Config/Config.php. Ese archivo se versiona en Git y acabaría publicado en GitHub y en Packagist. Configúralas en runtime desde variables de entorno:
<?php require __DIR__ . '/vendor/autoload.php'; use Homlity\Mobilia\SDK\Config\Config; use Homlity\Mobilia\SDK\App\SDKFachada; Config::loadFromEnv(); $sdk = new SDKFachada();
MOBILIA_CLIENT_ID=a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx MOBILIA_CLIENT_SECRET=tu-secret-aqui MOBILIA_VERIFY_SSL=1 MOBILIA_TIMEOUT=30 MOBILIA_MAX_RETRIES=2
loadFromEnv() reconoce MOBILIA_CLIENT_ID, MOBILIA_CLIENT_SECRET, MOBILIA_BASE_URI, MOBILIA_VERIFY_SSL, MOBILIA_TIMEOUT y MOBILIA_MAX_RETRIES. Solo sobrescribe lo que esté definido, así que puedes combinarla con ajustes manuales:
Config::loadFromEnv(); Config::setTimeout(90); // este cron necesita más margen Config::setMaxRetries(4);
📌 Importante:
Configse lee en el constructor deSDKFachada. Configura antes de hacernew SDKFachada(). Si faltan las credenciales, el constructor lanzaConfigurationException.
🔒 Sobre verifySSL
El valor por defecto es true. Desactivarlo es cómodo en un entorno local con un bundle de CAs desactualizado, pero en producción expone la conexión a ataques man-in-the-middle: un atacante en la red puede leer tu client_secret y toda la cartera.
Config::setVerifySSL(true); // ✅ Producción: siempre true
Si al activarlo obtienes cURL error 60: SSL certificate problem, la solución correcta no es desactivarlo, sino instalar un bundle de CAs actualizado y apuntar curl.cainfo / openssl.cafile a él en tu php.ini. Ver docs/02-configuracion.md.
🔁 Reintentos
El cliente reintenta automáticamente ante 5xx, 429 y errores de conexión, con backoff exponencial y jitter. No reintenta ante 400, 401, 403 ni 404: no se arreglan repitiendo.
Config::setMaxRetries(0); // desactivar Config::setMaxRetries(4); // más agresivo, para un cron nocturno
| Escenario | maxRetries sugerido |
|---|---|
| Petición web | 1–2 |
| API intermedia / BFF | 2 |
| Cron, sincronización | 3–5 |
| Tests | 0 |
🚀 Inicio rápido (60 segundos)
<?php require __DIR__ . '/vendor/autoload.php'; use Homlity\Mobilia\SDK\App\SDKFachada; use Homlity\Mobilia\SDK\Config\Config; Config::loadFromEnv(); $sdk = new SDKFachada(); // Primera página, 20 inmuebles. La autenticación ocurre sola. $response = $sdk->getProperties([], 1, 20); if (!$response->isSuccessful()) { exit('Error HTTP ' . $response->getStatusCode()); } foreach ($response->toSimpleArray() as $inmueble) { printf( "[%s] %s — %s (%s) · %s €%s", $inmueble['referencia'], $inmueble['titulo'] ?? $inmueble['tipo'], $inmueble['poblacion'], $inmueble['provincia'], number_format((float) $inmueble['precio'], 0, ',', '.'), PHP_EOL ); } echo "Mostrando {$response->count()} de {$response->getTotal()} inmuebles\n";
Salida esperada:
[1046] Piso reformado en el centro — Oviedo (Asturias) · 145.000 €
[1047] Chalet con jardín — Gijón (Asturias) · 320.000 €
...
Mostrando 20 de 187 inmuebles
▶️ Ejemplo ejecutable: examples/01-quickstart.php
📖 Guía de uso
1. Autenticación
El SDK usa el flujo OAuth2 client_credentials contra POST /token. No necesitas llamarlo manualmente: getProperties(), getAllProperties() y getAgent() autentican automáticamente si no hay token o si está a punto de caducar (margen de seguridad de 300 s).
$auth = $sdk->authenticate(); if ($auth->isSuccessful()) { echo 'Token: ' . $auth->getAccessToken() . PHP_EOL; echo 'Tipo: ' . $auth->getTokenType() . PHP_EOL; // "Bearer" echo 'Expira: ' . $auth->getExpiresIn() . ' s' . PHP_EOL; // 7200 = 2 h echo 'Caduca: ' . $auth->getExpirationDate()->format('Y-m-d H:i:s') . PHP_EOL; } else { // getStatusCode() será 400 o 401 con credenciales incorrectas print_r($auth->getData()); }
Reutilizar un token entre peticiones
Autenticarse en cada request HTTP de tu web es un viaje de red desperdiciado. Guarda el token en tu caché e inyéctalo:
$token = $cache->get('mobilia_token'); if ($token) { $sdk->setAccessToken($token, 7200); // reutiliza } else { $auth = $sdk->authenticate(); // Cachea 100 s menos que su vida real, para tener margen $cache->set('mobilia_token', $auth->getAccessToken(), $auth->getExpiresIn() - 100); } $sdk->clearToken(); // fuerza una re-autenticación en la próxima llamada
2. Listar inmuebles
// Firma: getProperties(array $filters = [], ?int $page = null, ?int $pageSize = null) $todos = $sdk->getProperties(); // sin paginar (lo que devuelva la API) $pagina1 = $sdk->getProperties([], 1, 50); // página 1, 50 por página $filtrado = $sdk->getProperties(['provincia' => 'Asturias'], 1, 25);
Los $filters se envían como query string a GET /inmuebles, junto con pagina y tamanoPagina. Los nombres de los parámetros los define la API de Mobilia; consulta a tu contacto de Mobilia Gestión qué campos admite tu cuenta.
$response = $sdk->getProperties(); $response->isSuccessful(); // bool — ¿HTTP 200? $response->getStatusCode(); // int — código HTTP $response->count(); // int — inmuebles en ESTA respuesta $response->hasProperties(); // bool $response->getProperties(); // array — array de inmuebles crudos $response->getProperty(0); // array|null — por índice $response->getTotal(); // int|null — total en la cartera (reconoce 'totalElementos') $response->getMetadata(); // array — ['totalElementos'=>187,'pagina'=>1,'tamanoPagina'=>50] $response->getRawData(); // mixed — el JSON decodificado tal cual
3. Paginación
Manual (recomendado para web)
$pagina = (int) ($_GET['p'] ?? 1); $porPagina = 24; $response = $sdk->getProperties([], $pagina, $porPagina); $total = (int) ($response->getTotal() ?? 0); $totalPages = $total > 0 ? (int) ceil($total / $porPagina) : 0; echo "Página {$pagina} de {$totalPages} ({$total} inmuebles)";
Automática (recomendado para cron / sincronización)
getAllProperties() recorre todas las páginas y devuelve un único PropertiesResponse con todo combinado:
// Firma: getAllProperties(array $filters = [], int $pageSize = 100, int $maxPages = 500) $todos = $sdk->getAllProperties([], 100); echo "Cartera completa: " . $todos->count() . " inmuebles\n";
El recorrido termina al alcanzar el total declarado, al recibir una página vacía, o al llegar a $maxPages.
⚠️ Hace N peticiones HTTP secuenciales. Con 2.000 inmuebles y
pageSize = 100son 20 llamadas. Úsalo en tareas en background, nunca en el ciclo de una petición web. SubeConfig::setTimeout()si tu API responde lento y vigilamax_execution_time/memory_limit.
▶️ Ejemplo ejecutable: examples/02-paginacion.php
4. Consultar un agente
$agente = $sdk->getAgent(12); if ($agente->hasAgent()) { print_r($agente->getAgent()); // contenido de la clave "elementos" } else { echo 'HTTP ' . $agente->getStatusCode(); }
hasAgent() distingue un 200 con datos de un 200 vacío o un 404. getAgent() devuelve null en lugar de emitir un warning si la clave no existe.
▶️ Ejemplo ejecutable: examples/04-agente.php
5. Uso avanzado: request directo
Si necesitas control fino (una cabecera extra, otro base_uri, un filtro encadenado), salta la fachada y usa las clases Request directamente:
use Homlity\Mobilia\SDK\Requests\GetPropertiesRequest; $request = new GetPropertiesRequest($token); $response = $request ->addFilter('provincia', 'Asturias') ->addFilter('venta', 1) ->setPage(1) ->setPageSize(50) ->disableSSLVerification() // solo en local ->execute();
🔎 Filtrado y transformación de resultados
Concepto clave: estos helpers filtran en memoria, sobre los inmuebles que ya te devolvió la API. No reducen el tráfico de red. Para filtrar en origen, pasa los criterios en el array
$filtersdegetProperties().
$r = $sdk->getProperties([], 1, 100); // Por tipología $r->filterByFamilia('Pisos'); $r->filterByTipo('Ático'); // Por operación: 'venta' | 'alquiler' | 'traspaso' | 'alquilerOpcionCompra' $r->filterByOperation('venta'); // Por ubicación $r->filterByPoblacion('Oviedo'); $r->filterByProvincia('Asturias'); // Por precio: (min, max, 'venta'|'alquiler') $r->filterByPriceRange(100000, 250000, 'venta'); $r->filterByPriceRange(null, 900, 'alquiler'); // hasta 900 €/mes // Por habitaciones: (min, max|null) $r->filterByHabitaciones(3); $r->filterByHabitaciones(2, 4); // Solo con fotos $r->getPropertiesWithPhotos();
Todos devuelven un array (conservando las claves originales — usa array_values() si necesitas reindexar).
Búsquedas puntuales
$r->getPropertyById(1234); // array|null $r->getPropertyByReference('1046'); // array|null
Fotos
$inmueble = $r->getProperty(0); $r->getFeaturedPhoto($inmueble); // string|null — la marcada destacada=1, o la primera $r->getPhotos($inmueble); // array — todas, ordenadas por el campo 'orden'
Encadenar filtros
Los helpers viven en PropertiesResponse, así que para encadenar usa array_filter sobre el resultado:
$chaletsCaros = array_filter( $r->filterByFamilia('Chalets'), fn ($p) => ($p['precioVenta'] ?? 0) > 300000 );
toSimpleArray() — la joya del SDK
Aplana el JSON anidado de Mobilia a una estructura plana lista para una plantilla, una tarjeta o un json_encode:
$simple = $r->toSimpleArray();
[
'id' => 1234,
'referencia' => '1046',
'tipo' => 'Piso',
'familia' => 'Pisos',
'precio' => 145000,
'habitaciones' => 3,
'banos' => 2,
'metrosConstruidos' => 95,
'poblacion' => 'Oviedo',
'provincia' => 'Asturias',
'direccion' => 'Calle Uría',
'latitud' => 43.3619,
'longitud' => -5.8494,
'fotoDestacada' => 'https://.../foto.jpg',
'totalFotos' => 14,
'descripcion' => 'Piso reformado en pleno centro...',
'titulo' => 'Piso reformado en el centro',
]
▶️ Ejemplo ejecutable: examples/03-filtros.php
📚 Referencia completa de clases
App\SDKFachada — punto de entrada
| Método | Devuelve | Descripción |
|---|---|---|
__construct() |
— | Carga credenciales de Config. Lanza ConfigurationException si faltan |
authenticate() |
AuthResponse |
Solicita un token nuevo y lo almacena internamente |
getProperties(array $filters = [], ?int $page = null, ?int $pageSize = null) |
PropertiesResponse |
Lista inmuebles. Autentica sola si hace falta |
getAllProperties(array $filters = [], int $pageSize = 100, int $maxPages = 500) |
PropertiesResponse |
Recorre todas las páginas y las combina |
getAgent(int $idAgent) |
GetAgentResponse |
Datos de un agente. Autentica sola si hace falta |
getAccessToken() |
?string |
Token actual, o null |
setAccessToken(string $token, int $expiresIn = 7200) |
self |
Inyecta un token existente (cacheado) |
clearToken() |
self |
Invalida el token en memoria |
verifiesSSL() |
bool |
¿Verifica esta instancia los certificados TLS? |
Config\Config — configuración estática
| Método | Devuelve |
|---|---|
loadFromEnv() |
void — lee las variables MOBILIA_* |
reset() |
void — restablece los defaults (útil en tests) |
getClientId() / setClientId(string) |
string / void |
getClientSecret() / setClientSecret(string) |
string / void |
getBaseUri() / setBaseUri(string) |
string / void — el setter garantiza la barra final |
shouldVerifySSL() / setVerifySSL(bool) |
bool / void |
getTimeout() / setTimeout(int) |
int / void |
getMaxRetries() / setMaxRetries(int) |
int / void |
getRetryDelayMs() / setRetryDelayMs(int) |
int / void |
Exceptions\* — errores del SDK
\Exception
└── MobiliaException
├── ConfigurationException · credenciales ausentes
├── AuthenticationException · getStatusCode(), getResponseData()
├── ApiException · getStatusCode(), getEndpoint()
└── InvalidResponseException · getStatusCode(), getBodyExcerpt()
Todas extienden \Exception, así que el código que ya capturaba \Exception sigue funcionando.
Responses\AuthResponse
| Método | Devuelve | Notas |
|---|---|---|
getAccessToken() |
?string |
El JWT/token de acceso |
getTokenType() |
?string |
Normalmente Bearer |
getExpiresIn() |
?int |
Segundos de vida (7200 = 2 h) |
getExpirationDate() |
?\DateTime |
Momento absoluto de caducidad |
getData() |
array |
Cuerpo completo |
getStatusCode() |
int |
Código HTTP |
isSuccessful() |
bool |
200 y token no vacío |
Responses\PropertiesResponse
Acceso
getProperties() · getProperty(int) · getPropertyById(int) · getPropertyByReference(string) · getRawData() · getMetadata() · getTotal()
Estado
isSuccessful() · getStatusCode() · count() · hasProperties()
Filtros
filterByFamilia(string) · filterByTipo(string) · filterByOperation(string) · filterByPoblacion(string) · filterByProvincia(string) · filterByPriceRange(?float, ?float, string) · filterByHabitaciones(int, ?int) · getPropertiesWithPhotos()
Fotos y transformación
getFeaturedPhoto(array) · getPhotos(array) · toSimpleArray()
Requests\AuthRequest
| Método | Descripción |
|---|---|
__construct(string $clientId, string $clientSecret) |
— |
setGrantType(string) |
Cambia el grant_type (por defecto client_credentials) |
disableSSLVerification() |
Solo desarrollo |
execute() |
POST /token → AuthResponse |
Requests\GetPropertiesRequest
| Método | Descripción |
|---|---|
__construct(string $accessToken) |
— |
setFilters(array) / addFilter(string, mixed) |
Query params de búsqueda |
setPage(int) / setPageSize(int) |
Envían pagina y tamanoPagina |
disableSSLVerification() |
Solo desarrollo |
execute() |
GET /inmuebles → PropertiesResponse |
Infrastructure\Requests\RequestGetAgent
| Método | Descripción |
|---|---|
__construct(string $accessToken) |
— |
setAgentId(int) |
ID del agente. Obligatorio antes de execute() |
disableSSLVerification() |
Solo desarrollo |
execute() |
GET /agentes/{id} → GetAgentResponse |
Infrastructure\Responses\GetAgentResponse
| Método | Devuelve |
|---|---|
hasAgent() |
bool — 200 y con datos de agente |
getAgent() |
mixed|null — contenido de elementos, o null |
getBodyArray() |
mixed — cuerpo completo |
getStatusCode() |
int |
isOk() / isSuccessful() |
bool — 200 |
Infrastructure\Builders\ClientHttpBuilder
Constructor fluido del cliente Guzzle. Útil si extiendes el SDK con nuevos endpoints.
$client = (new ClientHttpBuilder()) ->setBaseUri('https://api.mobiliagestion.es/api/v1') ->setBearerToken($token) ->setHeader('Accept', 'application/json') ->setHeaders(['X-Origen' => 'mi-app']) ->setTimeout(60) ->setMaxRetries(3) ->setHandlerStack($stackConMiddleware) // logging, mocks en tests ->build();
El cliente se construye con
http_errors => false: Guzzle no lanza excepciones ante 4xx/5xx. Comprueba siempreisSuccessful()/getStatusCode().
📄 Referencia ampliada: docs/06-referencia-api.md
🧬 Estructura de un inmueble
Forma típica de cada elemento de getProperties() (campos observados; tu cuenta puede devolver más):
{
"idInmueble": 1234,
"referencia": "1046",
"familiaInmueble": { "familiaInmueble": "Pisos" },
"tipoInmueble": { "tipoInmueble": "Ático" },
"poblacion": "Oviedo",
"provincia": "Asturias",
"direccionPublica": "Calle Uría",
"latitud": 43.3619,
"longitud": -5.8494,
"venta": 1, // flags de operación (0/1)
"alquiler": 0,
"traspaso": 0,
"alquilerOpcionCompra": 0,
"precioVenta": 145000,
"precioAlquiler": 0,
"caracteristicas": {
"habitaciones": 3,
"banos": 2,
"metrosConstruidos": 95
},
"fotos": [
{ "url": "https://.../1.jpg", "orden": 1, "destacada": 1 },
{ "url": "https://.../2.jpg", "orden": 2, "destacada": 0 }
],
"tituloWeb": { "txtTituloWeb": "Piso reformado en el centro" },
"descripcionAmpliada": { "txtDescripcionAmpliada": "Piso reformado..." }
}
Y la envoltura de la respuesta:
{
"elementos": [ /* ...inmuebles... */ ],
"totalElementos": 187,
"pagina": 1,
"tamanoPagina": 50
}
📄 Diccionario de campos completo: docs/04-inmuebles.md
🍳 Recetas de integración
Recetas completas y comentadas en docs/07-recetas.md. Un aperitivo:
Laravel — Service Provider + caché
// app/Providers/MobiliaServiceProvider.php public function register(): void { $this->app->singleton(SDKFachada::class, function () { Config::setClientId(config('services.mobilia.client_id')); Config::setClientSecret(config('services.mobilia.client_secret')); Config::setVerifySSL(app()->isProduction()); $sdk = new SDKFachada(); if ($token = Cache::get('mobilia_token')) { $sdk->setAccessToken($token); } return $sdk; }); }
// Uso en un controlador public function index(SDKFachada $sdk) { $inmuebles = Cache::remember('inmuebles:p1', 900, fn () => $sdk->getProperties([], 1, 24)->toSimpleArray() ); return view('inmuebles.index', compact('inmuebles')); }
WordPress — shortcode [inmuebles] con transients
add_shortcode('inmuebles', function ($atts) { $atts = shortcode_atts(['provincia' => '', 'limite' => 12], $atts); $key = 'mobilia_' . md5(serialize($atts)); if (false === ($items = get_transient($key))) { Config::setClientId(MOBILIA_CLIENT_ID); Config::setClientSecret(MOBILIA_CLIENT_SECRET); Config::setVerifySSL(true); $r = (new SDKFachada())->getProperties([], 1, (int) $atts['limite']); $items = $r->toSimpleArray(); set_transient($key, $items, HOUR_IN_SECONDS); } ob_start(); foreach ($items as $i) { printf( '<article class="inmueble"><img src="%s" alt="%s" loading="lazy"><h3>%s</h3><p>%s €</p></article>', esc_url($i['fotoDestacada']), esc_attr($i['titulo']), esc_html($i['titulo']), esc_html(number_format((float) $i['precio'], 0, ',', '.')) ); } return ob_get_clean(); });
Cron — sincronización nocturna a base de datos
$sdk = new SDKFachada(); $todos = $sdk->getAllProperties([], 100); $pdo->beginTransaction(); foreach ($todos->toSimpleArray() as $i) { $stmt->execute([ ':id' => $i['id'], ':ref' => $i['referencia'], ':datos' => json_encode($i, JSON_UNESCAPED_UNICODE), ]); } $pdo->commit(); error_log(sprintf('[mobilia] sincronizados %d inmuebles', $todos->count()));
Reintentos con backoff exponencial
function conReintentos(callable $fn, int $intentos = 3) { for ($i = 1; $i <= $intentos; $i++) { try { $r = $fn(); if ($r->isSuccessful()) return $r; if ($r->getStatusCode() < 500) return $r; // 4xx: no reintentar } catch (\GuzzleHttp\Exception\GuzzleException $e) { if ($i === $intentos) throw $e; } usleep((int) (2 ** $i * 250_000)); // 0.5s, 1s, 2s... } return $r ?? null; } $r = conReintentos(fn () => $sdk->getProperties([], 1, 50));
⚠️ Estado del SDK
Inventario honesto: qué se corrigió en la v2.0.0 y qué sigue vigente. Léelo antes de desplegar.
Corregido en la v2.0.0
Si vienes de la v1.0.0 o de dev-main, el inventario tenía catorce puntos abiertos. Estos doce están cerrados; los dos restantes siguen vigentes:
| # | Problema | Estado |
|---|---|---|
| 1 | Credenciales reales commiteadas en src/Config/Config.php |
✅ Fuera del código · ⚠️ requiere rotación manual |
| 2 | Namespace InfraStructure vs carpeta Infrastructure (rompía en Linux) |
✅ Corregido |
| 3 | getAgent() no autenticaba — TypeError sin token previo |
✅ Autentica sola |
| 4 | getAgent() ignoraba verifySSL |
✅ Respeta la configuración |
| 5 | getAllProperties() podía entrar en bucle infinito |
✅ Guardarraíl de página vacía + $maxPages |
| 6 | getTotal() no reconocía totalElementos (la clave de Mobilia) |
✅ Corregido |
| 7 | verifySSL era false por defecto |
✅ Ahora true |
| 9 | src/App/MobiliaFacade.php vacío (0 bytes) |
✅ Eliminado |
| 10 | json_decode sin validar — un cuerpo no-JSON parecía «0 resultados» |
✅ Lanza InvalidResponseException |
| 11 | tests/RequestTest.php no era un test |
✅ Suite real de 88 tests |
| 13 | Sin reintentos ni resiliencia | ✅ Backoff exponencial con jitter |
| 14 | codwelt/helpersman declarada y no usada |
✅ Eliminada |
🔴 Acción manual pendiente: quitar el secreto de la rama actual no lo borra del historial público de Git. Rota el
client_secreten Mobilia Gestión y purga el historial — pasos endocs/08.
Vigente
| # | Severidad | Tema | Cómo convivir con ello |
|---|---|---|---|
| 8 | ℹ️ Diseño | http_errors => false — los 4xx/5xx no lanzan excepción, llegan como respuesta normal |
Comprueba isSuccessful() / isOk() en cada respuesta |
| 12 | 🔵 Bajo | Packagist no refleja los tags — el webhook de GitHub no sincroniza | Instala por VCS o con dev-main hasta que se actualice |
Limitaciones de diseño conscientes:
| Tema | Detalle | Alternativa |
|---|---|---|
Config es estático y global |
No admite dos cuentas en paralelo en el mismo proceso | Reconstruir la fachada entre tenants |
| Los Request construyen su propio cliente | Dificulta mockear el SDK completo | ClientHttpBuilder::setHandlerStack() |
| Filtros en memoria | filterByX() no reduce tráfico |
Filtrar en origen con $filters |
| Sin caché de token integrada | Cada instancia autentica por su cuenta | setAccessToken() + tu caché |
| Comparaciones sin normalizar acentos | Gijon ≠ Gijón |
Normalizar antes de comparar |
| Cobertura de endpoints parcial | Solo token, inmuebles y agentes/{id} |
Añadir un endpoint |
📄 Detalle de cada punto, con el antes y el después: docs/08-errores-y-limitaciones.md
Códigos de estado
| Código | Significado | Qué hacer |
|---|---|---|
200 |
OK | — |
400 |
Petición mal formada / grant_type inválido |
Revisa los parámetros del token |
401 |
Token ausente, caducado o inválido | clearToken() y authenticate() |
403 |
Sin permisos para ese recurso | Verifica el alcance de tus credenciales |
404 |
Recurso inexistente (ej. agente) | Comprueba el ID |
429 |
Demasiadas peticiones | Backoff exponencial |
5xx |
Error del servidor de Mobilia | Reintenta con backoff |
Excepciones
Desde la v2.0.0 el SDK lanza excepciones tipadas, todas bajo Homlity\Mobilia\SDK\Exceptions:
\Exception
└── MobiliaException
├── ConfigurationException
├── AuthenticationException
├── ApiException
└── InvalidResponseException
| Excepción | Cuándo | Métodos propios |
|---|---|---|
MobiliaException |
Base: captúrala para cualquier fallo del SDK | — |
ConfigurationException |
Credenciales ausentes al construir SDKFachada |
— |
AuthenticationException |
No se pudo obtener o renovar el token | getStatusCode(), getResponseData() |
ApiException |
Una página de getAllProperties() devolvió error |
getStatusCode(), getEndpoint() |
InvalidResponseException |
El cuerpo no era JSON válido | getStatusCode(), getBodyExcerpt() |
\InvalidArgumentException |
RequestGetAgent::execute() sin setAgentId() |
— |
\GuzzleHttp\Exception\ConnectException |
DNS, timeout, TLS | — |
\GuzzleHttp\Exception\GuzzleException |
Base de todas las de Guzzle | — |
Como todas heredan de \Exception, el código que ya capturaba \Exception sigue funcionando.
use Homlity\Mobilia\SDK\Exceptions\MobiliaException; try { $r = $sdk->getProperties([], 1, 50); if (!$r->isSuccessful()) { throw new RuntimeException('Mobilia devolvió HTTP ' . $r->getStatusCode()); } } catch (MobiliaException $e) { // Configuración, autenticación, API o respuesta inválida } catch (\GuzzleHttp\Exception\GuzzleException $e) { // Red / TLS / timeout }
🏗️ Arquitectura interna
Tu aplicación
│
▼
┌─────────────────────────────────────────────┐
│ App\SDKFachada (Facade) │
│ · gestiona el ciclo de vida del token │
│ · orquesta requests y paginación │
└───────┬─────────────────────────────────────┘
│ lee ┌──────────────────────┐
├────────────────────────►│ Config\Config │
│ │ credenciales, SSL, │
│ │ baseUri, timeout │
│ construye └──────────┬───────────┘
▼ │ lee
┌──────────────────────────────┐ │
│ Requests\* │ │
│ · AuthRequest │ │
│ · GetPropertiesRequest │ │
│ · Infra\RequestGetAgent │ │
└───────┬──────────────────────┘ │
│ usa │
▼ ▼
┌─────────────────────────────────────────────┐
│ Infrastructure\Builders\ClientHttpBuilder │
│ (Builder) → GuzzleHttp\Client │
└───────┬─────────────────────────────────────┘
│ HTTPS
▼
api.mobiliagestion.es/api/v1/
│
▼ json_decode
┌─────────────────────────────────────────────┐
│ Responses\* │
│ · AuthResponse │
│ · PropertiesResponse (parseo + filtros) │
│ · Infra\GetAgentResponse │
└─────────────────────────────────────────────┘
Estructura de carpetas
sdk-mobilia/
├── src/
│ ├── App/
│ │ └── SDKFachada.php # Fachada principal
│ ├── Config/
│ │ └── Config.php # Configuración estática
│ ├── Exceptions/
│ │ ├── MobiliaException.php # Base de todas las del SDK
│ │ ├── ConfigurationException.php
│ │ ├── AuthenticationException.php
│ │ ├── ApiException.php
│ │ └── InvalidResponseException.php
│ ├── Requests/
│ │ ├── AuthRequest.php # POST /token
│ │ └── GetPropertiesRequest.php # GET /inmuebles
│ ├── Responses/
│ │ ├── AuthResponse.php
│ │ └── PropertiesResponse.php # Parseo, filtros, helpers
│ └── Infrastructure/
│ ├── Builders/ClientHttpBuilder.php # + middleware de reintentos
│ ├── Requests/RequestGetAgent.php # GET /agentes/{id}
│ └── Responses/GetAgentResponse.php
├── docs/ # Documentación extendida
├── examples/ # Scripts ejecutables
├── tests/ # Suite PHPUnit (sin red)
├── composer.json
└── phpunit.xml
Patrones aplicados
| Patrón | Dónde | Por qué |
|---|---|---|
| Facade | SDKFachada |
Una sola clase de entrada; oculta auth, requests y paginación |
| Builder | ClientHttpBuilder |
Configuración fluida y reutilizable del cliente Guzzle |
| Request/Response Object | Requests\* + Responses\* |
Un objeto por endpoint; respuestas con comportamiento, no arrays sueltos |
| Static Config | Config |
Configuración global accesible sin inyección (a costa de testabilidad) |
Endpoints cubiertos
| Método | Endpoint | Clase | Fachada |
|---|---|---|---|
POST |
/token |
AuthRequest |
authenticate() |
GET |
/inmuebles |
GetPropertiesRequest |
getProperties(), getAllProperties() |
GET |
/agentes/{id} |
RequestGetAgent |
getAgent() |
Añadir un endpoint nuevo
- Crea
src/Requests/MiRequest.phpcon unexecute()que useClientHttpBuilder. - Crea
src/Responses/MiResponse.phpque reciba(array $data, int $statusCode). - Expón un método en
SDKFachadaque garantice el token antes de ejecutar.
Guía paso a paso: docs/09-contribuir.md
🧪 Tests
cp tests/testing.env.php.example tests/testing.env.php # rellena client_id y client_secret composer test composer test:coverage # informe HTML en output/code-coverage/
88 tests / 162 aserciones, sin una sola llamada de red: las respuestas se simulan con GuzzleHttp\Handler\MockHandler.
| Archivo | Cubre |
|---|---|
tests/TestFather.php |
Base: carga el entorno, resetea Config, provee un payload de ejemplo |
tests/ConfigTest.php |
Defaults seguros, setters, loadFromEnv(), reset() |
tests/AuthResponseTest.php |
Parseo del token, caducidad, isSuccessful() |
tests/PropertiesResponseTest.php |
Las 6 envolturas, filtros, fotos, toSimpleArray(), getTotal() |
tests/GetAgentResponseTest.php |
Acceso seguro a elementos, hasAgent() |
tests/ClientHttpBuilderTest.php |
Cabeceras, SSL y los reintentos con MockHandler |
tests/SDKFachadaTest.php |
Excepciones de configuración, ciclo de vida del token |
tests/TestFather.php es la clase base: carga tests/testing.env.php como variables de entorno vía putenv(). Extiéndela en tus tests.
tests/testing.env.phpno debe commitearse. Ya está en.gitignore.
Prueba de humo manual (requiere credenciales reales y sí hace llamadas de red):
php examples/99-smoke-test.php
🗺️ Hoja de ruta
Hecho en la v2.0.0
- Credenciales fuera del código, con
Config::loadFromEnv() - Namespace
InfraStructure→Infrastructure - Auto-autenticación en
getAgent()y respeto deverifySSL -
getTotal()reconociendototalElementos - Guardarraíl anti-bucle en
getAllProperties() - Excepciones tipadas bajo
MobiliaException - Suite PHPUnit real con
MockHandlerde Guzzle (88 tests) - Reintentos con backoff exponencial y jitter
- Documentación completa en
docs/y ejemplos enexamples/
Siguiente, por orden de impacto:
- 🔴 Rotar el
client_secretexpuesto y purgar el historial de Git (acción manual) - 🟠 Sincronizar Packagist con los tags del repositorio (webhook de GitHub)
- 🟡 Inyección de dependencias en
SDKFachada, para mockear el SDK completo - 🟡 Soporte PSR-16 para cacheo del token
- 🟡 Iterador perezoso (
Generator) para carteras grandes - 🔵 Normalizar acentos en los filtros de texto
- 🔵 Cobertura de más endpoints de Mobilia (demandas, contactos, promociones)
- 🔵 CI en GitHub Actions (PHPUnit + PHPStan nivel 5) en PHP 7.4–8.4
🤝 Contribuir
- Haz un fork de
homlity/sdk-mobilia. - Crea tu rama:
git checkout -b feat/nuevo-endpoint. - Respeta PSR-12 y PSR-4; escribe la documentación de los métodos públicos en español.
- Añade tests con
MockHandler(sin llamadas de red reales). - Nunca incluyas credenciales en el código ni en los tests.
- Abre un Pull Request describiendo el problema y la solución.
📄 Guía completa: docs/09-contribuir.md
🔐 Seguridad
¿Has encontrado una vulnerabilidad? No abras un issue público. Escribe a desarrollador@codwelt.com o contacta por los canales de homlity.com.
Buenas prácticas al usar este SDK:
- Credenciales solo en variables de entorno o en un gestor de secretos.
Config::setVerifySSL(true)en producción.- No expongas el
access_tokenen logs, respuestas HTTP ni en el frontend. - Cachea el token en un almacén de servidor, jamás en
localStorageni en una cookie accesible por JS. - Escapa siempre (
esc_html,htmlspecialchars, Blade{{ }}) los textos que vengan de la API antes de renderizarlos.
📄 Licencia
Distribuido bajo licencia MIT. Consulta el archivo LICENSE.
🔗 Ecosistema Homlity
Homlity mantiene una familia de SDKs para el ecosistema PropTech, todos instalables vía Composer:
| SDK | Integra con |
|---|---|
| Mobilia SDK (este paquete) | Mobilia Gestión |
| Wasi PHP 8 SDK | Wasi |
| Domus SDK | Domus |
| Finca Raíz SDK | Finca Raíz |
| Ciencuadras SDK | Ciencuadras |
| Proppit SDK | Proppit |
| Softinm SDK | Softinm |
| SmartHome SDK | SmartHome |
| Chat SDK | Mensajería Homlity |
Hecho con ❤️ por Homlity para la comunidad de desarrolladores inmobiliarios.
homlity.com · Portal de Desarrolladores · GitHub