Search by

plusest / site

plusest-app

Заготовка сайту-бази нерухомості: синхронізація обʼєктів з CRM Plusest та рендеринг на власному сайті

Package info

github.com/plusest-app/site

pkg:composer/plusest/site

Statistics

Installs: 10

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.0.5 2026-09-22 08:13 UTC

This package is auto-updated.

Last update: 2026-09-22 08:16:10 UTC


README

Заготовка сайту-бази нерухомості: забирає обʼєкти з CRM Plusest у власну базу MySQL, завантажує фотографії та показує їх відвідувачам вашого сайту — списком із фільтром і окремою сторінкою на кожну публікацію.

Це розділ для вашого сайту, а не окремий сайт. Ядро лежить у vendor/ і оновлюється через Composer, а конфіг і шаблони копіюються у ваш проєкт — правити їх під свій дизайн можна вільно, оновлення пакета їх не зачепить. У шаблонах header.php і footer.php ви вставляєте шапку й підвал свого сайту, і розділ виглядає його частиною.

Демо: https://an.plusest.app/base/

Вимоги

  • PHP 7.3 або новіший (працює і на 8.x)
  • MySQL 5.6+ / MariaDB 10.x
  • Розширення PHP: pdo_mysql, curl, json, mbstring, gd (або imagick)

Встановлення

Є два шляхи. Вибирайте той, що вам ближче.

Шлях 1: ZIP-архів і встановлення в браузері

  1. Завантажте архів plusestSite-*.zip і розпакуйте його на хостинг. Беріть архів із останнього релізу: https://github.com/plusest-app/site/releases
  2. Налаштуйте вебсервер так, щоб каталог plusestSite/public/ віддавався за адресою вашого вибору — див. docs/nginx.md.
  3. Створіть базу даних MySQL у панелі хостингу.
  4. Відкрийте в браузері адресу установника, наприклад site.com/base/install/, і пройдіть чотири кроки.

Усі залежності вже лежать в архіві, composer install виконувати не потрібно.

Перший крок установника попросить ключ доступу. Установник запише його у файл plusestSite/data/installKey.txt — відкрийте цей файл тим самим FTP-клієнтом чи файловим менеджером, яким заливали архів, і скопіюйте ключ у форму. Так ніхто, крім вас, не встановить заготовку, навіть якщо вгадає адресу.

Каталог public/install/ після встановлення можна не видаляти: пройти його без ключа неможливо, а перенастроїти базу даних чи розміри фотографій через форму значно зручніше, ніж правити config.php по FTP. Повторний запуск попереджає, що config.php буде перезаписаний.

Перша синхронізація в браузері не запускається: завантаження кількох тисяч фотографій триває довше, ніж хостинг дозволяє тримати HTTP-запит. Останній крок установника показує готові рядки для cron — саме планувальник зробить перший прогін.

Шлях 2: Composer і консоль

Перейдіть до каталогу проекту та виконайте:

composer require plusest/site
php vendor/bin/plusestSite install

Установник створить каталог plusestSite/ з такою структурою:

plusestSite/
├── config.php            єдиний файл, який треба заповнити
├── install/schema.sql    схема бази даних
├── templates/            шаблони сторінок — правте під свій дизайн
├── data/                 моделі, лог, lock-файл — НЕ віддавати з вебу
└── public/               те, що віддає вебсервер
    ├── index.php         front controller: усі сторінки розділу
    ├── cron.php          запуск синхронізації за URL (за потреби)
    ├── install/          веб-установник
    ├── assets/           site.css і site.js
    └── photos/           завантажені фотографії

Далі:

# 1. Заповніть у config.php секцію db та feed.url
# 2. Створіть таблиці
php vendor/bin/plusestSite migrate --config=plusestSite/config.php

# 3. Завантажте моделі даних (підписи до полів обʼєкта)
php vendor/bin/plusestSite models --config=plusestSite/config.php

# 4. Перевірте, що все на місці
php vendor/bin/plusestSite check --config=plusestSite/config.php

Налаштування вебсервера — docs/nginx.md, готові рядки для cron — у кінці config.php.

Оновлення синхронізації за розкладом

Заготовка нічого не забирає з CRM сама. Запускати синхронізацію можна двома способами:

  • звичайний cron — рядки для crontab наведені в кінці config.php;
  • виклик URL за розкладом — якщо панель хостингу вміє лише це. Заповніть cron.token у конфізі, і синхронізація запускатиметься запитом на public/cron.php?token=…. Без токена цей файл відповідає 404.

