kimai-plugin-heatmap/.planning/milestones/v1.0-phases/phase-1/RESEARCH.md
Christopher Mühl 244c7c66fc
chore: archive v1.0 milestone
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 23:25:26 +02:00

19 KiB

Phase 1: Dev Environment - Research

Researched: 2026-04-08 Domain: Nix flake devshell, Kimai 2.x local instance, MariaDB, PHP 8.2+, data seeding Confidence: HIGH

Summary

Phase 1 is about getting a reproducible dev environment where nix develop gives you a working Kimai instance with test data ready for plugin development. The key findings are:

  1. Kimai requires MySQL/MariaDB -- SQLite support was removed years ago. The Nix flake must provision a local MariaDB instance (11.4.x available in nixpkgs). This is the single biggest complexity driver for this phase.
  2. Kimai 2.52.0 is the current latest stable release (March 2025), requiring PHP 8.1-8.5 and Symfony 6.4. PHP 8.2.29 is already available in the system nixpkgs as php82.
  3. Kimai ships built-in fixtures (bin/console kimai:reset:dev loads Doctrine fixtures that create ~100-1000 timesheet entries per user across 3 years of data). No need to hand-roll a seed script.
  4. Plugins live in var/plugins/ with a symlink from the project directory. The namespace must be KimaiPlugin\<BundleName> and the bundle class must implement App\Plugin\PluginInterface.

Primary recommendation: Use a shell script (dev/start.sh) invoked from the Nix devshell that starts MariaDB in a local data directory, runs Kimai install + fixture loading, symlinks the plugin, and starts the Symfony dev server. Use process-compose to manage MariaDB + web server lifecycle.

<phase_requirements>

Phase Requirements

ID Description Research Support
DEV-01 Nix flake/devshell provides a running local Kimai instance Nix flake with php82, mariadb, composer, nodejs, symfony-cli, process-compose; shell hook bootstraps Kimai
DEV-02 Local Kimai has a seeded database with realistic time entry data bin/console kimai:reset:dev loads built-in Doctrine fixtures with thousands of timesheet entries
DEV-03 Plugin is loadable in the local Kimai instance for manual testing Symlink project dir into Kimai's var/plugins/KimaiHeatmapBundle, clear cache
</phase_requirements>

Standard Stack

Core (Nix Flake Packages)

Package Nixpkgs Attr Version Purpose
PHP php82 8.2.29 Kimai runtime (with extensions: gd, intl, mbstring, pdo_mysql, xml, xsl, zip, tokenizer)
MariaDB mariadb 11.4.8 Database server (Kimai dropped SQLite support)
Composer php82Packages.composer 2.8.x PHP dependency management
Node.js nodejs 22.x JS tooling for d3/esbuild (later phases)
npm (bundled with nodejs) 10.x JS package manager
symfony-cli symfony-cli 5.16.0 Dev web server with PHP integration
process-compose process-compose 1.78.0 Process manager for MariaDB + web server

Kimai

Property Value Source
Latest stable 2.52.0 [VERIFIED: github.com/kimai/kimai/releases]
PHP support 8.1 - 8.5 [VERIFIED: composer.json require]
Symfony version 6.4 LTS [VERIFIED: composer.json extra]
Database MySQL 8.3+ or MariaDB 11.1+ [CITED: kimai.org/documentation/developers.html]
SQLite NOT supported (removed 2021) [VERIFIED: kimai.org/en/blog/2021/sqlite-and-ftp-support-removed]

Required PHP Extensions

From Kimai's composer.json [VERIFIED]:

ext-gd, ext-intl, ext-json, ext-mbstring, ext-pdo, ext-tokenizer, ext-xml, ext-xsl, ext-zip

Plus pdo_mysql for MariaDB connectivity.

Architecture Patterns

Nix Flake Structure

flake.nix                    # PHP, MariaDB, Composer, Node, symfony-cli, process-compose
flake.lock
dev/
  process-compose.yaml       # MariaDB + symfony web server process definitions
  setup.sh                   # One-time: clone Kimai, composer install, DB setup, fixtures, symlink plugin
  seed.sh                    # Re-run fixtures without full setup

Pattern 1: Local MariaDB with Data Directory in Project

