mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-09 08:14:59 +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
|
||||
|
||||
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:
|
||||
- `/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
|
||||
|
||||
Each bundle typically has the following structure:
|
||||
@@ -46,13 +46,13 @@ Each bundle typically has the following structure:
|
||||
|
||||
### 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
|
||||
|
||||
### 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`.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
5. **Set Up the Database**:
|
||||
6. **Set Up the Database**:
|
||||
```bash
|
||||
# Create the database
|
||||
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
|
||||
```
|
||||
|
||||
6. **Build Assets**:
|
||||
7. **Build Assets**:
|
||||
```bash
|
||||
nvm use 20
|
||||
yarn run encore dev
|
||||
```
|
||||
|
||||
7. **Start the Development Server**:
|
||||
8. **Start the Development Server**:
|
||||
```bash
|
||||
symfony server:start -d
|
||||
```
|
||||
|
||||
#### Docker Setup
|
||||
|
||||
The project includes Docker configuration for easier development:
|
||||
The project includes a Docker configuration for easier development:
|
||||
|
||||
1. **Start Docker Services**:
|
||||
```bash
|
||||
@@ -153,9 +153,9 @@ Key configuration files:
|
||||
|
||||
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\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
|
||||
`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`
|
||||
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).
|
||||
|
||||
##### 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`;
|
||||
- `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above);
|
||||
@@ -297,7 +297,7 @@ class TicketTest extends TestCase
|
||||
|
||||
#### 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
|
||||
|
||||
|
@@ -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;
|
||||
|
||||
/**
|
||||
* @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
|
||||
{
|
||||
|
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.
|
||||
*
|
||||
* @param string $role
|
||||
*
|
||||
* @return string the title of the role
|
||||
*/
|
||||
public function getRoleTitle($role)
|
||||
public function getRoleTitle(string $role): ?string
|
||||
{
|
||||
$this->initializeRolesTitlesCache();
|
||||
|
||||
@@ -73,7 +69,7 @@ class RoleProvider
|
||||
/**
|
||||
* initialize the array for caching role and titles.
|
||||
*/
|
||||
private function initializeRolesTitlesCache()
|
||||
private function initializeRolesTitlesCache(): void
|
||||
{
|
||||
// break if already initialized
|
||||
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:
|
||||
tags:
|
||||
- {name: console.command}
|
||||
|
||||
Chill\MainBundle\Command\DumpListPermissionsCommand:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
|
Reference in New Issue
Block a user