Skip to content
84 changes: 84 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## About this bundle

`FrameworkCoreBundle` is a Symfony 8 bundle used as a shared foundation across SumoCoders projects. It is not a
standalone application. It is installed into projects via `sumocoders/application-skeleton`.

Requirements:

- PHP ^8.5
- Symfony ^8.0,
- Doctrine ^3.3.

## Commands

```bash
# Install dependencies
composer install

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beter aanpassen naar symfony composer


# Run tests
./vendor/bin/phpunit

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best symfony php bijzetten


# Run a single test file
./vendor/bin/phpunit tests/path/to/FooTest.php
```

No phpstan or phpcs config exists in this repo. Those are configured per-project in the consuming application.

## Architecture

### Request lifecycle

Two event listeners fire on every request:

1. `BreadcrumbListener` (`kernel.controller_arguments`, priority -1): reads `#[Breadcrumb]` attributes from the matched
controller and populates `BreadcrumbTrail`.
2. `TitleListener` (`kernel.controller_arguments`, priority -1): reads `#[Title]` attributes and writes to `PageTitle`.
Falls back to breadcrumbs if no `#[Title]` is present.

`PageTitle` and `BreadcrumbTrail` are request-scoped services aliased for direct injection.

### Attribute-driven configuration

Controller behaviour is controlled via PHP 8 attributes, not YAML or annotations:

| Attribute | Target | Purpose |
|--------------------|-----------------|----------------------------------------------------------------------------------------------------|
| `#[Breadcrumb]` | method / class | Adds one crumb; repeatable for chains; supports `parent:` for automatic trail building |
| `#[Title]` | method | Explicit page title; supports `{param}` / `{object.property}` interpolation and `parent:` chaining |
| `#[AuditTrail]` | entity class | Enables Doctrine audit logging for that entity; `withData: false` skips field-level diff |
| `#[SensitiveData]` | entity property | Masks property value in audit log output |

### Subsystems

- **Pagination** (`src/Pagination/Paginator.php`): wraps a Doctrine `QueryBuilder`; exposes current page, total
results, prev/next. See `docs/pagination.md`.
- **Menu** (`src/Menu/MenuBuilder.php`): KnpMenu builder dispatching `ConfigureMenuEvent`; consuming apps listen to
that event to add items. See `docs/menu.md`.
- **Audit trail** (`src/DoctrineListener/DoctrineAuditListener.php`): Doctrine `onFlush` + `postPersist` listener that
logs creates/updates/deletes via `AuditLogger`. Entities opt in with `#[AuditTrail]`. See `docs/audit-trail.md`.
- **Forms** (`src/Form/`): custom types (`ImageType`, `FileType`, `BelgiumPostCodeType`) and extensions wiring date
pickers, toggle-password, and collection UI. See `docs/forms.md`.
- **DBAL types** (`src/DBALType/`): `EncryptedDBALType`, `AbstractImageType`, `AbstractFileType` for custom column
handling. See `docs/encrypted.md`, `docs/uploading-files.md`, `docs/uploading-images.md`.
- **Twig** (`src/Twig/`): `FrameworkExtension`, `PaginatorExtension`, `ContentExtension`; `PageTitle` is available as a
string in templates.
- **Doctrine extension** (`src/Extensions/Doctrine/MatchAgainst.php`): custom DQL function for MySQL
`MATCH ... AGAINST` full-text search.

### Service registration

All services are registered in `config/services.php` (PHP-format DI config, no YAML). The bundle extension
(`src/DependencyInjection/SumoCodersFrameworkCoreExtension.php`) loads that file. `Configuration.php` is intentionally
empty. No runtime bundle config is needed.

## Documentation

All docs live in `docs/`. Start at `docs/index.md` for an overview of all subsystems, the request lifecycle, and key
injectable services.