Завдання cron ставиться раз на 15 хвилин, а вирішує, звертатись цього разу до CRM чи ні, сам скрипт — CRM обмежує частоту запитів. Розклад задається в секції sync файлу config.php:

Коли Інтервал за замовчуванням
Пн-пт у денні години (dayFrom–dayTo) intervalDay, 2 години
Ніч і вихідні intervalNight, 6 годин
Після відмови CRM 8 годин, змінити не можна

Години та дні тижня рахуються в поясі з налаштування timezone — того самого, у якому час пишеться в базу й у лог. Якщо залишити його порожнім, PHP візьме пояс із php.ini, а в CLI (де працює cron) це зазвичай UTC, і вікно dayFrom–dayTo припаде на інші години. Перевірити, у чому саме живе синхронізація, можна командою sync state — вона показує час сервера і пояс першим рядком.

Уночі та у вихідні обʼєкти майже не публікують, тому там інтервал більший. Останнє правило спрацьовує, коли CRM відповіла відмовою — невірне посилання, не оплачений доступ, забагато запитів: довбати сервер щочверть години, поки причину не усунуто, немає сенсу.

Часті прогони потрібні не для фіда, а для фотографій: саме вони дозавантажуються тоді, коли до CRM звертатись ще рано. Ключ --force обходить розклад разово — знадобиться, коли ви щойно виправили налаштування.

Окремого завдання для моделей даних не потрібно: їх оновлює той самий прогін раз на добу. Посилання на моделі й термін їхньої свіжості в config.php не виносяться: моделі описують поля самої CRM, і підмінювати їх немає для чого — див. константи в класі Plusest\Site\Model\ModelStore.

Команди

Команда Призначення
install [каталог] [--force] Копіює шаблони у проєкт і генерує config.php. Без --force наявні файли не перезаписуються
migrate --config=ШЛЯХ Створює таблиці за schema.sql. Безпечно запускати повторно
check --config=ШЛЯХ Перевіряє конфіг, оточення PHP, каталоги, моделі даних і підключення до бази
sync --config=ШЛЯХ [--force] Забирає фід, оновлює базу й завантажує фотографії. --force не чекає розкладу
models --config=ШЛЯХ [--force] Оновлює моделі даних обʼєкта. Без --force свіжі копії не перезавантажуються
state --config=ШЛЯХ Час останнього прогону, підсумки, помилки й останні рядки логу
version Показує версію заготовки

Сторінки для відвідувача

Усі адреси розділу обробляє public/index.php, а верстка лежить у каталозі templates/. Три сторінки:

  • список обʼєктів — картки з фільтром, сортуванням і пагінацією;
  • сторінка публікації — галерея, параметри, характеристики, опис, адреса й контакти агента;
  • обране — обʼєкти, які відвідувач відклав сердечком.

Обране

Список обраного зберігається в браузері відвідувача (localStorage), а не на сервері: реєстрації в розділі немає, а тримати вибір анонімного відвідувача на сервері означало б ставити йому ще одну куку й відповідати за ці дані.

Тому сторінка /base/favorites/ працює у два кроки: сервер віддає порожній каркас, скрипт читає список і запитує /base/favorites/?ids=…, а сервер відповідає готовою версткою карточок. Рендерить її той самий partials/cards.php, що й у пошуку, — переробили картку під свій дизайн, вона й тут виглядатиме так само.

Обʼєкти, яких сервер більше не знайшов (їх зняли з продажу), скрипт прибирає зі списку сам, щоб той не ріс вічно.

Адреси

Фільтри стоять у шляху парами «ключ-значення»:

/base/operation-sale/type-house,apartment/rooms-2,3/floor-2_5/
priceUsd-50000_150000/cityId-ixi7l922xsyfctdasd292ldygxtossi8/sort-priceUp/page-2/
  • кілька значень — через кому: commerceType-office,shop
  • діапазон — через підкреслення: floor-2_5
  • діапазон з одного боку — порожня межа: floors-_9 (не вище девʼятого)
  • логічні значення — словом, а не одиницею: whole-part, newBuilding-yes
  • сторінка публікації: /base/object-6899c00963679e68c44d93c7/
  • обране: /base/favorites/

