Heatmap will render here (Phase 3)
{% endblock %}
{% endembed %}
```
### Routes Configuration
```yaml
# Resources/config/routes.yaml
heatmap_controllers:
resource: '../../Controller/'
type: attribute
```
### PHPUnit Test (Unit, mocked repository)
```php
namespace KimaiPlugin\KimaiHeatmapBundle\Tests\Service;
use App\Entity\User;
use App\Repository\TimesheetRepository;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\QueryBuilder;
use KimaiPlugin\KimaiHeatmapBundle\Service\HeatmapService;
use PHPUnit\Framework\TestCase;
class HeatmapServiceTest extends TestCase
{
public function testGetDailyAggregation(): void
{
$mockResults = [
['date' => '2026-04-01', 'duration' => 7200, 'count' => 3],
['date' => '2026-04-02', 'duration' => 3600, 'count' => 1],
];
$query = $this->createMock(AbstractQuery::class);
$query->method('getResult')->willReturn($mockResults);
$qb = $this->createMock(QueryBuilder::class);
$qb->method('select')->willReturnSelf();
$qb->method('addSelect')->willReturnSelf();
$qb->method('andWhere')->willReturnSelf();
$qb->method('setParameter')->willReturnSelf();
$qb->method('groupBy')->willReturnSelf();
$qb->method('orderBy')->willReturnSelf();
$qb->method('expr')->willReturn(new \Doctrine\ORM\Query\Expr());
$qb->method('getQuery')->willReturn($query);
$repo = $this->createMock(TimesheetRepository::class);
$repo->method('createQueryBuilder')->willReturn($qb);
$service = new HeatmapService($repo);
$user = $this->createMock(User::class);
$begin = new \DateTimeImmutable('2026-04-01');
$end = new \DateTimeImmutable('2026-04-30');
$result = $service->getDailyAggregation($user, $begin, $end);
$this->assertCount(2, $result);
$this->assertEquals('2026-04-01', $result[0]['date']);
$this->assertEquals(2.0, $result[0]['hours']);
$this->assertEquals(3, $result[0]['count']);
}
}
```
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | PHPUnit 10.x (Kimai's bundled version) |
| Config file | `phpunit.xml` in plugin root (NEW -- Wave 0) |
| Quick run command | `php vendor/bin/phpunit --configuration Tests/phpunit.xml` |
| Full suite command | Same (small test count at this phase) |
### Phase Requirements -> Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| TEST-01 | HeatmapService aggregates daily data correctly (duration, count, timezone grouping) | unit | `php vendor/bin/phpunit Tests/Service/HeatmapServiceTest.php` | Wave 0 |
| TEST-02 | API endpoint returns correct JSON format, requires auth | unit | `php vendor/bin/phpunit Tests/Controller/HeatmapControllerTest.php` | Wave 0 |
| PLUG-02 | Widget registers and renders on dashboard | manual | Boot Kimai, check dashboard | manual-only: requires running Kimai instance |
### Wave 0 Gaps
- [ ] `Tests/phpunit.xml` -- PHPUnit config for plugin standalone tests
- [ ] `Tests/Service/HeatmapServiceTest.php` -- covers TEST-01
- [ ] `Tests/Controller/HeatmapControllerTest.php` -- covers TEST-02
- [ ] PHPUnit autoload bootstrap pointing to Kimai's vendor autoloader
**Note on test bootstrapping:** The plugin lives inside Kimai's `var/plugins/` directory (symlinked). Tests need access to Kimai's autoloader. The `phpunit.xml` should set `bootstrap` to the Kimai `vendor/autoload.php`. For unit tests with mocked dependencies, this is sufficient. Full kernel-boot integration tests would need Kimai's test bootstrap, which is more complex and not needed for Phase 2.
### Sampling Rate
- **Per task commit:** `php dev/kimai/vendor/bin/phpunit --configuration Tests/phpunit.xml`
- **Per wave merge:** Same
- **Phase gate:** All PHPUnit tests green
## Security Domain
### Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | yes | Symfony `#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]` -- same as Kimai core |
| V3 Session Management | no | Handled by Kimai core |
| V4 Access Control | yes | `#[IsGranted('view_own_timesheet')]` permission check -- same as Kimai widgets |
| V5 Input Validation | yes | Date parameters validated/typed, no raw user input in queries |
| V6 Cryptography | no | No crypto operations |
### Known Threat Patterns
| Pattern | STRIDE | Standard Mitigation |
|---------|--------|---------------------|
| SQL injection via date params | Tampering | Doctrine parameterized queries (`:begin`, `:end`) -- same as Kimai core |
| Accessing other users' data | Information Disclosure | Query always filters by `$this->getUser()` -- never accept user ID from request |
| CSRF on data endpoint | Tampering | GET-only endpoint, read-only data |
## Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | `@KimaiHeatmapBundle/` Twig namespace resolves `Resources/views/` in the plugin bundle | Architecture Patterns | Template not found -- would need to register path manually |
| A2 | `autoconfigure: true` in plugin's `services.yaml` causes `WidgetInterface` auto-tagging | Architecture Patterns | Widget not discovered -- would need explicit tag in services.yaml |
| A3 | `@theme/embeds/card.html.twig` is available to plugin templates | Code Examples | Template inheritance fails -- would need different base template |
| A4 | Doctrine DQL `DATE()` function works with SQLite (dev) | Data Aggregation | Query fails in dev -- would need raw SQL or different function |
**A4 is notable:** Kimai uses MySQL/MariaDB in production, but the dev environment setup from Phase 1 may use SQLite. The `DATE()` DQL function's behavior on SQLite should be verified during implementation.
## Open Questions
1. **SQLite compatibility in dev**
- What we know: Kimai's production uses MySQL/MariaDB. Phase 1 dev environment may use SQLite.
- What's unclear: Whether Doctrine's `DATE()` DQL function works on SQLite, and whether the `date_tz` column type is correctly handled.
- Recommendation: Check the dev database type during implementation. If SQLite, test the query works. If not, may need to adjust or use MySQL in dev.
2. **Test bootstrap path**
- What we know: Plugin lives in `var/plugins/KimaiHeatmapBundle` (symlinked from project root). Tests need Kimai's autoloader.
- What's unclear: Exact relative path from plugin test config to Kimai's vendor autoloader.
- Recommendation: Resolve during test setup. The path is `../../vendor/autoload.php` from the plugin root when symlinked into `var/plugins/`.
## Sources
### Primary (HIGH confidence)
- Local Kimai 2.52.0 source at `/home/toph/code/toph/kimai-heatmap/dev/kimai/` -- ALL architecture claims verified against actual source code
- Files examined:
- `src/Widget/WidgetInterface.php` -- interface definition, `#[AutoconfigureTag]`
- `src/Widget/Type/AbstractWidget.php` -- base class, template name convention, timezone handling
- `src/Widget/Type/AbstractWidgetType.php` -- extended base class
- `src/Widget/WidgetService.php` -- widget registry
- `src/Widget/Type/DailyWorkingTimeChart.php` -- reference widget using TimesheetRepository
- `src/DependencyInjection/Compiler/WidgetCompilerPass.php` -- auto-registration
- `src/Twig/Runtime/WidgetExtension.php` -- render_widget() implementation
- `src/Controller/DashboardController.php` -- dashboard rendering, DashboardEvent
- `src/Event/DashboardEvent.php` -- default widget list management
- `src/Entity/Timesheet.php` -- entity structure, `date_tz` column
- `src/Repository/TimesheetRepository.php` -- query patterns
- `src/Timesheet/TimesheetStatisticService.php` -- daily aggregation reference
- `src/Timesheet/DateTimeFactory.php` -- timezone-aware date creation
- `src/API/BaseApiController.php` -- API controller pattern (FOS Rest)
- `src/Plugin/PluginInterface.php`, `PluginManager.php`, `PluginMetadata.php` -- plugin system
- `src/Kernel.php` -- plugin loading, route auto-import
- `templates/dashboard/index.html.twig` -- dashboard rendering
- `templates/widget/` -- existing widget templates
- `phpunit.xml.dist` -- test configuration
### Secondary (MEDIUM confidence)
- Symfony Bundle template namespace conventions (standard Symfony 6.x behavior) [ASSUMED for A1, A3]
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH -- verified against local Kimai source and composer.json
- Architecture: HIGH -- all widget, route, and DI patterns verified in source
- Data layer: HIGH -- `date_tz` column and query patterns verified in entity and service code
- Pitfalls: HIGH -- derived from actual source code behavior
- Testing: MEDIUM -- test bootstrap path needs verification during implementation
**Research date:** 2026-04-08
**Valid until:** 2026-05-08 (stable -- Kimai 2.52.0 is a fixed target)