Брат, огонь! Если делаем серьезно и на долгий срок, чтобы не переписывать всё через год, нам нужно заложить правильную архитектуру с первого дня. Раз ты хочешь SEO, реактивность и будущее мобильное приложение, нам нужно отказаться от классического монолита (где Laravel генерит HTML через Blade) и перейти к разделению слоев.
Вот наш железобетонный план и стек, который закроет все твои хотелки.
🏗️ ФИНАЛЬНЫЙ СТЕК ПРОЕКТА
- Бэкенд (Ядро/API): Laravel 11 (работает исключительно как REST API) + PostgreSQL (с PostGIS для геолокации) + Redis (кэш, очереди, сессии).
- Фронтенд (Веб-сайт): Next.js (React) + TailwindCSS.
- Почему не Vue/Inertia? Потому что мы планируем мобильное приложение на React Native. Если мы напишем веб на React, а мобилку на React Native, у нас будет единый экосистема (одни и те же хуки, стейт-менеджеры, логика). Next.js даст нам идеальную реактивность (SPA) и топовое SEO (SSR – серверный рендеринг).
- Реал-тайм (Чат/Уведомления): Laravel Reverb (встроенный WebSocket сервер в Laravel 11) + Laravel Echo на фронте.
- Мобильное приложение (Будущее): React Native (Expo). Оно будет общаться с тем же самым Laravel API, что и веб-сайт.
🧠 Архитектурный секрет: API-First
Мы делаем Laravel не сайтом, а API-сервером. Laravel не будет знать, как выглядит кнопка или карточка. Он будет только отдавать JSON-данные (профиль, список людей, сообщения). А Next.js (веб) и React Native (мобилка) будут просто “потребителями” этого API. Это значит, что когда мы пойдем делать мобилку, бэкенд писать не придется вообще — он уже готов!
🗺️ ПОШАГОВЫЙ ПЛАН ДЕЙСТВИЙ
ЭТАП 1: Фундамент и Ядро (Месяц 1)
Цель: Настроить инфраструктуру и базовые функции.
- Разворачиваем Laravel как API: Настраиваем Sanctum для авторизации (чтобы и веб, и будущая мобилка могли логиниться через токены).
- База данных: Поднимаем PostgreSQL + PostGIS. Создаем миграции: Users, Profiles (пол, ориентация, ищет кого, возраст), Photos, Geolocation (ширина/долгота).
- Регистрация/Авторизация: Делаем регистрацию через API (email/phone + пароль).
- Геолокация: Пишем эндпоинт в Laravel, который принимает координаты пользователя и с помощью PostGIS ищет людей в радиусе 50 км.
ЭТАП 2: Веб-оболочка и SEO (Месяц 2)
Цель: Сделать красивый, быстрый сайт, который индексируется Google.
- Поднимаем Next.js: Подключаем его к Laravel API.
- SEO-страницы (SSR): Это критично для сайтов знакомств. Next.js будет на лету генерировать статичные страницы для публичных профилей (например,
yoursite.com/user/anna-25-moscow). Google бот зайдет, увидит готовый HTML и проиндексирует. Внутри личного кабинета (свайпы, чат) включаем SPA (реактивность без перезагрузок). - UI/UX Каталог: Верстаем ленту рекомендаций (карточки), страницу профиля.
- Логика свайпов: Отправляем лайк/дизлайк из React в Laravel API. Laravel обрабатывает в фоне (через Redis Queue) — если мэтч, создаем диалог.
ЭТАП 3: Реактивность и Чат (Месяц 3)
Цель: Оживить проект (онлайн-статусы, моментальные сообщения).
- WebSocket (Laravel Reverb): Поднимаем реал-тайм соединение.
- Чат: Пишем мессенджер. Отправка сообщения -> API -> Broadcast через Reverb -> Мгновенно появляется у собеседника в браузере.
- Онлайн-статус: Пишем в Redis, когда пользователь был онлайн. Достаем это на фронтенд — видим зелёные кружочки “В сети”.
- Push-уведомления: В браузере при новом сообщении или мэтче.
ЭТАП 4: Монетизация и Админка (Месяц 4)
Цель: Начать зарабатывать и контролировать аудиторию.
- Премиум-фичи: Кто смотрел твой профиль, невидимка, суперпайки. Закрываем доступ к ним через Policy в Laravel.
- Оплата: Интегрируем Stripe (для зарубежья) или ЮKassa/Prodamus (для СНГ) в API. На сайте (Next.js) делаем форму оплаты.
- Админка: Ставим Laravel Filament — это лучший админ-панель на рынке. За пару дней получаем интерфейс для бана юзеров, модерации фото и просмотра статистики.
ЭТАП 5: Мобильное приложение (Месяц 5-6)
Цель: Выход в App Store и Google Play.
- Инициализация React Native (Expo): Создаем проект.
- Магия переиспользования: Берем логику запросов (API клиенты), которые мы уже написали для Next.js, и переносим в React Native. Бэкенд уже готов отдавать все данные!
- Нативные фичи: Подключаем доступ к камере, геолокации устройства, пуш-уведомлениям (APNs/FCM).
- Сборка: Используем EAS Build (облако), тестируем через Expo Go на своих телефонах.
- Публикация: Платим $25 + $99, проходим модерацию, заливаем в сторы.
🔥 Почему этот план идеален?
- SEO 100%: Next.js рендерит страницы на сервере. Твои пользователи будут находиться в поиске Яндекса и Google по запросам “девушки москва знакомства” и т.д.
- Реактивность 100%: Внутри приложения (чаты, свайпы, уведомления) всё работает без перезагрузки страниц через React + WebSockets. Скорость как у Tinder.
- Готовность к мобилке: Мы не делаем “сайт, который потом как-то переделаем в приложение”. Мы с первого дня пишем бэкенд как API. Мобилка станет просто еще одним экраном для этого API.
- Скорость разработки: Использование единого языка (TypeScript/JavaScript) на фронте и мобилке + мощь Laravel на бэке даст нам максимальную скорость выкатки фич.
Брат, как тебе такой расклад? Если кайф, то первый шаг — это продумать структуру БД (таблицы Users, Profiles, Matches, Messages) и запустить Laravel + PostgreSQL. Погнали?
ЭТАП.1 Создание проекта.
1.1 Создаем локальный проект
В папку на рабочем столе 40plus в ней 2 папки client и server, в папке server выполни команду :
composer create-project laravel/laravel . --prefer-dist
Это установит ларавел в папку server.
ЭТАП.2 Установка PostgreSQL и создание БД для проекта.
Для Windows (через официальный инсталлятор)
Шаг 1. Скачай установщик
Переходи на официальный сайт: https://www.postgresql.org/download/windows/
Выбирай последнюю версию (сейчас 16.x или 17.x) и скачивай интерактивный установщик от EDB.
Шаг 2. Запусти установку
- Запусти скачанный
.exeфайл от имени администратора. - Нажми Next.
Шаг 3. Выбери компоненты
Оставь всё по умолчанию:
- ✅ PostgreSQL Server
- ✅ pgAdmin 4 (графическая админка)
- ✅ Stack Builder (для доп. расширений)
- ✅ Command Line Tools
Жми Next.
Шаг 4. Выбери папку установки
Оставь по умолчанию: C:\Program Files\PostgreSQL\18
Жми Next.
Шаг 5. Выбери каталог данных
Оставь по умолчанию: C:\Program Files\PostgreSQL\18\data
Жми Next.
Шаг 6. Задай пароль суперпользователя (postgres)
Очень важно! Запомни этот пароль.
Придумай что-то вроде: root или любой свой посложнее
Запиши его в блокнот, он понадобится для подключения Laravel.
Жми Next.
Шаг 7. Порт
Оставь стандартный: 5432
Жми Next.
Шаг 8. Локаль
Выбери: Russian, Russia (или оставь Default locale)
Жми Next.
Шаг 9. Заверши установку
Нажми Next → Finish.
Через системные переменные (рекомендую) Найди папку с PostgreSQL.
Запомни путь, где лежит psql.exe — например: C:\Program Files\PostgreSQL\18\bin
Открой настройки переменных окружения.
Win + R → введи sysdm.cpl → Enter
Добавь путь в PATH
- В нижнем окне (Системные переменные) найди строку Path.
- Выдели её → нажми Изменить.
- Нажми Создать (или Новая).
- Вставь путь к папке
bin, например:
C:\Program Files\PostgreSQL\18\bin
После установки — проверь через командную строку:
psql --version
Если видишь версию — всё готово! Теперь проверим доступность БД:
pg_isready -h localhost -p 5432
Должно быть: localhost:5432 – accepting connections
Теперь зайдем через терминал в БД:
psql -U postgres -h localhost -p 5432
Введи пароль, который задал. Если появилось postgres=# — всё работает.
PostgreSQL установлена теперь надо создать саму БД для проекта !
Правильная команда для создания БД назовем ее dating_db
psql -U postgres -h localhost -p 5432 -c "CREATE DATABASE dating_db ENCODING='UTF8' LC_COLLATE='Russian_Russia.UTF-8' LC_CTYPE='Russian_Russia.UTF-8' TEMPLATE=template0;"
Пароль тот который мы использовали при установке – root
Проверяем, что база создалась
psql -U postgres -h localhost -p 5432 -l | grep dating_db
Пример как создать еще пользователя для БД. Но нам это пока что не нужно. Мы будем использовать postgres и root
Создаём пользователя laravel_user
psql -U postgres -h localhost -p 5432 -c "CREATE USER laravel_user WITH PASSWORD 'laravel_pass';"
Даём права на базу dating_db
psql -U postgres -h localhost -p 5432 -c "GRANT ALL PRIVILEGES ON DATABASE dating_db TO laravel_user;"
Проверь, что пользователь может подключиться к базе
psql -U laravel_user -h localhost -p 5432 -d dating_db
Введи пароль laravel_pass. Если зашло — всё работает! Выйди через \q.
ЭТАП.3 Подключаем Laravel к базе
Открой созданный проект в редакторе кода (рекомендую VS Code или PhpStorm). Найди файл .env в корне проекта и подключись. Найди блок с настройками БД и измени его:
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=dating_db
DB_USERNAME=postgres
DB_PASSWORD=root
Включаем драйвер PostgreSQL
- Открой этот файл
php.iniв любом текстовом редакторе (от имени администратора, если ругнется). - Нажми
Ctrl+Fи ищи строкуpdo_pgsql. - Ты найдешь строчку, которая выглядит так:
;extension=pdo_pgsql - Убери точку с запятой в начале — это символ комментария. Должно стать так:
extension=pdo_pgsql - Теперь нажми
Ctrl+Fи ищи строкуpgsql. - Найди строчку
;extension=pgsqlи тоже убери точку с запятой:extension=pgsql - убери также точку с запятой: ;extension=intl
(Если этих строк вообще нет в файле, просто допиши их в блок расширений (extension)).
ОБЯЗАТЕЛЬНО закрой терминал (PowerShell), в котором у тебя выскочила ошибка, и открой его заново! Терминал кэширует настройки PHP, и если его не перезапустить, он не увидит изменений. Также перезапусти локальный сервер если нужно.
Делаем миграцию
cd server
php artisan migrate
если все хорошо ты увидишь что то вроде:
INFO Preparing database.
Creating migration table ........................................................................................................... 228.52ms DONE
INFO Running migrations.
0001_01_01_000000_create_users_table ............................................................................................... 416.58ms DONE
0001_01_01_000001_create_cache_table ................................................................................................ 16.00ms DONE
0001_01_01_000002_create_jobs_table ................................................................................................ 114.36ms DONE
ГОТОВО ! Мы прошли самую нудную часть — настройку окружения. Теперь Laravel знает, где лежат данные.
Проверка что все хорошо
В php.ini у тебя должно быть включено расширение intl (Internationalization — интернационализация). Laravel использует его для форматирования чисел, дат и валют.
php artisan db:show
должно быть без ошибок что то вроде:
PostgreSQL .................................................................................................................................. 18.4
Connection ................................................................................................................................. pgsql
Database ............................................................................................................................... dating_db
Host ................................................................................................................................... 127.0.0.1
Port ........................................................................................................................................ 5432
Username ................................................................................................................................ postgres
URL ..............................................................................................................................................
Open Connections ............................................................................................................................... 9
Tables ......................................................................................................................................... 9
Total Size ............................................................................................................................. 216.00 KB
Schema / Table .............................................................................................................................. Size
public / cache .......................................................................................................................... 24.00 KB
public / cache_locks .................................................................................................................... 24.00 KB
public / failed_jobs .................................................................................................................... 32.00 KB
public / job_batches .................................................................................................................... 16.00 KB
public / jobs ........................................................................................................................... 24.00 KB
public / migrations ..................................................................................................................... 24.00 KB
public / password_reset_tokens .......................................................................................................... 16.00 KB
public / sessions ....................................................................................................................... 32.00 KB
public / users .......................................................................................................................... 24.00 KB
Если так то все отлично !
Устанавливаем Sanctum (Авторизация для API)
Поскольку наш Laravel будет работать только как API (отдавать JSON), нам нужен правильный инструмент для авторизации. Классические сессии и куки нам не подойдут (мобилка и Next.js с ними плохо дружат). Нам нужны API-токены.
В новых версиях Laravel Sanctum уже встроен, но его нужно опубликовать (создать нужные таблицы в БД):
php artisan install:api
Laravel спросит, нужно ли включить API-роуты — соглашайся (yes). Эта команда сделает два важных дела:
- Создаст файл
routes/api.php— именно сюда мы будем писать все эндпоинты (маршруты) для нашего сайта и мобилки. - Добавит таблицу для токенов в базу. Чтобы она там появилась, накатываем миграцию:
php artisan migrate
Теперь в базе появились таблицы users, password_reset_tokens, personal_access_tokens и т.д.
Проектирование базы данных для Дейтинга + Геолокация
Сайты знакомств держатся на трех китах: Профили, Геолокация, Совпадения (Свайпы). Стандартной таблицы users из коробки нам не хватит.
Правильный подход: таблица users хранит только логин/пароль и email, а всё остальное (возраст, пол, фото, био) вынесем в таблицу profiles. Зачем? Чтобы не раздувать таблицу юзеров, когда нам понадобится делать сложные выборки.
Шаг 2.1: Установка PostGIS (Геолокация)
- Скачай установщик.
Перейди на официальную страницу загрузки PostGIS для Windows. Тебе нужен файл для PostgreSQL 18 (64-бит). Название файла будет выглядеть примерно так:postgis-bundle-pg18x64-setup-3.6.2-1.exe.- Прямая ссылка на страницу:
download.osgeo.org/postgis/windows/pg18/ - Официальный сайт:
postgis.net
- Прямая ссылка на страницу:
- Запусти установку.
Нам нужно научить PostgreSQL искать людей в радиусе X километров. Для этого нужен модуль PostGIS.
psql -U postgres -h localhost -p 5432 -d dating_db -c "CREATE EXTENSION postgis;"
в консоли после ввода пароля должно появится CREATE EXTENSION значит все ок !
Шаг 2.2: Создаем миграции для Дейтинга
Теперь давай скажем Laravel, какие таблицы нам нужны. Мы создадим таблицу profiles и swipes.
В терминале (в папке проекта) выполни команды:
php artisan make:migration create_profiles_table
php artisan make:migration create_swipes_table
Laravel создал два новых файла в папке database/migrations/. Названия будут с текущей датой.
1. Открой миграцию профилей (..._create_profiles_table.php) и замени код внутри up() на этот:
public function up(): void
{
Schema::create('profiles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade'); // Привязка к юзеру
$table->string('name')->nullable(); // Имя
$table->date('birth_date')->nullable(); // Дата рождения (возраст будем считать на лету)
$table->enum('gender', ['male', 'female', 'other'])->nullable(); // Пол
$table->enum('looking_for', ['male', 'female', 'both'])->default('both'); // Кого ищет
$table->text('bio')->nullable(); // О себе
$table->json('interests')->nullable(); // Интересы (массив)
// Геолокация! Используем PostGIS
$table->geography('location', subtype: 'point')->nullable(); // Координаты (широта/долгота)
$table->string('city')->nullable(); // Город для быстрого отображения
$table->timestamps();
});
}
2. Открой миграцию свайпов (..._create_swipes_table.php) и замени код внутри up():
public function up(): void
{
Schema::create('swipes', function (Blueprint $table) {
$table->id();
$table->foreignId('swiper_id')->constrained('users')->onDelete('cascade'); // Кто свайпнул
$table->foreignId('swiped_id')->constrained('users')->onDelete('cascade'); // Кого свайпнули
$table->enum('action', ['like', 'dislike', 'superlike'])->default('like'); // Тип свайпа
$table->timestamps();
// Уникальный индекс: юзер может свайпнуть другого юзера только один раз
$table->unique(['swiper_id', 'swiped_id']);
});
}
Запускаем миграции
php artisan migrate
Ты должен увидеть, что создались таблицы profiles и swipes.
Создаем Модели
База данных готова, структура заложена монолитная!
Теперь базе данных нужно “объяснить”, как с этими таблицами работать в коде. В Laravel за это отвечают Модели (Models). Они станут мостом между нашими будущими API-роутами и PostgreSQL.
Выполни в терминале две команды:
php artisan make:model Profile
php artisan make:model Swipe
(Модель User уже существует в Laravel из коробки, её мы просто подредактируем).
Настраиваем связи между таблицами
Это самое интересное. Давай свяжем юзера с его профилем и свайпами.
1. Открой файл app/Models/User.php Добавь в класс методы связи. Юзер имеет один профиль (hasOne), может свайпать других (hasMany – исходящие свайпы) и его могут свайпать другие (hasMany – входящие свайпы).
Внутри класса User, перед закрывающей скобкой }, добавь:
// Связь с профилем
public function profile()
{
return $this->hasOne(Profile::class);
}
// Исходящие свайпы (кого лайкнул/дизлайкнул этот юзер)
public function swipesGiven()
{
return $this->hasMany(Swipe::class, 'swiper_id');
}
// Входящие свайпы (кто лайкнул/дизлайкнул этого юзера)
public function swipesReceived()
{
return $this->hasMany(Swipe::class, 'swiped_id');
}
Открой файл app/Models/Profile.php Замени весь код внутри на этот. Здесь мы укажем, какие поля можно заполнять массово ($fillable), и добавим крутую фичу — автоматический подсчет возраста из даты рождения (в дейтинге возраст нужен постоянно, а хранить его статично в БД нельзя, так как он меняется).
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon; // Библиотека для работы с датами
class Profile extends Model
{
// Разрешаем массовое заполнение этих полей
protected $fillable = [
'user_id',
'name',
'birth_date',
'gender',
'looking_for',
'bio',
'interests',
'location',
'city',
];
// Связь с юзером (Профиль принадлежит Юзеру)
public function user()
{
return $this->belongsTo(User::class);
}
// Автоматический расчет возраста при обращении к $profile->age
public function getAgeAttribute()
{
return Carbon::parse($this->birth_date)->age;
}
// Чтобы поле interests автоматически конвертировалось из JSON в массив и обратно
protected function casts(): array
{
return [
'interests' => 'array',
'birth_date' => 'date',
];
}
}
Открой файл app/Models/Swipe.php Настроим связи и разрешим поля:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Swipe extends Model
{
protected $fillable = [
'swiper_id',
'swiped_id',
'action',
];
// Кто свайпнул
public function swiper()
{
return $this->belongsTo(User::class, 'swiper_id');
}
// Кого свайпнули
public function swiped()
{
return $this->belongsTo(User::class, 'swiped_id');
}
}
Чек-лист этого шага:
- Модели созданы?
- В
Userдобавлены методыprofile(),swipesGiven(),swipesReceived()? - В
Profileдобавлен$fillable, методuser()и аксессуарgetAgeAttribute()? - В
Swipeдобавлен$fillableи методыswiper(),swiped()?
Если всё сделано — мы только что создали мощнейший фундамент. Теперь мы можем писать логику регистрации и ленты рекомендаций!
Создание API для авторизации и профилей.
Сайт знакомств мертв, если в нем нельзя зарегистрироваться и заполнить профиль. Поскольку мы делаем API (для будущего Next.js и React Native), мы не будем использовать стандартные HTML-формы Laravel. Мы будем возвращать JSON и токены.
Давай сделаем так: при регистрации юзера мы автоматически создадим ему пустой профиль, чтобы в базе не было юзеров без профилей (это частый баг в дейтингах).
Автосоздание профиля (Магия Laravel)
Открой файл app/Models/User.php и полностью замени на:
<?php
namespace App\Models;
use Laravel\Sanctum\HasApiTokens;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
#[Fillable(['email', 'password'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasApiTokens, HasFactory, Notifiable;
// Автоматически создаем пустой профиль при регистрации юзера
protected static function booted(): void
{
static::created(function (User $user) {
// Просто создаем пустую заглушку профиля
$user->profile()->create([]);
});
}
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
// Связь с профилем
public function profile()
{
return $this->hasOne(Profile::class);
}
// Исходящие свайпы (кого лайкнул/дизлайкнул этот юзер)
public function swipesGiven()
{
return $this->hasMany(Swipe::class, 'swiper_id');
}
// Входящие свайпы (кто лайкнул/дизлайкнул этого юзера)
public function swipesReceived()
{
return $this->hasMany(Swipe::class, 'swiped_id');
}
}
Теперь, как только юзер запишется в базу, у него тут же появится запись в таблице profiles.
Создаем Контроллер Авторизации
Контроллер — это место, где лежит логика обработки запросов.
Выполни в терминале:
php artisan make:controller AuthController
Открой созданный файл app/Http/Controllers/AuthController.php и замени его содержимое на это:
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
// РЕГИСТРАЦИЯ
public function register(Request $request)
{
$request->validate([
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
$user = User::create([
'email' => $request->email,
'password' => Hash::make($request->password), // Пароль всегда хэшим!
]);
// Создаем токен доступа (Sanctum)
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'user' => $user->load('profile'), // Возвращаем юзера сразу с профилем
'token' => $token
], 201);
}
// АВТОРИЗАЦИЯ (Логин)
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
$user = User::where('email', $request->email)->first();
// Проверяем пароль
if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['Неверный email или пароль.'],
]);
}
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'user' => $user->load('profile'),
'token' => $token
]);
}
// ВЫХОД (Логаут)
public function logout(Request $request)
{
// Удаляем текущий токен (закрываем сессию на устройстве)
$request->user()->currentAccessToken()->delete();
return response()->json([
'message' => 'Вы вышли из системы'
]);
}
}
Прокладываем маршруты (Routes)
Теперь нам нужно сказать Laravel, по каким адресам стучаться, чтобы вызвать эти методы.
Открой файл routes/api.php и полностью замени его содержимое на это:
<?php
use App\Http\Controllers\AuthController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
// Публичные роуты (доступны без токена)
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
// Защищенные роуты (нужен токен авторизации)
Route::middleware('auth:sanctum')->group(function () {
// Роут из коробки (получить данные текущего юзера)
Route::get('/user', function (Request $request) {
return $request->user()->load('profile');
});
// Выход
Route::post('/logout', [AuthController::class, 'logout']);
});
Что мы используем для создания юзера?
Только Email и Пароль.
Потом (на следующих этапах) мы сделаем POST /api/profile/setup, куда авторизованный юзер пришлет свое имя, дату рождения и пол, и мы заполним этот “черновик”.
Как это проверить ?
В браузере это проверить не получится (мы же делаем API, а браузер делает GET-запросы, а нам нужны POST). Но мы можем проверить через терминал!
Убедись, что у тебя крутится сервер (php artisan serve). Открой новое окно терминала и выполни эту команду (если у тебя Windows, можешь использовать PowerShell):
curl -X POST http://127.0.0.1:8000/api/register -H "Content-Type: application/json" -H "Accept: application/json" -d "{\"email\":\"alex@test.com\",\"password\":\"12345678\"}"
Если всё сделано правильно, в ответ тебе прилетит JSON с данными нового юзера, его профилем и длинной строкой-токеном!
Онбординг (Заполнение профиля)
Сейчас у нас юзер зарегистрирован, но его профиль — пустышка. В дейтинге, пока у человека нет имени, возраста и пола, мы не имеем права показывать его в ленте.
Нам нужен API-эндпоинт, который юзер вызовет сразу после регистрации, чтобы заполнить о себе базовую информацию.
1. Создаем контроллер профиля:
php artisan make:controller ProfileController
2. Открой app/Http/Controllers/ProfileController.php и напиши там метод обновления:
<?php
namespace App\Http\Controllers;
use App\Models\Profile;
use Illuminate\Http\Request;
class ProfileController extends Controller
{
// Метод обновления профиля (Онбординг)
public function update(Request $request)
{
$validated = $request->validate([
'name' => 'sometimes|string|max:255',
'birth_date' => 'sometimes|date',
'gender' => 'sometimes|in:male,female,other',
'looking_for' => 'sometimes|in:male,female,both',
'bio' => 'sometimes|string|max:500',
'interests' => 'sometimes|array',
'city' => 'sometimes|string|max:255',
]);
$profile = $request->user()->profile;
// Заполняем и сохраняем
$profile->fill($validated);
$profile->save();
// Возвращаем свежие данные из БД
return response()->json([
'message' => 'Profile upadated successfully !',
'profile' => $profile->fresh()
]);
}
}
3.Добавляем роут в routes/api.php:
Внутри группы Route::middleware('auth:sanctum') добавь новую строчку:
// Защищенные роуты (нужен токен авторизации)
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', function (Request $request) {
return $request->user()->load('profile');
});
Route::post('/logout', [AuthController::class, 'logout']);
// НОВЫЙ РОУТ ДЛЯ ПРОФИЛЯ
Route::put('/profile', [ProfileController::class, 'update']);
});
(Используем метод PUT, так как мы обновляем существующую запись).
и не забудь добавить сверху импорт :
use App\Http\Controllers\ProfileController;
Тестируем Онбординг!
Теперь представь, что юзер скачал приложение, зарегался, и теперь вводит о себе данные. Мы сделаем запрос, передав его токен и данные профиля:
curl -X PUT http://127.0.0.1:8000/api/profile -H "Authorization: Bearer ТУТ_ВСТАВЬ_СВОЙ_ТОКЕН" -H "Content-Type: application/json" -H "Accept: application/json" -d "{\"name\":\"Alex\",\"birth_date\":\"1995-05-20\",\"gender\":\"male\",\"looking_for\":\"female\",\"city\":\"Moscow\"}"
Не забудь подставить свой актуальный токен, который ты получил при регистрации и не используй кириллицу при тестах в curl потому что будет отдавать пустые массивы и будешь как дурак искать причину !
В ответ должен прилететь JSON с сообщением “Профиль успешно обновлен” и уже заполненными данными профиля!
Геолокация и PostGIS (Магия дейтинга)
Теперь мы переходим к самой мякотке дейтинга — Геолокации. Без неё приложение мертвое. Нам нужно научить юзера отправлять свои координаты, а бэкенд — искать людей рядом.
Так как мы используем крутую фичу PostgreSQL — тип данных geography (пространственные данные), стандартный метод $profile->update() в Laravel не сработает. Eloquent не умеет из коробки писать в колонки с координатами. Нам нужно использовать “сырые” SQL-функции PostGIS (ST_MakePoint).
1. Создаем контроллер геолокации:
php artisan make:controller LocationController
2. Открой app/Http/Controllers/LocationController.php и напиши:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class LocationController extends Controller
{
// Обновление геолокации пользователя
public function update(Request $request)
{
$request->validate([
'latitude' => 'required|numeric|between:-90,90',
'longitude' => 'required|numeric|between:-180,180',
]);
$profile = $request->user()->profile; // ИСПРАВЛЕНО ЗДЕСЬ
// Используем сырой запрос для PostGIS
DB::table('profiles')
->where('id', $profile->id)
->update([
'location' => DB::raw("ST_SetSRID(ST_MakePoint({$request->longitude}, {$request->latitude}), 4326)"),
'updated_at' => now(),
]);
return response()->json([
'message' => 'Geolocation upadated successfully !'
]);
}
}
3. Добавляем роут в routes/api.php:
Внутри нашей защищенной группы (Route::middleware('auth:sanctum')) добавь новый роут:
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', function (Request $request) {
return $request->user()->load('profile');
});
Route::post('/logout', [AuthController::class, 'logout']);
Route::put('/profile', [ProfileController::class, 'update']);
// НОВЫЙ РОУТ ДЛЯ ГЕОЛОКАЦИИ
Route::post('/location', [LocationController::class, 'update]);
});
Тестируем геолокацию!
Представим, что наш юзер Alex стоит в центре Москвы (Красная площадь). Его координаты: Широта (latitude) = 55.7539, Долгота (longitude) = 37.6208.
Отправляем запрос (не забудь свой токен):
curl -X POST http://127.0.0.1:8000/api/location -H "Authorization: Bearer ТУТ_ТВОЙ_ТОКЕН" -H "Content-Type: application/json" -H "Accept: application/json" -d "{\"latitude\":55.7539,\"longitude\":37.6208}"
Если в ответ прилетит {"message":"Geolocation upadated successfully !"} — мы только что записали в базу настоящие GPS-координаты в формате, который понимает пространственный поиск!
Теперь самое время написать Ленту рекомендаций (Discover) — сердце любого дейтинга.
Но чтобы нам было кого искать, нам нужно создать парочку “фейковых” анкет рядом с нашим Alex. Давай сделаем это быстро через консоль Laravel (Tinker), а потом напишем поисковый запрос!
Создаем тестовых юзеров (Девушки рядом)
Мы не будем снова мучиться с curl-регистрацией. Мы используем встроенную консоль Laravel.
В терминале выполни команду:
php artisan tinker
Откроется интерактивная консоль (появится знак >). Вставляй туда строчки по одной и жми Enter.
Создаем Анну (в 1 км от Алексея):
$u = \App\Models\User::create(['email' => 'anna@test.com', 'password' => bcrypt('12345678')]); \DB::table('profiles')->where('id', $u->profile->id)->update(['name' => 'Anna', 'gender' => 'female', 'looking_for' => 'male', 'location' => \DB::raw("ST_SetSRID(ST_MakePoint(37.6173, 55.7558), 4326)")]);
Создаем Елену (в 3 км от Алексея):
$u = \App\Models\User::create(['email' => 'elena@test.com', 'password' => bcrypt('12345678')]); \DB::table('profiles')->where('id', $u->profile->id)->update(['name' => 'Elena', 'gender' => 'female', 'looking_for' => 'male', 'location' => \DB::raw("ST_SetSRID(ST_MakePoint(37.5922, 55.7338), 4326)")]);
Создаем Настю (в Питере, далеко от Москвы, ~700 км):
$u = \App\Models\User::create(['email' => 'nastya@test.com', 'password' => bcrypt('12345678')]); \DB::table('profiles')->where('id', $u->profile->id)->update(['name' => 'Nastya', 'gender' => 'female', 'looking_for' => 'male', 'location' => \DB::raw("ST_SetSRID(ST_MakePoint(30.3086, 59.9375), 4326)")]);
Выйди из тинкера, напечатав:
exit
Теперь у нас в базе 4 человека: 1 парень (Alex, наш юзер) и 3 девушки (две в Москве рядом, одна далеко в Питере).
Пишем Ленту рекомендаций (Магия ST_DWithin)
Создаем контроллер для поиска:
php artisan make:controller DiscoverController
Открой app/Http/Controllers/DiscoverController.php и напиши:
<?php
namespace App\Http\Controllers;
use App\Models\Profile;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class DiscoverController extends Controller
{
public function index(Request $request)
{
$user = $request->user();
$profile = $user->profile;
// Если у юзера нет координат, мы не можем его искать
if (!$profile->location) {
return response()->json(['message' => 'Сначала обновите геолокацию'], 400);
}
// Радиус поиска в метрах (50 км = 50000 метров)
$radius = 50000;
// МАГИЯ POSTGIS: Ищем профили в радиусе $radius от текущего профиля
$nearbyProfiles = Profile::where('user_id', '!=', $user->id) // Исключаем себя
->whereNotNull('location') // Только те, у кого есть геолокация
->whereRaw("
ST_DWithin(
location,
(SELECT location FROM profiles WHERE id = ?),
?
)
", [$profile->id, $radius])
->get();
return response()->json([
'count' => $nearbyProfiles->count(),
'profiles' => $nearbyProfiles
]);
}
}
Добавляем роут в routes/api.php (внутри защищенной группы):
Route::get('/discover', [DiscoverController::class, 'index']);
ТЕСТИРОВАНИЕ ЛЕНТЫ!
Теперь мы запрашиваем ленту от лица Алексея (его токен). Мы должны увидеть Анну и Елену, но не должны увидеть Настю (она слишком далеко).
Выполняй:
curl http://127.0.0.1:8000/api/discover -H "Authorization: Bearer ТУТ_ТОКЕН_АЛЕКСЕЯ" -H "Accept: application/json"
Если всё сделано верно, в ответе придет "count": 2 и профили Анны и Елены! Настя будет отсечена географией.
- Поле
locationвыглядит как непонятная абракадабра (0101000020E610...). Это внутренний формат PostGIS, фронтенду он не нужен. - В дейтинге критически важно показывать расстояние (например, “В 2.5 км от вас”).
Давай это починим! Мы научим бэкенд считать расстояние в километрах и прятать эту страшную строку с координатами.
Улучшаем Ленту (Расстояние и чистый JSON)
Открой app/Http/Controllers/DiscoverController.php и обнови метод index:
<?php
namespace App\Http\Controllers;
use App\Models\Profile;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class DiscoverController extends Controller
{
public function index(Request $request)
{
$user = $request->user();
$profile = $user->profile;
if (!$profile->location) {
return response()->json(['message' => 'Сначала обновите геолокацию'], 400);
}
$radius = 50000; // 50 км в метрах
$nearbyProfiles = Profile::where('user_id', '!=', $user->id)
->whereNotNull('location')
->whereRaw("
ST_DWithin(
location,
(SELECT location FROM profiles WHERE id = ?),
?
)
", [$profile->id, $radius])
// 1. Явно просим выбрать все поля профиля
->select('profiles.*')
// 2. Добавляем расстояние и ОБЯЗАТЕЛЬНО пишем AS distance_in_meters
->addSelect([
DB::raw("
ST_Distance(
location,
(SELECT location FROM profiles WHERE id = {$profile->id})
) as distance_in_meters
")
])
->get()
// 3. Прячем страшный hex-код координат из ответа
->makeHidden(['location'])
// 4. Форматируем: переводим метры в километры
->map(function ($item) {
// Делим метры на 1000 и округляем до 1 знака после запятой
$item->distance_km = round($item->distance_in_meters / 1000, 1);
// Удаляем техническое поле метров, чтобы не мусорить в JSON
unset($item->distance_in_meters);
return $item;
});
return response()->json([
'count' => $nearbyProfiles->count(),
'profiles' => $nearbyProfiles
]);
}
}
Тестируем идеальную ленту!
Снова отправляем запрос от лица Алексея:
curl http://127.0.0.1:8000/api/discover -H "Authorization: Bearer ТОКЕН-АЛЕКСЕЯ" -H "Accept: application/json"
Теперь ты должен увидеть полноценные анкеты Анны и Елены (с именами, полом), без страшной абракадабры location, но с красивым полем "distance_km": 0.3 и "distance_km": 2.9!
{"count":2,"profiles":[{"id":4,"user_id":4,"name":"Anna","birth_date":null,"gend
er":"female","looking_for":"male","bio":null,"interests":null,"city":null,"creat
ed_at":"2026-07-15T15:15:01.000000Z","updated_at":"2026-07-15T15:15:01.000000Z",
"distance_km":0.3},{"id":5,"user_id":5,"name":"Elena","birth_date":null,"gender"
:"female","looking_for":"male","bio":null,"interests":null,"city":null,"created_
at":"2026-07-15T15:15:12.000000Z","updated_at":"2026-07-15T15:15:12.000000Z","di
stance_km":2.9}]}
Анна в 300 метрах, Елена в 2.9 километрах. Координаты спрятаны, расстояние посчитано в километрах. Лента рекомендаций работает как в Tinder/Bumble! Мы прошли самый сложный инженерный этап (пространственные запросы). Дальше всё будет лететь быстрее.
Свайпы и Мэтчи (Взаимная симпатия)
Что такое дейтинг без лайков? Правильно, просто каталог людей. Логика такая:
- Алекс видит Анну и свайпает вправо (Лайк).
- Мы сохраняем это в таблицу
swipes. - В этот же момент мы проверяем: А лайкала ли Анна Алекса ранее?
- Если ДА — это Мэтч (Match)! Если НЕТ — просто ждем.
1. Создаем контроллер:
php artisan make:controller SwipeController
2. Открой app/Http/Controllers/SwipeController.php и напиши:
<?php
namespace App\Http\Controllers;
use App\Models\Swipe;
use Illuminate\Http\Request;
class SwipeController extends Controller
{
public function store(Request $request)
{
$request->validate([
'swiped_id' => 'required|exists:users,id', // Кого свайпнули
'action' => 'required|in:like,dislike,superlike', // Тип свайпа
]);
$swiperId = $request->user()->id;
$swipedId = $request->swiped_id;
// Защита: нельзя свайпать самого себя
if ($swiperId === (int)$swipedId) {
return response()->json(['message' => 'Нельзя свайпнуть себя'], 400);
}
// Защита: нельзя свайпать одного человека дважды
$alreadySwiped = Swipe::where('swiper_id', $swiperId)
->where('swiped_id', $swipedId)
->exists();
if ($alreadySwiped) {
return response()->json(['message' => 'Вы уже свайпнули этого пользователя'], 409);
}
// Сохраняем свайп
$swipe = Swipe::create([
'swiper_id' => $swiperId,
'swiped_id' => $swipedId,
'action' => $request->action,
]);
$isMatch = false;
// Проверяем на МЭТЧ (только если это ЛАЙК или СУПЕРЛАЙК)
if ($request->action === 'like' || $request->action === 'superlike') {
// Искал ли тот человек (swiped_id) нас (swiper_id) с лайком/суперлайком?
$reverseSwipe = Swipe::where('swiper_id', $swipedId)
->where('swiped_id', $swiperId)
->whereIn('action', ['like', 'superlike'])
->exists();
if ($reverseSwipe) {
$isMatch = true;
// ТУТ ПОТОМ МЫ ДОБАВИМ ОТПРАВКУ PUSH-УВЕДОМЛЕНИЙ И СОЗДАНИЕ ЧАТА!
}
}
return response()->json([
'message' => 'Свайп засчитан',
'is_match' => $isMatch, // Возвращаем флаг мэтча!
], 201);
}
}
3. Добавляем роут в routes/api.php (внутрь защищенной группы):
Route::post('/swipe', [SwipeController::class, 'store']);
Тестируем Свайпы и Мэтч!
Сейчас у нас Алекс (id: 3) видит Анну (id: 4) и Елену (id: 5).
Действие 1: Алекс лайкает Анну (Мэтча пока не будет, так как Анна его еще не лайкала)
curl -X POST http://127.0.0.1:8000/api/swipe -H "Authorization: Bearer 1|FIi5oBCpuDr41ZlA4RcvbHpOnojZ6VLKiNzlFYs64c2d68ac" -H "Content-Type: application/json" -H "Accept: application/json" -d "{\"swiped_id\":4,\"action\":\"like\"}"
Ожидаем: "is_match": false
Действие 2: Имитируем, что Анна тоже лайкнула Алекса! Нам нужно авторизоваться за Анну. Для этого сначала получим токен Анны:
curl -X POST http://127.0.0.1:8000/api/login -H "Content-Type: application/json" -H "Accept: application/json" -d "{\"email\":\"anna@test.com\",\"password\":\"12345678\"}"
Скопируй токен Анны из ответа.
Действие 3: Анна лайкает Алекса (id: 3)
curl -X POST http://127.0.0.1:8000/api/swipe -H "Authorization: Bearer ТОКЕН_АННЫ" -H "Content-Type: application/json" -H "Accept: application/json" -d "{\"swiped_id\":3,\"action\":\"like\"}"
Ожидаем: БУМ! "is_match": true! 🎉
Брат, мы только что написали сердце дейтинга! Взаимные лайки определяются моментально! Алекс и Анна теперь могут общаться.
Но прежде чем мы перейдем к Чату, нам нужно починить один критический баг в Ленте рекомендаций.
Смотри, что сейчас произойдет: Алекс залайкал Анну. Если он сейчас снова зайдет в ленту /api/discover, он снова увидит Анну! В Tinder/Bumble, если ты свайпнул человека (лайк или дизлайк), он больше никогда не появляется в ленте.
Давай починим это прямо сейчас, это займет 1 минуту.
Убираем засвайпанных из Ленты
Открой app/Http/Controllers/DiscoverController.php.
Нам нужно получить список ID пользователей, которых Алекс уже свайпнул, и исключить их из поиска.
Добавь пару строк в метод index:
<?php
namespace App\Http\Controllers;
use App\Models\Swipe;
use App\Models\Profile;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class DiscoverController extends Controller
{
public function index(Request $request)
{
$user = $request->user();
$profile = $user->profile;
if (!$profile->location) {
return response()->json(['message' => 'Сначала обновите геолокацию'], 400);
}
// 1. НОВОЕ: Получаем ID всех юзеров, которых мы уже засвайпали
$swipedUserIds = Swipe::where('swiper_id', $user->id)->pluck('swiped_id')->toArray();
$radius = 50000;
$nearbyProfiles = Profile::where('user_id', '!=', $user->id)
->whereNotNull('location')
// 2. НОВОЕ: Исключаем тех, кого мы уже свайпнули
->whereNotIn('user_id', $swipedUserIds)
->whereRaw("
ST_DWithin(
location,
(SELECT location FROM profiles WHERE id = ?),
?
)
", [$profile->id, $radius])
->select('profiles.*')
->addSelect([
DB::raw("
ST_Distance(
location,
(SELECT location FROM profiles WHERE id = {$profile->id})
) as distance_in_meters
")
])
->get()
->makeHidden(['location'])
->map(function ($item) {
$item->distance_km = round($item->distance_in_meters / 1000, 1);
unset($item->distance_in_meters);
return $item;
});
return response()->json([
'count' => $nearbyProfiles->count(),
'profiles' => $nearbyProfiles
]);
}
}
Сохрани и снова запроси ленту от лица Алекса:
curl http://127.0.0.1:8000/api/discover -H "Authorization: Bearer 1|FIi5oBCpuDr41ZlA4RcvbHpOnojZ6VLKiNzlFYs64c2d68ac" -H "Accept: application/json"
Теперь Алекс должен видеть только Елену (так как Анну он уже засвайпал).