Открыт к предложениям

Fullstack-разработка.
10+ лет проектирование и развитие web интерфейсов, онлайн-сервисов на основе PHP и реактивных фреймворков.

final class Konstantin implements Developer
{
    public array $stack = [
        'PHP 8', 'Yii2', 'Laravel',
        'Vue 3', 'React', 'TypeScript',
        'MySQL', 'MariaDB', 'PostgreSQL',
        'Linux', 'Docker', 'Git',
    ];

    public int $experience = 10;
    public bool $remote = true;
    public bool $openToWork = true;
}
10+лет коммерческого опыта
8 летразвивал один продукт с нуля
100%удалённый формат
// чем занимаюсь

От схемы БД до интерфейса

🏗️

Архитектура и рефакторинг

Слоистая архитектура внутри монолита: сервисы, use case’ы, сущности, слушатели событий. Поэтапный рефакторинг без остановки продукта и без переписывания с нуля.

🗄️

Базы данных

Схема, нормализация, индексы, тяжёлые запросы, бэкапы в облако.

🔌

API и интеграции

REST API, платёжные системы, Mango Office через WebSocket, Telegram Bot API.

⚡

Frontend на Vue.js

SPA для клиентов и сотрудников: личные кабинеты, веб-мессенджер с обменом файлами, загрузка и обработка изображений, генерация PDF/Word-документов.

// код и подход

Как я пишу код

Бизнес-логика живёт в сервисах, данные — в сущностях, выборки — в finder’ах. Каждый сценарий стараюсь покрывать тестами. Ниже — фрагменты из реальных проектов.

1. Слои внутри монолита

app/Core
Core/
├─ Attributes
├─ Entities       // сущности
├─ Events         // события
├─ Finders        // выборки
├─ Helpers
├─ Http
├─ Infrastructure
├─ Jobs
├─ Mails
├─ Providers
├─ ReadServices
├─ Services       // сценарии
├─ Validators
└─ ValueObjects   // типы-значения
ServicesБизнес-сценарии: один метод — один сценарий (например, MangoService).
EntitiesСущности предметной области: Call, Employee.
ValueObjectsТипизированные значения вместо «голых» строк: NationalPhone, MangoCallId.
FindersВыборки из БД вынесены отдельно: CallFinder, EmployeeFinder.
EventsСобытия, например сообщения в WebSocket (UserEvent).

2. Сервис: сценарий в одном месте

Проверки, создание сущности и уведомление — в одном читаемом методе. Атрибут #[TestClass] связывает метод с его тестом.

Services/Mango/MangoService.php
class MangoService
{
    #[TestClass(AppearedTest::class)]
    public function appeared(MangoCallId $entryId, NationalPhone $phoneFrom,
                             NationalPhone $phoneTo, CarbonImmutable $appearedAt): void
    {
        if (CallFinder::hasByMangoId($entryId->getValue())) {
            throw new \DomainException('Звонок уже добавлен.');
        }

        if ($phoneFrom->getValue() == $phoneTo->getValue()) {
            throw new \DomainException('Номера не могут быть одинаковыми.');
        }

        $legal = LegalFinder::getByPhone($phoneTo->getValue());
        $call  = Call::createNew($legal, $entryId, $phoneFrom, $phoneTo, $appearedAt);

        $physIds = EmployeeFinder::findAllPhysIdWithCallAccess($legal);
        $this->sendWebSocketMessage(WebSocketAction::add, $physIds, $call);
    }
}

3. Тесты на каждый сценарий

Интеграционные тесты проверяют и ошибки, и успешный путь: запись в БД и событие, ушедшее в WebSocket.

tests/Integration/Core/Services/Mango/AppearedTest.php
#[CoversMethod(MangoService::class, 'appeared')]
class AppearedTest extends TestCase
{
    public function testSamePhones()
    {
        $this->expectException(\DomainException::class);
        $this->expectExceptionMessage('Номера не могут быть одинаковыми.');

        $this->service->appeared(
            new MangoCallId(Str::random(40)),
            new NationalPhone(79112223344),
            new NationalPhone(79112223344),
            now()->toImmutable(),
        );
    }

    public function testSuccess()
    {
        $legal = Legal::factory()->withPhone(['phone' => $phoneTo])->create();
        Employee::factory()->create(['legal_id' => $legal->id, 'can_call' => true]);
        // ... мок диспетчера событий, вызов сервиса

        $this->assertDatabaseCount(Call::getTableName(), 1);
        $this->assertEquals($phoneFrom, $call->phone_from);
        $this->assertEquals(WebSocketAction::add, $captured->data['action']);
    }
}
terminal
$ make test

   PASS  Tests\Integration\Core\Services\Mango\AppearedTest
  ✓ already exists
  ✓ same phones
  ✓ invalid legal id
  ✓ success

   PASS  Tests\Integration\Core\Services\Mango\ConnectedTest
  ✓ call not found
  ✓ employee not found
  ✓ employee call access
  ✓ success

   PASS  Tests\Integration\Core\Services\Mango\SummaryTest
  ✓ call not found
  ✓ invalid call missed status
  ✓ success with call missed
  ✓ success with call completed

  Tests:  12 passed

4. Yii2: ЧПУ-правила с редиректами

Абстрактный базовый класс для «человеко-понятных» URL. Наследнику остаётся реализовать только поиск сущности по slug. Старые адреса сами делают 301-редирект на актуальные (код сокращён).