When adding or changing a subsystem, update the corresponding doc file in `docs/`. Each doc follows the format: purpose,
prerequisites, usage, options table, examples, troubleshooting.
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
# SumoCoders FrameworkCoreBundle
This bundle is created and maintained by [SumoCoders](https://github.com/sumocoders). It contains a set of basic tools that enable us to build an application in a shorter timespan. The bundle is intended to be used together with our [npm package](https://github.com/sumocoders/FrameworkStylePackage).

Shared foundation bundle for SumoCoders Symfony projects. Provides page titles, breadcrumbs, navigation menus,
file/image uploads, encrypted fields, audit logging, pagination, and a frontend design system. Created and maintained
by [SumoCoders](https://github.com/sumocoders).

## Installation
To properly use this bundle, create a new project with our application skeleton:
```
$ composer create-project sumocoders/application-skeleton my_project

Create a new project with the application skeleton. It installs this bundle and all required config:

```bash
composer create-project sumocoders/application-skeleton my_project
```
The skeleton will load this bundle, install our npm package and add some extra config for CI, deployment, etc..

Requires:

- PHP ^8.5
- Symfony ^8.0
- Doctrine ^3.3.

## Documentation
All documentation is located in the `docs/` subdirectory.

Start at [`docs/index.md`](docs/index.md).

## Issues?

Feel free to add an Issue on Github, or even better create a PR.

## License

This software is published under the [MIT License](LICENSE.md)
40 changes: 37 additions & 3 deletions docs/frontend/ajax-client.md → docs/ajax-client.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
# AJAX client

The AJAX client is a simple wrapper around [`Axios`](https://axios-http.com/).
A pre-configured [Axios](https://axios-http.com/) wrapper with CSRF support, toast notifications, and busy-button
spinners. Provided by the bundle's JavaScript assets.

## Prerequisites

JavaScript assets must be installed: see [installation.md](installation.md).

Import the client in your Stimulus controller:

```javascript
import ajaxClient from '../js/ajax_client.js'
```

## Default Axios Config

Expand Down Expand Up @@ -116,7 +127,7 @@ A simple way to "protect" the AJAX calls is by using a CSRF token. This is done
```javascript
ajaxClient.csrf_token = this.csrfTokenValue
ajaxClient.post(this.urlValue, data)
...
...
```

With this the csrf token is added to the payload of the request, with the key `csrf_token`.
Expand All @@ -136,5 +147,28 @@ The content of a clicked button can be replaced by a spinner during the request.
```javascript
ajaxClient.busy_targets = [buttonNode]
ajaxClient.post(this.urlValue, data)
...
...
```

## File upload (multipart)

For file uploads, use `FormData` and set `Content-Type` to `multipart/form-data`:

```javascript
const formData = new FormData()
formData.append('file', fileInput.files[0])

ajaxClient.post(this.urlValue, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
}).then(response => {
// handle response
})
```

## Troubleshooting

- **CSRF token invalid**: ensure the token id passed to `isCsrfTokenValid()` on the server matches the id used to
generate the token in Twig
- **Toast not showing**: verify the response JSON contains a `message` key; without it, no toast is triggered
- **Request times out immediately**: the default timeout is 2500ms; override it per request:
`ajaxClient.get(url, { timeout: 10000 })`
7 changes: 4 additions & 3 deletions docs/frontend/asset-mapper.md → docs/asset-mapper.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Asset mapper

We use Symfony asset mapper for our css and js packages. See Symfony documentation for more information: https://symfony.com/doc/current/frontend/asset_mapper.html
We use Symfony asset mapper for our css and js packages. See Symfony documentation for more
information: https://symfony.com/doc/current/frontend/asset_mapper.html

## Add package

Expand Down Expand Up @@ -44,8 +45,8 @@ symfony console importmap:install

## Our CSS

Our main CSS is currently located in the framework-core-bundle as this still needs to be compile with scss for Bootstrap.
This CSS is included in the `assets/styles/style.scss` file.
Our main CSS is located in the framework-core-bundle as this still needs to be compile with scss for Bootstrap. This CSS
is included in the `assets/styles/style.scss` file.

To compile the CSS with SASS, you can run the following command:

Expand Down
90 changes: 66 additions & 24 deletions docs/development/audit-trail.md → docs/audit-trail.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# Audit trail

## Introduction
Logs entity creates, updates, and deletes to the `audit_trail` Monolog channel. Disabled by default. Entities opt in
with `#[AuditTrail]`.

The audit trail is a feature that allows you to track changes to your data.
It is useful for tracking changes to sensitive data, such as user accounts, or for tracking changes to data that is
important for compliance, such as financial records.
## Prerequisites

No extra configuration required. The `DoctrineAuditListener` is registered automatically. To capture logs, configure a
`audit_trail` channel in `config/packages/monolog.yaml`.

## Usage

The audit trail is NOT enabled by default. You will need to add the `#[AuditTrial]` attribute to the entity you want to track.
Add `#[AuditTrail]` to any entity class. The attribute is NOT added by default.

```php
#[ORM\Entity]
Expand All @@ -31,7 +33,8 @@ class Book
[2024-09-06T08:30:40.145881+00:00] audit_trail.INFO: Source: https://test.wip/trail; Entity: App\Entity\Book; Identifier: 1; Action: C; User: test@sumocoders.be; Roles: ROLE_ADMIN, ROLE_USER; IP: 127.0.0.1; Fields: []; Data: {"title":"The Lord of the Rings","author":"J. R. R. Tolkien","price":40.50} [] []
```

By default the following data is tracked:
By default, the following data is tracked:

* The date and time of the action
* The source of the action (e.g. the url of the request, the command that was run, etc.)
* The entity that was changed
Expand All @@ -43,7 +46,28 @@ By default the following data is tracked:
* The fields that were changed
* The data that was changed

You can specify which fields you want to track by adding the specifying the `field` property in the `#[AuditTrail]` attribute.
## `#[AuditTrail]` options

| Option | Type | Default | Description |
|------------|---------|---------|---------------------------------------------------------------------------|
| `fields` | `array` | `[]` | Limit tracking to these field names. Empty array = all fields |
| `withData` | `bool` | `true` | Include old/new values in the log. Set to `false` to log field names only |

## Log format

```
[datetime] audit_trail.INFO: Source: <url>; Entity: <FQCN>; Identifier: <id>; Action: <action>; User: <email>; Roles: <roles>; IP: <ip>; Fields: [<fields>]; Data: <data>
```

**Action codes:**

| Code | Meaning |
|------|----------------------------------------------|
| `C` | Create (entity persisted for the first time) |
| `U` | Update (entity modified) |
| `D` | Delete (entity removed) |

## Filter to specific fields

```php
#[ORM\Entity]
Expand All @@ -68,6 +92,7 @@ class Book

You can hide secure data from the audit trail by adding the `#[SensitiveData]` attribute to the property.
This will transform the data to `****` in the audit trail.

```php
#[AuditTrail]
#[ORM\Entity]
Expand All @@ -90,7 +115,8 @@ class User
[2024-09-06T09:48:53.540500+00:00] audit_trail.INFO: Source: https://test.wip/profile; Entity: App\Entity\User; Identifier: 2; Action: U; User: test@sumocoders.be; Roles: ROLE_ADMIN, ROLE_USER; IP: 127.0.0.1; Fields: ["password"]; Data: {"password":{"from":"*****","to":"*****"}} [] []
```

There is also an option to only track the fields that are changes without the data, with the option `withData` set to `false`.
There is also an option to only track the fields that are changes without the data, with the option `withData` set to
`false`.

```php
#[AuditTrail(withData: false)]
Expand All @@ -113,26 +139,42 @@ class User
[2024-09-06T09:48:53.540500+00:00] audit_trail.INFO: Source: https://test.wip/profile; Entity: App\Entity\User; Identifier: 2; Action: U; User: test@sumocoders.be; Roles: ROLE_ADMIN, ROLE_USER; IP: 127.0.0.1; Fields: ["password"]; Data: []} [] []
```

### Manually tracking changes
## Manually tracking actions

You can manually track changes by using the `AuditLogger` service.
Inject `AuditLogger` to log non-Doctrine actions (e.g. exports, logins, API calls):

```php
class TestController extends AbstractController
<?php

namespace App\Controller;

use SumoCoders\FrameworkCoreBundle\Logger\AuditLogger;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/export', name: 'data_export')]
final class ExportController extends AbstractController
{
#[Route('/test', name: 'test')]
public function __invoke(
AuditLogger $auditLogger,
): ResponseAlias {

$auditLogger->log(
data: ['test' => 'test'],
);

return $this->render(
'test/index.html.twig',
[]
);
public function __invoke(AuditLogger $auditLogger): Response
{
$auditLogger->log(data: ['format' => 'csv', 'rows' => 1500]);

return $this->file('export.csv');
}
}
```

## Performance note

The listener fires on every Doctrine `onFlush` event. For entities that change very frequently, use `fields:` to narrow
which changes are logged, or set `withData: false` to skip the field diff computation.

## Troubleshooting

- **No log entries appearing**: verify the `audit_trail` Monolog channel is configured and the log file/handler is
writable
- **All fields tracked instead of specific ones**: `fields:` must list the exact property names as defined on the
entity, not the column names
- **Sensitive data visible**: add `#[SensitiveData]` to the property; the value will be masked as `*****` in both old
and new values
Loading