Thank you for your interest in contributing to Little ISMS Helper! This document provides guidelines and instructions for contributing to this project.
- Code of Conduct
- Getting Started
- Development Workflow
- Coding Standards
- Commit Guidelines
- Pull Request Process
- Testing Requirements
- Documentation
We are committed to providing a welcoming and inspiring community for everyone. Please be respectful and constructive in your interactions.
- Use welcoming and inclusive language
- Be respectful of differing viewpoints
- Accept constructive criticism gracefully
- Focus on what is best for the community
- Show empathy towards other community members
First time contributing? Start with ONBOARDING.md and the step-by-step guides under
docs/onboarding/(environment setup → architecture tour → hot files → test runbook → anti-patterns). Takes about 30 minutes and will save hours later.
- PHP 8.4+ required (8.5 supported and tested in CI)
- Composer 2.x
- PostgreSQL 16+ or MySQL 8.0+ or MariaDB 10.11+
- Git
- Symfony CLI (optional but recommended)
-
Fork the repository
# Click "Fork" on GitHub, then clone your fork git clone https://github.com/YOUR-USERNAME/Little-ISMS-Helper.git cd Little-ISMS-Helper
-
Add upstream remote
git remote add upstream https://github.com/moag1000/Little-ISMS-Helper.git
-
Install dependencies
composer install php bin/console importmap:install
-
Configure environment
cp .env .env.local # Edit .env.local with your database credentials -
Setup database
php bin/console doctrine:database:create php bin/console doctrine:migrations:migrate php bin/console isms:load-annex-a-controls
-
Run development server
symfony serve # or php -S localhost:8000 -t public/
Use descriptive branch names that indicate the type of work:
feature/- New features (e.g.,feature/document-upload)bugfix/- Bug fixes (e.g.,bugfix/risk-calculation)hotfix/- Urgent production fixes (e.g.,hotfix/security-patch)refactor/- Code refactoring (e.g.,refactor/service-layer)docs/- Documentation updates (e.g.,docs/api-guide)test/- Test additions/improvements (e.g.,test/audit-coverage)
Example:
git checkout -b feature/training-certificates# Fetch latest changes from upstream
git fetch upstream
# Rebase your branch on upstream/main
git rebase upstream/main
# Force push to your fork (if already pushed)
git push --force-with-lease origin feature/your-featureWe follow Symfony Best Practices and PSR-12 coding standards.
- Indentation: 4 spaces (no tabs)
- Line length: Max 120 characters
- Naming conventions:
- Classes:
PascalCase(e.g.,RiskController) - Methods:
camelCase(e.g.,calculateRiskScore()) - Constants:
UPPER_SNAKE_CASE(e.g.,MAX_RISK_LEVEL) - Properties:
camelCase(e.g.,$riskLevel)
- Classes:
<?php
namespace App\Service;
use App\Entity\Risk;
use Doctrine\ORM\EntityManagerInterface;
class RiskIntelligenceService
{
private const MAX_RISK_LEVEL = 25;
public function __construct(
private EntityManagerInterface $entityManager
) {}
public function calculateRiskScore(Risk $risk): int
{
$likelihood = $risk->getLikelihood();
$impact = $risk->getImpact();
return min($likelihood * $impact, self::MAX_RISK_LEVEL);
}
}- Indentation: 4 spaces
- Use semantic HTML5 elements
- Include ARIA labels for accessibility
- Use Bootstrap 5 classes consistently
<div class="card">
<div class="card-header">
<h3 class="card-title">{{ 'risk.details'|trans }}</h3>
</div>
<div class="card-body">
{% if risk.isHighRisk %}
<div class="alert alert-danger" role="alert">
{{ 'risk.high_risk_warning'|trans }}
</div>
{% endif %}
</div>
</div>The application uses a single global form-theme — templates/form/fa_cyber_input.html.twig — set in config/packages/twig.yaml. All Symfony forms render through this theme automatically.
MUST do:
- All
FormTypeclasses extendAbstractTypeand setdata_class+translation_domaininconfigureOptions(). - Render forms via
{{ form_start(form) }}+{{ form_widget(form) }}(or{{ form_row(form.field) }}for custom layouts) +{{ form_end(form) }}.form_endauto-renders CSRF. - For new field types (e.g.
file,range,date,time,datetime), the theme adds.fa-cyber-input__field--<type>modifiers — no custom markup needed.
MUST NOT do:
- Override
getBlockPrefix()in aFormType— this breaks global theming. - Use
{% form_theme form '...' %}to switch tobootstrap_5_layout.html.twig. If you need to override one specific field, write a custom theme that{% use "form/fa_cyber_input.html.twig" %}and only overrides the specific block (seetemplates/supplier/_supplier_form_theme.html.twigfor an example). - Hand-roll
<input class="form-control">instead ofform_widget()inside Symfony forms. Filter forms (GET, stateless) may use raw inputs — they're styled via the Aurora CSS bridge infairy-aurora-components.css.
Validation + accessibility:
- The theme renders errors as
.fa-cyber-input__errors[role="alert"]automatically. - All inputs receive an associated
<label for="...">viaform_label. - Required fields show
<span class="fa-cyber-input__req" aria-label="form.required">*</span>.
CSS bridge: Raw Bootstrap classes (.form-control, .form-select) inside <form method="get"> filter bars are styled with Aurora tokens via fairy-aurora-components.css (~line 3751). This is intentional — filter forms don't go through Symfony's form system.
Forms and UI sections that depend on optional compliance modules must use the module-gating infrastructure introduced in T31 (May 2026).
Full developer reference: docs/MODULE_GATING_GUIDE.md
use App\Form\Trait\ModuleAwareFormTrait;
use App\Service\ModuleConfigurationService;
class MyFormType extends AbstractType
{
use ModuleAwareFormTrait;
public function __construct(
private readonly ModuleConfigurationService $moduleConfiguration,
) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// Core fields — always shown
$builder->add('title', TextType::class, ['label' => 'my.field.title']);
// Module-gated section with norm reference
// GDPR Art. 7(3) — consent withdrawal mandatory when privacy module active
if ($this->isModuleActive('privacy')) {
$builder->add('withdrawnAt', DateTimeType::class, [
'label' => 'my.field.withdrawn_at',
'required' => false,
]);
}
}
}Rules:
- Use
private readonly ModuleConfigurationService $moduleConfiguration(exact property name — the trait uses it). - Gate entire logical field groups, not individual fields.
- Add a doc comment with the norm reference next to every
ifblock. - Do NOT duplicate the
isModuleActive()method — use the trait.
For whole-module gating (entire controller blocked when module is inactive):
use App\Controller\Trait\ModuleGatedControllerTrait;
use App\Service\ModuleConfigurationService;
use Symfony\Contracts\Translation\TranslatorInterface;
class MyController extends AbstractController
{
use ModuleGatedControllerTrait;
public function __construct(
private readonly ModuleConfigurationService $moduleService,
private readonly TranslatorInterface $translator,
) {}
public function index(): Response
{
if ($redirect = $this->checkModuleActive('privacy')) return $redirect;
// ... controller logic
}
}{% if is_module_active('privacy') %}
<div class="card">
{# GDPR-specific content #}
</div>
{% endif %}Within a module-gated section, use data-depends-on for field-level conditional
visibility (no round-trip needed):
$builder->add('specialCategoryData', CheckboxType::class, [
'attr' => [
'data-depends-on' => 'my_form_involvesPersonalData',
'data-depends-on-value' => '1',
],
]);Module-gated fields use the same translation domain as the surrounding FormType.
No separate domain for gated fields. The "module inactive" flash message uses
the messages domain key common.module_not_active.
| Key | Trigger |
|---|---|
privacy |
GDPR / ISO 27701 |
nis2_dora |
NIS2 + DORA |
ai_governance |
EU AI Act + ISO 42001 |
cloud_security |
ISO 27017/18, BSI C5 |
vulnerability_intel |
Vulnerability + Threat Intelligence |
marisk |
MaRisk (DACH banks/insurers) |
tisax |
TISAX / VDA ISA |
quantitative_risk |
FAIR methodology |
bcm |
ISO 22301 BCM |
compliance |
Multi-framework compliance |
bsi_grundschutz |
BSI IT-Grundschutz |
core, assets, risks, controls, incidents, audits, training, reviews, authentication, audit_logging |
Always active |
- Use modern ES6+ syntax
- Follow Stimulus conventions
- Use meaningful variable names
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['modal', 'content'];
connect() {
this.boundCloseOnEscape = this.closeOnEscape.bind(this);
document.addEventListener('keydown', this.boundCloseOnEscape);
}
disconnect() {
document.removeEventListener('keydown', this.boundCloseOnEscape);
}
closeOnEscape(event) {
if (event.key === 'Escape' && this.modalTarget.classList.contains('show')) {
this.close();
}
}
}We follow the Conventional Commits specification:
<type>(<scope>): <subject>
<body>
<footer>
feat:- New featurefix:- Bug fixdocs:- Documentation changesstyle:- Code style changes (formatting, no logic change)refactor:- Code refactoringperf:- Performance improvementstest:- Adding or updating testsbuild:- Build system or dependency changesci:- CI/CD configuration changeschore:- Other changes (e.g., updating dependencies)
Good:
feat(risk): add automatic risk recalculation on asset change
- Add RiskRecalculationService
- Trigger recalculation on asset CIA value updates
- Add unit tests for recalculation logic
Closes #123
Good:
fix(audit): correct date filtering in audit list
The audit list was not filtering correctly by planned date.
Fixed the DQL query to use proper date comparison.
Fixes #456
Bad:
updated stuff
Bad:
fix bug
- One logical change per commit
- Write clear, descriptive commit messages
- Reference issue numbers (e.g.,
Fixes #123,Closes #456) - Keep commits atomic - each commit should be self-contained
- Avoid commits with "WIP" or "temp" - squash them before submitting PR
This project uses release-please for automated releases. The flow:
- Conventional commits land on
maincontinuously. - release-please opens (and keeps updating) a
chore(main): release X.Y.ZPR that bumpscomposer.json+CHANGELOG.mdfrom accumulated commits. - Auto-merge runs every Monday at 09:00 UTC via
.github/workflows/release-please-auto-merge.yml. Required checks must be green; PR is squash-merged + branch deleted. - Merge → tag (
vX.Y.Z) + GitHub Release auto-created + CI/CD Pipeline builds + pushes Docker image (:vX.Y.Z,:X.Y,:latest).
Skip a weekly release: add label release-blocked or do-not-merge to
the open release PR before Monday.
Force a release outside cadence: GitHub Actions → "Release Please Auto-Merge" → Run workflow (workflow_dispatch). Use sparingly — defeats the cadence-discipline purpose.
Version-bump rules driven by commit type:
fix:→ patch (e.g. 3.3.2 → 3.3.3)feat:→ minor (e.g. 3.3.2 → 3.4.0)feat!:/BREAKING CHANGE:footer → major (e.g. 3.3.2 → 4.0.0)docs:/chore:/test:→ no release (hidden in changelog)
Hot-fixes bypass this only when a production-breaking issue is live — otherwise let the release PR accumulate and ship on cadence.
For Docker test deployments without affecting :latest:
- Manual: trigger the Dev Release (manual) GitHub Action with the
desired bump (patch / minor / major). It computes the next semver,
appends
-dev.N(auto-incremented), tagsmainHEAD, pushes — CI builds Docker image with:vX.Y.Z-dev.N+:dev(rolling). Never:latest, never:X.Y(semver minor floating tag). - Manual via CLI (alternative):
git tag v3.4.0-dev.1 && git push origin v3.4.0-dev.1. CI applies the same dev-tag rules based on the-dev.segment.
Tag-to-image mapping enforced in .github/workflows/ci.yml:
| Tag | Image tags |
|---|---|
v3.4.0 |
:3.4.0, :3.4, :latest |
v3.4.0-dev.1 |
:3.4.0-dev.1, :dev |
v3.4.0-rc.1 |
:3.4.0-rc.1, :rc |
Pi / production deployments stay on :latest. Test environments pull
:dev (or pin to a specific :vX.Y.Z-dev.N) for QA cycles.
-
Update your branch
git fetch upstream git rebase upstream/main
-
Run tests
php bin/phpunit
-
Check code style
# If you have PHP CS Fixer installed vendor/bin/php-cs-fixer fix --dry-run --diff -
Update documentation if needed
-
Test manually - verify your changes work as expected
-
Push to your fork
git push origin feature/your-feature
-
Create PR on GitHub
- Go to https://github.com/moag1000/Little-ISMS-Helper
- Click "New Pull Request"
- Select your fork and branch
- Fill out the PR template
Follow the same format as commit messages:
feat(risk): Add automatic risk recalculation
## Description
Brief description of changes
## Type of Change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update
## Related Issues
Closes #123
## Changes Made
- Added RiskRecalculationService
- Updated RiskController to trigger recalculation
- Added unit tests
## Testing
- [ ] Unit tests added/updated
- [ ] Manual testing completed
- [ ] All tests pass
## Screenshots (if applicable)
[Add screenshots here]
## Checklist
- [ ] My code follows the project's coding standards
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes- At least one maintainer must approve the PR
- All CI checks must pass
- Address review comments promptly
- Keep the PR updated with upstream changes
- Squash commits if requested before merge
All new features must include unit tests using PHPUnit.
Location: tests/
Running tests:
php bin/phpunitExample:
<?php
namespace App\Tests\Service;
use App\Service\RiskIntelligenceService;
use App\Entity\Risk;
use PHPUnit\Framework\TestCase;
class RiskIntelligenceServiceTest extends TestCase
{
public function testCalculateRiskScore(): void
{
$service = new RiskIntelligenceService(/* dependencies */);
$risk = new Risk();
$risk->setLikelihood(5);
$risk->setImpact(4);
$score = $service->calculateRiskScore($risk);
$this->assertEquals(20, $score);
}
}- Aim for 80%+ code coverage for new code
- All business logic must be tested
- Edge cases should be covered
Before submitting a PR:
- Test in fresh database
- Test with different user roles
- Test error scenarios
- Test on different browsers (Chrome, Firefox, Safari)
- Verify mobile responsiveness
Update documentation when:
- Adding new features
- Changing existing behavior
- Adding new configuration options
- Updating dependencies
- Changing API endpoints
- README.md - Project overview, installation, quick start
- docs/ - Detailed documentation
docs/setup/- Setup guidesdocs/architecture/- Architecture documentationdocs/api/- API documentation
- Code comments - Complex logic explanation
- Docblocks - PHP class/method documentation
- Use clear, concise language
- Include code examples
- Add screenshots for UI changes
- Keep it up-to-date
- GitHub Issues: Open an issue for bugs or feature requests
- GitHub Discussions: Ask questions or discuss ideas
- Email: Contact the maintainers (see README.md)
When reporting bugs, include:
- Symfony version (
php bin/console about) - PHP version (
php -v) - Steps to reproduce
- Expected vs. actual behavior
- Error messages (full stack trace if possible)
- Screenshots (if UI-related)
When suggesting features:
- Explain the use case - why is this needed?
- Describe the solution - how should it work?
- Consider alternatives - are there other ways to solve this?
- Check existing issues - has this been suggested before?
By contributing to Little ISMS Helper, you agree that your contributions will be licensed under the same license as the project (see LICENSE file).
Your contributions make Little ISMS Helper better for everyone. We appreciate your time and effort! 🎉
For questions about these guidelines, please open an issue or contact the maintainers.
Last Updated: 2026-04-29 Project Version: 3.2.6