homlity/sdk-mobilia

SDK PHP oficial de Homlity para la API de Mobilia Gestión: autenticación OAuth2, inmuebles, paginación y agentes.

Maintainers

Package info

github.com/homlity/sdk-mobilia

Homepage

Documentation

pkg:composer/homlity/sdk-mobilia

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v2.0.0 2026-08-24 19:59 UTC

This package is auto-updated.

Last update: 2026-08-24 20:08:13 UTC


README

Homlity

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

v2.0.0 PHP 7.4 | 8.x Guzzle ^7.0 88 tests PSR-4 MIT

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 gestionar access_token y 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, properties o results — 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

🏘️ Portal web de la agencia

Listado paginado, ficha de detalle, galería de fotos y buscador por provincia/precio/habitaciones, alimentados en vivo desde el CRM.

🔄 Sincronización nocturna

Un cron que llama a getAllProperties(), vuelca a tu base de datos y regenera el sitemap y el feed XML para portales.

🧩 Plugin de WordPress

Un shortcode [inmuebles provincia="Asturias"] que pinta tarjetas con toSimpleArray() y cachea con transients.

📱 API intermedia (BFF)

Un endpoint Laravel/Slim que expone tu cartera ya simplificada y cacheada a una app móvil o a un front en React.

📊 Informes y BI

Exporta la cartera a CSV/JSON para analizar precios medios por población, rotación o distribución por tipología.

👤 Fichas de agente

Asocia cada inmueble con los datos de su agente comercial (getAgent()) para mostrar contacto real en la ficha.

📦 Instalación

Requisitos

Requisito Versión
PHP 7.4 o superior (compatible con 8.08.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-main es 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: Config se lee en el constructor de SDKFachada. Configura antes de hacer new SDKFachada(). Si faltan las credenciales, el constructor lanza ConfigurationException.

🔒 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 12
API intermedia / BFF 2
Cron, sincronización 35
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 = 100 son 20 llamadas. Úsalo en tareas en background, nunca en el ciclo de una petición web. Sube Config::setTimeout() si tu API responde lento y vigila max_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 $filters de getProperties().

$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 /tokenAuthResponse

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 /inmueblesPropertiesResponse

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() bool200 y con datos de agente
getAgent() mixed|null — contenido de elementos, o null
getBodyArray() mixed — cuerpo completo
getStatusCode() int
isOk() / isSuccessful() bool200

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 siempre isSuccessful() / 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_secret en Mobilia Gestión y purga el historial — pasos en docs/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 GijonGijó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

  1. Crea src/Requests/MiRequest.php con un execute() que use ClientHttpBuilder.
  2. Crea src/Responses/MiResponse.php que reciba (array $data, int $statusCode).
  3. Expón un método en SDKFachada que 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.php no debe commitearse. Ya está en .gitignore.

Prueba de humo manual (requiere credenciales reales y 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 InfraStructureInfrastructure
  • Auto-autenticación en getAgent() y respeto de verifySSL
  • getTotal() reconociendo totalElementos
  • Guardarraíl anti-bucle en getAllProperties()
  • Excepciones tipadas bajo MobiliaException
  • Suite PHPUnit real con MockHandler de Guzzle (88 tests)
  • Reintentos con backoff exponencial y jitter
  • Documentación completa en docs/ y ejemplos en examples/

Siguiente, por orden de impacto:

  • 🔴 Rotar el client_secret expuesto 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

  1. Haz un fork de homlity/sdk-mobilia.
  2. Crea tu rama: git checkout -b feat/nuevo-endpoint.
  3. Respeta PSR-12 y PSR-4; escribe la documentación de los métodos públicos en español.
  4. Añade tests con MockHandler (sin llamadas de red reales).
  5. Nunca incluyas credenciales en el código ni en los tests.
  6. 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_token en logs, respuestas HTTP ni en el frontend.
  • Cachea el token en un almacén de servidor, jamás en localStorage ni 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

Homlity

Hecho con ❤️ por Homlity para la comunidad de desarrolladores inmobiliarios.
homlity.com · Portal de Desarrolladores · GitHub