mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-10 08:44:58 +00:00
Merge branch 'master' into ticket-app-master
This commit is contained in:
6
.changes/unreleased/Feature-20250904-181032.yaml
Normal file
6
.changes/unreleased/Feature-20250904-181032.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
kind: Feature
|
||||||
|
body: Add a command to generate a list of permissions
|
||||||
|
time: 2025-09-04T18:10:32.334524026+02:00
|
||||||
|
custom:
|
||||||
|
Issue: ""
|
||||||
|
SchemaChange: No schema change
|
@@ -27,11 +27,11 @@ Chill is a comprehensive web application built as a set of Symfony bundles. It i
|
|||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
Note: This is a project which exists from a long time ago, and we found multiple structure inside each bundle. When having the choice, the developers should choose the new structure.
|
Note: This is a project that's existed for a long time, and throughout the years we've used multiple structures inside each bundle. When having the choice, the developers should choose the new structure.
|
||||||
|
|
||||||
The project follows a standard Symfony bundle structure:
|
The project follows a standard Symfony bundle structure:
|
||||||
- `/src/Bundle/`: Contains all the Chill bundles. The code is either at the root of the bundle directory, or within a `src/` directory (preferred). See psr4 mapping at the root's `composer.json`.
|
- `/src/Bundle/`: Contains all the Chill bundles. The code is either at the root of the bundle directory, or within a `src/` directory (preferred). See psr4 mapping at the root's `composer.json`.
|
||||||
- each bundle come with his own tests, either in the `Tests` directory (when the code is directly within the bundle directory (for instance `src/Bundle/ChillMainBundle/Tests`, `src/Bundle/ChillPersonBundle/Tests`)), or inside the `tests` directory, alongside to the `src/` sub-directory (example: `src/Bundle/ChillWopiBundle/tests`) (this is the preferred way).
|
- each bundle comes with its own tests, either in the `Tests` directory (when the code is directly within the bundle directory (for instance `src/Bundle/ChillMainBundle/Tests`, `src/Bundle/ChillPersonBundle/Tests`)), or inside the `tests` directory, alongside the `src/` sub-directory (example: `src/Bundle/ChillWopiBundle/tests`) (this is the preferred way).
|
||||||
- `/docs/`: Contains project documentation
|
- `/docs/`: Contains project documentation
|
||||||
|
|
||||||
Each bundle typically has the following structure:
|
Each bundle typically has the following structure:
|
||||||
@@ -46,13 +46,13 @@ Each bundle typically has the following structure:
|
|||||||
|
|
||||||
### A special word about TicketBundle
|
### A special word about TicketBundle
|
||||||
|
|
||||||
The ticket bundle is developed using a kind of "Command" pattern. The controller fill a "Command", and a "CommandHandler" handle this command. They are savec in the `src/Bundle/ChillTicketBundle/src/Action` directory.
|
The ticket bundle is developed using a kind of "Command" pattern. The controller fills a "Command," and a "CommandHandler" handles this command. They are saved in the `src/Bundle/ChillTicketBundle/src/Action` directory.
|
||||||
|
|
||||||
## Development Guidelines
|
## Development Guidelines
|
||||||
|
|
||||||
### Building and Configuration Instructions
|
### Building and Configuration Instructions
|
||||||
|
|
||||||
All the command should be run through the `symfony` command, which will configure the required variables.
|
All the commands should be run through the `symfony` command, which will configure the required variables.
|
||||||
|
|
||||||
For assets, we must ensure that we use node at version `^20.0.0`. This is done using `nvm use 20`.
|
For assets, we must ensure that we use node at version `^20.0.0`. This is done using `nvm use 20`.
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ For assets, we must ensure that we use node at version `^20.0.0`. This is done u
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Set Up the Database**:
|
6. **Set Up the Database**:
|
||||||
```bash
|
```bash
|
||||||
# Create the database
|
# Create the database
|
||||||
symfony console doctrine:database:create
|
symfony console doctrine:database:create
|
||||||
@@ -99,20 +99,20 @@ For assets, we must ensure that we use node at version `^20.0.0`. This is done u
|
|||||||
symfony console doctrine:fixtures:load
|
symfony console doctrine:fixtures:load
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **Build Assets**:
|
7. **Build Assets**:
|
||||||
```bash
|
```bash
|
||||||
nvm use 20
|
nvm use 20
|
||||||
yarn run encore dev
|
yarn run encore dev
|
||||||
```
|
```
|
||||||
|
|
||||||
7. **Start the Development Server**:
|
8. **Start the Development Server**:
|
||||||
```bash
|
```bash
|
||||||
symfony server:start -d
|
symfony server:start -d
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Docker Setup
|
#### Docker Setup
|
||||||
|
|
||||||
The project includes Docker configuration for easier development:
|
The project includes a Docker configuration for easier development:
|
||||||
|
|
||||||
1. **Start Docker Services**:
|
1. **Start Docker Services**:
|
||||||
```bash
|
```bash
|
||||||
@@ -153,9 +153,9 @@ Key configuration files:
|
|||||||
|
|
||||||
Each time a doctrine entity is created, we generate migration to adapt the database.
|
Each time a doctrine entity is created, we generate migration to adapt the database.
|
||||||
|
|
||||||
The migration are created using the command `symfony console doctrine:migrations:diff --no-interaction --namespace <namespace>`, where the namespace is the relevant namespace for migration. As this is a bash script, do not forget to quote the `\` (`\` must become `\\` in your command).
|
The migration is created using the command `symfony console doctrine:migrations:diff --no-interaction --namespace <namespace>`, where the namespace is the relevant namespace for migration. As this is a bash script, remember to quote the `\` (`\` must become `\\` in your command).
|
||||||
|
|
||||||
Each bundle has his own namespace for migration (always ask me to confirm that command, with a list of updated / created entities so that I can confirm you that it is ok):
|
Each bundle has his own namespace for migration (always ask me to confirm that command with a list of updated / created entities so that I can confirm to you that it is ok):
|
||||||
|
|
||||||
- `Chill\Bundle\ActivityBundle` writes migrations to `Chill\Migrations\Activity`;
|
- `Chill\Bundle\ActivityBundle` writes migrations to `Chill\Migrations\Activity`;
|
||||||
- `Chill\Bundle\BudgetBundle` writes migrations to `Chill\Migrations\Budget`;
|
- `Chill\Bundle\BudgetBundle` writes migrations to `Chill\Migrations\Budget`;
|
||||||
@@ -183,7 +183,7 @@ Once created the, comment's classes should be removed and a description of the c
|
|||||||
|
|
||||||
When we need to use a DateTime or DateTimeImmutable that need to express "now", we prefer the usage of
|
When we need to use a DateTime or DateTimeImmutable that need to express "now", we prefer the usage of
|
||||||
`Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities,
|
`Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities,
|
||||||
where injection does not work when restoring an entity from database, but usually possible in services.
|
where injection does not work when restoring an entity from a database, but usually possible in services.
|
||||||
|
|
||||||
In test, we use `\Symfony\Component\Clock\MockClock` which is an implementation of `Symfony\Component\Clock\ClockInterface`
|
In test, we use `\Symfony\Component\Clock\MockClock` which is an implementation of `Symfony\Component\Clock\ClockInterface`
|
||||||
where we have full and easy control of the date.
|
where we have full and easy control of the date.
|
||||||
@@ -198,9 +198,9 @@ The project uses PHPUnit for testing. Each bundle has its own test suite, and th
|
|||||||
|
|
||||||
For creating mock, we prefer using prophecy (library phpspec/prophecy).
|
For creating mock, we prefer using prophecy (library phpspec/prophecy).
|
||||||
|
|
||||||
##### Useful helpers and tips that avoid create a mock
|
##### Useful helpers and tips that avoid creating a mock
|
||||||
|
|
||||||
Some notable implementations that are tests helper, and avoid to create a mock:
|
Some notable implementations that are test helpers and avoid creating a mock:
|
||||||
|
|
||||||
- `\Psr\Log\NullLogger`, an implementation of `\Psr\Log\LoggerInterface`;
|
- `\Psr\Log\NullLogger`, an implementation of `\Psr\Log\LoggerInterface`;
|
||||||
- `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above);
|
- `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above);
|
||||||
@@ -297,7 +297,7 @@ class TicketTest extends TestCase
|
|||||||
|
|
||||||
#### Test Database
|
#### Test Database
|
||||||
|
|
||||||
For tests that require a database, the project uses postgresql database filled by fixtures (usage of doctrine-fixtures). You can configure a different database for testing in the `.env.test` file.
|
For tests that require a database, the project uses a postgresql database filled with fixtures (usage of doctrine-fixtures). You can configure a different database for testing in the `.env.test` file.
|
||||||
|
|
||||||
### Code Quality Tools
|
### Code Quality Tools
|
||||||
|
|
||||||
|
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Command;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Security\RoleDumper;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
#[AsCommand(name: 'chill:main:dump-list-permissions', description: 'Print a markdown reference of permissions (roles) grouped by title with dependencies).')]
|
||||||
|
final class DumpListPermissionsCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(private readonly RoleDumper $roleDumper)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$markdown = $this->roleDumper->dumpAsMarkdown();
|
||||||
|
$output->writeln($markdown);
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
@@ -20,7 +20,7 @@ use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
|||||||
use Chill\MainBundle\Repository\RegroupmentRepositoryInterface;
|
use Chill\MainBundle\Repository\RegroupmentRepositoryInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @phpstan-type NormalizedData array{centers: array{centers: list<int>, regroupments: list<int>}, export: array{form: array<string, mixed>, version: int}, filters: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, aggregators: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, pick_formatter: string, formatter: array{form: array<string, mixed>, version: int}}
|
* @phpstan-type NormalizedData array{centers: array{centers: list<int>, regroupments: list<int>}, export: array{form: array<string, mixed>, version: int}, filters: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, aggregators: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, pick_formatter?: string, formatter: array{form: array<string, mixed>, version: int}}
|
||||||
*/
|
*/
|
||||||
class ExportConfigNormalizer
|
class ExportConfigNormalizer
|
||||||
{
|
{
|
||||||
|
86
src/Bundle/ChillMainBundle/Security/RoleDumper.php
Normal file
86
src/Bundle/ChillMainBundle/Security/RoleDumper.php
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Security;
|
||||||
|
|
||||||
|
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
final readonly class RoleDumper
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private RoleProvider $roleProvider,
|
||||||
|
private RoleHierarchyInterface $roleHierarchy,
|
||||||
|
private TranslatorInterface $translator,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function dumpAsMarkdown(): string
|
||||||
|
{
|
||||||
|
$roles = $this->roleProvider->getRoles();
|
||||||
|
$rolesWithoutScopes = $this->roleProvider->getRolesWithoutScopes();
|
||||||
|
|
||||||
|
// Group roles by title
|
||||||
|
$groups = [];
|
||||||
|
foreach ($roles as $role) {
|
||||||
|
$title = $this->roleProvider->getRoleTitle($role);
|
||||||
|
$title ??= 'Other';
|
||||||
|
$groups[$title][] = $role;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort groups by title
|
||||||
|
ksort($groups, SORT_NATURAL | SORT_FLAG_CASE);
|
||||||
|
|
||||||
|
$lines = [];
|
||||||
|
foreach ($groups as $title => $roleList) {
|
||||||
|
// Sort roles by translated label for deterministic output
|
||||||
|
usort($roleList, function (string $a, string $b): int {
|
||||||
|
$ta = $this->translator->trans($a);
|
||||||
|
$tb = $this->translator->trans($b);
|
||||||
|
|
||||||
|
return strcasecmp($ta, $tb);
|
||||||
|
});
|
||||||
|
|
||||||
|
$translatedTitle = $this->translator->trans($title);
|
||||||
|
$lines[] = '## '.$translatedTitle;
|
||||||
|
|
||||||
|
foreach ($roleList as $role) {
|
||||||
|
// Translate primary role
|
||||||
|
$translatedRole = $this->translator->trans($role);
|
||||||
|
|
||||||
|
// Scope marker: (S) if needs scope, (~~S~~) if no scope required
|
||||||
|
$needsScope = !in_array($role, $rolesWithoutScopes, true);
|
||||||
|
$scopeMarker = $needsScope ? '(S)' : '(~~S~~)';
|
||||||
|
|
||||||
|
// Compute dependent roles from hierarchy (exclude itself)
|
||||||
|
$reachable = $this->roleHierarchy->getReachableRoleNames([$role]);
|
||||||
|
$dependents = array_values(array_filter($reachable, static fn (string $r): bool => $r !== $role));
|
||||||
|
|
||||||
|
// Translate dependents and sort deterministically
|
||||||
|
$translatedDependents = array_map(fn (string $r) => $this->translator->trans($r), $dependents);
|
||||||
|
sort($translatedDependents, SORT_NATURAL | SORT_FLAG_CASE);
|
||||||
|
|
||||||
|
if (count($translatedDependents) > 0) {
|
||||||
|
$lines[] = sprintf('- **%s** %s: %s', $translatedRole, $scopeMarker, implode(', ', $translatedDependents));
|
||||||
|
} else {
|
||||||
|
$lines[] = sprintf('- **%s** %s', $translatedRole, $scopeMarker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a blank line between groups
|
||||||
|
$lines[] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim possible trailing blank line
|
||||||
|
$markdown = rtrim(implode("\n", $lines));
|
||||||
|
|
||||||
|
return $markdown."\n"; // End with newline for POSIX friendliness
|
||||||
|
}
|
||||||
|
}
|
@@ -52,12 +52,8 @@ class RoleProvider
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the title for each role.
|
* Get the title for each role.
|
||||||
*
|
|
||||||
* @param string $role
|
|
||||||
*
|
|
||||||
* @return string the title of the role
|
|
||||||
*/
|
*/
|
||||||
public function getRoleTitle($role)
|
public function getRoleTitle(string $role): ?string
|
||||||
{
|
{
|
||||||
$this->initializeRolesTitlesCache();
|
$this->initializeRolesTitlesCache();
|
||||||
|
|
||||||
@@ -73,7 +69,7 @@ class RoleProvider
|
|||||||
/**
|
/**
|
||||||
* initialize the array for caching role and titles.
|
* initialize the array for caching role and titles.
|
||||||
*/
|
*/
|
||||||
private function initializeRolesTitlesCache()
|
private function initializeRolesTitlesCache(): void
|
||||||
{
|
{
|
||||||
// break if already initialized
|
// break if already initialized
|
||||||
if (null !== $this->rolesTitlesCache) {
|
if (null !== $this->rolesTitlesCache) {
|
||||||
|
98
src/Bundle/ChillMainBundle/Tests/Security/RoleDumperTest.php
Normal file
98
src/Bundle/ChillMainBundle/Tests/Security/RoleDumperTest.php
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Tests\Security;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
|
||||||
|
use Chill\MainBundle\Security\RoleDumper;
|
||||||
|
use Chill\MainBundle\Security\RoleProvider;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class RoleDumperTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testDumpAsMarkdownGroupsByTitleTranslatesAndListsDependencies(): void
|
||||||
|
{
|
||||||
|
// Fake provider with two groups
|
||||||
|
$provider = new class () implements ProvideRoleHierarchyInterface {
|
||||||
|
public const R_PERSON_SEE = 'CHILL_PERSON_SEE';
|
||||||
|
public const R_PERSON_UPDATE = 'CHILL_PERSON_UPDATE';
|
||||||
|
public const R_REPORT_SEE = 'CHILL_REPORT_SEE';
|
||||||
|
|
||||||
|
public function getRoles(): array
|
||||||
|
{
|
||||||
|
return [self::R_PERSON_SEE, self::R_PERSON_UPDATE, self::R_REPORT_SEE];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRolesWithoutScope(): array
|
||||||
|
{
|
||||||
|
// In this test, assume REPORT_SEE does not need scope, others do
|
||||||
|
return [self::R_REPORT_SEE];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRolesWithHierarchy(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Person' => [self::R_PERSON_SEE, self::R_PERSON_UPDATE],
|
||||||
|
'Report' => [self::R_REPORT_SEE],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$roleProvider = new RoleProvider([$provider]);
|
||||||
|
|
||||||
|
// Fake role hierarchy: UPDATE implies SEE; others none
|
||||||
|
$roleHierarchy = new class () implements RoleHierarchyInterface {
|
||||||
|
public function getReachableRoleNames(array $roles): array
|
||||||
|
{
|
||||||
|
$output = [];
|
||||||
|
foreach ($roles as $r) {
|
||||||
|
$output[] = $r;
|
||||||
|
if ('CHILL_PERSON_UPDATE' === $r) {
|
||||||
|
$output[] = 'CHILL_PERSON_SEE';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($output));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fake translator that clearly shows translation applied
|
||||||
|
$translator = new class () implements TranslatorInterface {
|
||||||
|
public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
|
||||||
|
{
|
||||||
|
return 'T('.$id.')';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLocale(): string
|
||||||
|
{
|
||||||
|
return 'en';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$dumper = new RoleDumper($roleProvider, $roleHierarchy, $translator);
|
||||||
|
$md = $dumper->dumpAsMarkdown();
|
||||||
|
|
||||||
|
$expected = "## T(Person)\n"
|
||||||
|
."- **T(CHILL_PERSON_SEE)** (S)\n"
|
||||||
|
."- **T(CHILL_PERSON_UPDATE)** (S): T(CHILL_PERSON_SEE)\n\n"
|
||||||
|
."## T(Report)\n"
|
||||||
|
."- **T(CHILL_REPORT_SEE)** (~~S~~)\n";
|
||||||
|
|
||||||
|
self::assertSame($expected, $md);
|
||||||
|
}
|
||||||
|
}
|
@@ -80,3 +80,7 @@ services:
|
|||||||
Chill\MainBundle\Command\SynchronizeEntityInfoViewsCommand:
|
Chill\MainBundle\Command\SynchronizeEntityInfoViewsCommand:
|
||||||
tags:
|
tags:
|
||||||
- {name: console.command}
|
- {name: console.command}
|
||||||
|
|
||||||
|
Chill\MainBundle\Command\DumpListPermissionsCommand:
|
||||||
|
autoconfigure: true
|
||||||
|
autowire: true
|
||||||
|
Reference in New Issue
Block a user