diff --git a/CHANGELOG b/CHANGELOG
index 20cb4d0..05a5966 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,7 @@
3.9.0 (unreleased):
* Feature: (SF6.2+) Supports automatic value resolving in controllers (with or without MapEntity), see [https://github.com/symfony/symfony/blob/7.2/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php](Doctrine Bridge)
* Feature: supports serializer options in attributes
+ * Feature: Support UserProvider
3.8.2 (2025-01-09):
* Fix: better detection for Uuid subclasses in autowiring
diff --git a/composer.json b/composer.json
index f2821db..4012097 100644
--- a/composer.json
+++ b/composer.json
@@ -18,6 +18,7 @@
"atoum/stubs": "^2.2",
"brick/geo": ">=0.5 <=1.0",
"symfony/expression-language": "^6.3 || ^7.2",
+ "symfony/security-bundle": "^6.0 || ^7.0",
"symfony/uid": "^6.0 || ^7.0"
},
"autoload": {
diff --git a/src/TingBundle/DependencyInjection/EntityFactory.php b/src/TingBundle/DependencyInjection/EntityFactory.php
new file mode 100644
index 0000000..6ab1088
--- /dev/null
+++ b/src/TingBundle/DependencyInjection/EntityFactory.php
@@ -0,0 +1,39 @@
+setDefinition($id, new ChildDefinition('ting.security.user_provider'))
+ ->addArgument($config['class'])
+ ->addArgument($config['property'])
+ ;
+ }
+
+ public function getKey(): string
+ {
+ return 'ting';
+ }
+
+ public function addConfiguration(NodeDefinition $builder): void
+ {
+ $builder
+ ->children()
+ ->scalarNode('class')
+ ->isRequired()
+ ->info('The full entity class name of your user class.')
+ ->cannotBeEmpty()
+ ->end()
+ ->scalarNode('property')->defaultNull()->end()
+ ->end()
+ ;
+ }
+}
diff --git a/src/TingBundle/Resources/config/services.xml b/src/TingBundle/Resources/config/services.xml
index 00c0da4..e4335e1 100644
--- a/src/TingBundle/Resources/config/services.xml
+++ b/src/TingBundle/Resources/config/services.xml
@@ -106,6 +106,11 @@
+
+
+
+
+
diff --git a/src/TingBundle/Security/EntityUserProvider.php b/src/TingBundle/Security/EntityUserProvider.php
new file mode 100644
index 0000000..3c72461
--- /dev/null
+++ b/src/TingBundle/Security/EntityUserProvider.php
@@ -0,0 +1,137 @@
+
+ */
+class EntityUserProvider implements UserProviderInterface, PasswordUpgraderInterface
+{
+ public function __construct(
+ private readonly MetadataRepository $metadataRepository,
+ private readonly RepositoryFactory $repositoryFactory,
+ private readonly string $class,
+ private readonly ?string $property = null,
+ ) {
+ }
+
+ public function loadUserByIdentifier(string $identifier): UserInterface
+ {
+ $repository = $this->getRepository();
+ if (null !== $this->property) {
+ $user = $repository->getOneBy([$this->property => $identifier]);
+ } else {
+ if (!$repository instanceof UserLoaderInterface) {
+ throw new \InvalidArgumentException(\sprintf('You must either make the "%s" entity Ting Repository ("%s") implement "CCMBenchmark\TingBundle\Security\UserLoaderInterface" or set the "property" option in the corresponding entity provider configuration.', $this->class, get_debug_type($repository)));
+ }
+
+ $user = $repository->loadUserByIdentifier($identifier);
+ }
+
+ if (null === $user) {
+ $e = new UserNotFoundException(\sprintf('User "%s" not found.', $identifier));
+ $e->setUserIdentifier($identifier);
+
+ throw $e;
+ }
+
+ return $user;
+ }
+
+ public function refreshUser(UserInterface $user): UserInterface
+ {
+ if (!$user instanceof $this->class) {
+ throw new UnsupportedUserException(\sprintf('Instances of "%s" are not supported.', get_debug_type($user)));
+ }
+
+ $repository = $this->getRepository();
+ if ($repository instanceof UserProviderInterface) {
+ $refreshedUser = $repository->refreshUser($user);
+ } else {
+ // The user must be reloaded via the primary key as all other data
+ // might have changed without proper persistence in the database.
+ // That's the case when the user has been changed by a form with
+ // validation errors.
+ if (!$id = $this->getIdentifierValues($user)) {
+ throw new \InvalidArgumentException('You cannot refresh a user from the EntityUserProvider that does not contain an identifier. The user object has to be serialized with its own identifier mapped by Ting.');
+ }
+
+ $refreshedUser = $repository->get($id);
+ if (null === $refreshedUser) {
+ $e = new UserNotFoundException('User with id '.json_encode($id).' not found.');
+ $e->setUserIdentifier(json_encode($id));
+
+ throw $e;
+ }
+ }
+
+ return $refreshedUser;
+ }
+
+ public function supportsClass(string $class): bool
+ {
+ return $class === $this->class || is_subclass_of($class, $this->class);
+ }
+
+ /**
+ * @final
+ */
+ public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
+ {
+ if (!$user instanceof $this->class) {
+ throw new UnsupportedUserException(\sprintf('Instances of "%s" are not supported.', get_debug_type($user)));
+ }
+
+ $repository = $this->getRepository();
+ if ($repository instanceof PasswordUpgraderInterface) {
+ $repository->upgradePassword($user, $newHashedPassword);
+ }
+ }
+
+ private function getMetadata(): Metadata
+ {
+ $metadata = null;
+ $this->metadataRepository->findMetadataForEntity($this->class, function (Metadata $innerMetadata) use (&$metadata) {
+ $metadata = $innerMetadata;
+ }, fn () => null);
+
+ if ($metadata === null) {
+ throw new \InvalidArgumentException(\sprintf('No metadata found for entity "%s".', $this->class));
+ }
+
+ return $metadata;
+ }
+
+ private function getRepository(): Repository
+ {
+ return $this->repositoryFactory->get($this->getMetadata()->getRepository());
+ }
+
+ private function getIdentifierValues($user): ?array
+ {
+ $metadata = $this->getMetadata();
+ $primaries = $metadata->getPrimaries();
+ if ($primaries === []) {
+ return null;
+ }
+ $identifierValues = [];
+ foreach ($primaries as $primary) {
+ $identifierValues[$primary] = $metadata->getEntityPropertyByFieldName($user, $primary);
+ }
+
+ return $identifierValues;
+ }
+}
diff --git a/src/TingBundle/Security/UserLoaderInterface.php b/src/TingBundle/Security/UserLoaderInterface.php
new file mode 100644
index 0000000..4064174
--- /dev/null
+++ b/src/TingBundle/Security/UserLoaderInterface.php
@@ -0,0 +1,15 @@
+hasExtension('security')) {
+ $container->getExtension('security')->addUserProviderFactory(new EntityFactory());
+ }
+ }
}