86dc930915
Sprint 3 Phase A. Закрытие audit O-refactor-01 + O-perf-01 + O-perf-05.
O-refactor-01: DealController (802 строки) → split на 3 контроллера
по ответственности:
* DealController (466 строк) — single-resource CRUD (index/show/store/update).
* DealBulkActionController (264 строки, новый) — bulk transition/destroy/restore.
* DealExportController (120 строк, новый) — export() с streaming.
API endpoints без изменений; в routes/web.php обновлены только controller@method.
O-perf-01: N+1 в bulk-actions устранён в новом DealBulkActionController.
* transition: SELECT (id, status) → filter NO-OP → bulk-UPDATE
whereIn(changed)->update(status) + ActivityLog::insert(массив).
100 сделок: ~200 SQL → 2 SQL (после SELECT 1 UPDATE + 1 INSERT).
* destroy/restore: SELECT id'шников живых/удалённых → bulk-UPDATE
deleted_at + ActivityLog::insert. Аналогично 2 SQL вместо N.
Defense-in-depth where(tenant_id) сохранён — DealTransitionTest
«не апдейтит чужие сделки» проходит.
O-perf-05: export() переписан на OpenSpout streaming (composer require
openspout/openspout ^5.3). PhpSpreadsheet строил весь .xlsx в памяти
(10K сделок ≈ 100+ MB RAM). Теперь:
* Writer::openToFile('php://output') + Row::fromValues + chunkById(500).
* StreamedResponse → пик памяти O(1) от размера экспорта.
* CSV: OpenSpout Options(FIELD_DELIMITER=';', SHOULD_ADD_BOM=true) —
Excel-friendly RU-локаль сохранена.
* XLSX: getCurrentSheet()->setName('Сделки'), header через
Row::fromValuesWithStyle с (new Style)->withFontBold(true).
DealCreateTest.php (4 теста про export): getContent() → streamedContent()
для StreamedResponse + getCell()->getFormattedValue() для inline-string
ячеек, которые OpenSpout пишет как RichText (PhpSpreadsheet writer писал
как plain shared-string). Логика тестов и assertion'ы не меняются.
Verification:
Pest: 416/416 passed (+ 2 skipped), 1388 assertions, 47.5s.
Larastan: 0 errors above baseline.
Pint: passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
108 lines
3.7 KiB
JSON
108 lines
3.7 KiB
JSON
{
|
|
"$schema": "https://getcomposer.org/schema.json",
|
|
"name": "laravel/laravel",
|
|
"type": "project",
|
|
"description": "The skeleton application for the Laravel framework.",
|
|
"keywords": ["laravel", "framework"],
|
|
"license": "MIT",
|
|
"require": {
|
|
"php": "^8.3",
|
|
"laravel/framework": "^13.7",
|
|
"laravel/sanctum": "^4.3",
|
|
"laravel/tinker": "^3.0",
|
|
"openspout/openspout": "^5.3",
|
|
"phpoffice/phpspreadsheet": "^5.0",
|
|
"pragmarx/google2fa": "^9.0",
|
|
"predis/predis": "^3.4"
|
|
},
|
|
"require-dev": {
|
|
"barryvdh/laravel-ide-helper": "*",
|
|
"fakerphp/faker": "^1.23",
|
|
"infection/infection": "^0.32.7",
|
|
"larastan/larastan": "*",
|
|
"laravel/boost": "^2.4",
|
|
"laravel/pail": "^1.2.5",
|
|
"laravel/pao": "^1.0.6",
|
|
"laravel/pint": "^1.29",
|
|
"mockery/mockery": "^1.6",
|
|
"nunomaduro/collision": "^8.6",
|
|
"pestphp/pest": "^4.7",
|
|
"pestphp/pest-plugin-laravel": "^4.1",
|
|
"roave/security-advisories": "dev-latest"
|
|
},
|
|
"autoload": {
|
|
"psr-4": {
|
|
"App\\": "app/",
|
|
"Database\\Factories\\": "database/factories/",
|
|
"Database\\Seeders\\": "database/seeders/"
|
|
}
|
|
},
|
|
"autoload-dev": {
|
|
"psr-4": {
|
|
"Tests\\": "tests/"
|
|
}
|
|
},
|
|
"scripts": {
|
|
"setup": [
|
|
"composer install",
|
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
|
"@php artisan key:generate",
|
|
"@php artisan migrate --force",
|
|
"npm install --ignore-scripts",
|
|
"npm run build"
|
|
],
|
|
"dev": [
|
|
"Composer\\Config::disableProcessTimeout",
|
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
|
],
|
|
"test": [
|
|
"@php artisan config:clear --ansi @no_additional_args",
|
|
"@php artisan test"
|
|
],
|
|
"pint": "@php vendor/bin/pint",
|
|
"pint:test": "@php vendor/bin/pint --test",
|
|
"stan": "@php vendor/bin/phpstan analyse --memory-limit=512M",
|
|
"mutation": "@php vendor/bin/infection --threads=2 --min-msi=50",
|
|
"audit-offline": "@composer audit --locked",
|
|
"ide-helper": [
|
|
"@php artisan ide-helper:generate",
|
|
"@php artisan ide-helper:meta"
|
|
],
|
|
"post-autoload-dump": [
|
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
|
"@php artisan package:discover --ansi"
|
|
],
|
|
"post-update-cmd": [
|
|
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
|
],
|
|
"post-root-package-install": [
|
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
|
],
|
|
"post-create-project-cmd": [
|
|
"@php artisan key:generate --ansi",
|
|
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
|
"@php artisan migrate --graceful --ansi"
|
|
],
|
|
"pre-package-uninstall": [
|
|
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
|
]
|
|
},
|
|
"extra": {
|
|
"laravel": {
|
|
"dont-discover": []
|
|
}
|
|
},
|
|
"config": {
|
|
"optimize-autoloader": true,
|
|
"preferred-install": "dist",
|
|
"sort-packages": true,
|
|
"allow-plugins": {
|
|
"pestphp/pest-plugin": true,
|
|
"php-http/discovery": true,
|
|
"infection/extension-installer": true
|
|
}
|
|
},
|
|
"minimum-stability": "stable",
|
|
"prefer-stable": true
|
|
}
|