Skip to content

Commit 80c8e22

Browse files
bastien70jmsche
andauthored
Added User CRUD (#13)
* Added User CRUD * Finished User CRUD * Fixed changes * Using templates in fixtures * Add line break between constants and properties * Updated User CRUD permissions * Added menu links count tests Co-authored-by: jmsche <[email protected]>
1 parent 39b3422 commit 80c8e22

File tree

16 files changed

+445
-17
lines changed

16 files changed

+445
-17
lines changed

fixtures/backup.yaml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
App\Entity\Backup:
2-
backup__{1..30}:
2+
base_backup (template):
33
context: <randomElement(['manual', 'automatic'])>
44
backupFile: <getSqlFile()>
5-
database: '@database__*'
5+
6+
backup__user_{1..15} (extends base_backup):
7+
database: '@database__user_*'
8+
9+
backup__admin_{1..15} (extends base_backup):
10+
database: '@database__admin_*'

fixtures/database.yaml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
App\Entity\Database:
2-
database__{1..10}:
2+
base_database (template):
33
host: localhost
44
user: <userName()>
55
password: <word()>
66
name: <word()>
77
maxBackups: <numberBetween(5, 20)>
8-
owner: '@user__*'
8+
9+
database__user_{1..5} (extends base_database):
10+
owner: '@user'
11+
12+
database__admin_{1..5} (extends base_database):
13+
owner: '@admin'

fixtures/user.yaml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
App\Entity\User:
2-
user__{1..2}:
3-
email: <generateEmailAddressForUser(<current()>)>
2+
base_user (template):
43
password: <encodePassword(@self, 'test')>
5-
role: <randomElement(['ROLE_USER', 'ROLE_ADMIN'])>
4+
5+
user (extends base_user):
6+
7+
role: 'ROLE_USER'
8+
9+
admin (extends base_user):
10+
11+
role: 'ROLE_ADMIN'

src/Admin/Field/BadgeField.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Admin\Field;
6+
7+
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
8+
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
9+
10+
class BadgeField implements FieldInterface
11+
{
12+
use FieldTrait;
13+
14+
public const BADGE_CLASS = 'badge_class';
15+
public const BADGE_BACKGROUND_COLOR = 'badge_background_color';
16+
public const BADGE_ROUNDED = 'badge_rounded';
17+
public const BADGE_FONT_SIZE = 'badge_font_size';
18+
public const BADGE_TEXT_CLASS = 'badge_text_class';
19+
20+
public static function new(string $propertyName, ?string $label = null): self
21+
{
22+
return (new self())
23+
->setProperty($propertyName)
24+
->setLabel($label)
25+
->setTemplatePath('bundles/EasyAdminBundle/field/badge.html.twig')
26+
->setCustomOption(self::BADGE_CLASS, 'primary')
27+
->setCustomOption(self::BADGE_BACKGROUND_COLOR, null)
28+
->setCustomOption(self::BADGE_ROUNDED, false)
29+
->setCustomOption(self::BADGE_FONT_SIZE, null)
30+
->setCustomOption(self::BADGE_TEXT_CLASS, null);
31+
}
32+
33+
/**
34+
* Available classes : primary, secondary, success, danger, warning, info, light, dark.
35+
*/
36+
public function setBadgeClass(string $class = 'primary'): self
37+
{
38+
$this->setCustomOption(self::BADGE_CLASS, $class);
39+
40+
return $this;
41+
}
42+
43+
public function setTextClass(?string $class = null): self
44+
{
45+
$this->setCustomOption(self::BADGE_TEXT_CLASS, $class);
46+
47+
return $this;
48+
}
49+
50+
/**
51+
* Hex color.
52+
* Example : #0277bd.
53+
*/
54+
public function setBackgroundColor(?string $backgroundColor = null): self
55+
{
56+
$this->setCustomOption(self::BADGE_BACKGROUND_COLOR, $backgroundColor);
57+
58+
return $this;
59+
}
60+
61+
public function setRounded(bool $rounded = true): self
62+
{
63+
$this->setCustomOption(self::BADGE_ROUNDED, $rounded);
64+
65+
return $this;
66+
}
67+
68+
public function setFontSize(?string $fontSize = null): self
69+
{
70+
$this->setCustomOption(self::BADGE_FONT_SIZE, $fontSize);
71+
72+
return $this;
73+
}
74+
}

src/Controller/Admin/DashboardController.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use App\Entity\Backup;
88
use App\Entity\Database;
9+
use App\Entity\User;
910
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
1011
use EasyCorp\Bundle\EasyAdminBundle\Config\Menu\RouteMenuItem;
1112
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
@@ -55,6 +56,8 @@ public function configureMenuItems(): iterable
5556
yield MenuItem::linktoDashboard('menu.home', 'fa fa-home');
5657
yield MenuItem::linkToCrud('menu.databases', 'fas fa-database', Database::class);
5758
yield MenuItem::linkToCrud('menu.backups', 'fas fa-shield-alt', Backup::class);
59+
yield MenuItem::linkToCrud('menu.users', 'fas fa-users', User::class)
60+
->setPermission(User::ROLE_ADMIN);
5861

5962
$localeLinks = array_map(static function (string $locale): RouteMenuItem {
6063
return MenuItem::linkToRoute(ucfirst(Languages::getName($locale, $locale)), null, 'admin_switch_locale', ['locale' => $locale]);
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Controller\Admin;
6+
7+
use App\Admin\Field\BadgeField;
8+
use App\Entity\User;
9+
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
10+
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
11+
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
12+
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
13+
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
14+
use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField;
15+
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
16+
17+
class UserCrudController extends AbstractCrudController
18+
{
19+
public static function getEntityFqcn(): string
20+
{
21+
return User::class;
22+
}
23+
24+
public function configureCrud(Crud $crud): Crud
25+
{
26+
return $crud
27+
->setPageTitle(Crud::PAGE_INDEX, 'user.index.title')
28+
->setPageTitle(Crud::PAGE_EDIT, 'user.edit.title')
29+
->setPageTitle(Crud::PAGE_NEW, 'user.new.title')
30+
->setEntityLabelInPlural('user.admin_label.plural')
31+
->setEntityLabelInSingular('user.admin_label.singular')
32+
;
33+
}
34+
35+
public function configureActions(Actions $actions): Actions
36+
{
37+
return $actions
38+
->update(Crud::PAGE_INDEX, Action::NEW, function (Action $action) {
39+
return $action->setLabel('user.action.new');
40+
})
41+
->update(Crud::PAGE_INDEX, Action::EDIT, function (Action $action) {
42+
return $action->setLabel('user.action.edit');
43+
})
44+
->update(Crud::PAGE_INDEX, Action::DELETE, function (Action $action) {
45+
return $action->setLabel('user.action.delete');
46+
})
47+
->update(Crud::PAGE_INDEX, Action::BATCH_DELETE, function (Action $action) {
48+
return $action->setLabel('user.action.delete');
49+
})
50+
->setPermission(Action::NEW, User::ROLE_ADMIN)
51+
->setPermission(Action::DELETE, User::ROLE_ADMIN)
52+
->setPermission(Action::BATCH_DELETE, User::ROLE_ADMIN)
53+
->setPermission(Action::EDIT, User::ROLE_ADMIN)
54+
->add(Crud::PAGE_NEW, Action::INDEX)
55+
->remove(Crud::PAGE_EDIT, Action::SAVE_AND_CONTINUE)
56+
->add(Crud::PAGE_EDIT, Action::INDEX);
57+
}
58+
59+
public function configureFields(string $pageName): iterable
60+
{
61+
yield EmailField::new('email', 'user.field.email');
62+
yield TextField::new('plainPassword', 'user.field.password')
63+
->onlyOnForms()
64+
->setRequired(Crud::PAGE_NEW === $pageName);
65+
yield ChoiceField::new('role', 'user.field.role')
66+
->setChoices([
67+
'user.choices.role.user' => User::ROLE_USER,
68+
'user.choices.role.admin' => User::ROLE_ADMIN,
69+
]);
70+
yield BadgeField::new('databases', 'user.field.databases')
71+
->formatValue(function ($value) {
72+
return \count($value);
73+
})
74+
->hideOnForm();
75+
}
76+
}

src/Entity/User.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, \String
3333
#[ORM\Column(type: 'string')]
3434
private string $password;
3535

36+
private ?string $plainPassword = null;
37+
3638
#[ORM\OneToMany(mappedBy: 'owner', targetEntity: Database::class, orphanRemoval: true)]
3739
private Collection $databases;
3840

@@ -128,7 +130,7 @@ public function getSalt(): ?string
128130
public function eraseCredentials(): void
129131
{
130132
// If you store any temporary, sensitive data on the user, clear it here
131-
// $this->plainPassword = null;
133+
$this->plainPassword = null;
132134
}
133135

134136
/**
@@ -162,4 +164,14 @@ public static function getAvailableRoles(): array
162164
{
163165
return [self::ROLE_USER, self::ROLE_ADMIN];
164166
}
167+
168+
public function getPlainPassword(): ?string
169+
{
170+
return $this->plainPassword;
171+
}
172+
173+
public function setPlainPassword(?string $plainPassword): void
174+
{
175+
$this->plainPassword = $plainPassword;
176+
}
165177
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\EventSubscriber;
6+
7+
use App\Entity\User;
8+
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityPersistedEvent;
9+
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityUpdatedEvent;
10+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
11+
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
12+
13+
class UserSubscriber implements EventSubscriberInterface
14+
{
15+
public function __construct(
16+
private UserPasswordHasherInterface $hasher,
17+
) {
18+
}
19+
20+
public static function getSubscribedEvents(): array
21+
{
22+
return [
23+
BeforeEntityPersistedEvent::class => 'beforePersistedEvent',
24+
BeforeEntityUpdatedEvent::class => 'beforeUpdatedEvent',
25+
];
26+
}
27+
28+
public function beforePersistedEvent(BeforeEntityPersistedEvent $event): void
29+
{
30+
$entity = $event->getEntityInstance();
31+
32+
if (!$entity instanceof User) {
33+
return;
34+
}
35+
36+
$this->handleUserPasswordChange($entity);
37+
}
38+
39+
public function beforeUpdatedEvent(BeforeEntityUpdatedEvent $event): void
40+
{
41+
$entity = $event->getEntityInstance();
42+
43+
if (!$entity instanceof User) {
44+
return;
45+
}
46+
47+
$this->handleUserPasswordChange($entity);
48+
}
49+
50+
private function handleUserPasswordChange(User $user): void
51+
{
52+
if (null !== $user->getPlainPassword()) {
53+
$user->setPassword($this->hasher->hashPassword($user, $user->getPlainPassword()));
54+
}
55+
56+
$user->eraseCredentials();
57+
}
58+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
2+
{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #}
3+
{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #}
4+
5+
{% set styles = '' %}
6+
7+
{% if field.customOptions.get('badge_background_color') != null %}
8+
{% set styles = styles ~ 'background-color:' ~ field.customOptions.get('badge_background_color') ~ ';' %}
9+
{% endif %}
10+
11+
{% if field.customOptions.get('badge_font_size') != null %}
12+
{% set styles = styles ~ 'font-size:' ~ field.customOptions.get('badge_font_size') ~ ';' %}
13+
{% endif %}
14+
15+
<span
16+
class="badge
17+
{% if field.customOptions.get('badge_rounded') %} badge-pill {% endif %}
18+
{% if field.customOptions.get('badge_rounded') %} text-{{ field.customOptions.get('badge_text_class') }} {% endif %}
19+
badge-{{ field.customOptions.get('badge_class') }}"
20+
style="{{ styles }}">
21+
{{ field.formattedValue }}
22+
</span>

tests/Controller/AbstractControllerTest.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,24 @@
1010

1111
abstract class AbstractControllerTest extends WebTestCase
1212
{
13+
public const USER_ROLE_USER = 1;
14+
public const USER_ROLE_ADMIN = 2;
15+
1316
protected static KernelBrowser $client;
1417

1518
protected function setUp(): void
1619
{
1720
static::$client = static::createClient();
1821
}
1922

20-
protected function loginAsUser1(): void
23+
protected function loginAsUser(): void
24+
{
25+
$this->login(self::USER_ROLE_USER);
26+
}
27+
28+
protected function loginAsAdmin(): void
2129
{
22-
$this->login(1);
30+
$this->login(self::USER_ROLE_ADMIN);
2331
}
2432

2533
private function login(int $userId): void

0 commit comments

Comments
 (0)