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; }
Слоистая архитектура внутри монолита: сервисы, use case’ы, сущности, слушатели событий. Поэтапный рефакторинг без остановки продукта и без переписывания с нуля.
Схема, нормализация, индексы, тяжёлые запросы, бэкапы в облако.
REST API, платёжные системы, Mango Office через WebSocket, Telegram Bot API.
SPA для клиентов и сотрудников: личные кабинеты, веб-мессенджер с обменом файлами, загрузка и обработка изображений, генерация PDF/Word-документов.
Бизнес-логика живёт в сервисах, данные — в сущностях, выборки — в finder’ах. Каждый сценарий стараюсь покрывать тестами. Ниже — фрагменты из реальных проектов.
Core/ ├─ Attributes ├─ Entities // сущности ├─ Events // события ├─ Finders // выборки ├─ Helpers ├─ Http ├─ Infrastructure ├─ Jobs ├─ Mails ├─ Providers ├─ ReadServices ├─ Services // сценарии ├─ Validators └─ ValueObjects // типы-значения
MangoService).Call, Employee.NationalPhone, MangoCallId.CallFinder, EmployeeFinder.UserEvent).Проверки, создание сущности и уведомление — в одном читаемом методе. Атрибут #[TestClass] связывает метод с его тестом.
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); } }
Интеграционные тесты проверяют и ошибки, и успешный путь: запись в БД и событие, ушедшее в WebSocket.
#[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']); } }
$ 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
Абстрактный базовый класс для «человеко-понятных» URL. Наследнику остаётся реализовать только поиск сущности по slug. Старые адреса сами делают 301-редирект на актуальные (код сокращён).
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(); } }
Модальное окно с плавным появлением и исчезновением без сторонних библиотек: компонент остаётся в DOM на время анимации, ширина задаётся пропсом, а таймер очищается в cleanup-функции useEffect.
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> ) }
Ищу полную или частичную занятость и проектную работу, формат удалённый. Быстрее всего отвечаю в Telegram и MAX.