Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[make:user] implement getUserIdentifier if required #862

Merged
merged 1 commit into from
Apr 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Maker/MakeUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Yaml\Yaml;

Expand Down Expand Up @@ -171,6 +172,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
$userClassConfiguration->getUserProviderClass(),
'security/UserProvider.tpl.php',
[
'uses_user_identifier' => class_exists(UserNotFoundException::class),
'user_short_name' => $userClassNameDetails->getShortName(),
]
);
Expand Down
16 changes: 7 additions & 9 deletions src/Resources/skeleton/security/UserProvider.tpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace <?= $namespace; ?>;

use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\<?= $uses_user_identifier ? 'UserNotFoundException' : 'UsernameNotFoundException' ?>;
<?= ($password_upgrader = interface_exists('Symfony\Component\Security\Core\User\PasswordUpgraderInterface')) ? "use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;\n" : '' ?>
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
Copy link
Member

Choose a reason for hiding this comment

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

We really need to, at some point, write a util class where we plug all the use statements into... then we just dump echo $useStatements->render() in the template :p

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No kidding.. use statements are getting harder and harder to manage lately.. have a solution on the todo list ;)

Expand All @@ -17,17 +17,15 @@ class <?= $class_name ?> implements UserProviderInterface<?= $password_upgrader
* If you're not using these features, you do not need to implement
* this method.
*
* @return UserInterface
*
* @throws UsernameNotFoundException if the user is not found
* @throws <?= $uses_user_identifier ? 'UserNotFoundException' : 'UsernameNotFoundException' ?> if the user is not found
*/
public function loadUserByUsername($username)
public function <?= $uses_user_identifier ? 'loadUserByIdentifier($identifier)' : 'loadUserByUsername($username)' ?>: UserInterface
{
// Load a User object from your data source or throw UsernameNotFoundException.
// The $username argument may not actually be a username:
// it is whatever value is being returned by the getUsername()
// Load a User object from your data source or throw <?= $uses_user_identifier ? 'UserNotFoundException' : 'UsernameNotFoundException' ?>.
// The <?= $uses_user_identifier ? '$identifier' : '$username' ?> argument may not actually be a username:
// it is whatever value is being returned by the <?= $uses_user_identifier ? 'getUserIdentifier' : 'getUsername' ?>()
// method in your User class.
throw new \Exception('TODO: fill in loadUserByUsername() inside '.__FILE__);
throw new \Exception('TODO: fill in <?= $uses_user_identifier ? 'loadUserByIdentifier()' : 'loadUserByUsername()' ?> inside '.__FILE__);
}

/**
Expand Down
12 changes: 10 additions & 2 deletions src/Security/UserClassBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use PhpParser\Node;
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

Expand Down Expand Up @@ -67,7 +68,7 @@ private function addPasswordImplementation(ClassSourceManipulator $manipulator,
$this->addGetPassword($manipulator, $userClassConfig);
}

private function addGetUsername(ClassSourceManipulator $manipulator, UserClassConfiguration $userClassConfig)
private function addGetUsername(ClassSourceManipulator $manipulator, UserClassConfiguration $userClassConfig): void
{
if ($userClassConfig->isEntity()) {
// add entity property
Expand Down Expand Up @@ -97,10 +98,17 @@ private function addGetUsername(ClassSourceManipulator $manipulator, UserClassCo
);
}

$getterIdentifierName = 'getUsername';

// Check if we're using Symfony 5.3+ - UserInterface::getUsername() was replaced with UserInterface::getUserIdentifier()
if (class_exists(InMemoryUser::class)) {
Copy link
Member

Choose a reason for hiding this comment

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

This class DOES only exist on 5.3. But why did you choose this class specifically? Could we use something more direct like method_exists(Symfony\Component\Security\Core\User\User::class, 'getUserIdentifier')?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

$getterIdentifierName = 'getUserIdentifier';
}
jrushlow marked this conversation as resolved.
Show resolved Hide resolved

// define getUsername (if it was defined above, this will override)
$manipulator->addAccessorMethod(
$userClassConfig->getIdentityPropertyName(),
'getUsername',
$getterIdentifierName,
'string',
false,
[
Expand Down
170 changes: 135 additions & 35 deletions tests/Security/UserClassBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,75 +16,175 @@
use Symfony\Bundle\MakerBundle\Security\UserClassConfiguration;
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\User;

class UserClassBuilderTest extends TestCase
{
/**
* @dataProvider getUserInterfaceTests
*/
public function testAddUserInterfaceImplementation(UserClassConfiguration $userClassConfig, string $expectedFilename)
public function testAddUserInterfaceImplementation(UserClassConfiguration $userClassConfig, string $expectedFilename): void
{
$sourceFilename = __DIR__.'/fixtures/source/'.($userClassConfig->isEntity() ? 'UserEntity.php' : 'UserModel.php');
if (!interface_exists(PasswordAuthenticatedUserInterface::class)) {
self::markTestSkipped('Requires Symfony >= 5.3');
}

$manipulator = new ClassSourceManipulator(
file_get_contents($sourceFilename),
true
);
$manipulator = $this->getClassSourceManipulator($userClassConfig);

$classBuilder = new UserClassBuilder();
$classBuilder->addUserInterfaceImplementation($manipulator, $userClassConfig);

$expectedPath = __DIR__.'/fixtures/expected';
$expectedPath = $this->getExpectedPath($expectedFilename);

// Can be removed when < Symfony 6 support is dropped.
if (!interface_exists(PasswordAuthenticatedUserInterface::class)) {
$expectedPath = sprintf('%s/legacy', $expectedPath);
}
self::assertStringEqualsFile($expectedPath, $manipulator->getSourceCode());
}