Локації передаються ідентифікаторами CRM, без транслітерації назв: місто чи вулицю можуть перейменувати, а ідентифікатор залишиться, і збережені відвідувачем посилання не зламаються.

Розбір адреси суворий: невідомий ключ, повторений ключ або сміття у значенні — це 404. Якщо ту саму вибірку можна записати коротше (page-1, переставлені місцями частини), сторінка віддає 301 на канонічну адресу. Так одна вибірка завжди має одну адресу.

Поля фільтра

Операція, тип нерухомості й ціна стоять на видноті, решта — у блоці «Більше параметрів», який виїжджає знизу й закривається кліком поза ним.

Поле Вибір Показується
операція одне значення завжди
тип нерухомості одне значення завжди
ціна у вибраній валюті діапазон завжди
весь обʼєкт / частина обʼєкта одне значення завжди
тип комерції кілька лише для комерції
призначення ділянки кілька лише для ділянок
тип автомісця кілька лише для автомість
новобуд, зданий в експлуатацію одне значення квартири, будинки, комерція
кількість кімнат кілька квартири, будинки, комерція
загальна площа діапазон усе, крім ділянок
площа ділянки діапазон ділянки й будинки
поверх, поверховість діапазон усе, крім ділянок
населений пункт кілька завжди
район міста кілька лише де є райони
станції метро, відстань до них кілька лише де є метро

Операція й тип нерухомості — саме одиничний вибір: пошук «продаж або оренда одночасно» не має сенсу, бо в цих двох випадків різні ціни й різні очікування. А ще від одиничного вибору залежить решта фільтра — саме за вибраним типом приховуються поля, які для нього безглузді: кімнати й поверхи для земельної ділянки, тип комерції для квартири.

Значення для списків фільтра беруться з самої бази: показуємо лише те, що справді є в обʼєктах, а список із одного значення не показуємо взагалі. Тому фільтр «метро» зʼявляється лише у Києві, Харкові та Дніпрі — там, де метро є.

Мова й валюта

Вибір відвідувача зберігається в куках, а перемикачі — це посилання з параметром (?lang=ru), після якого відбувається редирект на ту саму адресу. Ціна приходить із CRM одразу в трьох валютах, тому перемикання валюти нічого не перераховує — просто показує інший стовпець.

Що правити під свій сайт

Файл Для чого
templates/header.php, templates/footer.php шапка й підвал вашого сайту
templates/layout.php підключення шрифтів, бібліотек і власних стилів
templates/partials/nav.php смужка з обраним і перемикачами
templates/list.php, templates/partials/card.php вигляд списку й карточки
templates/partials/cards.php сітка карточок: скільки в рядок
templates/object.php сторінка публікації
templates/partials/gallery.php галерея фотографій
templates/partials/agent.php картка агента й продаючий текст
templates/partials/filter.php поля фільтра
templates/favorites.php розділ обраного
templates/lang.php (створіть самі) власні тексти інтерфейсу
public/assets/site.css оформлення
public/assets/site.js обране, карусель, галерея, логіка фільтра

Шаблони — звичайні PHP-файли без власного синтаксису. Дані для них готує клас Plusest\Site\Web\Present: він розшифровує машинні значення з CRM за моделями даних і віддає готові пари «назва — значення».

Оформлення

Розділ не потребує Bootstrap чи іншого фреймворка — site.css самостійний. З CDN підключаються лише вузькі бібліотеки: шрифти Exo 2 і Electrolize, іконки Flaticon UIcons, випадні списки Select2, карусель фотографій Owl Carousel, галерея Fancybox і розкладка характеристик Masonry. Select2 та Owl Carousel написані як плагіни jQuery, тому в layout.php підключений і він.

Прибрати з layout.php можна будь-яку з них: site.js перед кожним викликом перевіряє, чи бібліотека є на сторінці. Без Owl Carousel картка покаже одну фотографію, без Fancybox клік по фото відкриє файл звичайним посиланням, без Select2 фільтр залишиться зі звичайними select. Розділ працює в усіх випадках. Якщо на вашому сайті jQuery вже є — приберіть його рядок, але залиште Select2 та Owl Carousel після свого jQuery.

Майже все оформлення зібране у CSS-змінних у блоці :root на початку site.css. Замініть у ньому --pl-accent на колір свого сайту й --pl-font на свій шрифт — і розділ стане частиною вашого дизайну без правки решти файлу.