What: MariaDB runs from a local data directory (./dev/.mariadb-data/) using mysqld --datadir without needing root or system-level MySQL installation. [ASSUMED] When to use: Always for this project -- avoids polluting the system. Example:

# Initialize MariaDB data directory
mysql_install_db --datadir=./dev/.mariadb-data --auth-root-authentication-method=normal

# Start MariaDB on a non-standard port (to avoid conflicts)
mysqld --datadir=./dev/.mariadb-data --socket=./dev/.mariadb.sock --port=3307 --skip-grant-tables

Pattern 2: Kimai Clone as Git-Ignored Dependency

What: Clone Kimai into ./dev/kimai/ (git-ignored), run composer install there, then symlink the plugin directory into ./dev/kimai/var/plugins/KimaiHeatmapBundle. Why: Keeps the host application separate from plugin source. Kimai is a dependency, not part of the plugin repo.

dev/
  kimai/                     # .gitignore'd -- cloned Kimai instance
    var/
      plugins/
        KimaiHeatmapBundle -> ../../../../   # symlink to project root

What: Symlink the project root (which IS the plugin bundle) into Kimai's var/plugins/ directory. [CITED: kimai.org/documentation/plugins.html] Why: Code changes in the plugin are immediately reflected. No copy/install step needed.

ln -sf "$(pwd)" ./dev/kimai/var/plugins/KimaiHeatmapBundle

Critical: The symlink target directory name MUST match the bundle class name (KimaiHeatmapBundle).

Pattern 4: process-compose for Multi-Service Dev

What: process-compose manages MariaDB and Symfony dev server as a single dev stack. Why: Both services need to run simultaneously. process-compose handles startup order, log aggregation, and clean shutdown. Better than manual terminal management.

# dev/process-compose.yaml
version: "0.5"
processes:
  mariadb:
    command: mysqld --datadir=./dev/.mariadb-data --socket=./dev/.mariadb.sock --port=3307
    readiness_probe:
      exec:
        command: mysqladmin --socket=./dev/.mariadb.sock ping
      initial_delay_seconds: 2
      period_seconds: 1
  kimai:
    command: symfony server:start --no-tls --dir=./dev/kimai
    depends_on:
      mariadb:
        condition: process_healthy

Anti-Patterns to Avoid

  • Using SQLite for dev: Kimai dropped SQLite support. Migrations will fail. No workaround. [VERIFIED]
  • Installing Kimai globally or via Composer require: Kimai is the host application, not a library. Clone it.
  • Trying to Nixify Composer dependencies: Let Composer manage vendor/ normally inside the Kimai clone. Nix provides the PHP binary and extensions only.
  • Running MariaDB as a system service: Use a local data directory. No root, no systemd, no conflicts with existing databases.

Don't Hand-Roll

Problem Don't Build Use Instead Why
Test data seeding Custom SQL insert scripts bin/console kimai:reset:dev Kimai ships Doctrine fixtures that create users, customers, projects, activities, and 100-1000+ timesheet entries per user spanning 3 years [VERIFIED: TimesheetFixtures.php]
PHP dev web server nginx/Apache config symfony server:start Handles PHP-FPM, routing, and .env loading automatically [VERIFIED: nixpkgs symfony-cli 5.16.0]
Process management Shell scripts with & and trap process-compose Handles startup ordering, health checks, log aggregation, clean shutdown [VERIFIED: nixpkgs process-compose 1.78.0]
MariaDB initialization Manual SQL DDL mysql_install_db + kimai:install Standard tooling that handles system tables, migrations, and schema creation

Database Schema (for Seeding Context)

Key tables for timesheet data [VERIFIED: Kimai source Entity classes]:

Table Key Columns Notes
kimai2_timesheet id, start_time, end_time, duration, timezone, date_tz, user (FK), project_id (FK), activity_id (FK), rate, billable, exported Duration in seconds
kimai2_customers id, name, country, currency, timezone, visible Required parent for projects
kimai2_projects id, name, customer_id (FK), visible, billable Required parent for activities
kimai2_activities id, name, project_id (FK, nullable), visible, billable Can be global (null project) or project-specific
kimai2_users id, username, email, roles, password, timezone Created via kimai:user:create