$expectedPath = sprintf('%s/%s', $expectedPath, $expectedFilename);
public function getUserInterfaceTests(): \Generator
{
yield 'entity_with_email_as_identifier' => [
new UserClassConfiguration(true, 'email', true),
'UserEntityWithEmailAsIdentifier.php',
];

if (!file_exists($expectedPath)) {
throw new \Exception(sprintf('Expected file missing: "%s"', $expectedPath));
yield 'entity_with_password' => [
new UserClassConfiguration(true, 'userIdentifier', true),
'UserEntityWithPassword.php',
];

yield 'entity_with_user_identifier_as_identifier' => [
new UserClassConfiguration(true, 'user_identifier', true),
'UserEntityWithUser_IdentifierAsIdentifier.php',
];

yield 'entity_without_password' => [
new UserClassConfiguration(true, 'userIdentifier', false),
'UserEntityWithoutPassword.php',
];

yield 'model_with_email_as_identifier' => [
new UserClassConfiguration(false, 'email', true),
'UserModelWithEmailAsIdentifier.php',
];

yield 'model_with_password' => [
new UserClassConfiguration(false, 'userIdentifier', true),
'UserModelWithPassword.php',
];

yield 'model_without_password' => [
new UserClassConfiguration(false, 'userIdentifier', false),
'UserModelWithoutPassword.php',
];
}

/**
* Covers Symfony <= 5.2 UserInterface::getUsername().
*
* In Symfony 5.3, getUsername was replaced with getUserIdentifier()
*
* @dataProvider legacyUserInterfaceGetUsernameDataProvider
*/
public function testLegacyUserInterfaceGetUsername(UserClassConfiguration $userClassConfig, string $expectedFilename): void
{
if (method_exists(User::class, 'getUserIdentifier')) {
self::markTestSkipped();
}

$this->assertSame(file_get_contents($expectedPath), $manipulator->getSourceCode());
$manipulator = $this->getClassSourceManipulator($userClassConfig);

$classBuilder = new UserClassBuilder();
$classBuilder->addUserInterfaceImplementation($manipulator, $userClassConfig);

$expectedPath = $this->getExpectedPath($expectedFilename, 'legacy_get_username');

self::assertStringEqualsFile($expectedPath, $manipulator->getSourceCode());
}

public function getUserInterfaceTests()
public function legacyUserInterfaceGetUsernameDataProvider(): \Generator
{
yield 'entity_email_password' => [
new UserClassConfiguration(true, 'email', true),
'UserEntityEmailWithPassword.php',
yield 'entity_with_get_username' => [
new UserClassConfiguration(true, 'username', false),
'UserEntityGetUsername.php',
];

yield 'entity_username_password' => [
new UserClassConfiguration(true, 'username', true),
'UserEntityUsernameWithPassword.php',
yield 'model_with_get_username' => [
new UserClassConfiguration(false, 'username', false),
'UserModelGetUsername.php',
];

yield 'entity_user_name_password' => [
new UserClassConfiguration(true, 'user_name', true),
'UserEntityUser_nameWithPassword.php',
yield 'model_with_email_as_username' => [
new UserClassConfiguration(false, 'email', false),
'UserModelWithEmailAsUsername.php',
];
}

yield 'entity_username_no_password' => [
new UserClassConfiguration(true, 'username', false),
'UserEntityUsernameNoPassword.php',
/**
* Covers Symfony <= 5.2 UserInterface::getPassword().
*
* In Symfony 5.3, getPassword was moved from UserInterface::class to the
* new PasswordAuthenticatedUserInterface::class.
*
* @dataProvider legacyUserInterfaceGetPasswordDataProvider
*/
public function testLegacyUserInterfaceGetPassword(UserClassConfiguration $userClassConfig, string $expectedFilename): void
{
if (interface_exists(PasswordAuthenticatedUserInterface::class)) {
self::markTestSkipped();
}

$manipulator = $this->getClassSourceManipulator($userClassConfig);

$classBuilder = new UserClassBuilder();
$classBuilder->addUserInterfaceImplementation($manipulator, $userClassConfig);

$expectedPath = $this->getExpectedPath($expectedFilename, 'legacy_get_password');

self::assertStringEqualsFile($expectedPath, $manipulator->getSourceCode());
}

public function legacyUserInterfaceGetPasswordDataProvider(): \Generator
{
yield 'entity_with_password' => [
new UserClassConfiguration(true, 'username', true),
'UserEntityWithPassword.php',
];

yield 'model_email_password' => [
new UserClassConfiguration(false, 'email', true),
'UserModelEmailWithPassword.php',
yield 'entity_without_password' => [
new UserClassConfiguration(true, 'username', false),
'UserEntityNoPassword.php',
];

yield 'model_username_password' => [
yield 'model_with_password' => [
new UserClassConfiguration(false, 'username', true),
'UserModelUsernameWithPassword.php',
'UserModelWithPassword.php',
];

yield 'model_username_no_password' => [
yield 'model_without_password' => [
new UserClassConfiguration(false, 'username', false),
'UserModelUsernameNoPassword.php',
'UserModelNoPassword.php',
];
}

private function getClassSourceManipulator(UserClassConfiguration $userClassConfiguration): ClassSourceManipulator
{
$sourceFilename = __DIR__.'/fixtures/source/'.($userClassConfiguration->isEntity() ? 'UserEntity.php' : 'UserModel.php');

return new ClassSourceManipulator(
file_get_contents($sourceFilename),
true
);
}

private function getExpectedPath(string $expectedFilename, string $subDirectory = null): string
{
$basePath = __DIR__.'/fixtures/expected';

$expectedPath = null === $subDirectory ? sprintf('%s/%s', $basePath, $expectedFilename) : sprintf('%s/%s/%s', $basePath, $subDirectory, $expectedFilename);

if (!file_exists($expectedPath)) {
throw new \Exception(sprintf('Expected file missing: "%s"', $expectedPath));
}

return $expectedPath;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public function setEmail(string $email): self
*
* @see UserInterface
*/
public function getUsername(): string
public function getUserIdentifier(): string
{
return (string) $this->email;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
/**
* @ORM\Column(type="string", length=180, unique=true)
*/
private $user_name;
private $userIdentifier;

/**
* @ORM\Column(type="json")
Expand All @@ -44,14 +44,14 @@ public function getId(): ?int
*
* @see UserInterface
*/
public function getUsername(): string
public function getUserIdentifier(): string
{
return (string) $this->user_name;
return (string) $this->userIdentifier;
}

public function setUserName(string $user_name): self
public function setUserIdentifier(string $userIdentifier): self
{
$this->user_name = $user_name;
$this->userIdentifier = $userIdentifier;

return $this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
/**
* @ORM\Column(type="string", length=180, unique=true)
*/
private $username;
private $user_identifier;

/**
* @ORM\Column(type="json")
Expand All @@ -44,14 +44,14 @@ public function getId(): ?int
*
* @see UserInterface
*/
public function getUsername(): string
public function getUserIdentifier(): string
{
return (string) $this->username;
return (string) $this->user_identifier;
}

public function setUsername(string $username): self
public function setUserIdentifier(string $user_identifier): self
{
$this->username = $username;
$this->user_identifier = $user_identifier;

return $this;
}
Expand Down
Loading