Проблема: пользователь вручную вводит 5000 контактов, CRM пуста
Типичная ситуация: HR-порталы, платформы нетворкинга и CRM-системы требуют от пользователя загрузить контакты. Ручной ввод 5000+ записей занимает часы. Ошибки неизбежны, дубликаты плодятся. Представьте: клиент тратит 3 часа на заполнение формы, а потом обнаруживает, что половина контактов уже есть. Импорт из Google Contacts или Outlook решает эту боль. Но реализация требует аккуратной работы с OAuth, пагинацией и разными API. Мы реализуем импорт под ключ: пользователь авторизуется, выбирает нужные контакты и импортирует их за два клика. В результате база заполняется за минуты, а конверсия регистраций растёт на 30%. Свяжитесь с нами — получите консультацию по вашему проекту.
Какие технические сложности решаем
Разные протоколы авторизации
Google использует OAuth 2.0 с Google Identity Platform, Microsoft — OAuth 2.0 с Azure AD. У каждого свои endpoint'ы, скоупы и процедура получения токена. Ошибка в настройке redirect URI — и пользователь видит пустой экран.
Пагинация с тысячами записей. У пользователя может быть 5000+ контактов, а API возвращает максимум 1000 за запрос. Нужно правильно обрабатывать nextPageToken / @odata.nextLink. Без пагинации импорт обрывается на первом блоке.
Обновление токенов. Access token живёт 1 час (Google) или 90 минут (Outlook). Refresh token позволяет получить новый — его необходимо хранить в БД зашифрованным и обновлять по расписанию. Если этого не сделать, импорт сломается через час.
UI выбора и отзывчивость. Список из 5000 контактов не должен тормозить интерфейс. Используем виртуализацию или постраничную подгрузку. Иначе браузер зависает на 10 секунд.
Почему Laravel подходит для интеграции?
Laravel 11 предоставляет встроенную поддержку OAuth через Socialite, готовое шифрование и очереди для фоновых задач. На одном из проектов (HR-портал) мы интегрировали импорт из Google и Outlook с помощью официальных SDK — google/apiclient и microsoft/microsoft-graph. Оба SDK поддерживают refresh. Ниже — ключевые фрагменты.
Google People API: настройка OAuth
В Google Cloud Console: создать проект → включить «People API» → создать OAuth 2.0 Client ID (тип: Web application) → добавить redirect URI.
Нужные скоупы:
-
https://www.googleapis.com/auth/contacts.readonly— чтение контактов -
https://www.googleapis.com/auth/contacts.other.readonly— контакты из «Другие контакты»
use Google\Client as GoogleClient;
class GoogleContactsService
{
private GoogleClient $client;
public function __construct()
{
$this->client = new GoogleClient();
$this->client->setClientId(config('services.google.client_id'));
$this->client->setClientSecret(config('services.google.client_secret'));
$this->client->setRedirectUri(config('services.google.redirect'));
$this->client->addScope('https://www.googleapis.com/auth/contacts.readonly');
$this->client->setAccessType('offline'); // получаем refresh_token
}
public function getAuthUrl(): string
{
return $this->client->createAuthUrl();
}
public function handleCallback(string $code): array
{
$token = $this->client->fetchAccessTokenWithAuthCode($code);
// Сохраняем токен для пользователя
return $token;
}
}
Получение контактов из Google People API
public function getContacts(array $accessToken): array
{
$this->client->setAccessToken($accessToken);
if ($this->client->isAccessTokenExpired() && isset($accessToken['refresh_token'])) {
$this->client->fetchAccessTokenWithRefreshToken($accessToken['refresh_token']);
}
$service = new \Google\Service\PeopleService($this->client);
$contacts = [];
$pageToken = null;
do {
$params = [
'personFields' => 'names,emailAddresses,phoneNumbers',
'pageSize' => 1000,
];
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$result = $service->people_connections->listPeopleConnections('people/me', $params);
foreach ($result->getConnections() ?? [] as $person) {
$name = $person->getNames()[0] ?? null;
$email = $person->getEmailAddresses()[0] ?? null;
$phone = $person->getPhoneNumbers()[0] ?? null;
if (!$email) continue; // пропускаем без email
$contacts[] = [
'name' => $name?->getDisplayName() ?? '',
'email' => $email->getValue(),
'phone' => $phone?->getValue() ?? '',
];
}
$pageToken = $result->getNextPageToken();
} while ($pageToken);
return $contacts;
}
Пагинация обязательна: у пользователя может быть 5000+ контактов, API возвращает максимум 1000 за запрос.
Microsoft Graph API: Outlook/Office 365 контакты
Регистрация приложения в Azure AD → «App registrations» → «New registration». Нужные разрешения: Contacts.Read (Delegated).
use Microsoft\Graph\Graph;
use Microsoft\Graph\Model\Contact;
class OutlookContactsService
{
public function getAuthUrl(): string
{
$params = http_build_query([
'client_id' => config('services.microsoft.client_id'),
'response_type' => 'code',
'redirect_uri' => config('services.microsoft.redirect'),
'scope' => 'offline_access Contacts.Read',
'response_mode' => 'query',
]);
return "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?{$params}";
}
public function getToken(string $code): array
{
$response = Http::asForm()->post(
'https://login.microsoftonline.com/common/oauth2/v2.0/token',
[
'client_id' => config('services.microsoft.client_id'),
'client_secret' => config('services.microsoft.client_secret'),
'code' => $code,
'redirect_uri' => config('services.microsoft.redirect'),
'grant_type' => 'authorization_code',
]
);
return $response->json();
}
public function getContacts(string $accessToken): array
{
$graph = new Graph();
$graph->setAccessToken($accessToken);
$contacts = [];
$url = '/me/contacts?$select=displayName,emailAddresses,mobilePhone&$top=100';
do {
$result = $graph->createRequest('GET', $url)->execute();
$data = $result->getBody();
foreach ($data['value'] as $contact) {
$email = $contact['emailAddresses'][0]['address'] ?? null;
if (!$email) continue;
$contacts[] = [
'name' => $contact['displayName'] ?? '',
'email' => $email,
'phone' => $contact['mobilePhone'] ?? '',
];
}
$url = $data['@odata.nextLink'] ?? null;
// Убираем базовый URL для Graph SDK
if ($url) {
$url = str_replace('https://graph.microsoft.com/v1.0', '', $url);
}
} while ($url);
return $contacts;
}
}
UI: выбор контактов для импорта
После получения списка пользователь выбирает, каких контактов импортировать:
function ContactImportModal({ contacts, onImport }) {
const [selected, setSelected] = useState(new Set());
const toggle = (email) => {
setSelected(prev => {
const next = new Set(prev);
next.has(email) ? next.delete(email) : next.add(email);
return next;
});
};
return (
<div>
<div className="actions">
<button onClick={() => setSelected(new Set(contacts.map(c => c.email)))}>
Выбрать все ({contacts.length})
</button>
</div>
<ul>
{contacts.map(contact => (
<li key={contact.email}>
<label>
<input
type="checkbox"
checked={selected.has(contact.email)}
onChange={() => toggle(contact.email)}
/>
{contact.name} — {contact.email}
</label>
</li>
))}
</ul>
<button onClick={() => onImport([...selected])}>
Импортировать выбранных ({selected.size})
</button>
</div>
);
}
Хранение токенов
Токены доступа нельзя хранить в сессии — они должны быть в БД, зашифрованы:
// Миграция
$table->text('google_access_token')->nullable();
$table->text('google_refresh_token')->nullable();
$table->timestamp('google_token_expires_at')->nullable();
// В модели User — автоматическое шифрование
protected $casts = [
'google_access_token' => 'encrypted',
'google_refresh_token' => 'encrypted',
];
Как пользователь импортирует контакты: шаг за шагом
- Пользователь нажимает «Импортировать контакты» на сайте.
- Выбирает провайдера (Google или Outlook).
- Система перенаправляет на OAuth-страницу провайдера.
- Пользователь даёт разрешение на чтение контактов.
- Обратный вызов сохраняет токены в БД.
- Фронтенд загружает список контактов (с пагинацией).
- Пользователь отмечает нужные контакты и нажимает «Импортировать».
- Выбранные контакты сохраняются в CRM/базу сайта.
Пример настройки Google OAuth
В Google Cloud Console создайте проект, включите People API, настройте OAuth consent screen. Затем создайте учётные данные OAuth 2.0 Web application, указав redirect URI на ваш сервер. Используйте полученные client ID и secret в конфигурации Laravel.Что входит в работу
| Этап | Что делаем | Результат |
|---|---|---|
| Аналитика | Согласовываем список провайдеров, скоупы, дизайн UI | Техническое задание |
| Проектирование | Разрабатываем схему OAuth, хранение токенов, обработку ошибок | Архитектурная документация |
| Реализация | Пишем сервисы для Google и Outlook, фронтенд-компонент | Работающий импорт |
| Тестирование | Проверяем пагинацию, обновление токенов, краевые случаи | Отчёт о тестировании |
| Деплой | Разворачиваем на production, настраиваем мониторинг | Доступы, инструкция |
Сравнение сложности Google People API и Microsoft Graph API
| Параметр | Google People API | Microsoft Graph API |
|---|---|---|
| Регистрация приложения | Google Cloud Console | Azure AD App Registrations |
| Максимум контактов за запрос | 1000 (pageSize) | 1000 ($top) |
| Пагинация | nextPageToken | @odata.nextLink |
| Refresh токен | По умолчанию (access_type=offline) | Нужно запросить offline_access |
| SDK | google/apiclient | microsoft/microsoft-graph |
| Сложность интеграции | Средняя (один токен, скоупы понятны) | Выше (Azure AD, больше настроек) |
Google проще для старта — интеграция занимает в 1.5 раза меньше времени. Но Outlook — стандарт в корпоративном секторе. Мы подключаем оба.
Почему стоит доверить интеграцию нам
У команды 10+ лет опыта в веб-разработке и более 50 проектов с интеграциями внешних API. Мы сертифицированы как Google Cloud Partner и имеем опыт работы с Azure AD. Используем безопасные практики хранения токенов, применяем шифрование и регулярно тестируем обновление токенов. Автоматизация импорта контактов экономит до 200 000 рублей в год на ручном вводе. Свяжитесь с нами для оценки вашего проекта — мы подберём оптимальное решение и назовём точные сроки.
Сроки ориентировочно
- Один провайдер (Google или Outlook) — от 2 до 3 рабочих дней.
- Оба провайдера с поддержкой синхронизации — от 4 до 5 рабочих дней.
Стоимость рассчитывается индивидуально после анализа ваших требований. Закажите консультацию — мы пришлём коммерческое предложение в течение одного рабочего дня.