Іконки взяті з набору Flaticon UIcons «thin straight» (fi-ts-*). Двох загальних назв у цьому наборі немає — fi-ts-marker і fi-ts-heart, — тому для адреси використано fi-ts-location-alt, а сердечко «в обране» намальоване вбудованим SVG (templates/partials/heart.php): йому все одно потрібні два стани — контур і залите, — а це заливка SVG, не глиф шрифту.

Як це працює

  1. Cron запускає bin/sync.php, той забирає JSON-фід за посиланням публікації з CRM.
  2. Для кожної публікації рахується хеш hashData — усіх її даних, разом зі списком фотографій. Збігся з тим, що в базі, — рядок не перезаписується.
  3. Змінені обʼєкти оновлюються. Фотографії — окремий крок: він порівнює hashPhotos (що повинно бути на диску) з hashPhotosDisk (що там реально є) і завантажує лише різницю, масштабуючи її в розміри з config.php. Позначка про завершення ставиться тільки після того, як файли записані, тому обірваний прогін продовжується з того самого місця.
  4. Публікації, яких більше немає у фіді, залежно від sync.missingMode позначаються реалізованими, ховаються з пошуку або видаляються разом із фотографіями.
  5. Якщо співробітник зник зі складу агентства — його звільнили, і всі його публікації видаляються безповоротно, незалежно від sync.missingMode. Обʼєкт із контактами звільненого агента на сайті показувати не можна.
  6. Той самий прогін раз на добу оновлює дві JSON-моделі даних, за якими шаблони підписують параметри та характеристики обʼєктів.

Структура таблиць

  • objects — публікації обʼєктів. Один рядок = одна публікація (objectPublicationId), а не обʼєкт: у CRM для одного обʼєкта можна створити кілька публікацій, тому objectId у таблиці може повторюватись. Параметри для фільтрації лежать в окремих стовпцях, а повний JSON обʼєкта — у стовпці data, саме з нього рендеряться сторінки. Фотографії лежать у photos/objects/{objectPublicationId}/{idФото}-{назваРозміру}.jpg.
  • objectMetro — станції метро публікацій, рядок на станцію. Ті самі дані є у стовпці objects.metro, але фільтр «поруч зі станціями A, B, C у межах 800 метрів» по JSON робився б повним перебором таблиці, а тут це JOIN по індексу. Таблицю заповнює синхронізація.
  • staff — співробітники агентства, звʼязок з обʼєктами через agentId. Фотографія співробітника лежить у photos/staff/{agentId}/{photo}.jpg — на розміри вона не ріжеться, бо картка агента показує один портрет.
  • syncState — службові значення: час останнього прогону, версії моделей.

Якщо сторінки відкриваються повільно

Увімкніть у config.php сводку налагодження:

'debug' => [
    'summary' => true,
],

Після цього в кінець кожної сторінки — після закритого </html> — дописується HTML-комментар із хронометражем. Відвідувач його не бачить: він видний лише у початковому коді сторінки (Ctrl+U) і в curl.

<!--
plusest debug

  запит: GET /base/operation-sale/
  усього: 1843.2 ms
  база: 641.7 ms у 16 запитах, підключення 12.4 ms
  php: 1201.5 ms

  етапи (свій час | від початку)
        18.1 ms |       18.1 ms  підготовка (конфіг, тексти, обʼєкти)
       980.2 ms |      998.3 ms  вибірка обʼєктів
       ...

  запити (час | рядків)
       1.    412.7 ms |      30  SELECT o.*, s.name FROM `objects` o ...
       ...
-->

Так одразу видно, де саме час: у підключенні до MySQL, в окремому запиті, у рендерингу шаблонів чи ще до початку роботи заготовки. Сторінка списку робить близько двох десятків запитів — сам пошук, підрахунок кількості й по запиту на кожен список фільтра, — і сводка показує кожен окремо.

На робочому сайті вимикайте: у сводці видно текст SQL-запитів.

Збірка релізного архіву

Для розробки самого пакета:

composer install --no-dev --optimize-autoloader
php build/makeRelease.php

Отримаєте build/plusestSite-{версія}.zip — самодостатній каталог із вкладеним vendor/, готовий до розпакування на хостинг.

Ліцензія

MIT