The kimai:reset:dev command loads Doctrine fixtures that populate ALL these tables with realistic data, including:

  • 5 users (clara_customer, john_user, tony_teamlead, anna_admin, susan_super) -- all with password "password" [CITED: kimai.org/documentation/developers.html]
  • Multiple customers, projects, and activities
  • 100-1000 timesheet entries per user spanning ~3 years back [VERIFIED: TimesheetFixtures.php source]

Kimai Setup Commands (Exact Sequence)

# 1. Clone Kimai at specific version
git clone -b 2.52.0 --depth 1 https://github.com/kimai/kimai.git dev/kimai

# 2. Configure environment
cp dev/kimai/.env.dist dev/kimai/.env
# Edit .env: set DATABASE_URL, APP_SECRET

# 3. Install PHP dependencies (--no-dev is fine for plugin dev; add --dev if you need Kimai's own test tools)
cd dev/kimai && composer install --no-dev --optimize-autoloader

# 4. Create database and run migrations
bin/console kimai:install -n

# 5. Load demo fixtures (drops and recreates schema with test data!)
bin/console kimai:reset:dev

# 6. Symlink plugin
ln -sf "$(pwd)/../../" var/plugins/KimaiHeatmapBundle

# 7. Clear cache (required after adding plugin)
bin/console cache:clear

DATABASE_URL format for local MariaDB:

DATABASE_URL=mysql://root@127.0.0.1:3307/kimai?charset=utf8mb4&serverVersion=11.4.8-MariaDB

Common Pitfalls

Pitfall 1: MariaDB Socket vs TCP Connection

What goes wrong: Kimai tries to connect via Unix socket (default MySQL behavior) but the local MariaDB is listening on a custom socket path or TCP port. Why it happens: Default MySQL client config looks for /var/run/mysqld/mysqld.sock which doesn't exist in a Nix devshell. How to avoid: Use TCP connection (127.0.0.1:3307) in DATABASE_URL, not localhost (which triggers socket mode on Linux). Or set --socket path explicitly. Warning signs: "Can't connect to local MySQL server through socket" errors.

What goes wrong: After symlinking the plugin, Kimai doesn't see it or throws container compilation errors. Why it happens: Symfony caches the container, routes, and bundle list. A new plugin requires cache clear. How to avoid: Always run bin/console cache:clear after symlinking. In dev, set APP_ENV=dev to enable auto-recompilation. Warning signs: Plugin not listed in admin, or DI errors referencing old cached container.

Pitfall 3: PHP Extension Mismatch in Nix

What goes wrong: Kimai fails at runtime with "extension not found" errors even though you listed extensions in flake.nix. Why it happens: Nix PHP needs explicit extension configuration via php.withExtensions or php.buildEnv. Simply adding php82 doesn't include all extensions. How to avoid: Build a custom PHP with required extensions:

php = pkgs.php82.buildEnv {
  extensions = { enabled, all }: enabled ++ (with all; [ xsl ]);
};

The default enabled set includes most common extensions (gd, intl, mbstring, pdo, xml, zip, etc.) but xsl is often missing. [ASSUMED] Warning signs: composer install fails, or Kimai throws "class not found" for Intl/GD operations.

Pitfall 4: kimai:reset:dev Drops Everything

What goes wrong: Running kimai:reset:dev drops the entire database schema and reloads fixtures. Developers run it thinking it adds data on top of existing data. Why it happens: The command runs doctrine:fixtures:load which purges all tables first. How to avoid: Only run during initial setup or when you explicitly want a clean slate. For adding entries, write a separate seed script or use Kimai's API. Warning signs: Custom data disappears after running the command.

Pitfall 5: Plugin Directory Name Must Match Bundle Class

What goes wrong: Symlink named heatmap-plugin or KimaiHeatmap instead of KimaiHeatmapBundle. Kimai's plugin loader silently ignores it. Why it happens: Kimai expects the directory name in var/plugins/ to match the bundle class name exactly. How to avoid: Name the symlink KimaiHeatmapBundle to match the class KimaiHeatmapBundle.php. Warning signs: Plugin not appearing in Kimai admin, no errors in logs.

Validation Architecture

Test Framework