frontend/config/router/rules/AbstractBaseSlugRule.php
abstract class AbstractBaseSlugRule implements UrlRuleInterface
{
    abstract protected function getRoutes(): array;
    abstract protected function findEntityBySlug(string $slug): ?SlugRuleEntity;
    abstract protected function findSlugByOldSlug(string $slug): ?string;

    public function parseRequest($manager, $request)
    {
        // /route?slug=steam  →  /route/steam
        $this->redirectIfQuerySlugPresent($manager, $request);

        $slug = $this->validateRequestUrl($request, $matchedRoute);
        
	if (!$slug) { 
	    return false; 
	}

        $entity = $this->findEntityBySlug($slug);
        
	if ($entity !== null) {
            return [$matchedRoute, [$entity->getName() => $entity->getValue(), 'slug' => $slug]];
        }

        // старый slug → 301 на актуальный
        if ($oldSlug = $this->findSlugByOldSlug($slug)) {
            $this->toRedirect($manager, $oldSlug, $matchedRoute, $request);
            return;
        }

        // удалённая сущность → 301 на указанный адрес
        $deleted = $this->findDeletedEntityBySlug($slug);
        
	if ($deleted instanceof RedirectInterface) {
            $this->toRedirectByUrl($deleted->getRedirectTarget());
            return;
        }

        $this->notFoundResponse();
    }
}

5. React: переиспользуемый компонент

Модальное окно с плавным появлением и исчезновением без сторонних библиотек: компонент остаётся в DOM на время анимации, ширина задаётся пропсом, а таймер очищается в cleanup-функции useEffect.

components/Modal.tsx
import './Modal.css';
import React, { useEffect, useState } from "react";

type TProps = {
    title: string;
    widthPercent?: number;
    isShow: boolean;
    onChange: Function;
    children: React.ReactNode;
}

export default function Modal({ title, widthPercent = 33, isShow, onChange, children } : TProps) {
    const [visible, setVisible] = useState<boolean>(false);
    const [render, setRender] = useState<boolean>(false);

    useEffect(() => {
        if (!isShow) {
            setVisible(false);
            setTimeout(() => setRender(false), 400);
            return;
        }

        setRender(true);
        const timer = setTimeout(() => setVisible(true), 50);
        return () => clearTimeout(timer);
    }, [isShow]);

    return render && (
        <div className={`app-modal-wrapper ${visible ? 'show' : ''}`}>
            <div className='app-modal' style={widthPercent ? {width: `${widthPercent}%`} : undefined}>
                <div className="app-modal-header">
                    <h5 className="app-modal-title">{title}</h5>
                    <button type="button" className='app-modal-close' onClick={() => onChange(false)}>
                        <span aria-hidden="true">×</span>
                    </button>
                </div>
                <div className="app-modal-body">{ children }</div>
            </div>
        </div>
    )
}
// навыки

Технологии

Backend

PHP 8.xYii2LaravelООПSOLIDDDD-принципы

Базы данных

MySQLMariaDBPostgreSQLEXPLAINТранзакции и блокировки

Инфраструктура

Docker ComposeLinuxNginxGitRedis

Тестирование

PHPUnitCodeception

Frontend

Vue.js 2/3ReactJavaScriptTypeScriptjQuery
// опыт

Где работал

Fullstack-разработчик

ФЕВ 2026 — ПО НАСТОЯЩЕЕ ВРЕМЯ
Текущий проект · Laravel + React
  • Разрабатываю проект на Laravel (backend) и React (frontend).

Fullstack-разработчик

НОЯ 2017 — ЯНВ 2026 · 8 ЛЕТ 3 МЕС
ООО «ФИНЭК-КОНСАЛТ-АУДИТ»
  • Внутренняя ERP/CRM: заказы, клиенты, документы, личные кабинеты. Единственный разработчик, поднял сервис с нуля.
  • Стек: PHP / Yii2 Advanced, Vue.js 3, MariaDB, Docker Compose, Nginx.
  • Роли и права доступа, несколько независимых кабинетов с общей бизнес-логикой.
  • Интеграции: платёжные системы, Mango Office, Telegram Bot API, архивация файлов в Yandex Cloud.

Backend-разработчик (Yii2)

АПР 2025 — НОЯ 2025 · 8 МЕС
Студия Structo
  • Вёл три проекта на Yii2 и MariaDB в международной команде.
  • Интернет-магазин: оптимизация SQL, кэширование, рефакторинг.
  • Админ-панель для поставщиков и менеджеров с разграничением ролей.
  • Внутренний инструмент: интеграция с API ClickUp, генерация промптов для ИИ, код-ревью.

PHP-разработчик (Yii2)

АПР 2022 — ФЕВ 2023 · 11 МЕС
Система управления обучением «ЭДУС»
  • Платформа онлайн-школы: модульный монолит на Yii2 и MySQL, команда из двух разработчиков.
  • Кабинеты для учеников, учителей, родителей и тьюторов, онлайн-уроки, домашние задания.

Fullstack-разработчик

ЯНВ 2014 — ИЮН 2017 · 3 ГОДА 6 МЕС
ООО «Возрождение»
  • Корпоративные сайты и веб-приложения на PHP, Yii2, Joomla, MySQL.
  • Онлайн-конструктор турникетов: параметры изделия, чертежи, 3D-визуализация.

Давайте работать вместе

Ищу полную или частичную занятость и проектную работу, формат удалённый. Быстрее всего отвечаю в Telegram и MAX.