plusest / site
Заготовка сайту-бази нерухомості: синхронізація обʼєктів з CRM Plusest та рендеринг на власному сайті
Requires
- php: >=7.3
- ext-curl: *
- ext-json: *
- ext-mbstring: *
- ext-pdo: *
- intervention/image: ^2.7
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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-архів і встановлення в браузері
- Завантажте архів
plusestSite-*.zipі розпакуйте його на хостинг. Беріть архів із останнього релізу: https://github.com/plusest-app/site/releases - Налаштуйте вебсервер так, щоб каталог
plusestSite/public/віддавався за адресою вашого вибору — див. docs/nginx.md. - Створіть базу даних MySQL у панелі хостингу.
- Відкрийте в браузері адресу установника, наприклад
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, не глиф шрифту.
Як це працює
- Cron запускає
bin/sync.php, той забирає JSON-фід за посиланням публікації з CRM. - Для кожної публікації рахується хеш
hashData— усіх її даних, разом зі списком фотографій. Збігся з тим, що в базі, — рядок не перезаписується. - Змінені обʼєкти оновлюються. Фотографії — окремий крок: він порівнює
hashPhotos(що повинно бути на диску) зhashPhotosDisk(що там реально є) і завантажує лише різницю, масштабуючи її в розміри зconfig.php. Позначка про завершення ставиться тільки після того, як файли записані, тому обірваний прогін продовжується з того самого місця. - Публікації, яких більше немає у фіді, залежно від
sync.missingModeпозначаються реалізованими, ховаються з пошуку або видаляються разом із фотографіями. - Якщо співробітник зник зі складу агентства — його звільнили, і всі його
публікації видаляються безповоротно, незалежно від
sync.missingMode. Обʼєкт із контактами звільненого агента на сайті показувати не можна. - Той самий прогін раз на добу оновлює дві 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