Property Value
Framework PHPUnit (match Kimai's version)
Config file None yet -- Wave 0 task
Quick run command php vendor/bin/phpunit --filter TestName
Full suite command php vendor/bin/phpunit

Phase Requirements to Test Map

Req ID Behavior Test Type Automated Command File Exists?
DEV-01 Nix devshell provides PHP, Composer, Node smoke nix develop --command bash -c "php --version && composer --version && node --version" N/A (manual)
DEV-02 Seeded database has timesheet entries smoke cd dev/kimai && bin/console doctrine:query:sql "SELECT COUNT(*) FROM kimai2_timesheet" N/A (manual)
DEV-03 Plugin is loadable smoke cd dev/kimai && bin/console kimai:plugins (should list the plugin) N/A (manual)

Wave 0 Gaps

  • None for this phase -- DEV-01/02/03 are infrastructure tasks verified by smoke commands, not unit tests.

Environment Availability

Dependency Required By Available Version Fallback
Nix Flake/devshell Yes 2.93.3 (Lix) --
PHP 8.2 Kimai runtime Yes (system) 8.2.29 Provided by Nix flake
Composer PHP deps Yes (system) 2.8.12 Provided by Nix flake
Node.js JS tooling Yes (system) 22.21.1 Provided by Nix flake
MariaDB Database No (not installed) -- Provided by Nix flake (11.4.8 available)
symfony-cli Dev server No (not installed) -- Provided by Nix flake (5.16.0 available)
process-compose Process mgmt No (not installed) -- Provided by Nix flake (1.78.0 available)

Missing dependencies with no fallback: None -- all provided by the Nix flake.

Plugin Loading Mechanism

Kimai discovers plugins automatically from var/plugins/. Key details [CITED: kimai.org/documentation/plugins.html]:

  1. Plugin directory must contain a bundle class that implements PluginInterface
  2. Namespace must be KimaiPlugin\<BundleName> (the KimaiPlugin vendor prefix is mandatory)
  3. Routes auto-load from Resources/config/routes.{php,yaml} inside the plugin directory
  4. Plugins load in ALL environments without modifying config/bundles.php
  5. The composer.json must have "type": "kimai-plugin" and an extra.kimai section with require (minimum Kimai version as integer: major10000 + minor100 + patch) and name

Minimum Plugin composer.json

{
    "name": "kimai-plugin/heatmap-bundle",
    "type": "kimai-plugin",
    "description": "GitHub-style activity heatmap dashboard widget",
    "license": "MIT",
    "autoload": {
        "psr-4": {
            "KimaiPlugin\\KimaiHeatmapBundle\\": ""
        }
    },
    "extra": {
        "kimai": {
            "require": 25200,
            "name": "Activity Heatmap"
        }
    }
}

Assumptions Log

# Claim Section Risk if Wrong
A1 MariaDB can run with --datadir pointing to a local project directory without root Architecture Patterns HIGH -- would need alternative DB setup approach (Docker, system service)
A2 Nix php82 default enabled extensions cover most of Kimai's requirements except xsl Common Pitfalls MEDIUM -- may need to explicitly list more extensions
A3 symfony server:start works with the Nix-provided PHP binary Architecture Patterns LOW -- symfony-cli auto-detects PHP; fallback is php -S

Open Questions

  1. Exact php.buildEnv extension list needed

    • What we know: Kimai requires gd, intl, mbstring, pdo_mysql, xml, xsl, zip, tokenizer, json
    • What's unclear: Which of these are in Nix's default enabled set vs. needing explicit addition
    • Recommendation: Start with enabled ++ [xsl pdo_mysql] and iterate if composer install fails
  2. MariaDB version compatibility with Kimai migrations

    • What we know: Kimai docs say MariaDB 11.1+; nixpkgs has 11.4.8
    • What's unclear: Whether any migration uses MySQL-specific syntax that breaks on MariaDB 11.4
    • Recommendation: Use 11.4.8 (latest in nixpkgs); if issues arise, try 10.11.14 (mariadb_1011)

Sources

Primary (HIGH confidence)

Secondary (MEDIUM confidence)

Tertiary (LOW confidence)

  • None

Metadata

Confidence breakdown:

  • Standard stack: HIGH -- versions verified against nixpkgs and Kimai source
  • Architecture: HIGH for Kimai plugin mechanics (verified against source), MEDIUM for Nix MariaDB setup (assumed pattern)
  • Pitfalls: HIGH -- based on verified Kimai constraints (no SQLite, cache clearing, fixture behavior)

Research date: 2026-04-08 Valid until: 2026-05-08 (Kimai releases roughly monthly; pin to 2.52.0)