diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
index d5322bf..300e54f 100644
--- a/.github/workflows/phpunit.yml
+++ b/.github/workflows/phpunit.yml
@@ -16,9 +16,9 @@ jobs:
strategy:
matrix:
php-version:
+ - "8.4"
- "8.3"
- "8.2"
- - "8.1"
steps:
- uses: actions/checkout@v4
@@ -33,5 +33,6 @@ jobs:
with:
folder: php
project: ${{ github.event.repository.name }}
- secrets: inherit
+ secrets:
+ DOC_TOKEN: ${{ secrets.DOC_TOKEN }}
diff --git a/.gitignore b/.gitignore
index f495eec..256a0e6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ vendor
.idea/*
!.idea/runConfigurations
.phpunit.result.cache
+/.claude/settings.local.json
diff --git a/.idea/runConfigurations/Psalm.xml b/.idea/runConfigurations/Psalm.xml
deleted file mode 100644
index 6f80a21..0000000
--- a/.idea/runConfigurations/Psalm.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/psalm.xml b/.idea/runConfigurations/psalm.xml
new file mode 100644
index 0000000..d9c1b61
--- /dev/null
+++ b/.idea/runConfigurations/psalm.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index a16b060..a45a321 100644
--- a/README.md
+++ b/README.md
@@ -6,111 +6,81 @@
[](https://opensource.byjg.com/opensource/licensing.html)
[](https://github.com/byjg/php-authuser/releases/)
-A simple and customizable class for enable user authentication inside your application. It is available on XML files, Relational Databases.
+A simple and customizable library for user authentication in PHP applications. It supports multiple storage backends including databases and XML files.
-The main purpose is just to handle all complexity of validate a user, add properties and create access token abstracting the database layer.
-This class can persist into session (or file, memcache, etc) the user data between requests.
+The main purpose is to handle all complexity of user validation, authentication, properties management, and access tokens, abstracting the database layer.
+This class can persist user data into session (or file, memcache, etc.) between requests.
-## Creating a Users handling class
+## Documentation
-Using the FileSystem (XML) as the user storage:
+- [Getting Started](docs/getting-started.md)
+- [Installation](docs/installation.md)
+- [User Management](docs/user-management.md)
+- [Authentication](docs/authentication.md)
+- [Session Context](docs/session-context.md)
+- [User Properties](docs/user-properties.md)
+- [Database Storage](docs/database-storage.md)
+- [Password Validation](docs/password-validation.md)
+- [JWT Tokens](docs/jwt-tokens.md)
+- [Custom Fields](docs/custom-fields.md)
+- [Mappers](docs/mappers.md)
+- [Examples](docs/examples.md)
-```php
-isAuthenticated()) {
-
- // Get the userId of the authenticated users
- $userId = $sessionContext->userInfo();
-
- // Get the user and your name
- $user = $users->getById($userId);
- echo "Hello: " . $user->getName();
+use ByJG\Authenticate\UsersDBDataset;
+use ByJG\Authenticate\SessionContext;
+use ByJG\AnyDataset\Db\Factory as DbFactory;
+use ByJG\Cache\Factory;
+
+// Initialize with database
+$users = new UsersDBDataset(DbFactory::getDbInstance('mysql://user:pass@host/db'));
+
+// Create and authenticate a user
+$user = $users->addUser('John Doe', 'johndoe', 'john@example.com', 'SecurePass123');
+$authenticatedUser = $users->isValidUser('johndoe', 'SecurePass123');
+
+if ($authenticatedUser !== null) {
+ $sessionContext = new SessionContext(Factory::createSessionPool());
+ $sessionContext->registerLogin($authenticatedUser->getUserid());
+ echo "Welcome, " . $authenticatedUser->getName();
}
```
-## Saving extra info into the user session
-
-You can save data in the session data exists only during the user is logged in. Once the user logged off the
-data stored with the user session will be released.
-
-Store the data for the current user session:
-
-```php
-setSessionData('key', 'value');
-```
-
-Getting the data from the current user session:
-
-```php
-getSessionData('key');
-```
-
-Note: If the user is not logged an error will be throw
+See [Getting Started](docs/getting-started.md) for a complete introduction and [Examples](docs/examples.md) for more use cases.
-## Adding a custom property to the users
+## Features
-```php
-getById($userId);
-$user->setField('somefield', 'somevalue');
-$users->save();
-```
+- **User Management** - Complete CRUD operations. See [User Management](docs/user-management.md)
+- **Authentication** - Username/email + password or JWT tokens. See [Authentication](docs/authentication.md) and [JWT Tokens](docs/jwt-tokens.md)
+- **Session Management** - PSR-6 compatible cache storage. See [Session Context](docs/session-context.md)
+- **User Properties** - Store custom key-value metadata. See [User Properties](docs/user-properties.md)
+- **Password Validation** - Built-in strength requirements. See [Password Validation](docs/password-validation.md)
+- **Multiple Storage** - Database (MySQL, PostgreSQL, SQLite, etc.) or XML files. See [Database Storage](docs/database-storage.md)
+- **Custom Schema** - Map to existing database tables. See [Database Storage](docs/database-storage.md)
+- **Field Mappers** - Transform data during read/write. See [Mappers](docs/mappers.md)
+- **Extensible Model** - Add custom fields easily. See [Custom Fields](docs/custom-fields.md)
-## Logout from a session
-
-```php
-registerLogout();
-```
-
-## Important note about SessionContext
-
-`SessionContext` object will store the info about the current context.
-As SessionContext uses CachePool interface defined in PSR-6 you can set any storage
-to save your session context.
-
-In our examples we are using a regular PHP Session for store the user context
-(`Factory::createSessionPool()`). But if you are using another store like MemCached
-you have to define a UNIQUE prefix for that session. Note if TWO users have the same
-prefix you probably have an unexpected result for the SessionContext.
+## Running Tests
-Example for memcached:
+Because this project uses PHP Session you need to run the unit test the following manner:
-```php
- 'fieldname of userid',
- UserDefinition::FIELD_NAME => 'fieldname of name',
- UserDefinition::FIELD_EMAIL => 'fieldname of email',
- UserDefinition::FIELD_USERNAME => 'fieldname of username',
- UserDefinition::FIELD_PASSWORD => 'fieldname of password',
- UserDefinition::FIELD_CREATED => 'fieldname of created',
- UserDefinition::FIELD_ADMIN => 'fieldname of admin'
- ]
-);
-```
-
-### Adding custom modifiers for read and update
-
-```php
- the current value to be updated
-// $instance -> The array with all other fields;
-$userDefinition->defineClosureForUpdate(UserDefinition::FIELD_PASSWORD, function ($value, $instance) {
- return strtoupper(sha1($value));
-});
-
-// Defines a custom function to be applied After the field UserDefinition::FIELD_CREATED is read but before
-// the user get the result
-// $value --> the current value retrieved from database
-// $instance -> The array with all other fields;
-$userDefinition->defineClosureForSelect(UserDefinition::FIELD_CREATED, function ($value, $instance) {
- return date('Y', $value);
-});
-
-// If you want make the field READONLY just do it:
-$userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED);
-```
-
-## Extending UserModel
-
-It is possible extending the UserModel table, since you create a new class extending from UserModel to add the new fields.
-
-For example, imagine your table has one field called "otherfield".
-
-You'll have to extend like this:
-
-```php
-setOtherfield($field);
- }
-
- public function getOtherfield()
- {
- return $this->otherfield;
- }
-
- public function setOtherfield($otherfield)
- {
- $this->otherfield = $otherfield;
- }
-}
-```
-
-After that you can use your new definition:
-
-```php
- byjg/micro-orm
byjg/authuser --> byjg/cache-engine
- byjg/authuser --> byjg/jwt-wrapper
+ byjg/authuser --> byjg/jwt-wrapper
```
-
----
[Open source ByJG](http://opensource.byjg.com)
diff --git a/composer.json b/composer.json
index ea629b9..3f5a5d7 100644
--- a/composer.json
+++ b/composer.json
@@ -14,14 +14,18 @@
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
- "php": ">=8.1 <8.4",
- "byjg/micro-orm": "^5.0",
- "byjg/cache-engine": "^5.0",
- "byjg/jwt-wrapper": "^5.0"
+ "php": ">=8.2 <8.5",
+ "byjg/micro-orm": "^6.0",
+ "byjg/cache-engine": "^6.0",
+ "byjg/jwt-wrapper": "^6.0"
},
"require-dev": {
- "phpunit/phpunit": "^9.6",
- "vimeo/psalm": "^5.9"
+ "phpunit/phpunit": "^10|^11",
+ "vimeo/psalm": "^5.9|^6.12"
+ },
+ "scripts": {
+ "test": "vendor/bin/phpunit",
+ "psalm": "vendor/bin/psalm"
},
"license": "MIT"
}
diff --git a/docs/authentication.md b/docs/authentication.md
new file mode 100644
index 0000000..8a52ed4
--- /dev/null
+++ b/docs/authentication.md
@@ -0,0 +1,162 @@
+---
+sidebar_position: 4
+title: Authentication
+---
+
+# Authentication
+
+## Validating User Credentials
+
+Use the `isValidUser()` method to validate a username/email and password combination:
+
+```php
+isValidUser('johndoe', 'SecurePass123');
+
+if ($user !== null) {
+ echo "Authentication successful!";
+ echo "User ID: " . $user->getUserid();
+} else {
+ echo "Invalid credentials";
+}
+```
+
+:::tip Login Field
+The `isValidUser()` method uses the login field defined in your `UserDefinition`. This can be either the email or username field.
+:::
+
+## Password Hashing
+
+By default, passwords are automatically hashed using SHA-1 when saved. The library uses the `PasswordSha1Mapper` for this purpose.
+
+```php
+setPassword('plaintext password');
+$users->save($user);
+
+// The password is stored as SHA-1 hash in the database
+```
+
+:::warning SHA-1 Deprecation
+SHA-1 is used for backward compatibility. For new projects, consider implementing a custom password hasher using bcrypt or Argon2. See [Mappers](mappers.md#example-bcrypt-password-mapper) for details.
+:::
+
+:::tip Enforce Password Strength
+To enforce password policies (minimum length, complexity rules, etc.), see [Password Validation](password-validation.md).
+:::
+
+## JWT Token Authentication (Recommended)
+
+For modern, stateless authentication, use JWT tokens. This is the **recommended approach** for new applications as it provides better security and scalability.
+
+```php
+createAuthToken(
+ 'johndoe', // Login
+ 'SecurePass123', // Password
+ $jwtWrapper,
+ 3600, // Expires in 1 hour (seconds)
+ [], // Additional user info to save
+ ['role' => 'admin'] // Additional token data
+);
+
+if ($token !== null) {
+ echo "Token: " . $token;
+}
+```
+
+### Validating JWT Tokens
+
+```php
+isValidToken('johndoe', $jwtWrapper, $token);
+
+if ($result !== null) {
+ $user = $result['user'];
+ $tokenData = $result['data'];
+
+ echo "User: " . $user->getName();
+ echo "Role: " . $tokenData['role'];
+}
+```
+
+:::info Token Storage
+When a JWT token is created, a hash of the token is stored in the user's properties as `TOKEN_HASH`. This ensures tokens can be invalidated if needed.
+:::
+
+:::tip Why JWT?
+JWT tokens provide stateless authentication, better scalability, and easier integration with modern frontend frameworks and mobile applications. They're also more secure than traditional PHP sessions.
+:::
+
+## Session-Based Authentication (Legacy)
+
+:::warning Deprecated
+SessionContext relies on traditional PHP sessions and is less secure than JWT tokens. It's maintained for backward compatibility only. **For new projects, use JWT tokens instead.**
+:::
+
+### Basic Authentication Flow
+
+```php
+isValidUser('johndoe', 'SecurePass123');
+
+if ($user !== null) {
+ // 2. Create session context
+ $sessionContext = new SessionContext(Factory::createSessionPool());
+
+ // 3. Register login
+ $sessionContext->registerLogin($user->getUserid());
+
+ // 4. User is now authenticated
+ echo "Welcome, " . $user->getName();
+}
+```
+
+### Checking Authentication Status
+
+```php
+isAuthenticated()) {
+ $userId = $sessionContext->userInfo();
+ $user = $users->get($userId);
+ echo "Hello, " . $user->getName();
+} else {
+ echo "Please log in";
+}
+```
+
+### Logging Out
+
+```php
+registerLogout();
+```
+
+## Security Best Practices
+
+1. **Always use HTTPS** in production to prevent credential theft
+2. **Implement rate limiting** to prevent brute force attacks
+3. **Use strong passwords** - see [Password Validation](password-validation.md)
+4. **Set appropriate session timeouts**
+5. **Validate and sanitize** all user inputs
+
+## Next Steps
+
+- [Session Context](session-context.md) - Manage user sessions
+- [JWT Tokens](jwt-tokens.md) - Deep dive into JWT authentication
+- [Password Validation](password-validation.md) - Enforce password policies
diff --git a/docs/custom-fields.md b/docs/custom-fields.md
new file mode 100644
index 0000000..4312bb6
--- /dev/null
+++ b/docs/custom-fields.md
@@ -0,0 +1,379 @@
+---
+sidebar_position: 10
+title: Custom Fields
+---
+
+# Custom Fields
+
+You can extend the `UserModel` to add custom fields that match your database schema.
+
+:::info When to Use This
+This guide is for **adding new fields** beyond the standard user fields. If you just need to **map existing database columns** to the standard fields, see [Database Storage](database-storage.md#custom-database-schema) instead.
+:::
+
+## Extending UserModel
+
+### Creating a Custom User Model
+
+```php
+phone = $phone;
+ $this->department = $department;
+ }
+
+ public function getPhone(): ?string
+ {
+ return $this->phone;
+ }
+
+ public function setPhone(?string $phone): void
+ {
+ $this->phone = $phone;
+ }
+
+ public function getDepartment(): ?string
+ {
+ return $this->department;
+ }
+
+ public function setDepartment(?string $department): void
+ {
+ $this->department = $department;
+ }
+
+ public function getTitle(): ?string
+ {
+ return $this->title;
+ }
+
+ public function setTitle(?string $title): void
+ {
+ $this->title = $title;
+ }
+
+ public function getProfilePicture(): ?string
+ {
+ return $this->profilePicture;
+ }
+
+ public function setProfilePicture(?string $profilePicture): void
+ {
+ $this->profilePicture = $profilePicture;
+ }
+}
+```
+
+## Database Schema
+
+Add the custom fields to your users table:
+
+```sql
+CREATE TABLE users
+(
+ userid INTEGER AUTO_INCREMENT NOT NULL,
+ name VARCHAR(50),
+ email VARCHAR(120),
+ username VARCHAR(15) NOT NULL,
+ password CHAR(40) NOT NULL,
+ created DATETIME,
+ admin ENUM('Y','N'),
+ -- Custom fields
+ phone VARCHAR(20),
+ department VARCHAR(50),
+ title VARCHAR(50),
+ profile_picture VARCHAR(255),
+
+ CONSTRAINT pk_users PRIMARY KEY (userid)
+) ENGINE=InnoDB;
+```
+
+## Configuring UserDefinition
+
+Map the custom fields in your `UserDefinition`:
+
+```php
+ 'userid',
+ UserDefinition::FIELD_NAME => 'name',
+ UserDefinition::FIELD_EMAIL => 'email',
+ UserDefinition::FIELD_USERNAME => 'username',
+ UserDefinition::FIELD_PASSWORD => 'password',
+ UserDefinition::FIELD_CREATED => 'created',
+ UserDefinition::FIELD_ADMIN => 'admin',
+ // Custom fields
+ 'phone' => 'phone',
+ 'department' => 'department',
+ 'title' => 'title',
+ 'profilePicture' => 'profile_picture'
+ ]
+);
+```
+
+## Using the Custom Model
+
+### Creating Users
+
+```php
+setName('John Doe');
+$user->setEmail('john@example.com');
+$user->setUsername('johndoe');
+$user->setPassword('SecurePass123');
+$user->setPhone('+1-555-1234');
+$user->setDepartment('Engineering');
+$user->setTitle('Senior Developer');
+
+$users->save($user);
+```
+
+### Retrieving Users
+
+```php
+get($userId);
+
+// Access custom fields
+echo $user->getName();
+echo $user->getPhone();
+echo $user->getDepartment();
+echo $user->getTitle();
+```
+
+### Updating Custom Fields
+
+```php
+get($userId);
+$user->setDepartment('Sales');
+$user->setTitle('Sales Manager');
+$users->save($user);
+```
+
+## Read-Only Fields
+
+You can mark fields as read-only to prevent updates:
+
+```php
+markPropertyAsReadOnly(UserDefinition::FIELD_CREATED);
+
+// Make custom field read-only
+$userDefinition->markPropertyAsReadOnly('phone');
+```
+
+Read-only fields:
+- Can be set during creation
+- Cannot be updated after creation
+- Are ignored during updates
+
+## Auto-Generated Fields
+
+### Auto-Increment IDs
+
+For auto-increment IDs, the database handles generation automatically. No configuration needed.
+
+### UUID Fields
+
+For UUID primary keys:
+
+```php
+defineGenerateKey(UserIdGeneratorMapper::class);
+```
+
+### Custom ID Generation
+
+Create a custom mapper for custom ID generation:
+
+```php
+defineGenerateKey(CustomIdMapper::class);
+```
+
+## Field Transformation
+
+You can transform fields during read/write operations using mappers. See [Mappers](mappers.md) for details.
+
+## Complex Data Types
+
+### JSON Fields
+
+For storing JSON data in custom fields:
+
+```php
+defineMapperForUpdate('metadata', JsonMapper::class);
+$userDefinition->defineMapperForSelect('metadata', JsonDecodeMapper::class);
+```
+
+### Date/Time Fields
+
+```php
+format('Y-m-d H:i:s');
+ }
+ return $value;
+ }
+}
+
+$userDefinition->defineMapperForUpdate('created', DateTimeMapper::class);
+```
+
+## Complete Example
+
+```php
+ 'userid',
+ UserDefinition::FIELD_NAME => 'name',
+ UserDefinition::FIELD_EMAIL => 'email',
+ UserDefinition::FIELD_USERNAME => 'username',
+ UserDefinition::FIELD_PASSWORD => 'password',
+ UserDefinition::FIELD_CREATED => 'created',
+ UserDefinition::FIELD_ADMIN => 'admin',
+ 'phone' => 'phone',
+ 'department' => 'department',
+ 'title' => 'title',
+ 'profilePicture' => 'profile_picture'
+ ]
+);
+
+// Make created field read-only
+$userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED);
+
+// Initialize user management
+$users = new UsersDBDataset($dbDriver, $userDefinition);
+
+// Create a user
+$user = new CustomUserModel();
+$user->setName('Jane Smith');
+$user->setEmail('jane@example.com');
+$user->setUsername('janesmith');
+$user->setPassword('SecurePass123');
+$user->setPhone('+1-555-5678');
+$user->setDepartment('Marketing');
+$user->setTitle('Marketing Director');
+
+$savedUser = $users->save($user);
+
+// Retrieve and update
+$user = $users->get($savedUser->getUserid());
+$user->setTitle('VP of Marketing');
+$users->save($user);
+```
+
+## When to Use Custom Fields vs Properties
+
+| Use Custom Fields When | Use Properties When |
+|------------------------|---------------------|
+| Field is used frequently | Field is rarely used |
+| Field is searched/filtered | Field is key-value metadata |
+| Field is fixed schema | Field is dynamic/flexible |
+| Better performance needed | Schema flexibility needed |
+| Field is required | Field is optional |
+
+## Next Steps
+
+- [Mappers](mappers.md) - Custom field transformations
+- [Database Storage](database-storage.md) - Schema configuration
+- [User Properties](user-properties.md) - Flexible metadata storage
diff --git a/docs/database-storage.md b/docs/database-storage.md
new file mode 100644
index 0000000..4484591
--- /dev/null
+++ b/docs/database-storage.md
@@ -0,0 +1,256 @@
+---
+sidebar_position: 7
+title: Database Storage
+---
+
+# Database Storage
+
+The library supports storing users in relational databases through the `UsersDBDataset` class.
+
+## Database Setup
+
+### Default Schema
+
+The default database structure uses two tables:
+
+```sql
+CREATE TABLE users
+(
+ userid INTEGER AUTO_INCREMENT NOT NULL,
+ name VARCHAR(50),
+ email VARCHAR(120),
+ username VARCHAR(15) NOT NULL,
+ password CHAR(40) NOT NULL,
+ created DATETIME,
+ admin ENUM('Y','N'),
+
+ CONSTRAINT pk_users PRIMARY KEY (userid)
+) ENGINE=InnoDB;
+
+CREATE TABLE users_property
+(
+ customid INTEGER AUTO_INCREMENT NOT NULL,
+ name VARCHAR(20),
+ value VARCHAR(100),
+ userid INTEGER NOT NULL,
+
+ CONSTRAINT pk_custom PRIMARY KEY (customid),
+ CONSTRAINT fk_custom_user FOREIGN KEY (userid) REFERENCES users (userid)
+) ENGINE=InnoDB;
+```
+
+## Basic Usage
+
+### Using Default Configuration
+
+```php
+ 'user_id',
+ UserDefinition::FIELD_NAME => 'full_name',
+ UserDefinition::FIELD_EMAIL => 'email_address',
+ UserDefinition::FIELD_USERNAME => 'user_name',
+ UserDefinition::FIELD_PASSWORD => 'password_hash',
+ UserDefinition::FIELD_CREATED => 'date_created',
+ UserDefinition::FIELD_ADMIN => 'is_admin'
+ ]
+);
+
+$users = new UsersDBDataset($dbDriver, $userDefinition);
+```
+
+### Custom Properties Table
+
+```php
+ 'id',
+ UserDefinition::FIELD_NAME => 'fullname',
+ UserDefinition::FIELD_EMAIL => 'email',
+ UserDefinition::FIELD_USERNAME => 'username',
+ UserDefinition::FIELD_PASSWORD => 'pwd',
+ UserDefinition::FIELD_CREATED => 'created_at',
+ UserDefinition::FIELD_ADMIN => 'is_admin'
+ ]
+);
+
+// Custom properties definition
+$propertiesDefinition = new UserPropertiesDefinition(
+ 'app_user_meta',
+ 'id',
+ 'meta_key',
+ 'meta_value',
+ 'user_id'
+);
+
+// Initialize
+$users = new UsersDBDataset($dbDriver, $userDefinition, $propertiesDefinition);
+
+// Use it
+$user = $users->addUser('John Doe', 'johndoe', 'john@example.com', 'password123');
+```
+
+## XML/File Storage
+
+For simple applications or development, you can use XML file storage:
+
+```php
+addUser('John Doe', 'johndoe', 'john@example.com', 'password123');
+```
+
+:::warning Production Use
+XML file storage is suitable for development and small applications. For production applications with multiple users, use database storage.
+:::
+
+## Architecture
+
+```text
+ ┌───────────────────┐
+ │ SessionContext │
+ └───────────────────┘
+ │
+┌────────────────────────┐ ┌────────────────────────┐
+│ UserDefinition │─ ─ ┐ │ ─ ─ ┤ UserModel │
+└────────────────────────┘ ┌───────────────────┐ │ └────────────────────────┘
+┌────────────────────────┐ └────│ UsersInterface │────┐ ┌────────────────────────┐
+│ UserPropertyDefinition │─ ─ ┘ └───────────────────┘ ─ ─ ┤ UserPropertyModel │
+└────────────────────────┘ ▲ └────────────────────────┘
+ │
+ ┌────────────────────────┼─────────────────────────┐
+ │ │ │
+ │ │ │
+ │ │ │
+ ┌───────────────────┐ ┌───────────────────┐ ┌────────────────────┐
+ │ UsersAnyDataset │ │ UsersDBDataset │ │ Custom Impl. │
+ └───────────────────┘ └───────────────────┘ └────────────────────┘
+```
+
+- **UserInterface**: Base interface for all implementations
+- **UsersDBDataset**: Database implementation
+- **UsersAnyDataset**: XML file implementation
+- **UserModel**: The user data model
+- **UserPropertyModel**: The user property data model
+- **UserDefinition**: Maps model to database schema
+- **UserPropertiesDefinition**: Maps properties to database schema
+
+## Next Steps
+
+- [User Management](user-management.md) - Managing users
+- [Custom Fields](custom-fields.md) - Extending UserModel
+- [Mappers](mappers.md) - Custom field transformations
diff --git a/docs/examples.md b/docs/examples.md
new file mode 100644
index 0000000..0d04a97
--- /dev/null
+++ b/docs/examples.md
@@ -0,0 +1,520 @@
+---
+sidebar_position: 12
+title: Complete Examples
+---
+
+# Complete Examples
+
+This page contains complete, working examples for common use cases.
+
+## Simple Web Application
+
+### Setup
+
+```php
+isValidUser($username, $password);
+
+ if ($user !== null) {
+ $sessionContext->registerLogin($user->getUserid());
+ $sessionContext->setSessionData('login_time', time());
+
+ header('Location: dashboard.php');
+ exit;
+ } else {
+ $error = 'Invalid username or password';
+ }
+ } catch (Exception $e) {
+ $error = 'An error occurred: ' . $e->getMessage();
+ }
+}
+?>
+
+
+
+ Login
+
+
+ Login
+
+
+ = htmlspecialchars($error) ?>
+
+
+
+
+ Create an account
+
+
+```
+
+### Registration Page
+
+```php
+ 8,
+ PasswordDefinition::REQUIRE_UPPERCASE => 1,
+ PasswordDefinition::REQUIRE_LOWERCASE => 1,
+ PasswordDefinition::REQUIRE_NUMBERS => 1,
+ ]);
+
+ $result = $passwordDef->matchPassword($password);
+ if ($result !== PasswordDefinition::SUCCESS) {
+ throw new Exception('Password does not meet requirements');
+ }
+
+ // Create user
+ $user = $users->addUser($name, $username, $email, $password);
+
+ // Auto-login
+ $sessionContext->registerLogin($user->getUserid());
+
+ header('Location: dashboard.php');
+ exit;
+
+ } catch (UserExistsException $e) {
+ $error = 'Username or email already exists';
+ } catch (Exception $e) {
+ $error = $e->getMessage();
+ }
+}
+?>
+
+
+
+ Register
+
+
+ Create Account
+
+
+ = htmlspecialchars($error) ?>
+
+
+
+
+ Already have an account?
+
+
+```
+
+### Protected Dashboard
+
+```php
+isAuthenticated()) {
+ header('Location: login.php');
+ exit;
+}
+
+// Get current user
+$userId = $sessionContext->userInfo();
+$user = $users->get($userId);
+$loginTime = $sessionContext->getSessionData('login_time');
+?>
+
+
+
+ Dashboard
+
+
+ Welcome, = htmlspecialchars($user->getName()) ?>
+
+ Email: = htmlspecialchars($user->getEmail()) ?>
+ Logged in at: = date('Y-m-d H:i:s', $loginTime) ?>
+
+ isAdmin()): ?>
+ You are an administrator
+ Admin Panel
+
+
+ Edit Profile
+ Logout
+
+
+```
+
+### Logout
+
+```php
+registerLogout();
+session_destroy();
+
+header('Location: login.php');
+exit;
+```
+
+## REST API with JWT
+
+### API Configuration
+
+```php
+ 'Method not allowed'], 405);
+}
+
+$input = json_decode(file_get_contents('php://input'), true);
+$username = $input['username'] ?? '';
+$password = $input['password'] ?? '';
+
+try {
+ $token = $users->createAuthToken(
+ $username,
+ $password,
+ $jwtWrapper,
+ 3600, // 1 hour
+ [
+ 'last_login' => date('Y-m-d H:i:s'),
+ 'last_ip' => $_SERVER['REMOTE_ADDR']
+ ],
+ [
+ 'ip' => $_SERVER['REMOTE_ADDR']
+ ]
+ );
+
+ if ($token === null) {
+ jsonResponse(['error' => 'Invalid credentials'], 401);
+ }
+
+ jsonResponse([
+ 'success' => true,
+ 'token' => $token,
+ 'expires_in' => 3600
+ ]);
+
+} catch (Exception $e) {
+ jsonResponse(['error' => $e->getMessage()], 500);
+}
+```
+
+### Protected Endpoint
+
+```php
+ 'No token provided'], 401);
+}
+
+$token = $matches[1];
+
+try {
+ // Decode token to get username
+ $jwtData = $jwtWrapper->extractData($token);
+ $username = $jwtData->data['login'] ?? null;
+
+ if (!$username) {
+ jsonResponse(['error' => 'Invalid token'], 401);
+ }
+
+ // Validate token
+ $result = $users->isValidToken($username, $jwtWrapper, $token);
+
+ if ($result === null) {
+ jsonResponse(['error' => 'Token validation failed'], 401);
+ }
+
+ $user = $result['user'];
+
+ // Handle request
+ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
+ // Get user info
+ jsonResponse([
+ 'id' => $user->getUserid(),
+ 'name' => $user->getName(),
+ 'email' => $user->getEmail(),
+ 'username' => $user->getUsername(),
+ 'admin' => $user->isAdmin()
+ ]);
+ } elseif ($_SERVER['REQUEST_METHOD'] === 'PUT') {
+ // Update user info
+ $input = json_decode(file_get_contents('php://input'), true);
+
+ if (isset($input['name'])) {
+ $user->setName($input['name']);
+ }
+ if (isset($input['email'])) {
+ $user->setEmail($input['email']);
+ }
+
+ $users->save($user);
+
+ jsonResponse(['success' => true, 'message' => 'User updated']);
+ } else {
+ jsonResponse(['error' => 'Method not allowed'], 405);
+ }
+
+} catch (Exception $e) {
+ jsonResponse(['error' => $e->getMessage()], 500);
+}
+```
+
+## Multi-Tenant Application
+
+```php
+addProperty($userId, 'organization', $orgId);
+ $users->addProperty($userId, "org_{$orgId}_role", $role);
+}
+
+// Check if user has access to organization
+function hasOrganizationAccess($users, $userId, $orgId)
+{
+ return $users->hasProperty($userId, 'organization', $orgId);
+}
+
+// Get user's role in organization
+function getOrganizationRole($users, $userId, $orgId)
+{
+ return $users->getProperty($userId, "org_{$orgId}_role");
+}
+
+// Get all users in organization
+function getOrganizationUsers($users, $orgId)
+{
+ return $users->getUsersByProperty('organization', $orgId);
+}
+
+// Usage
+$userId = 1;
+$orgId = 'org-123';
+
+// Add user to organization
+addUserToOrganization($users, $userId, $orgId, 'admin');
+
+// Check access
+if (hasOrganizationAccess($users, $userId, $orgId)) {
+ $role = getOrganizationRole($users, $userId, $orgId);
+ echo "User has access as: $role\n";
+
+ // Get all members
+ $members = getOrganizationUsers($users, $orgId);
+ foreach ($members as $member) {
+ echo "- " . $member->getName() . "\n";
+ }
+}
+```
+
+## Permission System
+
+```php
+users = $users;
+ }
+
+ public function grantPermission($userId, $resource, $action)
+ {
+ $permission = "$resource:$action";
+ $this->users->addProperty($userId, 'permission', $permission);
+ }
+
+ public function revokePermission($userId, $resource, $action)
+ {
+ $permission = "$resource:$action";
+ $this->users->removeProperty($userId, 'permission', $permission);
+ }
+
+ public function hasPermission($userId, $resource, $action)
+ {
+ $permission = "$resource:$action";
+ return $this->users->hasProperty($userId, 'permission', $permission);
+ }
+
+ public function getPermissions($userId)
+ {
+ $permissions = $this->users->getProperty($userId, 'permission');
+ return is_array($permissions) ? $permissions : [$permissions];
+ }
+}
+
+// Usage
+$permissionManager = new PermissionManager($users);
+
+// Grant permissions
+$permissionManager->grantPermission($userId, 'posts', 'create');
+$permissionManager->grantPermission($userId, 'posts', 'edit');
+$permissionManager->grantPermission($userId, 'posts', 'delete');
+$permissionManager->grantPermission($userId, 'users', 'view');
+
+// Check permissions
+if ($permissionManager->hasPermission($userId, 'posts', 'delete')) {
+ echo "User can delete posts\n";
+}
+
+// Get all permissions
+$permissions = $permissionManager->getPermissions($userId);
+print_r($permissions);
+
+// Revoke permission
+$permissionManager->revokePermission($userId, 'posts', 'delete');
+```
+
+## Next Steps
+
+- [Getting Started](getting-started.md) - Basic concepts
+- [User Management](user-management.md) - Managing users
+- [Authentication](authentication.md) - Authentication methods
+- [JWT Tokens](jwt-tokens.md) - Token-based authentication
diff --git a/docs/getting-started.md b/docs/getting-started.md
new file mode 100644
index 0000000..fecc7c9
--- /dev/null
+++ b/docs/getting-started.md
@@ -0,0 +1,38 @@
+---
+sidebar_position: 1
+title: Getting Started
+---
+
+# Getting Started
+
+Auth User PHP is a simple and customizable library for user authentication in PHP applications. It provides an abstraction layer for managing users, authentication, and user properties, supporting multiple storage backends including databases and XML files.
+
+## Quick Example
+
+```php
+addUser('John Doe', 'johndoe', 'john@example.com', 'SecurePass123');
+$user = $users->isValidUser('johndoe', 'SecurePass123');
+
+if ($user !== null) {
+ $sessionContext = new SessionContext(Factory::createSessionPool());
+ $sessionContext->registerLogin($user->getUserid());
+ echo "User authenticated successfully!";
+}
+```
+
+## Next Steps
+
+- [Installation](installation.md) - Install the library via Composer
+- [User Management](user-management.md) - Learn how to manage users
+- [Authentication](authentication.md) - Understand authentication methods
+- [Session Context](session-context.md) - Manage user sessions
diff --git a/docs/installation.md b/docs/installation.md
new file mode 100644
index 0000000..6b3c7f9
--- /dev/null
+++ b/docs/installation.md
@@ -0,0 +1,32 @@
+---
+sidebar_position: 2
+title: Installation
+---
+
+# Installation
+
+## Requirements
+
+- PHP 8.2 or higher
+- Composer
+
+## Install via Composer
+
+Install the library using Composer:
+
+```bash
+composer require byjg/authuser
+```
+
+## Running Tests
+
+Because this project uses PHP Session, you need to run the unit tests with the `--stderr` flag:
+
+```bash
+./vendor/bin/phpunit --stderr
+```
+
+## Next Steps
+
+- [Getting Started](getting-started.md) - Learn the basics
+- [Database Storage](database-storage.md) - Set up database storage
diff --git a/docs/jwt-tokens.md b/docs/jwt-tokens.md
new file mode 100644
index 0000000..02cdf2d
--- /dev/null
+++ b/docs/jwt-tokens.md
@@ -0,0 +1,382 @@
+---
+sidebar_position: 9
+title: JWT Tokens
+---
+
+# JWT Tokens
+
+The library provides built-in support for JWT (JSON Web Token) authentication through integration with [byjg/jwt-wrapper](https://github.com/byjg/jwt-wrapper).
+
+## What is JWT?
+
+JWT (JSON Web Tokens) is a compact, URL-safe means of representing claims to be transferred between two parties. JWTs are commonly used for:
+
+- **Stateless authentication** - No server-side session storage needed
+- **API authentication** - Perfect for REST APIs and microservices
+- **Single Sign-On (SSO)** - Share authentication across domains
+- **Mobile apps** - Efficient token-based authentication
+
+## Setup
+
+### Creating a JWT Wrapper
+
+```php
+createAuthToken(
+ 'johndoe', // Login (username or email)
+ 'password123', // Password
+ $jwtWrapper, // JWT wrapper instance
+ 3600 // Expires in 1 hour (seconds)
+);
+
+if ($token !== null) {
+ // Return token to client
+ echo json_encode(['token' => $token]);
+} else {
+ // Authentication failed
+ http_response_code(401);
+ echo json_encode(['error' => 'Invalid credentials']);
+}
+```
+
+### Token with Custom Data
+
+You can include additional data in the JWT payload:
+
+```php
+createAuthToken(
+ 'johndoe',
+ 'password123',
+ $jwtWrapper,
+ 3600,
+ [], // Update user properties (optional)
+ [ // Additional token data
+ 'role' => 'admin',
+ 'permissions' => ['read', 'write'],
+ 'tenant_id' => '12345'
+ ]
+);
+```
+
+### Update User Properties on Login
+
+```php
+createAuthToken(
+ 'johndoe',
+ 'password123',
+ $jwtWrapper,
+ 3600,
+ [ // User properties to update
+ 'last_login' => date('Y-m-d H:i:s'),
+ 'login_count' => $loginCount + 1
+ ],
+ [ // Token data
+ 'role' => 'admin'
+ ]
+);
+```
+
+## Validating JWT Tokens
+
+### Token Validation
+
+```php
+isValidToken('johndoe', $jwtWrapper, $token);
+
+ if ($result !== null) {
+ $user = $result['user']; // UserModel instance
+ $tokenData = $result['data']; // Token payload data
+
+ echo "Authenticated: " . $user->getName();
+ echo "Role: " . $tokenData['role'];
+ }
+
+} catch (\ByJG\Authenticate\Exception\UserNotFoundException $e) {
+ echo "User not found";
+} catch (\ByJG\Authenticate\Exception\NotAuthenticatedException $e) {
+ echo "Token validation failed: " . $e->getMessage();
+} catch (\ByJG\JwtWrapper\JwtWrapperException $e) {
+ echo "JWT error: " . $e->getMessage();
+}
+```
+
+### Validation Checks
+
+The `isValidToken()` method performs the following checks:
+
+1. **User exists** - Verifies the user account exists
+2. **Token hash matches** - Compares stored token hash
+3. **JWT signature** - Validates the token signature
+4. **Token expiration** - Checks if token has expired
+
+## Token Storage and Invalidation
+
+### How Tokens Are Stored
+
+When a token is created:
+
+```php
+set('TOKEN_HASH', $tokenHash);
+```
+
+This allows you to invalidate tokens without maintaining a token blacklist.
+
+### Invalidating Tokens
+
+#### Logout (Invalidate Current Token)
+
+```php
+removeProperty($userId, 'TOKEN_HASH');
+```
+
+#### Force Re-authentication (Invalidate All Tokens)
+
+```php
+createAuthToken($login, $password, $jwtWrapper, 3600);
+```
+
+## Complete API Example
+
+### Login Endpoint
+
+```php
+createAuthToken(
+ $input['username'],
+ $input['password'],
+ $jwtWrapper,
+ 3600, // 1 hour expiration
+ [
+ 'last_login' => date('Y-m-d H:i:s'),
+ 'last_ip' => $_SERVER['REMOTE_ADDR']
+ ],
+ [
+ 'ip' => $_SERVER['REMOTE_ADDR'],
+ 'user_agent' => $_SERVER['HTTP_USER_AGENT']
+ ]
+ );
+
+ if ($token === null) {
+ throw new Exception('Authentication failed');
+ }
+
+ echo json_encode([
+ 'success' => true,
+ 'token' => $token,
+ 'expires_in' => 3600
+ ]);
+
+} catch (Exception $e) {
+ http_response_code(401);
+ echo json_encode([
+ 'success' => false,
+ 'error' => $e->getMessage()
+ ]);
+}
+```
+
+### Protected Endpoint
+
+```php
+ 'No token provided']);
+ exit;
+}
+
+$token = $matches[1];
+
+// Extract username from token (you need to decode it first)
+try {
+ $jwtData = $jwtWrapper->extractData($token);
+ $username = $jwtData->data['login'] ?? null;
+
+ if (!$username) {
+ throw new Exception('Invalid token structure');
+ }
+
+ // Validate token
+ $result = $users->isValidToken($username, $jwtWrapper, $token);
+
+ if ($result === null) {
+ throw new Exception('Invalid token');
+ }
+
+ $user = $result['user'];
+
+ // Process request
+ echo json_encode([
+ 'success' => true,
+ 'user' => [
+ 'id' => $user->getUserid(),
+ 'name' => $user->getName(),
+ 'email' => $user->getEmail()
+ ]
+ ]);
+
+} catch (Exception $e) {
+ http_response_code(401);
+ echo json_encode(['error' => $e->getMessage()]);
+}
+```
+
+### Logout Endpoint
+
+```php
+extractData($token);
+ $username = $jwtData->data['login'] ?? null;
+
+ $user = $users->get($username, $users->getUserDefinition()->loginField());
+ if ($user !== null) {
+ $users->removeProperty($user->getUserid(), 'TOKEN_HASH');
+ }
+
+ echo json_encode(['success' => true, 'message' => 'Logged out']);
+
+ } catch (Exception $e) {
+ echo json_encode(['success' => false, 'error' => $e->getMessage()]);
+ }
+} else {
+ http_response_code(400);
+ echo json_encode(['error' => 'No token provided']);
+}
+```
+
+## Token Expiration
+
+### Setting Expiration Time
+
+```php
+createAuthToken($login, $password, $jwtWrapper, 900);
+
+// 1 hour
+$token = $users->createAuthToken($login, $password, $jwtWrapper, 3600);
+
+// 24 hours
+$token = $users->createAuthToken($login, $password, $jwtWrapper, 86400);
+
+// 7 days
+$token = $users->createAuthToken($login, $password, $jwtWrapper, 604800);
+```
+
+### Refresh Tokens
+
+For long-lived sessions, implement a refresh token pattern:
+
+```php
+createAuthToken(
+ $login,
+ $password,
+ $jwtWrapper,
+ 900, // 15 minutes
+ [],
+ ['type' => 'access']
+);
+
+// Create long-lived refresh token
+$refreshToken = $users->createAuthToken(
+ $login,
+ $password,
+ $jwtWrapperRefresh, // Different wrapper/key
+ 604800, // 7 days
+ [],
+ ['type' => 'refresh']
+);
+
+echo json_encode([
+ 'access_token' => $accessToken,
+ 'refresh_token' => $refreshToken
+]);
+```
+
+## Security Best Practices
+
+1. **Use HTTPS** - Always transmit tokens over HTTPS
+2. **Short expiration times** - Use short-lived tokens (15-60 minutes)
+3. **Implement refresh tokens** - For longer sessions
+4. **Validate on every request** - Don't trust the client
+5. **Store securely** - Don't store tokens in localStorage if possible
+6. **Include audience claims** - Limit token usage scope
+7. **Monitor for abuse** - Track token usage patterns
+8. **Rotate secrets** - Periodically rotate JWT secrets
+
+## Common Pitfalls
+
+❌ **Don't store sensitive data in JWT payload** - It's not encrypted, only signed
+
+❌ **Don't use weak secret keys** - Use cryptographically random keys
+
+❌ **Don't skip expiration** - Always set reasonable expiration times
+
+❌ **Don't forget to invalidate** - Provide logout functionality
+
+❌ **Don't use HTTP** - Always use HTTPS in production
+
+## Next Steps
+
+- [Authentication](authentication.md) - Other authentication methods
+- [Session Context](session-context.md) - Session-based authentication
+- [User Properties](user-properties.md) - Managing user data
diff --git a/docs/mappers.md b/docs/mappers.md
new file mode 100644
index 0000000..26e693d
--- /dev/null
+++ b/docs/mappers.md
@@ -0,0 +1,433 @@
+---
+sidebar_position: 11
+title: Mappers and Entity Processors
+---
+
+# Mappers and Entity Processors
+
+Mappers and Entity Processors allow you to transform data as it's read from or written to the database.
+
+## What Are Mappers?
+
+Mappers implement the `MapperFunctionInterface` and transform individual field values during database operations.
+
+- **Update Mappers**: Transform values **before** saving to database
+- **Select Mappers**: Transform values **after** reading from database
+
+## Built-in Mappers
+
+### PasswordSha1Mapper
+
+Automatically hashes passwords using SHA-1:
+
+```php
+defineMapperForUpdate(
+ UserDefinition::FIELD_PASSWORD,
+ PasswordSha1Mapper::class
+);
+```
+
+### StandardMapper
+
+Default mapper that passes values through unchanged:
+
+```php
+defineMapperForUpdate('name', StandardMapper::class);
+```
+
+### ReadOnlyMapper
+
+Prevents field updates:
+
+```php
+markPropertyAsReadOnly(UserDefinition::FIELD_CREATED);
+
+// Or explicitly
+$userDefinition->defineMapperForUpdate(
+ UserDefinition::FIELD_CREATED,
+ ReadOnlyMapper::class
+);
+```
+
+## Creating Custom Mappers
+
+### Mapper Interface
+
+```php
+ 12]);
+ }
+}
+
+// Use it
+$userDefinition->defineMapperForUpdate(
+ UserDefinition::FIELD_PASSWORD,
+ BcryptPasswordMapper::class
+);
+```
+
+### Example: Email Normalization Mapper
+
+```php
+defineMapperForUpdate(
+ UserDefinition::FIELD_EMAIL,
+ EmailNormalizationMapper::class
+);
+```
+
+### Example: JSON Serialization Mappers
+
+```php
+defineMapperForUpdate('preferences', JsonEncodeMapper::class);
+$userDefinition->defineMapperForSelect('preferences', JsonDecodeMapper::class);
+```
+
+### Example: Date Formatting Mapper
+
+```php
+format('Y-m-d H:i:s');
+ }
+ if (is_string($value)) {
+ return $value;
+ }
+ if (is_int($value)) {
+ return date('Y-m-d H:i:s', $value);
+ }
+ return $value;
+ }
+}
+
+class DateParseMapper implements MapperFunctionInterface
+{
+ public function processedValue(mixed $value, mixed $instance): mixed
+ {
+ if (empty($value)) {
+ return null;
+ }
+ try {
+ return new \DateTime($value);
+ } catch (\Exception $e) {
+ return $value;
+ }
+ }
+}
+
+$userDefinition->defineMapperForUpdate('created', DateFormatMapper::class);
+$userDefinition->defineMapperForSelect('created', DateParseMapper::class);
+```
+
+## Entity Processors
+
+Entity Processors transform the **entire entity** (UserModel) before insert or update operations.
+
+### Entity Processor Interface
+
+```php
+setBeforeInsert(new PassThroughEntityProcessor());
+```
+
+### Custom Entity Processors
+
+#### Example: Auto-Set Created Timestamp
+
+```php
+getCreated())) {
+ $instance->setCreated(date('Y-m-d H:i:s'));
+ }
+ }
+ }
+}
+
+$userDefinition->setBeforeInsert(new CreatedTimestampProcessor());
+```
+
+#### Example: Username Validation
+
+```php
+getUsername();
+
+ if (strlen($username) < 3) {
+ throw new \InvalidArgumentException('Username must be at least 3 characters');
+ }
+
+ if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
+ throw new \InvalidArgumentException('Username can only contain letters, numbers, and underscores');
+ }
+ }
+ }
+}
+
+$userDefinition->setBeforeInsert(new UsernameValidationProcessor());
+$userDefinition->setBeforeUpdate(new UsernameValidationProcessor());
+```
+
+#### Example: Audit Trail
+
+```php
+userId = $userId;
+ }
+
+ public function process(mixed $instance): void
+ {
+ if ($instance instanceof UserModel) {
+ $instance->set('modified_by', $this->userId);
+ $instance->set('modified_at', date('Y-m-d H:i:s'));
+ }
+ }
+}
+
+$userDefinition->setBeforeUpdate(new AuditProcessor($currentUserId));
+```
+
+## Using Closures (Legacy)
+
+For backward compatibility, you can use closures instead of dedicated mapper classes:
+
+```php
+defineMapperForUpdate(
+ UserDefinition::FIELD_EMAIL,
+ new ClosureMapper(function ($value, $instance) {
+ return strtolower(trim($value));
+ })
+);
+
+// Select mapper
+$userDefinition->defineMapperForSelect(
+ UserDefinition::FIELD_CREATED,
+ new ClosureMapper(function ($value, $instance) {
+ return date('Y', strtotime($value));
+ })
+);
+```
+
+:::warning Deprecated Methods
+The following methods are deprecated but still work:
+- `defineClosureForUpdate()` - Use `defineMapperForUpdate()` with `ClosureMapper`
+- `defineClosureForSelect()` - Use `defineMapperForSelect()` with `ClosureMapper`
+- `getClosureForUpdate()` - Use `getMapperForUpdate()`
+- `getClosureForSelect()` - Use `getMapperForSelect()`
+:::
+
+## Complete Example
+
+```php
+getCreated())) {
+ $instance->setCreated(date('Y-m-d H:i:s'));
+ }
+ if (empty($instance->getAdmin())) {
+ $instance->setAdmin('no');
+ }
+ }
+ }
+}
+
+// Configure User Definition
+$userDefinition = new UserDefinition();
+
+// Apply mappers
+$userDefinition->defineMapperForUpdate('name', TrimMapper::class);
+$userDefinition->defineMapperForUpdate('email', LowercaseMapper::class);
+$userDefinition->defineMapperForUpdate('username', LowercaseMapper::class);
+
+// Apply entity processors
+$userDefinition->setBeforeInsert(new DefaultsProcessor());
+
+// Initialize
+$users = new UsersDBDataset($dbDriver, $userDefinition);
+```
+
+## Best Practices
+
+1. **Keep mappers simple** - Each mapper should do one thing
+2. **Chain mappers** - Use composition for complex transformations
+3. **Handle null values** - Always check for null/empty values
+4. **Be idempotent** - Applying mapper multiple times should be safe
+5. **Use entity processors for validation** - Validate complete entities
+6. **Document side effects** - Make it clear what each mapper does
+
+## Next Steps
+
+- [Custom Fields](custom-fields.md) - Extending UserModel
+- [Password Validation](password-validation.md) - Password policies
+- [Database Storage](database-storage.md) - Schema configuration
diff --git a/docs/password-validation.md b/docs/password-validation.md
new file mode 100644
index 0000000..1306ac4
--- /dev/null
+++ b/docs/password-validation.md
@@ -0,0 +1,288 @@
+---
+sidebar_position: 8
+title: Password Validation
+---
+
+# Password Validation
+
+The `PasswordDefinition` class provides comprehensive password strength validation and generation capabilities.
+
+## Basic Usage
+
+### Creating a Password Definition
+
+```php
+withPasswordDefinition($passwordDefinition);
+
+// Now password is validated when set
+$userModel->setPassword('WeakPwd'); // Throws InvalidArgumentException
+```
+
+## Password Rules
+
+### Default Rules
+
+The default password policy requires:
+
+| Rule | Default Value | Description |
+|---------------------|---------------|------------------------------------------|
+| `minimum_chars` | 8 | Minimum password length |
+| `require_uppercase` | 0 | Number of uppercase letters required |
+| `require_lowercase` | 1 | Number of lowercase letters required |
+| `require_symbols` | 0 | Number of symbols required |
+| `require_numbers` | 1 | Number of digits required |
+| `allow_whitespace` | 0 | Allow whitespace characters (0 = no) |
+| `allow_sequential` | 0 | Allow sequential characters (0 = no) |
+| `allow_repeated` | 0 | Allow repeated patterns (0 = no) |
+
+### Custom Rules
+
+```php
+ 12,
+ PasswordDefinition::REQUIRE_UPPERCASE => 2,
+ PasswordDefinition::REQUIRE_LOWERCASE => 2,
+ PasswordDefinition::REQUIRE_SYMBOLS => 1,
+ PasswordDefinition::REQUIRE_NUMBERS => 2,
+ PasswordDefinition::ALLOW_WHITESPACE => 0,
+ PasswordDefinition::ALLOW_SEQUENTIAL => 0,
+ PasswordDefinition::ALLOW_REPEATED => 0
+]);
+```
+
+### Setting Individual Rules
+
+```php
+setRule(PasswordDefinition::MINIMUM_CHARS, 10);
+$passwordDefinition->setRule(PasswordDefinition::REQUIRE_UPPERCASE, 1);
+$passwordDefinition->setRule(PasswordDefinition::REQUIRE_SYMBOLS, 1);
+```
+
+## Validating Passwords
+
+### Validation Result Codes
+
+The `matchPassword()` method returns a bitwise result:
+
+```php
+matchPassword('weak');
+
+if ($result === PasswordDefinition::SUCCESS) {
+ echo "Password is valid";
+} else {
+ // Check specific failures
+ if ($result & PasswordDefinition::FAIL_MINIMUM_CHARS) {
+ echo "Password is too short\n";
+ }
+ if ($result & PasswordDefinition::FAIL_UPPERCASE) {
+ echo "Missing uppercase letters\n";
+ }
+ if ($result & PasswordDefinition::FAIL_LOWERCASE) {
+ echo "Missing lowercase letters\n";
+ }
+ if ($result & PasswordDefinition::FAIL_NUMBERS) {
+ echo "Missing numbers\n";
+ }
+ if ($result & PasswordDefinition::FAIL_SYMBOLS) {
+ echo "Missing symbols\n";
+ }
+ if ($result & PasswordDefinition::FAIL_WHITESPACE) {
+ echo "Whitespace not allowed\n";
+ }
+ if ($result & PasswordDefinition::FAIL_SEQUENTIAL) {
+ echo "Sequential characters detected\n";
+ }
+ if ($result & PasswordDefinition::FAIL_REPEATED) {
+ echo "Repeated patterns detected\n";
+ }
+}
+```
+
+### Available Failure Codes
+
+| Constant | Value | Description |
+|---------------------------|-------|----------------------------------|
+| `SUCCESS` | 0 | Password is valid |
+| `FAIL_MINIMUM_CHARS` | 1 | Password too short |
+| `FAIL_UPPERCASE` | 2 | Missing uppercase letters |
+| `FAIL_LOWERCASE` | 4 | Missing lowercase letters |
+| `FAIL_SYMBOLS` | 8 | Missing symbols |
+| `FAIL_NUMBERS` | 16 | Missing numbers |
+| `FAIL_WHITESPACE` | 32 | Whitespace not allowed |
+| `FAIL_SEQUENTIAL` | 64 | Sequential characters detected |
+| `FAIL_REPEATED` | 128 | Repeated patterns detected |
+
+## Password Generation
+
+### Generate a Random Password
+
+```php
+generatePassword();
+echo $password; // e.g., "aB3dE7fG9"
+```
+
+### Generate Longer Passwords
+
+```php
+generatePassword(5);
+```
+
+The generated password will:
+- Meet all defined rules
+- Be cryptographically random
+- Avoid sequential and repeated patterns
+
+## User Registration with Password Validation
+
+### Complete Example
+
+```php
+ 10,
+ PasswordDefinition::REQUIRE_UPPERCASE => 1,
+ PasswordDefinition::REQUIRE_LOWERCASE => 1,
+ PasswordDefinition::REQUIRE_SYMBOLS => 1,
+ PasswordDefinition::REQUIRE_NUMBERS => 1,
+]);
+
+// Create user with password validation
+try {
+ $user = new UserModel();
+ $user->withPasswordDefinition($passwordDefinition);
+
+ $user->setName('John Doe');
+ $user->setEmail('john@example.com');
+ $user->setUsername('johndoe');
+ $user->setPassword($_POST['password']); // Validated automatically
+
+ $users->save($user);
+ echo "User created successfully";
+
+} catch (InvalidArgumentException $e) {
+ echo "Password validation failed: " . $e->getMessage();
+}
+```
+
+## User-Friendly Error Messages
+
+```php
+matchPassword($_POST['password']);
+if ($result !== PasswordDefinition::SUCCESS) {
+ $errors = getPasswordErrors($result);
+ foreach ($errors as $error) {
+ echo "- " . $error . "\n";
+ }
+}
+```
+
+## Sequential and Repeated Patterns
+
+### Sequential Characters
+
+Sequential patterns that are detected include:
+- **Alphabetic**: abc, bcd, cde, xyz, etc. (case-insensitive)
+- **Numeric**: 012, 123, 234, 789, 890, etc.
+- **Reverse**: 987, 876, 765, 321, etc.
+
+### Repeated Patterns
+
+Repeated patterns include:
+- **Repeated characters**: aaa, 111, etc.
+- **Repeated sequences**: ababab, 123123, etc.
+
+## Password Change Flow
+
+```php
+get($userId);
+ $user->withPasswordDefinition($passwordDefinition);
+
+ // Verify old password
+ $existingUser = $users->isValidUser($user->getUsername(), $_POST['old_password']);
+ if ($existingUser === null) {
+ throw new Exception("Current password is incorrect");
+ }
+
+ // Set new password (validated automatically)
+ $user->setPassword($_POST['new_password']);
+ $users->save($user);
+
+ echo "Password changed successfully";
+
+} catch (InvalidArgumentException $e) {
+ echo "New password validation failed: " . $e->getMessage();
+}
+```
+
+## Best Practices
+
+1. **Balance security and usability** - Don't make rules too restrictive
+2. **Educate users** - Provide clear error messages
+3. **Use password generation** - Offer to generate strong passwords
+4. **Consider passphrases** - Allow longer passwords with spaces if appropriate
+5. **Combine with rate limiting** - Prevent brute force attacks
+
+## Next Steps
+
+- [Authentication](authentication.md) - Validating credentials
+- [User Management](user-management.md) - Managing users
+- [Mappers](mappers.md) - Custom password hashing
diff --git a/docs/session-context.md b/docs/session-context.md
new file mode 100644
index 0000000..d361105
--- /dev/null
+++ b/docs/session-context.md
@@ -0,0 +1,197 @@
+---
+sidebar_position: 5
+title: Session Context
+---
+
+# Session Context
+
+The `SessionContext` class manages user authentication state using PSR-6 compatible cache storage.
+
+## Creating a Session Context
+
+```php
+registerLogin($userId);
+
+// With additional session data
+$sessionContext->registerLogin($userId, ['ip' => $_SERVER['REMOTE_ADDR']]);
+```
+
+### Check Authentication Status
+
+```php
+isAuthenticated()) {
+ echo "User is logged in";
+} else {
+ echo "User is not authenticated";
+}
+```
+
+### Get Current User Info
+
+```php
+isAuthenticated()) {
+ $userId = $sessionContext->userInfo();
+ // Use $userId to fetch user details
+}
+```
+
+### Logout
+
+```php
+registerLogout();
+```
+
+## Storing Session Data
+
+You can store custom data in the user's session. This data exists only while the user is logged in.
+
+### Store Data
+
+```php
+setSessionData('shopping_cart', [
+ 'item1' => 'Product A',
+ 'item2' => 'Product B'
+]);
+
+$sessionContext->setSessionData('last_page', '/products');
+```
+
+:::warning Authentication Required
+The user must be authenticated to use `setSessionData()`. If not, a `NotAuthenticatedException` will be thrown.
+:::
+
+### Retrieve Data
+
+```php
+getSessionData('shopping_cart');
+$lastPage = $sessionContext->getSessionData('last_page');
+```
+
+Returns `false` if:
+- The user is not authenticated
+- The key doesn't exist
+
+### Session Data Lifecycle
+
+- Session data is stored when the user logs in
+- It persists across requests while the user remains logged in
+- It is automatically deleted when the user logs out
+- It is lost if the session expires
+
+## Complete Example
+
+```php
+isValidUser($_POST['username'], $_POST['password']);
+
+ if ($user !== null) {
+ $sessionContext->registerLogin($user->getUserid());
+ $sessionContext->setSessionData('login_time', time());
+ header('Location: /dashboard');
+ exit;
+ }
+}
+
+// Protected pages
+if (!$sessionContext->isAuthenticated()) {
+ header('Location: /login');
+ exit;
+}
+
+$userId = $sessionContext->userInfo();
+$user = $users->get($userId);
+$loginTime = $sessionContext->getSessionData('login_time');
+
+echo "Welcome, " . $user->getName();
+echo "Logged in at: " . date('Y-m-d H:i:s', $loginTime);
+
+// Logout
+if (isset($_POST['logout'])) {
+ $sessionContext->registerLogout();
+ header('Location: /login');
+ exit;
+}
+```
+
+## Best Practices
+
+1. **Use PHP Session storage** unless you have specific requirements for distributed sessions
+2. **Always check authentication** before accessing protected resources
+3. **Clear sensitive session data** when no longer needed
+4. **Set appropriate session timeouts** based on your security requirements
+5. **Regenerate session IDs** after login to prevent session fixation attacks
+
+## Next Steps
+
+- [Authentication](authentication.md) - User authentication methods
+- [User Properties](user-properties.md) - Store persistent user data
diff --git a/docs/user-management.md b/docs/user-management.md
new file mode 100644
index 0000000..cd749c6
--- /dev/null
+++ b/docs/user-management.md
@@ -0,0 +1,173 @@
+---
+sidebar_position: 3
+title: User Management
+---
+
+# User Management
+
+## Creating Users
+
+### Using addUser() Method
+
+The simplest way to add a user:
+
+```php
+addUser(
+ 'John Doe', // Full name
+ 'johndoe', // Username
+ 'john@example.com', // Email
+ 'SecurePass123' // Password
+);
+```
+
+### Using UserModel
+
+For more control, create a `UserModel` instance:
+
+```php
+setName('John Doe');
+$userModel->setUsername('johndoe');
+$userModel->setEmail('john@example.com');
+$userModel->setPassword('SecurePass123');
+$userModel->setAdmin('no');
+
+$savedUser = $users->save($userModel);
+```
+
+## Retrieving Users
+
+To retrieve users you can use the `get($value, ?string $field = null)` method.
+When the `$field` argument is omitted it defaults to the primary key
+defined in your `UserDefinition`. Passing any other column automatically builds the right filter and throws an
+`\InvalidArgumentException` if the field is not one of the allowed values (`userid`, `username`, or `email`).
+
+```php
+get('john@example.com', $users->getUserDefinition()->getEmail());
+```
+
+The following examples show the common calls:
+
+### Get User by ID
+
+```php
+get($userId);
+# OR
+$user = $users->get($userId, $users->getUserDefinition()->getUserid());
+```
+
+### Get User by Email
+
+```php
+get('john@example.com', $users->getUserDefinition()->getEmail());
+```
+
+### Get User by Username
+
+```php
+get('johndoe', $users->getUserDefinition()->getUsername());
+```
+
+### Get User by Login Field
+
+The login field is determined by the `UserDefinition::loginField()` (either email or username):
+
+```php
+get('johndoe', $users->getUserDefinition()->loginField());
+```
+
+### Using Custom Filters
+
+For advanced queries, use `IteratorFilter`:
+
+```php
+and('email', Relation::EQUAL, 'john@example.com');
+$filter->and('admin', Relation::EQUAL, 'yes');
+
+$user = $users->getUser($filter);
+```
+
+## Updating Users
+
+```php
+get($userId);
+
+// Update fields
+$user->setName('Jane Doe');
+$user->setEmail('jane@example.com');
+
+// Save changes
+$users->save($user);
+```
+
+## Deleting Users
+
+### Delete by ID
+
+```php
+removeUserById($userId);
+```
+
+### Delete by Login
+
+```php
+removeByLoginField('johndoe');
+```
+
+## Checking Admin Status
+
+The admin flag is now interpreted entirely inside `UserModel`. Use `$user->isAdmin()` to read the computed boolean value,
+and `$user->setAdmin(true)` (or one of the accepted string values) to change it. This replaces the old `$users->isAdmin()`
+method that lived in `UsersInterface`.
+
+```php
+isAdmin()) {
+ echo "User is an administrator";
+}
+```
+
+The admin field accepts the following values as `true`:
+- `yes`, `YES`, `y`, `Y`
+- `true`, `TRUE`, `t`, `T`
+- `1`
+- `s`, `S` (from Portuguese "sim")
+
+## UserModel Properties
+
+The `UserModel` class provides the following properties:
+
+| Property | Type | Description |
+|------------|---------------------|--------------------------------|
+| userid | string\|int\|null | User ID (auto-generated) |
+| name | string\|null | User's full name |
+| email | string\|null | User's email address |
+| username | string\|null | User's username |
+| password | string\|null | User's password (hashed) |
+| created | string\|null | Creation timestamp |
+| admin | string\|null | Admin flag (yes/no) |
+
+## Next Steps
+
+- [Authentication](authentication.md) - Validate user credentials
+- [User Properties](user-properties.md) - Store custom user data
+- [Password Validation](password-validation.md) - Enforce password policies
diff --git a/docs/user-properties.md b/docs/user-properties.md
new file mode 100644
index 0000000..ff6f4dd
--- /dev/null
+++ b/docs/user-properties.md
@@ -0,0 +1,248 @@
+---
+sidebar_position: 6
+title: User Properties
+---
+
+# User Properties
+
+User properties allow you to store custom key-value data associated with users. This is useful for storing additional information beyond the standard user fields.
+
+## Adding Properties
+
+### Add a Single Property
+
+```php
+addProperty($userId, 'phone', '555-1234');
+$users->addProperty($userId, 'department', 'Engineering');
+```
+
+:::info Duplicates
+`addProperty()` will not add the property if it already exists with the same value.
+:::
+
+### Add Multiple Values for the Same Property
+
+Users can have multiple values for the same property:
+
+```php
+addProperty($userId, 'role', 'developer');
+$users->addProperty($userId, 'role', 'manager');
+```
+
+### Set a Property (Update or Create)
+
+Use `setProperty()` to update an existing property or create it if it doesn't exist:
+
+```php
+setProperty($userId, 'phone', '555-5678');
+```
+
+## Using UserModel
+
+You can also manage properties directly through the `UserModel`:
+
+```php
+get($userId);
+
+// Set a property value
+$user->set('phone', '555-1234');
+
+// Add a property model
+$property = new UserPropertiesModel('department', 'Engineering');
+$user->addProperty($property);
+
+// Save the user to persist properties
+$users->save($user);
+```
+
+## Retrieving Properties
+
+### Get a Single Property
+
+```php
+getProperty($userId, 'phone');
+// Returns: '555-1234'
+```
+
+### Get Multiple Values
+
+If a property has multiple values, an array is returned:
+
+```php
+getProperty($userId, 'role');
+// Returns: ['developer', 'manager']
+```
+
+Returns `null` if the property doesn't exist.
+
+### Get Properties from UserModel
+
+```php
+get($userId);
+
+// Get property value(s)
+$phone = $user->get('phone');
+
+// Get property as UserPropertiesModel instance
+$propertyModel = $user->get('phone', true);
+
+// Get all properties
+$allProperties = $user->getProperties();
+foreach ($allProperties as $property) {
+ echo $property->getName() . ': ' . $property->getValue();
+}
+```
+
+## Checking Properties
+
+### Check if User Has a Property
+
+```php
+hasProperty($userId, 'phone')) {
+ echo "User has a phone number";
+}
+
+// Check if property has a specific value
+if ($users->hasProperty($userId, 'role', 'admin')) {
+ echo "User is an admin";
+}
+```
+
+:::tip Admin Bypass
+The `hasProperty()` method always returns `true` for admin users, regardless of the actual property values.
+:::
+
+## Removing Properties
+
+### Remove a Specific Property Value
+
+```php
+removeProperty($userId, 'role', 'developer');
+```
+
+### Remove All Values of a Property
+
+```php
+removeProperty($userId, 'phone');
+```
+
+### Remove Property from All Users
+
+```php
+removeAllProperties('temporary_flag');
+
+// Remove a specific value from all users
+$users->removeAllProperties('role', 'guest');
+```
+
+## Finding Users by Properties
+
+### Find Users with a Specific Property Value
+
+```php
+getUsersByProperty('department', 'Engineering');
+// Returns array of UserModel objects
+```
+
+### Find Users with Multiple Properties
+
+```php
+getUsersByPropertySet([
+ 'department' => 'Engineering',
+ 'role' => 'senior',
+ 'status' => 'active'
+]);
+// Returns users that have ALL these properties with the specified values
+```
+
+## Common Use Cases
+
+### User Roles and Permissions
+
+```php
+addProperty($userId, 'role', 'viewer');
+$users->addProperty($userId, 'role', 'editor');
+$users->addProperty($userId, 'role', 'admin');
+
+// Check permissions
+if ($users->hasProperty($userId, 'role', 'admin')) {
+ // Allow admin actions
+}
+
+// Get all roles
+$roles = $users->getProperty($userId, 'role');
+```
+
+### User Preferences
+
+```php
+setProperty($userId, 'theme', 'dark');
+$users->setProperty($userId, 'language', 'en');
+$users->setProperty($userId, 'timezone', 'America/New_York');
+
+// Retrieve preferences
+$theme = $users->getProperty($userId, 'theme');
+```
+
+### Multi-tenant Applications
+
+```php
+addProperty($userId, 'organization', 'org-123');
+$users->addProperty($userId, 'organization', 'org-456');
+
+// Find all users in an organization
+$orgUsers = $users->getUsersByProperty('organization', 'org-123');
+
+// Check access
+if ($users->hasProperty($userId, 'organization', $requestedOrgId)) {
+ // Grant access
+}
+```
+
+## Property Storage
+
+### Database Storage
+
+Properties are stored in a separate table (default: `users_property`):
+
+| Column | Description |
+|------------|--------------------------|
+| customid | Property ID |
+| userid | User ID (foreign key) |
+| name | Property name |
+| value | Property value |
+
+### XML/AnyDataset Storage
+
+Properties are stored as fields within each user's record, with arrays used for multiple values.
+
+## Next Steps
+
+- [User Management](user-management.md) - Basic user operations
+- [Database Storage](database-storage.md) - Configure property storage
+- [Custom Fields](custom-fields.md) - Extend the UserModel
diff --git a/example.php b/example.php
index 99f0e14..a33cf2f 100644
--- a/example.php
+++ b/example.php
@@ -2,20 +2,41 @@
require "vendor/autoload.php";
-$users = new ByJG\Authenticate\UsersAnyDataset('/tmp/pass.anydata.xml');
+use ByJG\AnyDataset\Core\AnyDataset;
+use ByJG\Authenticate\SessionContext;
+use ByJG\Authenticate\UsersAnyDataset;
+use ByJG\Cache\Factory;
-$users->addUser('Some User Full Name', 'someuser', 'someuser@someemail.com', '12345');
-//$users->save();
+// Create or load AnyDataset from XML file
+$anyDataset = new AnyDataset('/tmp/users.xml');
-$user = $users->isValidUser('someuser', '12345');
-var_dump($user);
-if (!is_null($user))
-{
- $session = new \ByJG\Authenticate\SessionContext();
- $session->registerLogin($userId);
+// Initialize user management
+$users = new UsersAnyDataset($anyDataset);
- echo "Authenticated: " . $session->isAuthenticated();
- print_r($session->userInfo());
+// Add a new user
+$user = $users->addUser('Some User Full Name', 'someuser', 'someuser@someemail.com', '12345');
+echo "User created with ID: " . $user->getUserid() . "\n";
+
+// Validate user credentials
+$authenticatedUser = $users->isValidUser('someuser', '12345');
+var_dump($authenticatedUser);
+
+if ($authenticatedUser !== null) {
+ // Create session context
+ $session = new SessionContext(Factory::createSessionPool());
+
+ // Register login
+ $session->registerLogin($authenticatedUser->getUserid());
+
+ echo "Authenticated: " . ($session->isAuthenticated() ? 'yes' : 'no') . "\n";
+ echo "User ID: " . $session->userInfo() . "\n";
+
+ // Store some session data
+ $session->setSessionData('login_time', time());
+
+ // Get the user info
+ $currentUser = $users->get($session->userInfo());
+ echo "Welcome, " . $currentUser->getName() . "\n";
}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 254ada2..90e6448 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -6,15 +6,21 @@ and open the template in the editor.
-->
-
+ displayDetailsOnTestsThatTriggerDeprecations="true"
+ displayDetailsOnTestsThatTriggerErrors="true"
+ displayDetailsOnTestsThatTriggerNotices="true"
+ displayDetailsOnTestsThatTriggerWarnings="true"
+ displayDetailsOnPhpunitDeprecations="true"
+ failOnWarning="true"
+ failOnNotice="true"
+ failOnDeprecation="true"
+ failOnPhpunitDeprecation="true"
+ stopOnFailure="false"
+ xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd">
@@ -22,11 +28,11 @@ and open the template in the editor.
-
-
- ./src
-
-
+
+
+ ./src/
+
+
diff --git a/psalm.xml b/psalm.xml
index ebabb1a..b208114 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -4,6 +4,7 @@
resolveFromConfigFile="true"
findUnusedBaselineEntry="true"
findUnusedCode="false"
+ cacheDirectory="/tmp/psalm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
diff --git a/src/Definition/PasswordDefinition.php b/src/Definition/PasswordDefinition.php
index e4e1e6b..ad2c644 100644
--- a/src/Definition/PasswordDefinition.php
+++ b/src/Definition/PasswordDefinition.php
@@ -3,6 +3,7 @@
namespace ByJG\Authenticate\Definition;
use InvalidArgumentException;
+use Random\RandomException;
class PasswordDefinition
{
@@ -113,6 +114,84 @@ public function matchPassword(string $password): int
return $result;
}
+ /**
+ * @throws RandomException
+ */
+ public function generatePassword(int $extendSize = 0): string
+ {
+ $charsList = [
+ self::REQUIRE_UPPERCASE => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ self::REQUIRE_LOWERCASE => 'abcdefghijklmnopqrstuvwxyz',
+ self::REQUIRE_SYMBOLS => '!@#$%^&*()-_=+{};:,<.>',
+ self::REQUIRE_NUMBERS => '0123456789'
+ ];
+
+ $charsCount = [
+ self::REQUIRE_UPPERCASE => 0,
+ self::REQUIRE_LOWERCASE => 0,
+ self::REQUIRE_SYMBOLS => 0,
+ self::REQUIRE_NUMBERS => 0
+ ];
+ foreach ($this->rules as $rule => $value) {
+ if ($rule == self::MINIMUM_CHARS) {
+ continue;
+ }
+
+ switch ($rule) {
+ case self::REQUIRE_UPPERCASE:
+ $charsCount[self::REQUIRE_UPPERCASE] = $value;
+ break;
+ case self::REQUIRE_LOWERCASE:
+ $charsCount[self::REQUIRE_LOWERCASE] = $value;
+ break;
+ case self::REQUIRE_SYMBOLS:
+ $charsCount[self::REQUIRE_SYMBOLS] = $value;
+ break;
+ case self::REQUIRE_NUMBERS:
+ $charsCount[self::REQUIRE_NUMBERS] = $value;
+ break;
+ }
+ }
+ $size = intval($this->rules[self::MINIMUM_CHARS]) + $extendSize;
+ $totalChars = intval(array_sum($charsCount));
+ $rulesWithValueGreaterThanZero = array_filter($charsCount, function ($value) {
+ return $value > 0;
+ });
+ if (empty($rulesWithValueGreaterThanZero)) {
+ $rulesWithValueGreaterThanZero[self::REQUIRE_LOWERCASE] = 1;
+ $rulesWithValueGreaterThanZero[self::REQUIRE_NUMBERS] = 1;
+ }
+ while ($totalChars < $size) {
+ $rule = array_rand($rulesWithValueGreaterThanZero);
+ $rulesWithValueGreaterThanZero[$rule]++;
+ $totalChars++;
+ }
+
+ $password = '';
+ while (strlen($password) < $size) {
+ foreach ($rulesWithValueGreaterThanZero as $rule => $value) {
+ if ($value == 0) {
+ continue;
+ }
+
+ do {
+ $char = $charsList[$rule][random_int(0, strlen($charsList[$rule]) - 1)];
+ $previousChar = $password[strlen($password) - 1] ?? "\0";
+ $isRepeated = ($char == $previousChar);
+ $previousChar = strtoupper($previousChar);
+ $upperChar = strtoupper($char);
+ $isSequential = ($upperChar == chr(ord($previousChar) + 1)) || ($upperChar == chr(ord($previousChar) - 1));
+ if (!$isRepeated && !$isSequential) {
+ break;
+ }
+ } while (true);
+ $password .= $char;
+ $rulesWithValueGreaterThanZero[$rule]--;
+ }
+ }
+
+ return $password;
+ }
}
\ No newline at end of file
diff --git a/src/Definition/UserDefinition.php b/src/Definition/UserDefinition.php
index 1019c98..a927402 100644
--- a/src/Definition/UserDefinition.php
+++ b/src/Definition/UserDefinition.php
@@ -2,23 +2,38 @@
namespace ByJG\Authenticate\Definition;
+use ByJG\Authenticate\EntityProcessors\ClosureEntityProcessor;
+use ByJG\Authenticate\EntityProcessors\PassThroughEntityProcessor;
+use ByJG\Authenticate\MapperFunctions\ClosureMapper;
+use ByJG\Authenticate\MapperFunctions\PasswordSha1Mapper;
use ByJG\Authenticate\Model\UserModel;
-use ByJG\MicroOrm\MapperClosure;
+use ByJG\MicroOrm\Interface\EntityProcessorInterface;
+use ByJG\MicroOrm\Interface\MapperFunctionInterface;
+use ByJG\MicroOrm\MapperFunctions\ReadOnlyMapper;
+use ByJG\MicroOrm\MapperFunctions\StandardMapper;
use ByJG\Serializer\Serialize;
use Closure;
use InvalidArgumentException;
/**
* Structure to represent the users
+ *
+ * @method string getUserid()
+ * @method string getName()
+ * @method string getEmail()
+ * @method string getUsername()
+ * @method string getPassword()
+ * @method string getCreated()
+ * @method string getAdmin()
*/
class UserDefinition
{
protected string $__table = 'users';
- protected array $__closures = ["select" => [], "update" => [] ];
+ protected array $__mappers = ["select" => [], "update" => [] ];
protected string $__loginField;
protected string $__model;
protected array $__properties = [];
- protected Closure|null $__generateKey = null;
+ protected MapperFunctionInterface|string|null $__generateKey = null;
const FIELD_USERID = 'userid';
const FIELD_NAME = 'name';
@@ -65,32 +80,15 @@ public function __construct(
$this->__properties[$property] = $value;
}
- $this->defineClosureForUpdate(UserDefinition::FIELD_PASSWORD, function ($value) {
- // Already have a SHA1 password
- if (strlen($value) === 40) {
- return $value;
- }
-
- // Leave null
- if (empty($value)) {
- return null;
- }
-
- // Return the hash password
- return strtolower(sha1($value));
- });
+ $this->defineMapperForUpdate(UserDefinition::FIELD_PASSWORD, PasswordSha1Mapper::class);
if ($loginField !== self::LOGIN_IS_USERNAME && $loginField !== self::LOGIN_IS_EMAIL) {
throw new InvalidArgumentException('Login field is invalid. ');
}
$this->__loginField = $loginField;
- $this->beforeInsert = function ($instance) {
- return $instance;
- };
- $this->beforeUpdate = function ($instance) {
- return $instance;
- };
+ $this->beforeInsert = new PassThroughEntityProcessor();
+ $this->beforeUpdate = new PassThroughEntityProcessor();
}
/**
@@ -140,77 +138,147 @@ private function checkProperty(string $property): void
/**
* @param string $event
* @param string $property
- * @param Closure $closure
+ * @param MapperFunctionInterface|string $mapper
*/
- private function updateClosureDef(string $event, string $property, Closure $closure): void
+ private function updateMapperDef(string $event, string $property, MapperFunctionInterface|string $mapper): void
{
$this->checkProperty($property);
- $this->__closures[$event][$property] = $closure;
+ $this->__mappers[$event][$property] = $mapper;
}
- private function getClosureDef(string $event, string $property): Closure
+ private function getMapperDef(string $event, string $property): MapperFunctionInterface|string
{
$this->checkProperty($property);
- if (!$this->existsClosure($event, $property)) {
- return MapperClosure::standard();
+ if (!$this->existsMapper($event, $property)) {
+ return StandardMapper::class;
}
- return $this->__closures[$event][$property];
+ return $this->__mappers[$event][$property];
}
- public function existsClosure(string $event, string $property): bool
+ public function existsMapper(string $event, string $property): bool
{
// Event not set
- if (!isset($this->__closures[$event])) {
+ if (!isset($this->__mappers[$event])) {
return false;
}
// Event is set but there is no property
- if (!array_key_exists($property, $this->__closures[$event])) {
+ if (!array_key_exists($property, $this->__mappers[$event])) {
return false;
}
return true;
}
+ /**
+ * @deprecated Use existsMapper instead
+ */
+ public function existsClosure(string $event, string $property): bool
+ {
+ return $this->existsMapper($event, $property);
+ }
+
public function markPropertyAsReadOnly(string $property): void
{
- $this->updateClosureDef(self::UPDATE, $property, MapperClosure::readOnly());
+ $this->updateMapperDef(self::UPDATE, $property, ReadOnlyMapper::class);
}
+ public function defineMapperForUpdate(string $property, MapperFunctionInterface|string $mapper): void
+ {
+ $this->updateMapperDef(self::UPDATE, $property, $mapper);
+ }
+
+ public function defineMapperForSelect(string $property, MapperFunctionInterface|string $mapper): void
+ {
+ $this->updateMapperDef(self::SELECT, $property, $mapper);
+ }
+
+ /**
+ * @deprecated Use defineMapperForUpdate instead
+ */
public function defineClosureForUpdate(string $property, Closure $closure): void
{
- $this->updateClosureDef(self::UPDATE, $property, $closure);
+ $this->updateMapperDef(self::UPDATE, $property, new ClosureMapper($closure));
}
+ /**
+ * @deprecated Use defineMapperForSelect instead
+ */
public function defineClosureForSelect(string $property, Closure $closure): void
{
- $this->updateClosureDef(self::SELECT, $property, $closure);
+ $this->updateMapperDef(self::SELECT, $property, new ClosureMapper($closure));
+ }
+
+ public function getMapperForUpdate(string $property): MapperFunctionInterface|string
+ {
+ return $this->getMapperDef(self::UPDATE, $property);
}
+ /**
+ * @deprecated Use getMapperForUpdate instead. Returns a Closure for backward compatibility.
+ */
public function getClosureForUpdate(string $property): Closure
{
- return $this->getClosureDef(self::UPDATE, $property);
+ $mapper = $this->getMapperDef(self::UPDATE, $property);
+
+ // Return a closure that wraps the mapper
+ return function($value, $instance = null) use ($mapper) {
+ if (is_string($mapper)) {
+ $mapper = new $mapper();
+ }
+ return $mapper->processedValue($value, $instance);
+ };
}
+ /**
+ * @deprecated Use defineGenerateKey instead
+ */
public function defineGenerateKeyClosure(Closure $closure): void
{
- $this->__generateKey = $closure;
+ throw new InvalidArgumentException('defineGenerateKeyClosure is deprecated. Use defineGenerateKey with MapperFunctionsInterface instead.');
+ }
+
+ public function defineGenerateKey(MapperFunctionInterface|string $generator): void
+ {
+ $this->__generateKey = $generator;
+ }
+
+ public function getGenerateKey(): MapperFunctionInterface|string|null
+ {
+ return $this->__generateKey;
}
- public function getGenerateKeyClosure(): ?Closure
+ /**
+ * @deprecated Use getGenerateKey instead
+ */
+ public function getGenerateKeyClosure(): MapperFunctionInterface|string|null
{
return $this->__generateKey;
}
+ public function getMapperForSelect(string $property): MapperFunctionInterface|string
+ {
+ return $this->getMapperDef(self::SELECT, $property);
+ }
+
/**
+ * @deprecated Use getMapperForSelect instead. Returns a Closure for backward compatibility.
* @param $property
* @return Closure
*/
public function getClosureForSelect($property): Closure
{
- return $this->getClosureDef(self::SELECT, $property);
+ $mapper = $this->getMapperDef(self::SELECT, $property);
+
+ // Return a closure that wraps the mapper
+ return function($value, $instance = null) use ($mapper) {
+ if (is_string($mapper)) {
+ $mapper = new $mapper();
+ }
+ return $mapper->processedValue($value, $instance);
+ };
}
public function model(): string
@@ -224,39 +292,45 @@ public function modelInstance(): UserModel
return new $model();
}
- protected Closure $beforeInsert;
+ protected EntityProcessorInterface $beforeInsert;
/**
- * @return Closure
+ * @return EntityProcessorInterface
*/
- public function getBeforeInsert(): Closure
+ public function getBeforeInsert(): EntityProcessorInterface
{
return $this->beforeInsert;
}
/**
- * @param Closure $beforeInsert
+ * @param EntityProcessorInterface|Closure $beforeInsert
*/
- public function setBeforeInsert(Closure $beforeInsert): void
+ public function setBeforeInsert(EntityProcessorInterface|Closure $beforeInsert): void
{
+ if ($beforeInsert instanceof Closure) {
+ $beforeInsert = new ClosureEntityProcessor($beforeInsert);
+ }
$this->beforeInsert = $beforeInsert;
}
- protected Closure $beforeUpdate;
+ protected EntityProcessorInterface $beforeUpdate;
/**
- * @return Closure
+ * @return EntityProcessorInterface
*/
- public function getBeforeUpdate(): Closure
+ public function getBeforeUpdate(): EntityProcessorInterface
{
return $this->beforeUpdate;
}
/**
- * @param mixed $beforeUpdate
+ * @param EntityProcessorInterface|Closure $beforeUpdate
*/
- public function setBeforeUpdate(Closure $beforeUpdate): void
+ public function setBeforeUpdate(EntityProcessorInterface|Closure $beforeUpdate): void
{
+ if ($beforeUpdate instanceof Closure) {
+ $beforeUpdate = new ClosureEntityProcessor($beforeUpdate);
+ }
$this->beforeUpdate = $beforeUpdate;
}
}
diff --git a/src/EntityProcessors/ClosureEntityProcessor.php b/src/EntityProcessors/ClosureEntityProcessor.php
new file mode 100644
index 0000000..b1bc747
--- /dev/null
+++ b/src/EntityProcessors/ClosureEntityProcessor.php
@@ -0,0 +1,32 @@
+closure = $closure;
+ }
+
+ #[\Override]
+ public function process(array $instance): array
+ {
+ $result = ($this->closure)($instance);
+
+ // If closure returns an object, convert it to array
+ if (is_object($result)) {
+ return (array) $result;
+ }
+
+ return $result;
+ }
+}
diff --git a/src/EntityProcessors/PassThroughEntityProcessor.php b/src/EntityProcessors/PassThroughEntityProcessor.php
new file mode 100644
index 0000000..d1aef84
--- /dev/null
+++ b/src/EntityProcessors/PassThroughEntityProcessor.php
@@ -0,0 +1,17 @@
+|null|string String vector with all sites
*/
- public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|\ByJG\Authenticate\Model\UserPropertiesModel|null;
+ public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|UserPropertiesModel|null;
/**
*
@@ -156,7 +129,7 @@ public function removeAllProperties(string $propertyName, string|null $value = n
* @param int $expires
* @param array $updateUserInfo
* @param array $updateTokenInfo
- * @return string|null Return the TOKEN or null, if can't create it.
+ * @return string|null Return the TOKEN or null, if we can't create it.
*/
public function createAuthToken(
string $login,
diff --git a/src/MapperFunctions/ClosureMapper.php b/src/MapperFunctions/ClosureMapper.php
new file mode 100644
index 0000000..0e8cd0d
--- /dev/null
+++ b/src/MapperFunctions/ClosureMapper.php
@@ -0,0 +1,39 @@
+closure = $closure;
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ #[\Override]
+ public function processedValue(mixed $value, mixed $instance, mixed $executor = null): mixed
+ {
+ $reflection = new ReflectionFunction($this->closure);
+ $paramCount = $reflection->getNumberOfParameters();
+
+ // Call closure with the appropriate number of parameters
+ return match($paramCount) {
+ 1 => ($this->closure)($value),
+ 2 => ($this->closure)($value, $instance),
+ default => ($this->closure)($value, $instance, $executor)
+ };
+ }
+}
diff --git a/src/MapperFunctions/PasswordSha1Mapper.php b/src/MapperFunctions/PasswordSha1Mapper.php
new file mode 100644
index 0000000..97c2bac
--- /dev/null
+++ b/src/MapperFunctions/PasswordSha1Mapper.php
@@ -0,0 +1,29 @@
+getUsername()));
+ }
+
+ return $value;
+ }
+}
diff --git a/src/Model/UserModel.php b/src/Model/UserModel.php
index 3dd4ee3..4209cff 100644
--- a/src/Model/UserModel.php
+++ b/src/Model/UserModel.php
@@ -120,7 +120,7 @@ public function setPassword(?string $password): void
if (!empty($this->passwordDefinition) && !empty($password) && strlen($password) != 40) {
$match = $this->passwordDefinition->matchPassword($password);
if ($match != PasswordDefinition::SUCCESS) {
- throw new InvalidArgumentException("Password does not match the password definition [{$match}]");
+ throw new InvalidArgumentException("Password does not match the password definition [$match]");
}
}
@@ -224,4 +224,11 @@ public function withPasswordDefinition(PasswordDefinition $passwordDefinition):
$this->passwordDefinition = $passwordDefinition;
return $this;
}
+
+ public function isAdmin(): bool
+ {
+ return
+ preg_match('/^(yes|YES|[yY]|true|TRUE|[tT]|1|[sS])$/', $this->getAdmin()) === 1
+ ;
+ }
}
diff --git a/src/SessionContext.php b/src/SessionContext.php
index fe7a115..214d134 100644
--- a/src/SessionContext.php
+++ b/src/SessionContext.php
@@ -5,6 +5,7 @@
use ByJG\Authenticate\Exception\NotAuthenticatedException;
use ByJG\Authenticate\Interfaces\UserContextInterface;
use ByJG\Cache\Psr6\CachePool;
+use Override;
use Psr\SimpleCache\InvalidArgumentException;
class SessionContext implements UserContextInterface
@@ -39,6 +40,7 @@ public function __construct(CachePool $cachePool, string $key = 'default')
* @return bool Return true if authenticated; false otherwise.
* @throws InvalidArgumentException
*/
+ #[Override]
public function isAuthenticated(): bool
{
$item = $this->session->getItem("user.$this->key");
@@ -52,6 +54,7 @@ public function isAuthenticated(): bool
* @return string|int The authenticated username if exists.
* @throws InvalidArgumentException
*/
+ #[Override]
public function userInfo(): string|int
{
$item = $this->session->getItem("user.$this->key");
@@ -63,6 +66,7 @@ public function userInfo(): string|int
* @param array $data
* @throws InvalidArgumentException
*/
+ #[Override]
public function registerLogin(string|int $userId, array $data = []): void
{
$item = $this->session->getItem("user.$this->key");
@@ -84,6 +88,7 @@ public function registerLogin(string|int $userId, array $data = []): void
* @throws InvalidArgumentException
* @throws InvalidArgumentException
*/
+ #[Override]
public function setSessionData(string $name, mixed $value): void
{
if (!$this->isAuthenticated()) {
@@ -110,6 +115,7 @@ public function setSessionData(string $name, mixed $value): void
* @throws InvalidArgumentException
* @throws InvalidArgumentException
*/
+ #[Override]
public function getSessionData(string $name): mixed
{
if (!$this->isAuthenticated()) {
@@ -137,6 +143,7 @@ public function getSessionData(string $name): mixed
* @throws InvalidArgumentException
* @throws InvalidArgumentException
*/
+ #[Override]
public function registerLogout(): void
{
$this->session->deleteItem("user.$this->key");
diff --git a/src/UsersAnyDataset.php b/src/UsersAnyDataset.php
index 64a253b..3f7b489 100644
--- a/src/UsersAnyDataset.php
+++ b/src/UsersAnyDataset.php
@@ -7,16 +7,18 @@
use ByJG\AnyDataset\Core\Exception\DatabaseException;
use ByJG\AnyDataset\Core\IteratorFilter;
use ByJG\AnyDataset\Core\IteratorInterface;
-use ByJG\AnyDataset\Core\Row;
+use ByJG\AnyDataset\Core\RowInterface;
use ByJG\Authenticate\Definition\UserDefinition;
use ByJG\Authenticate\Definition\UserPropertiesDefinition;
use ByJG\Authenticate\Exception\UserExistsException;
use ByJG\Authenticate\Exception\UserNotFoundException;
+use ByJG\Authenticate\MapperFunctions\UserIdGeneratorMapper;
use ByJG\Authenticate\Model\UserModel;
use ByJG\Authenticate\Model\UserPropertiesModel;
use ByJG\MicroOrm\Literal\HexUuidLiteral;
use ByJG\Serializer\Exception\InvalidArgumentException;
use ByJG\XmlUtil\Exception\FileException;
+use Override;
class UsersAnyDataset extends UsersBase
{
@@ -38,19 +40,14 @@ class UsersAnyDataset extends UsersBase
*/
public function __construct(
AnyDataset $anyDataset,
- UserDefinition $userTable = null,
- UserPropertiesDefinition $propertiesTable = null
+ UserDefinition|null $userTable = null,
+ UserPropertiesDefinition|null $propertiesTable = null
) {
$this->anyDataSet = $anyDataset;
$this->anyDataSet->save();
$this->userTable = $userTable;
- if (!$userTable->existsClosure('update', UserDefinition::FIELD_USERID)) {
- $userTable->defineClosureForUpdate(UserDefinition::FIELD_USERID, function ($value, $instance) {
- if (empty($value)) {
- return preg_replace('/(?:([\w])|([\W]))/', '\1', strtolower($instance->getUsername()));
- }
- return $value;
- });
+ if (!$userTable->existsMapper('update', UserDefinition::FIELD_USERID)) {
+ $userTable->defineMapperForUpdate(UserDefinition::FIELD_USERID, UserIdGeneratorMapper::class);
}
$this->propertiesTable = $propertiesTable;
}
@@ -65,6 +62,7 @@ public function __construct(
* @throws UserExistsException
* @throws FileException
*/
+ #[Override]
public function save(UserModel $model): UserModel
{
$new = true;
@@ -78,16 +76,33 @@ public function save(UserModel $model): UserModel
$propertyDefinition = $this->getUserDefinition()->toArray();
foreach ($propertyDefinition as $property => $map) {
- $closure = $this->getUserDefinition()->getClosureForUpdate($property);
- $value = $closure($model->{"get$property"}(), $model);
+ $mapper = $this->getUserDefinition()->getMapperForUpdate($property);
+ if (is_string($mapper)) {
+ $mapper = new $mapper();
+ }
+ $value = $mapper->processedValue($model->{"get$property"}(), $model);
if ($value !== false) {
$this->anyDataSet->addField($map, $value);
}
}
- $properties = $model->getProperties();
- foreach ($properties as $value) {
- $this->anyDataSet->addField($value->getName(), $value->getValue());
+ // Group properties by name to handle multiple values
+ $propertiesByName = [];
+ foreach ($model->getProperties() as $property) {
+ $name = $property->getName();
+ if (!isset($propertiesByName[$name])) {
+ $propertiesByName[$name] = [];
+ }
+ $propertiesByName[$name][] = $property->getValue();
+ }
+
+ // Add properties, using array if multiple values exist
+ foreach ($propertiesByName as $name => $values) {
+ if (count($values) === 1) {
+ $this->anyDataSet->addField($name, $values[0]);
+ } else {
+ $this->anyDataSet->addField($name, $values);
+ }
}
$this->anyDataSet->save();
@@ -101,16 +116,16 @@ public function save(UserModel $model): UserModel
*
* @param IteratorFilter $filter Filter to find user
* @return UserModel|null
- * @throws InvalidArgumentException
*/
+ #[Override]
public function getUser(IteratorFilter $filter): UserModel|null
{
$iterator = $this->anyDataSet->getIterator($filter);
- if (!$iterator->hasNext()) {
+ if (!$iterator->valid()) {
return null;
}
- return $this->createUserModel($iterator->moveNext());
+ return $this->createUserModel($iterator->current());
}
/**
@@ -123,6 +138,7 @@ public function getUser(IteratorFilter $filter): UserModel|null
* @throws FileException
* @throws InvalidArgumentException
*/
+ #[Override]
public function removeByLoginField(string $login): bool
{
//anydataset.Row
@@ -130,8 +146,8 @@ public function removeByLoginField(string $login): bool
$iteratorFilter->and($this->getUserDefinition()->loginField(), Relation::EQUAL, $login);
$iterator = $this->anyDataSet->getIterator($iteratorFilter);
- if ($iterator->hasNext()) {
- $oldRow = $iterator->moveNext();
+ if ($iterator->valid()) {
+ $oldRow = $iterator->current();
$this->anyDataSet->removeRow($oldRow);
$this->anyDataSet->save();
return true;
@@ -146,22 +162,27 @@ public function removeByLoginField(string $login): bool
* @param IteratorFilter|null $filter
* @return IteratorInterface
*/
- public function getIterator(IteratorFilter $filter = null): IteratorInterface
+ public function getIterator(IteratorFilter|null $filter = null): IteratorInterface
{
return $this->anyDataSet->getIterator($filter);
}
/**
- * @throws InvalidArgumentException
+ * @param string $propertyName
+ * @param string $value
+ * @return array
*/
+ #[Override]
public function getUsersByProperty(string $propertyName, string $value): array
{
return $this->getUsersByPropertySet([$propertyName => $value]);
}
/**
- * @throws InvalidArgumentException
+ * @param array $propertiesArray
+ * @return array
*/
+ #[Override]
public function getUsersByPropertySet(array $propertiesArray): array
{
$filter = new IteratorFilter();
@@ -186,10 +207,11 @@ public function getUsersByPropertySet(array $propertiesArray): array
* @throws UserExistsException
* @throws UserNotFoundException
*/
+ #[Override]
public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool
{
//anydataset.Row
- $user = $this->getById($userId);
+ $user = $this->get($userId);
if ($user !== null) {
if (!$this->hasProperty($user->getUserid(), $propertyName, $value)) {
$user->addProperty(new UserPropertiesModel($propertyName, $value));
@@ -207,9 +229,10 @@ public function addProperty(string|HexUuidLiteral|int $userId, string $propertyN
* @throws UserExistsException
* @throws FileException
*/
+ #[Override]
public function setProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool
{
- $user = $this->getById($userId);
+ $user = $this->get($userId);
if ($user !== null) {
$user->set($propertyName, $value);
$this->save($user);
@@ -228,9 +251,10 @@ public function setProperty(string|HexUuidLiteral|int $userId, string $propertyN
* @throws InvalidArgumentException
* @throws UserExistsException
*/
+ #[Override]
public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool
{
- $user = $this->getById($userId);
+ $user = $this->get($userId);
if (!empty($user)) {
$properties = $user->getProperties();
foreach ($properties as $key => $property) {
@@ -258,22 +282,21 @@ public function removeProperty(string|HexUuidLiteral|int $userId, string $proper
* @throws InvalidArgumentException
* @throws UserExistsException
*/
+ #[Override]
public function removeAllProperties(string $propertyName, string|null $value = null): void
{
- $iterator = $this->getIterator(null);
- while ($iterator->hasNext()) {
+ $iterator = $this->getIterator();
+ foreach ($iterator as $user) {
//anydataset.Row
- $user = $iterator->moveNext();
$this->removeProperty($user->get($this->getUserDefinition()->getUserid()), $propertyName, $value);
}
}
/**
- * @param Row $row
+ * @param RowInterface $row
* @return UserModel
- * @throws InvalidArgumentException
*/
- private function createUserModel(Row $row): UserModel
+ private function createUserModel(RowInterface $row): UserModel
{
$allProp = $row->toArray();
$userModel = new UserModel();
@@ -287,8 +310,17 @@ private function createUserModel(Row $row): UserModel
}
foreach (array_keys($allProp) as $property) {
- foreach ($row->getAsArray($property) as $eachValue) {
- $userModel->addProperty(new UserPropertiesModel($property, $eachValue));
+ $values = $row->get($property);
+
+ // Handle both single values and arrays
+ if (!is_array($values)) {
+ if ($values !== null) {
+ $userModel->addProperty(new UserPropertiesModel($property, $values));
+ }
+ } else {
+ foreach ($values as $eachValue) {
+ $userModel->addProperty(new UserPropertiesModel($property, $eachValue));
+ }
}
}
@@ -300,14 +332,15 @@ private function createUserModel(Row $row): UserModel
* @return bool
* @throws InvalidArgumentException
*/
+ #[Override]
public function removeUserById(string|HexUuidLiteral|int $userid): bool
{
$iteratorFilter = new IteratorFilter();
$iteratorFilter->and($this->getUserDefinition()->getUserid(), Relation::EQUAL, $userid);
$iterator = $this->anyDataSet->getIterator($iteratorFilter);
- if ($iterator->hasNext()) {
- $oldRow = $iterator->moveNext();
+ if ($iterator->valid()) {
+ $oldRow = $iterator->current();
$this->anyDataSet->removeRow($oldRow);
return true;
}
diff --git a/src/UsersBase.php b/src/UsersBase.php
index e8ed42d..706f068 100644
--- a/src/UsersBase.php
+++ b/src/UsersBase.php
@@ -16,6 +16,7 @@
use ByJG\JwtWrapper\JwtWrapperException;
use ByJG\MicroOrm\Literal\HexUuidLiteral;
use ByJG\Serializer\Exception\InvalidArgumentException;
+use Override;
/**
* Base implementation to search and handle users in XMLNuke.
@@ -36,6 +37,7 @@ abstract class UsersBase implements UsersInterface
/**
* @return UserDefinition
*/
+ #[Override]
public function getUserDefinition(): UserDefinition
{
if ($this->userTable === null) {
@@ -47,6 +49,7 @@ public function getUserDefinition(): UserDefinition
/**
* @return UserPropertiesDefinition
*/
+ #[Override]
public function getUserPropertiesDefinition(): UserPropertiesDefinition
{
if ($this->propertiesTable === null) {
@@ -60,6 +63,7 @@ public function getUserPropertiesDefinition(): UserPropertiesDefinition
*
* @param UserModel $model
*/
+ #[Override]
abstract public function save(UserModel $model): UserModel;
/**
@@ -71,6 +75,7 @@ abstract public function save(UserModel $model): UserModel;
* @param string $password
* @return UserModel
*/
+ #[Override]
public function addUser(string $name, string $userName, string $email, string $password): UserModel
{
$model = $this->getUserDefinition()->modelInstance();
@@ -88,9 +93,10 @@ public function addUser(string $name, string $userName, string $email, string $p
* @throws UserExistsException
* @throws InvalidArgumentException
*/
+ #[Override]
public function canAddUser(UserModel $model): bool
{
- if ($this->getByEmail($model->getEmail()) !== null) {
+ if (!empty($model->getUserid()) && $this->get($model->getUserid()) !== null) {
throw new UserExistsException('Email already exists');
}
$filter = new IteratorFilter();
@@ -109,65 +115,36 @@ public function canAddUser(UserModel $model): bool
* @param IteratorFilter $filter Filter to find user
* @return UserModel|null
* */
+ #[Override]
abstract public function getUser(IteratorFilter $filter): UserModel|null;
/**
- * Get the user based on his email.
- * Return Row if user was found; null, otherwise
- *
- * @param string $email
- * @return UserModel|null
- * @throws InvalidArgumentException
- */
- public function getByEmail(string $email): UserModel|null
- {
- $filter = new IteratorFilter();
- $filter->and($this->getUserDefinition()->getEmail(), Relation::EQUAL, strtolower($email));
- return $this->getUser($filter);
- }
-
- /**
- * Get the user based on his username.
+ * Get the user based on his id.
* Return Row if user was found; null, otherwise
*
- * @param string $username
+ * @param string|HexUuidLiteral|int $value
+ * @param string|null $field
* @return UserModel|null
* @throws InvalidArgumentException
*/
- public function getByUsername(string $username): UserModel|null
+ #[Override]
+ public function get(string|HexUuidLiteral|int $value, ?string $field = null): UserModel|null
{
- $filter = new IteratorFilter();
- $filter->and($this->getUserDefinition()->getUsername(), Relation::EQUAL, $username);
- return $this->getUser($filter);
- }
-
- /**
- * Get the user based on his login.
- * Return Row if user was found; null, otherwise
- *
- * @param string $login
- * @return UserModel|null
- */
- public function getByLoginField(string $login): UserModel|null
- {
- $filter = new IteratorFilter();
- $filter->and($this->getUserDefinition()->loginField(), Relation::EQUAL, strtolower($login));
+ if (empty($field)) {
+ $field = $this->getUserDefinition()->getUserid();
+ }
- return $this->getUser($filter);
- }
+ $validFields = [
+ $this->getUserDefinition()->getUserid(),
+ $this->getUserDefinition()->getUsername(),
+ $this->getUserDefinition()->getEmail()
+ ];
- /**
- * Get the user based on his id.
- * Return Row if user was found; null, otherwise
- *
- * @param string|HexUuidLiteral|int $userid
- * @return UserModel|null
- * @throws InvalidArgumentException
- */
- public function getById(string|HexUuidLiteral|int $userid): UserModel|null
- {
+ if (!in_array($field, $validFields)) {
+ throw new \InvalidArgumentException("Invalid field type provided. Should be one of " . implode(", ", $validFields));
+ }
$filter = new IteratorFilter();
- $filter->and($this->getUserDefinition()->getUserid(), Relation::EQUAL, $userid);
+ $filter->and($field, Relation::EQUAL, $value);
return $this->getUser($filter);
}
@@ -177,6 +154,7 @@ public function getById(string|HexUuidLiteral|int $userid): UserModel|null
* @param string $login
* @return bool
* */
+ #[Override]
abstract public function removeByLoginField(string $login): bool;
/**
@@ -188,15 +166,19 @@ abstract public function removeByLoginField(string $login): bool;
* @return UserModel|null
* @throws InvalidArgumentException
*/
+ #[Override]
public function isValidUser(string $userName, string $password): UserModel|null
{
$filter = new IteratorFilter();
- $passwordGenerator = $this->getUserDefinition()->getClosureForUpdate(UserDefinition::FIELD_PASSWORD);
+ $passwordMapper = $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_PASSWORD);
+ if (is_string($passwordMapper)) {
+ $passwordMapper = new $passwordMapper();
+ }
$filter->and($this->getUserDefinition()->loginField(), Relation::EQUAL, strtolower($userName));
$filter->and(
$this->getUserDefinition()->getPassword(),
Relation::EQUAL,
- $passwordGenerator($password, null)
+ $passwordMapper->processedValue($password, null)
);
return $this->getUser($filter);
}
@@ -212,16 +194,21 @@ public function isValidUser(string $userName, string $password): UserModel|null
* @throws UserNotFoundException
* @throws InvalidArgumentException
*/
- public function hasProperty(string|int|HexUuidLiteral|null $userId, string $propertyName, string $value = null): bool
+ #[Override]
+ public function hasProperty(string|int|HexUuidLiteral|null $userId, string $propertyName, string|null $value = null): bool
{
//anydataset.Row
- $user = $this->getById($userId);
+ $userIdMapper = $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_USERID);
+ if (is_string($userIdMapper)) {
+ $userIdMapper = new $userIdMapper();
+ }
+ $user = $this->get($userIdMapper->processedValue($userId, null));
if (empty($user)) {
return false;
}
- if ($this->isAdmin($userId)) {
+ if ($user->isAdmin()) {
return true;
}
@@ -245,16 +232,16 @@ public function hasProperty(string|int|HexUuidLiteral|null $userId, string $prop
* @param string|int|HexUuidLiteral $userId User ID
* @param string $propertyName Property name
* @return array|string|UserPropertiesModel|null
- * @throws UserNotFoundException
* @throws InvalidArgumentException
*/
+ #[Override]
public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|UserPropertiesModel|null
{
- $user = $this->getById($userId);
+ $user = $this->get($userId);
if ($user !== null) {
$values = $user->get($propertyName);
- if ($this->isAdmin($userId)) {
+ if ($user->isAdmin()) {
return array(UserDefinition::FIELD_ADMIN => "admin");
}
@@ -274,6 +261,7 @@ abstract public function getUsersByPropertySet(array $propertiesArray): array;
* @param string $propertyName
* @param string|null $value
*/
+ #[Override]
abstract public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool;
/**
@@ -285,6 +273,7 @@ abstract public function addProperty(string|HexUuidLiteral|int $userId, string $
* @param string|null $value Property value with a site
* @return bool
* */
+ #[Override]
abstract public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool;
/**
@@ -295,27 +284,9 @@ abstract public function removeProperty(string|HexUuidLiteral|int $userId, strin
* @param string|null $value Property value with a site
* @return void
* */
+ #[Override]
abstract public function removeAllProperties(string $propertyName, string|null $value = null): void;
- /**
- * @param string|int|HexUuidLiteral $userId
- * @return bool
- * @throws UserNotFoundException
- * @throws InvalidArgumentException
- */
- public function isAdmin(string|HexUuidLiteral|int $userId): bool
- {
- $user = $this->getById($userId);
-
- if (is_null($user)) {
- throw new UserNotFoundException("Cannot find the user");
- }
-
- return
- preg_match('/^(yes|YES|[yY]|true|TRUE|[tT]|1|[sS])$/', $user->getAdmin()) === 1
- ;
- }
-
/**
* Authenticate a user and create a token if it is valid
*
@@ -329,6 +300,7 @@ public function isAdmin(string|HexUuidLiteral|int $userId): bool
* @throws UserNotFoundException
* @throws InvalidArgumentException
*/
+ #[Override]
public function createAuthToken(
string $login,
string $password,
@@ -373,9 +345,10 @@ public function createAuthToken(
* @throws NotAuthenticatedException
* @throws UserNotFoundException
*/
+ #[Override]
public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $token): array|null
{
- $user = $this->getByLoginField($login);
+ $user = $this->get($login, $this->getUserDefinition()->loginField());
if (is_null($user)) {
throw new UserNotFoundException('User not found!');
@@ -398,5 +371,6 @@ public function isValidToken(string $login, JwtWrapper $jwtWrapper, string $toke
/**
* @param string|int|HexUuidLiteral $userid
*/
+ #[Override]
abstract public function removeUserById(string|HexUuidLiteral|int $userid): bool;
}
diff --git a/src/UsersDBDataset.php b/src/UsersDBDataset.php
index 8c29126..4c04845 100644
--- a/src/UsersDBDataset.php
+++ b/src/UsersDBDataset.php
@@ -2,8 +2,11 @@
namespace ByJG\Authenticate;
+use ByJG\AnyDataset\Core\Exception\DatabaseException;
use ByJG\AnyDataset\Core\IteratorFilter;
+use ByJG\AnyDataset\Db\DatabaseExecutor;
use ByJG\AnyDataset\Db\DbDriverInterface;
+use ByJG\AnyDataset\Db\Exception\DbDriverNotConnected;
use ByJG\AnyDataset\Db\IteratorFilterSqlFormatter;
use ByJG\Authenticate\Definition\UserDefinition;
use ByJG\Authenticate\Definition\UserPropertiesDefinition;
@@ -24,7 +27,10 @@
use ByJG\MicroOrm\Query;
use ByJG\MicroOrm\Repository;
use ByJG\Serializer\Exception\InvalidArgumentException;
+use ByJG\XmlUtil\Exception\FileException;
+use ByJG\XmlUtil\Exception\XmlUtilException;
use Exception;
+use Override;
use ReflectionException;
class UsersDBDataset extends UsersBase
@@ -41,25 +47,33 @@ class UsersDBDataset extends UsersBase
protected Repository $propertiesRepository;
/**
- * @var DbDriverInterface
+ * @var DatabaseExecutor
*/
- protected DbDriverInterface $provider;
+ protected DatabaseExecutor $executor;
/**
* UsersDBDataset constructor
*
- * @param DbDriverInterface $dbDriver
+ * @param DbDriverInterface|DatabaseExecutor $dbDriver
* @param UserDefinition|null $userTable
* @param UserPropertiesDefinition|null $propertiesTable
*
* @throws OrmModelInvalidException
* @throws ReflectionException
+ * @throws ExceptionInvalidArgumentException
+ * @throws ExceptionInvalidArgumentException
*/
public function __construct(
- DbDriverInterface $dbDriver,
- UserDefinition $userTable = null,
- UserPropertiesDefinition $propertiesTable = null
+ DatabaseExecutor|DbDriverInterface $dbDriver,
+ UserDefinition|null $userTable = null,
+ UserPropertiesDefinition|null $propertiesTable = null
) {
+ // Convert DbDriverInterface to DatabaseExecutor if needed
+ if ($dbDriver instanceof DbDriverInterface && !($dbDriver instanceof DatabaseExecutor)) {
+ $dbDriver = new DatabaseExecutor($dbDriver);
+ }
+ $this->executor = $dbDriver;
+
if (empty($userTable)) {
$userTable = new UserDefinition();
}
@@ -73,7 +87,7 @@ public function __construct(
$userTable->table(),
$userTable->getUserid()
);
- $seed = $userTable->getGenerateKeyClosure();
+ $seed = $userTable->getGenerateKey();
if (!empty($seed)) {
$userMapper->withPrimaryKeySeedFunction($seed);
}
@@ -83,11 +97,11 @@ public function __construct(
foreach ($propertyDefinition as $property => $map) {
$userMapper->addFieldMapping(FieldMapping::create($property)
->withFieldName($map)
- ->withUpdateFunction($userTable->getClosureForUpdate($property))
- ->withSelectFunction($userTable->getClosureForSelect($property))
+ ->withUpdateFunction($userTable->getMapperForUpdate($property))
+ ->withSelectFunction($userTable->getMapperForSelect($property))
);
}
- $this->userRepository = new Repository($dbDriver, $userMapper);
+ $this->userRepository = new Repository($this->executor, $userMapper);
$propertiesMapper = new Mapper(
UserPropertiesModel::class,
@@ -99,10 +113,10 @@ public function __construct(
$propertiesMapper->addFieldMapping(FieldMapping::create('value')->withFieldName($propertiesTable->getValue()));
$propertiesMapper->addFieldMapping(FieldMapping::create(UserDefinition::FIELD_USERID)
->withFieldName($propertiesTable->getUserid())
- ->withUpdateFunction($userTable->getClosureForUpdate(UserDefinition::FIELD_USERID))
- ->withSelectFunction($userTable->getClosureForSelect(UserDefinition::FIELD_USERID))
+ ->withUpdateFunction($userTable->getMapperForUpdate(UserDefinition::FIELD_USERID))
+ ->withSelectFunction($userTable->getMapperForSelect(UserDefinition::FIELD_USERID))
);
- $this->propertiesRepository = new Repository($dbDriver, $propertiesMapper);
+ $this->propertiesRepository = new Repository($this->executor, $propertiesMapper);
$this->userTable = $userTable;
$this->propertiesTable = $propertiesTable;
@@ -114,10 +128,12 @@ public function __construct(
* @param UserModel $model
* @return UserModel
* @throws UserExistsException
+ * @throws UserNotFoundException
* @throws OrmBeforeInvalidException
* @throws OrmInvalidFieldsException
* @throws Exception
*/
+ #[Override]
public function save(UserModel $model): UserModel
{
$newUser = false;
@@ -136,23 +152,51 @@ public function save(UserModel $model): UserModel
}
if ($newUser) {
- $model = $this->getByEmail($model->getEmail());
+ $model = $this->get($model->getUserid());
}
if ($model === null) {
- throw new UserExistsException("User not found");
+ throw new UserNotFoundException("User not found");
}
return $model;
}
+ #[\Override]
+ public function get(string|HexUuidLiteral|int $value, ?string $field = null): ?UserModel
+ {
+ if (empty($field)) {
+ $field = $this->getUserDefinition()->getUserid();
+ }
+
+ $function = match ($field) {
+ $this->getUserDefinition()->getEmail() => $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_EMAIL),
+ $this->getUserDefinition()->getUsername() => $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_USERNAME),
+ default => $this->getUserDefinition()->getMapperForUpdate(UserDefinition::FIELD_USERID),
+ };
+
+ if (!empty($function)) {
+ if (is_string($function)) {
+ $function = new $function();
+ }
+ $value = $function->processedValue($value, null);
+ }
+
+ return parent::get($value, $field);
+ }
+
/**
* Get the users database information based on a filter.
*
* @param IteratorFilter|null $filter Filter to find user
* @return UserModel[]
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
+ * @throws FileException
+ * @throws XmlUtilException
+ * @throws \Psr\SimpleCache\InvalidArgumentException
*/
- public function getIterator(IteratorFilter $filter = null): array
+ public function getIterator(IteratorFilter|null $filter = null): array
{
if (is_null($filter)) {
$filter = new IteratorFilter();
@@ -175,7 +219,14 @@ public function getIterator(IteratorFilter $filter = null): array
*
* @param IteratorFilter $filter Filter to find user
* @return UserModel|null
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
+ * @throws FileException
+ * @throws RepositoryReadOnlyException
+ * @throws XmlUtilException
+ * @throws \Psr\SimpleCache\InvalidArgumentException
*/
+ #[Override]
public function getUser(IteratorFilter $filter): UserModel|null
{
$result = $this->getIterator($filter);
@@ -197,9 +248,10 @@ public function getUser(IteratorFilter $filter): UserModel|null
* @return bool
* @throws Exception
*/
+ #[Override]
public function removeByLoginField(string $login): bool
{
- $user = $this->getByLoginField($login);
+ $user = $this->get($login, $this->getUserDefinition()->loginField());
if ($user !== null) {
return $this->removeUserById($user->getUserid());
@@ -215,6 +267,7 @@ public function removeByLoginField(string $login): bool
* @return bool
* @throws Exception
*/
+ #[Override]
public function removeUserById(string|HexUuidLiteral|int $userid): bool
{
$updateTableProperties = DeleteQuery::getInstance()
@@ -238,9 +291,15 @@ public function removeUserById(string|HexUuidLiteral|int $userid): bool
* @param string $propertyName
* @param string $value
* @return array
- * @throws InvalidArgumentException
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
* @throws ExceptionInvalidArgumentException
+ * @throws FileException
+ * @throws InvalidArgumentException
+ * @throws XmlUtilException
+ * @throws \Psr\SimpleCache\InvalidArgumentException
*/
+ #[Override]
public function getUsersByProperty(string $propertyName, string $value): array
{
return $this->getUsersByPropertySet([$propertyName => $value]);
@@ -251,9 +310,15 @@ public function getUsersByProperty(string $propertyName, string $value): array
*
* @param array $propertiesArray
* @return array
- * @throws InvalidArgumentException
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
* @throws ExceptionInvalidArgumentException
+ * @throws FileException
+ * @throws InvalidArgumentException
+ * @throws XmlUtilException
+ * @throws \Psr\SimpleCache\InvalidArgumentException
*/
+ #[Override]
public function getUsersByPropertySet(array $propertiesArray): array
{
$query = Query::getInstance()
@@ -281,10 +346,11 @@ public function getUsersByPropertySet(array $propertiesArray): array
* @throws OrmInvalidFieldsException
* @throws Exception
*/
+ #[Override]
public function addProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool
{
//anydataset.Row
- $user = $this->getById($userId);
+ $user = $this->get($userId);
if (empty($user)) {
return false;
}
@@ -299,13 +365,22 @@ public function addProperty(string|HexUuidLiteral|int $userId, string $propertyN
}
/**
- * @throws UpdateConstraintException
- * @throws RepositoryReadOnlyException
- * @throws InvalidArgumentException
- * @throws OrmBeforeInvalidException
+ * @param string|HexUuidLiteral|int $userId
+ * @param string $propertyName
+ * @param string|null $value
+ * @return bool
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
* @throws ExceptionInvalidArgumentException
+ * @throws FileException
+ * @throws OrmBeforeInvalidException
* @throws OrmInvalidFieldsException
+ * @throws RepositoryReadOnlyException
+ * @throws UpdateConstraintException
+ * @throws XmlUtilException
+ * @throws \Psr\SimpleCache\InvalidArgumentException
*/
+ #[Override]
public function setProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value): bool
{
$query = Query::getInstance()
@@ -335,13 +410,16 @@ public function setProperty(string|HexUuidLiteral|int $userId, string $propertyN
* @param string $propertyName Property name
* @param string|null $value Property value with a site
* @return bool
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
* @throws ExceptionInvalidArgumentException
* @throws InvalidArgumentException
* @throws RepositoryReadOnlyException
*/
+ #[Override]
public function removeProperty(string|HexUuidLiteral|int $userId, string $propertyName, string|null $value = null): bool
{
- $user = $this->getById($userId);
+ $user = $this->get($userId);
if ($user !== null) {
$updateable = DeleteQuery::getInstance()
@@ -368,9 +446,12 @@ public function removeProperty(string|HexUuidLiteral|int $userId, string $proper
* @param string $propertyName Property name
* @param string|null $value Property value with a site
* @return void
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
* @throws ExceptionInvalidArgumentException
* @throws RepositoryReadOnlyException
*/
+ #[Override]
public function removeAllProperties(string $propertyName, string|null $value = null): void
{
$updateable = DeleteQuery::getInstance()
@@ -384,6 +465,14 @@ public function removeAllProperties(string $propertyName, string|null $value = n
$this->propertiesRepository->deleteByQuery($updateable);
}
+ /**
+ * @throws XmlUtilException
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
+ * @throws FileException
+ * @throws \Psr\SimpleCache\InvalidArgumentException
+ */
+ #[Override]
public function getProperty(string|HexUuidLiteral|int $userId, string $propertyName): array|string|UserPropertiesModel|null
{
$query = Query::getInstance()
@@ -411,11 +500,19 @@ public function getProperty(string|HexUuidLiteral|int $userId, string $propertyN
* Return all property's fields from this user
*
* @param UserModel $userRow
+ * @throws DatabaseException
+ * @throws DbDriverNotConnected
+ * @throws FileException
* @throws RepositoryReadOnlyException
+ * @throws XmlUtilException
+ * @throws \Psr\SimpleCache\InvalidArgumentException
*/
protected function setPropertiesInUser(UserModel $userRow): void
{
- $value = $this->propertiesRepository->getMapper()->getFieldMap(UserDefinition::FIELD_USERID)->getUpdateFunctionValue($userRow->getUserid(), $userRow, $this->propertiesRepository->getDbDriverWrite()->getDbHelper());
+ $value = $this->propertiesRepository
+ ->getMapper()
+ ->getFieldMap(UserDefinition::FIELD_USERID)
+ ->getUpdateFunctionValue($userRow->getUserid(), $userRow, $this->propertiesRepository->getExecutorWrite());
$query = Query::getInstance()
->table($this->getUserPropertiesDefinition()->table())
->where("{$this->getUserPropertiesDefinition()->getUserid()} = :id", ['id' => $value]);
diff --git a/tests/Fixture/MyUserModel.php b/tests/Fixture/MyUserModel.php
new file mode 100644
index 0000000..3142fe7
--- /dev/null
+++ b/tests/Fixture/MyUserModel.php
@@ -0,0 +1,26 @@
+setOtherfield($field);
+ }
+
+ public function getOtherfield()
+ {
+ return $this->otherfield;
+ }
+
+ public function setOtherfield($otherfield): void
+ {
+ $this->otherfield = $otherfield;
+ }
+}
diff --git a/tests/Fixture/PasswordMd5Mapper.php b/tests/Fixture/PasswordMd5Mapper.php
new file mode 100644
index 0000000..96cbb1f
--- /dev/null
+++ b/tests/Fixture/PasswordMd5Mapper.php
@@ -0,0 +1,28 @@
+prefix = $prefix;
+ }
+
+ #[Override]
+ public function processedValue(mixed $value, mixed $instance, ?DatabaseExecutor $executor = null): mixed
+ {
+ return $this->prefix . uniqid();
+ }
+}
diff --git a/tests/PasswordDefinitionTest.php b/tests/PasswordDefinitionTest.php
index 3454239..8e8af3e 100644
--- a/tests/PasswordDefinitionTest.php
+++ b/tests/PasswordDefinitionTest.php
@@ -18,41 +18,41 @@ class PasswordDefinitionTest extends TestCase
PasswordDefinition::ALLOW_REPEATED => 0 // Allow repeated characters
];
- public function test__construct()
+ public function test__construct(): void
{
// Create Empty Password Definition
$passwordDefinition = new PasswordDefinition();
$this->assertEquals($this->defaultRules, $passwordDefinition->getRules());
}
- public function testSetRule()
+ public function testSetRule(): void
{
$passwordDefinition = new PasswordDefinition();
$passwordDefinition->setRule(PasswordDefinition::MINIMUM_CHARS, 10);
$this->assertEquals(10, $passwordDefinition->getRule(PasswordDefinition::MINIMUM_CHARS));
}
- public function testSetRuleInvalid()
+ public function testSetRuleInvalid(): void
{
$this->expectException(\InvalidArgumentException::class);
$passwordDefinition = new PasswordDefinition();
$passwordDefinition->setRule('invalid', 10);
}
- public function testGetRule()
+ public function testGetRule(): void
{
$passwordDefinition = new PasswordDefinition();
$this->assertEquals(8, $passwordDefinition->getRule(PasswordDefinition::MINIMUM_CHARS));
}
- public function testGetRuleInvalid()
+ public function testGetRuleInvalid(): void
{
$this->expectException(\InvalidArgumentException::class);
$passwordDefinition = new PasswordDefinition();
$passwordDefinition->getRule('invalid');
}
- public function testMatchPasswordMinimumChars()
+ public function testMatchPasswordMinimumChars(): void
{
$passwordDefinition = new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 8,
@@ -68,7 +68,7 @@ public function testMatchPasswordMinimumChars()
$this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword('12345678'));
}
- public function testMatchPasswordUppercase()
+ public function testMatchPasswordUppercase(): void
{
$passwordDefinition = new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 8,
@@ -85,7 +85,7 @@ public function testMatchPasswordUppercase()
$this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword('1234567BA'));
}
- public function testMatchPasswordLowercase()
+ public function testMatchPasswordLowercase(): void
{
$passwordDefinition = new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 8,
@@ -102,7 +102,7 @@ public function testMatchPasswordLowercase()
$this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword('1234567ba'));
}
- public function testMatchPasswordSymbols()
+ public function testMatchPasswordSymbols(): void
{
$passwordDefinition = new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 8,
@@ -119,7 +119,7 @@ public function testMatchPasswordSymbols()
$this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword('1234567!!'));
}
- public function testMatchPasswordNumbers()
+ public function testMatchPasswordNumbers(): void
{
$passwordDefinition = new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 8,
@@ -136,7 +136,7 @@ public function testMatchPasswordNumbers()
$this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword('abcdef11'));
}
- public function testMatchPasswordWhitespace()
+ public function testMatchPasswordWhitespace(): void
{
$passwordDefinition = new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 8,
@@ -151,7 +151,7 @@ public function testMatchPasswordWhitespace()
$this->assertEquals(PasswordDefinition::FAIL_WHITESPACE, $passwordDefinition->matchPassword('1234 678'));
}
- public function testMatchPasswordSequential()
+ public function testMatchPasswordSequential(): void
{
$passwordDefinition = new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 8,
@@ -170,7 +170,7 @@ public function testMatchPasswordSequential()
$this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword('diykdsn132'));
}
- public function testMatchCharsRepeated()
+ public function testMatchCharsRepeated(): void
{
$passwordDefinition = new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 8,
@@ -188,4 +188,76 @@ public function testMatchCharsRepeated()
$this->assertEquals(PasswordDefinition::FAIL_REPEATED, $passwordDefinition->matchPassword('oilalalapo')); // lalala is repeated
$this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword('hay1d11oihsc'));
}
+
+ public function testGeneratePassword(): void
+ {
+ for ($i = 0; $i < 100; $i++) {
+ $passwordDefinition = new PasswordDefinition([
+ PasswordDefinition::MINIMUM_CHARS => 8,
+ PasswordDefinition::REQUIRE_UPPERCASE => 2, // Number of uppercase characters
+ PasswordDefinition::REQUIRE_LOWERCASE => 2, // Number of lowercase characters
+ PasswordDefinition::REQUIRE_SYMBOLS => 2, // Number of symbols
+ PasswordDefinition::REQUIRE_NUMBERS => 2, // Number of numbers
+ PasswordDefinition::ALLOW_WHITESPACE => 0, // Allow whitespace
+ PasswordDefinition::ALLOW_SEQUENTIAL => 0, // Allow sequential characters
+ PasswordDefinition::ALLOW_REPEATED => 0 // Allow repeated characters
+ ]);
+
+ $password = $passwordDefinition->generatePassword();
+ $this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword($password));
+ $this->assertEquals(8, strlen($password));
+
+ $password = $passwordDefinition->generatePassword(2);
+ $this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword($password));
+ $this->assertEquals(10, strlen($password));
+ }
+ }
+
+ public function testGeneratePassword2(): void
+ {
+ for ($i = 0; $i < 100; $i++) {
+ $passwordDefinition = new PasswordDefinition([
+ PasswordDefinition::MINIMUM_CHARS => 8,
+ PasswordDefinition::REQUIRE_UPPERCASE => 1, // Number of uppercase characters
+ PasswordDefinition::REQUIRE_LOWERCASE => 1, // Number of lowercase characters
+ PasswordDefinition::REQUIRE_SYMBOLS => 0, // Number of symbols
+ PasswordDefinition::REQUIRE_NUMBERS => 0, // Number of numbers
+ PasswordDefinition::ALLOW_WHITESPACE => 0, // Allow whitespace
+ PasswordDefinition::ALLOW_SEQUENTIAL => 0, // Allow sequential characters
+ PasswordDefinition::ALLOW_REPEATED => 0 // Allow repeated characters
+ ]);
+
+ $password = $passwordDefinition->generatePassword();
+ $this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword($password));
+ $this->assertEquals(8, strlen($password));
+
+ $password = $passwordDefinition->generatePassword(2);
+ $this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword($password));
+ $this->assertEquals(10, strlen($password));
+ }
+ }
+
+ public function testGeneratePasswordEmpty(): void
+ {
+ for ($i = 0; $i < 100; $i++) {
+ $passwordDefinition = new PasswordDefinition([
+ PasswordDefinition::MINIMUM_CHARS => 8,
+ PasswordDefinition::REQUIRE_UPPERCASE => 0, // Number of uppercase characters
+ PasswordDefinition::REQUIRE_LOWERCASE => 0, // Number of lowercase characters
+ PasswordDefinition::REQUIRE_SYMBOLS => 0, // Number of symbols
+ PasswordDefinition::REQUIRE_NUMBERS => 0, // Number of numbers
+ PasswordDefinition::ALLOW_WHITESPACE => 0, // Allow whitespace
+ PasswordDefinition::ALLOW_SEQUENTIAL => 0, // Allow sequential characters
+ PasswordDefinition::ALLOW_REPEATED => 0 // Allow repeated characters
+ ]);
+
+ $password = $passwordDefinition->generatePassword();
+ $this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword($password));
+ $this->assertEquals(8, strlen($password));
+
+ $password = $passwordDefinition->generatePassword(2);
+ $this->assertEquals(PasswordDefinition::SUCCESS, $passwordDefinition->matchPassword($password));
+ $this->assertEquals(10, strlen($password));
+ }
+ }
}
\ No newline at end of file
diff --git a/tests/PasswordMd5MapperTest.php b/tests/PasswordMd5MapperTest.php
new file mode 100644
index 0000000..a39b580
--- /dev/null
+++ b/tests/PasswordMd5MapperTest.php
@@ -0,0 +1,199 @@
+db = Factory::getDbInstance(self::CONNECTION_STRING);
+ $this->db->execute('create table users (
+ userid integer primary key autoincrement,
+ name varchar(45),
+ email varchar(200),
+ username varchar(20),
+ password varchar(40),
+ created datetime default (datetime(\'2017-12-04\')),
+ admin char(1));'
+ );
+
+ $this->db->execute('create table users_property (
+ id integer primary key autoincrement,
+ userid integer,
+ name varchar(45),
+ value varchar(45));'
+ );
+
+ // Create user definition with custom MD5 password mapper
+ $this->userDefinition = new UserDefinition('users', UserModel::class, UserDefinition::LOGIN_IS_USERNAME);
+ $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_PASSWORD, PasswordMd5Mapper::class);
+ $this->userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED);
+
+ $this->propertyDefinition = new UserPropertiesDefinition();
+ }
+
+ #[\Override]
+ public function tearDown(): void
+ {
+ $uri = new Uri(self::CONNECTION_STRING);
+ if (file_exists($uri->getPath())) {
+ unlink($uri->getPath());
+ }
+ $this->db = null;
+ $this->userDefinition = null;
+ $this->propertyDefinition = null;
+ }
+
+ public function testPasswordIsHashedWithMd5OnSave(): void
+ {
+ $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition);
+
+ // Add a user with a plain text password
+ $plainPassword = 'mySecretPassword123';
+ $user = $dataset->addUser('John Doe', 'johndoe', 'john@example.com', $plainPassword);
+
+ // Verify the password was hashed with MD5
+ $expectedHash = md5($plainPassword);
+ $this->assertEquals($expectedHash, $user->getPassword());
+ $this->assertEquals(32, strlen($user->getPassword())); // MD5 is always 32 chars
+ $this->assertTrue(ctype_xdigit($user->getPassword())); // MD5 is hexadecimal
+ }
+
+ public function testPasswordIsNotRehashedIfAlreadyMd5(): void
+ {
+ $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition);
+
+ // Create a user
+ $user = $dataset->addUser('Jane Doe', 'janedoe', 'jane@example.com', 'password123');
+ $originalHash = $user->getPassword();
+
+ // Update the user without changing password
+ $user->setName('Jane Smith');
+ $updatedUser = $dataset->save($user);
+
+ // Password hash should remain the same
+ $this->assertEquals($originalHash, $updatedUser->getPassword());
+ }
+
+ public function testPasswordIsHashedWhenUpdating(): void
+ {
+ $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition);
+
+ // Create a user
+ $user = $dataset->addUser('Jane Doe', 'janedoe', 'jane@example.com', 'oldPassword');
+ $oldHash = $user->getPassword();
+
+ // Update the password with a new plain text password
+ $newPlainPassword = 'newPassword123';
+ $user->setPassword($newPlainPassword);
+ $updatedUser = $dataset->save($user);
+
+ // Verify the new password was hashed with MD5
+ $expectedNewHash = md5($newPlainPassword);
+ $this->assertEquals($expectedNewHash, $updatedUser->getPassword());
+
+ // Verify it's different from the old hash
+ $this->assertNotEquals($oldHash, $updatedUser->getPassword());
+
+ // Verify user can login with new password
+ $authenticatedUser = $dataset->isValidUser('janedoe', $newPlainPassword);
+ $this->assertNotNull($authenticatedUser);
+
+ // Verify user cannot login with old password
+ $authenticatedUserOld = $dataset->isValidUser('janedoe', 'oldPassword');
+ $this->assertNull($authenticatedUserOld);
+ }
+
+ public function testPasswordRemainsUnchangedWhenUpdatingOtherFields(): void
+ {
+ $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition);
+
+ // Create a user
+ $originalPassword = 'myPassword123';
+ $user = $dataset->addUser('John Smith', 'johnsmith', 'john@example.com', $originalPassword);
+ $originalHash = $user->getPassword();
+
+ // Update other fields WITHOUT touching the password
+ $user->setName('John Updated');
+ $user->setEmail('johnupdated@example.com');
+ $user->setAdmin('y');
+ $updatedUser = $dataset->save($user);
+
+ // Verify the password hash remained exactly the same
+ $this->assertEquals($originalHash, $updatedUser->getPassword());
+ $this->assertEquals(32, strlen($updatedUser->getPassword()));
+
+ // Verify other fields were updated
+ $this->assertEquals('John Updated', $updatedUser->getName());
+ $this->assertEquals('johnupdated@example.com', $updatedUser->getEmail());
+ $this->assertEquals('y', $updatedUser->getAdmin());
+
+ // Verify user can still login with original password
+ $authenticatedUser = $dataset->isValidUser('johnsmith', $originalPassword);
+ $this->assertNotNull($authenticatedUser);
+ $this->assertEquals('John Updated', $authenticatedUser->getName());
+ }
+
+ public function testUserCanLoginWithMd5HashedPassword(): void
+ {
+ $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition);
+
+ // Add a user
+ $plainPassword = 'testPassword456';
+ $dataset->addUser('Test User', 'testuser', 'test@example.com', $plainPassword);
+
+ // Verify user can login with the plain text password
+ $authenticatedUser = $dataset->isValidUser('testuser', $plainPassword);
+
+ $this->assertNotNull($authenticatedUser);
+ $this->assertEquals('Test User', $authenticatedUser->getName());
+ $this->assertEquals('testuser', $authenticatedUser->getUsername());
+ }
+
+ public function testUserCannotLoginWithWrongPassword(): void
+ {
+ $dataset = new UsersDBDataset($this->db, $this->userDefinition, $this->propertyDefinition);
+
+ // Add a user
+ $dataset->addUser('Test User', 'testuser', 'test@example.com', 'correctPassword');
+
+ // Try to login with wrong password
+ $authenticatedUser = $dataset->isValidUser('testuser', 'wrongPassword');
+
+ $this->assertNull($authenticatedUser);
+ }
+
+ public function testEmptyPasswordReturnsNull(): void
+ {
+ $mapper = new PasswordMd5Mapper();
+
+ $this->assertNull($mapper->processedValue('', null));
+ $this->assertNull($mapper->processedValue(null, null));
+ }
+
+ public function testExistingMd5HashIsNotRehashed(): void
+ {
+ $mapper = new PasswordMd5Mapper();
+ $existingHash = '5f4dcc3b5aa765d61d8327deb882cf99'; // MD5 of 'password'
+
+ $result = $mapper->processedValue($existingHash, null);
+
+ $this->assertEquals($existingHash, $result);
+ }
+}
diff --git a/tests/SessionContextTest.php b/tests/SessionContextTest.php
index 33b1067..f09f672 100644
--- a/tests/SessionContextTest.php
+++ b/tests/SessionContextTest.php
@@ -14,17 +14,19 @@ class SessionContextTest extends TestCase
*/
protected $object;
+ #[\Override]
public function setUp(): void
{
$this->object = new SessionContext(Factory::createSessionPool());
}
+ #[\Override]
public function tearDown(): void
{
$this->object = null;
}
- public function testUserContext()
+ public function testUserContext(): void
{
$this->assertFalse($this->object->isAuthenticated());
@@ -44,13 +46,13 @@ public function testUserContext()
$this->assertFalse($this->object->isAuthenticated());
}
- public function testUserContextNotActiveSession()
+ public function testUserContextNotActiveSession(): void
{
$this->expectException(\ByJG\Authenticate\Exception\NotAuthenticatedException::class);
$this->assertEmpty($this->object->getSessionData('property1'));
}
- public function testUserContextNotActive2Session()
+ public function testUserContextNotActive2Session(): void
{
$this->expectException(\ByJG\Authenticate\Exception\NotAuthenticatedException::class);
$this->object->setSessionData('property1', 'value');
diff --git a/tests/UserModelTest.php b/tests/UserModelTest.php
index a63b081..c5b0be9 100644
--- a/tests/UserModelTest.php
+++ b/tests/UserModelTest.php
@@ -15,17 +15,19 @@ class UserModelTest extends TestCase
*/
protected $object;
+ #[\Override]
public function setUp(): void
{
$this->object = new UserModel();
}
+ #[\Override]
public function tearDown(): void
{
$this->object = null;
}
- public function testUserModel()
+ public function testUserModel(): void
{
$this->object->setUserid("10");
$this->object->setName('John');
@@ -40,7 +42,7 @@ public function testUserModel()
$this->assertEquals('johnuser', $this->object->getUsername());
}
- public function testUserModelProperties()
+ public function testUserModelProperties(): void
{
$this->object->setUserid("10");
$this->object->setName('John');
@@ -64,7 +66,7 @@ public function testUserModelProperties()
], $this->object->getProperties());
}
- public function testPasswordDefinition()
+ public function testPasswordDefinition(): void
{
$this->object->withPasswordDefinition(new PasswordDefinition([
PasswordDefinition::MINIMUM_CHARS => 12,
@@ -77,12 +79,18 @@ public function testPasswordDefinition()
PasswordDefinition::ALLOW_REPEATED => 0 // Allow repeated characters
]));
- $this->assertEmpty($this->object->setPassword(null));
- $this->assertEmpty($this->object->setPassword(''));
- $this->assertEmpty($this->object->setPassword('!Ab18Uk*H2oU9NQ'));
+ // These passwords should be accepted (null, empty, and valid password)
+ $this->object->setPassword(null);
+ $this->assertNull($this->object->getPassword());
+
+ $this->object->setPassword('');
+ $this->assertEmpty($this->object->getPassword());
+
+ $this->object->setPassword('!Ab18Uk*H2oU9NQ');
+ $this->assertEquals('!Ab18Uk*H2oU9NQ', $this->object->getPassword());
}
- public function testPasswordDefinitionError()
+ public function testPasswordDefinitionError(): void
{
$this->expectException(InvalidArgumentException::class);
diff --git a/tests/UsersAnyDataset2ByEmailTest.php b/tests/UsersAnyDataset2ByEmailTest.php
index 19ba913..2091e71 100644
--- a/tests/UsersAnyDataset2ByEmailTest.php
+++ b/tests/UsersAnyDataset2ByEmailTest.php
@@ -4,8 +4,9 @@
use ByJG\Authenticate\Definition\UserDefinition;
-class UsersAnyDataset2EmailTest extends UsersAnyDatasetByUsernameTest
+class UsersAnyDataset2ByEmailTest extends UsersAnyDatasetByUsernameTest
{
+ #[\Override]
public function setUp(): void
{
$this->__setUp(UserDefinition::LOGIN_IS_EMAIL);
diff --git a/tests/UsersAnyDataset2ByUsernameTest.php b/tests/UsersAnyDataset2ByUsernameTest.php
index 22d41e9..50eaa86 100644
--- a/tests/UsersAnyDataset2ByUsernameTest.php
+++ b/tests/UsersAnyDataset2ByUsernameTest.php
@@ -11,6 +11,7 @@
class UsersAnyDataset2ByUsernameTest extends UsersAnyDatasetByUsernameTest
{
+ #[\Override]
public function __setUp($loginField)
{
$this->prefix = "user";
@@ -50,6 +51,7 @@ public function __setUp($loginField)
$this->object->addUser('User 3', 'user3', 'user3@gmail.com', 'pwd3');
}
+ #[\Override]
public function setUp(): void
{
$this->__setUp(UserDefinition::LOGIN_IS_USERNAME);
diff --git a/tests/UsersAnyDatasetByEmailTest.php b/tests/UsersAnyDatasetByEmailTest.php
index 560ba99..b7ad3bf 100644
--- a/tests/UsersAnyDatasetByEmailTest.php
+++ b/tests/UsersAnyDatasetByEmailTest.php
@@ -6,6 +6,7 @@
class UsersAnyDatasetByEmailTest extends UsersAnyDatasetByUsernameTest
{
+ #[\Override]
public function setUp(): void
{
$this->__setUp(UserDefinition::LOGIN_IS_EMAIL);
diff --git a/tests/UsersAnyDatasetByUsernameTest.php b/tests/UsersAnyDatasetByUsernameTest.php
index c6b957c..e258900 100644
--- a/tests/UsersAnyDatasetByUsernameTest.php
+++ b/tests/UsersAnyDatasetByUsernameTest.php
@@ -52,7 +52,7 @@ public function __setUp($loginField)
$this->object->addUser('User 3', 'user3', 'user3@gmail.com', 'pwd3');
}
- public function __chooseValue($forUsername, $forEmail)
+ public function __chooseValue($forUsername, $forEmail): string
{
$searchForList = [
$this->userDefinition->getUsername() => $forUsername,
@@ -61,16 +61,23 @@ public function __chooseValue($forUsername, $forEmail)
return $searchForList[$this->userDefinition->loginField()];
}
+ #[\Override]
public function setUp(): void
{
$this->__setUp(UserDefinition::LOGIN_IS_USERNAME);
}
+ /**
+ * @return void
+ */
public function testAddUser()
{
$this->object->addUser('John Doe', 'john', 'johndoe@gmail.com', 'mypassword');
- $user = $this->object->getByLoginField($this->__chooseValue('john', 'johndoe@gmail.com'));
+ $user = $this->object->get(
+ $this->__chooseValue('john', 'johndoe@gmail.com'),
+ $this->object->getUserDefinition()->loginField()
+ );
$this->assertEquals('john', $user->getUserid());
$this->assertEquals('John Doe', $user->getName());
@@ -79,26 +86,26 @@ public function testAddUser()
$this->assertEquals('91dfd9ddb4198affc5c194cd8ce6d338fde470e2', $user->getPassword());
}
- public function testAddUserError()
+ public function testAddUserError(): void
{
$this->expectException(UserExistsException::class);
$this->object->addUser('some user with same username', 'user2', 'user2@gmail.com', 'mypassword');
}
- public function testAddProperty()
+ public function testAddProperty(): void
{
// Check state
- $user = $this->object->getById($this->prefix . '2');
+ $user = $this->object->get($this->prefix . '2');
$this->assertEmpty($user->get('city'));
// Add one property
$this->object->addProperty($this->prefix . '2', 'city', 'Rio de Janeiro');
- $user = $this->object->getById($this->prefix . '2');
+ $user = $this->object->get($this->prefix . '2');
$this->assertEquals('Rio de Janeiro', $user->get('city'));
// Add another property (cannot change)
$this->object->addProperty($this->prefix . '2', 'city', 'Belo Horizonte');
- $user = $this->object->getById($this->prefix . '2');
+ $user = $this->object->get($this->prefix . '2');
$this->assertEquals(['Rio de Janeiro', 'Belo Horizonte'], $user->get('city'));
// Get Property
@@ -106,12 +113,12 @@ public function testAddProperty()
// Add another property
$this->object->addProperty($this->prefix . '2', 'state', 'RJ');
- $user = $this->object->getById($this->prefix . '2');
+ $user = $this->object->get($this->prefix . '2');
$this->assertEquals('RJ', $user->get('state'));
// Remove Property
$this->object->removeProperty($this->prefix . '2', 'state', 'RJ');
- $user = $this->object->getById($this->prefix . '2');
+ $user = $this->object->get($this->prefix . '2');
$this->assertEmpty($user->get('state'));
// Remove Property Again
@@ -120,63 +127,63 @@ public function testAddProperty()
}
- public function testRemoveAllProperties()
+ public function testRemoveAllProperties(): void
{
// Add the properties
$this->object->addProperty($this->prefix . '2', 'city', 'Rio de Janeiro');
$this->object->addProperty($this->prefix . '2', 'city', 'Niteroi');
$this->object->addProperty($this->prefix . '2', 'state', 'RJ');
- $user = $this->object->getById($this->prefix . '2');
+ $user = $this->object->get($this->prefix . '2');
$this->assertEquals(['Rio de Janeiro', 'Niteroi'], $user->get('city'));
$this->assertEquals('RJ', $user->get('state'));
// Add another properties
$this->object->addProperty($this->prefix . '1', 'city', 'Niteroi');
$this->object->addProperty($this->prefix . '1', 'state', 'BA');
- $user = $this->object->getById($this->prefix . '1');
+ $user = $this->object->get($this->prefix . '1');
$this->assertEquals('Niteroi', $user->get('city'));
$this->assertEquals('BA', $user->get('state'));
// Remove Properties
$this->object->removeAllProperties('state');
- $user = $this->object->getById($this->prefix . '2');
+ $user = $this->object->get($this->prefix . '2');
$this->assertEquals(['Rio de Janeiro', 'Niteroi'], $user->get('city'));
$this->assertEmpty($user->get('state'));
- $user = $this->object->getById($this->prefix . '1');
+ $user = $this->object->get($this->prefix . '1');
$this->assertEquals('Niteroi', $user->get('city'));
$this->assertEmpty($user->get('state'));
// Remove Properties Again
$this->object->removeAllProperties('city', 'Niteroi');
- $user = $this->object->getById($this->prefix . '2');
+ $user = $this->object->get($this->prefix . '2');
$this->assertEquals('Rio de Janeiro', $user->get('city'));
$this->assertEmpty($user->get('state'));
- $user = $this->object->getById($this->prefix . '1');
+ $user = $this->object->get($this->prefix . '1');
$this->assertEmpty($user->get('city'));
$this->assertEmpty($user->get('state'));
}
- public function testRemoveByLoginField()
+ public function testRemoveByLoginField(): void
{
$login = $this->__chooseValue('user1', 'user1@gmail.com');
- $user = $this->object->getByLoginField($login);
+ $user = $this->object->get($login, $this->object->getUserDefinition()->loginField());
$this->assertNotNull($user);
$result = $this->object->removeByLoginField($login);
$this->assertTrue($result);
- $user = $this->object->getByLoginField($login);
+ $user = $this->object->get($login, $this->object->getUserDefinition()->loginField());
$this->assertNull($user);
}
- public function testEditUser()
+ public function testEditUser(): void
{
$login = $this->__chooseValue('user1', 'user1@gmail.com');
// Getting data
- $user = $this->object->getByLoginField($login);
+ $user = $this->object->get($login, $this->object->getUserDefinition()->loginField());
$this->assertEquals('User 1', $user->getName());
// Change and Persist data
@@ -184,11 +191,11 @@ public function testEditUser()
$this->object->save($user);
// Check if data persists
- $user = $this->object->getById($this->prefix . '1');
+ $user = $this->object->get($this->prefix . '1');
$this->assertEquals('Other name', $user->getName());
}
- public function testIsValidUser()
+ public function testIsValidUser(): void
{
$login = $this->__chooseValue('user3', 'user3@gmail.com');
$loginFalse = $this->__chooseValue('user3@gmail.com', 'user3');
@@ -202,22 +209,24 @@ public function testIsValidUser()
$this->assertNull($user);
}
- public function testIsAdmin()
+ public function testIsAdmin(): void
{
// Check is Admin
- $this->assertFalse($this->object->isAdmin($this->prefix . '3'));
+ $user3 = $this->object->get($this->prefix . '3');
+ $this->assertFalse($user3->isAdmin());
// Set the Admin Flag
$login = $this->__chooseValue('user3', 'user3@gmail.com');
- $user = $this->object->getByLoginField($login);
+ $user = $this->object->get($login, $this->object->getUserDefinition()->loginField());
$user->setAdmin('Y');
$this->object->save($user);
// Check is Admin
- $this->assertTrue($this->object->isAdmin($this->prefix . '3'));
+ $user3 = $this->object->get($this->prefix . '3');
+ $this->assertTrue($user3->isAdmin());
}
- protected function expectedToken($tokenData, $login, $userId)
+ protected function expectedToken($tokenData, $login, $userId): void
{
$loginCreated = $this->__chooseValue('user2', 'user2@gmail.com');
@@ -232,7 +241,7 @@ protected function expectedToken($tokenData, $login, $userId)
['tokenData'=>$tokenData]
);
- $user = $this->object->getByLoginField($login);
+ $user = $this->object->get($login, $this->object->getUserDefinition()->loginField());
$dataFromToken = new \stdClass();
$dataFromToken->tokenData = $tokenData;
@@ -248,6 +257,9 @@ protected function expectedToken($tokenData, $login, $userId)
);
}
+ /**
+ * @return void
+ */
public function testCreateAuthToken()
{
$login = $this->__chooseValue('user2', 'user2@gmail.com');
@@ -255,7 +267,7 @@ public function testCreateAuthToken()
$this->expectedToken('tokenValue', $login, 'user2');
}
- public function testValidateTokenWithAnotherUser()
+ public function testValidateTokenWithAnotherUser(): void
{
$this->expectException(NotAuthenticatedException::class);
$login = $this->__chooseValue('user2', 'user2@gmail.com');
@@ -274,30 +286,33 @@ public function testValidateTokenWithAnotherUser()
$this->object->isValidToken($loginToFail, $jwtWrapper, $token);
}
+ /**
+ * @return void
+ */
public function testSaveAndSave()
{
- $user = $this->object->getById('user1');
+ $user = $this->object->get('user1');
$this->object->save($user);
- $user2 = $this->object->getById('user1');
+ $user2 = $this->object->get('user1');
$this->assertEquals($user, $user2);
}
- public function testRemoveUserById()
+ public function testRemoveUserById(): void
{
- $user = $this->object->getById($this->prefix . '1');
+ $user = $this->object->get($this->prefix . '1');
$this->assertNotNull($user);
$this->object->removeUserById($this->prefix . '1');
- $user2 = $this->object->getById($this->prefix . '1');
+ $user2 = $this->object->get($this->prefix . '1');
$this->assertNull($user2);
}
- public function testGetByUsername()
+ public function testGetByUsername(): void
{
- $user = $this->object->getByUsername('user2');
+ $user = $this->object->get('user2', $this->object->getUserDefinition()->getUsername());
$this->assertEquals($this->prefix . '2', $user->getUserid());
$this->assertEquals('User 2', $user->getName());
@@ -306,15 +321,15 @@ public function testGetByUsername()
$this->assertEquals('c88b5c841897dafe75cdd9f8ba98b32f007d6bc3', $user->getPassword());
}
- public function testGetByUserProperty()
+ public function testGetByUserProperty(): void
{
// Add property to user1
- $user = $this->object->getById($this->prefix . '1');
+ $user = $this->object->get($this->prefix . '1');
$user->set('property1', 'somevalue');
$this->object->save($user);
// Add property to user2
- $user = $this->object->getById($this->prefix . '2');
+ $user = $this->object->get($this->prefix . '2');
$user->set('property1', 'value1');
$user->set('property2', 'value2');
$this->object->save($user);
@@ -339,7 +354,7 @@ public function testGetByUserProperty()
}
- public function testSetProperty()
+ public function testSetProperty(): void
{
$this->assertFalse($this->object->hasProperty($this->prefix . '1', 'propertySet'));
$this->object->setProperty($this->prefix . '1', 'propertySet', 'somevalue');
diff --git a/tests/UsersDBDataset2ByEmailTest.php b/tests/UsersDBDataset2ByEmailTest.php
index 694f024..8a33475 100644
--- a/tests/UsersDBDataset2ByEmailTest.php
+++ b/tests/UsersDBDataset2ByEmailTest.php
@@ -6,6 +6,7 @@
class UsersDBDataset2ByEmailTest extends UsersDBDatasetByUsernameTest
{
+ #[\Override]
public function setUp(): void
{
$this->__setUp(UserDefinition::LOGIN_IS_EMAIL);
diff --git a/tests/UsersDBDataset2ByUserNameTest.php b/tests/UsersDBDataset2ByUserNameTest.php
index 382179c..a13cc78 100644
--- a/tests/UsersDBDataset2ByUserNameTest.php
+++ b/tests/UsersDBDataset2ByUserNameTest.php
@@ -17,6 +17,7 @@ class UsersDBDataset2ByUserNameTest extends UsersDBDatasetByUsernameTest
* @throws \ReflectionException
* @throws OrmModelInvalidException
*/
+ #[\Override]
public function __setUp($loginField)
{
$this->prefix = "";
@@ -68,6 +69,7 @@ public function __setUp($loginField)
$this->object->addUser('User 3', 'user3', 'user3@gmail.com', 'pwd3');
}
+ #[\Override]
public function setUp(): void
{
$this->__setUp(UserDefinition::LOGIN_IS_USERNAME);
diff --git a/tests/UsersDBDatasetByEmailTest.php b/tests/UsersDBDatasetByEmailTest.php
index edaa39b..e37efe5 100644
--- a/tests/UsersDBDatasetByEmailTest.php
+++ b/tests/UsersDBDatasetByEmailTest.php
@@ -6,6 +6,7 @@
class UsersDBDatasetByEmailTest extends UsersAnyDatasetByUsernameTest
{
+ #[\Override]
public function setUp(): void
{
$this->__setUp(UserDefinition::LOGIN_IS_EMAIL);
diff --git a/tests/UsersDBDatasetByUsernameTest.php b/tests/UsersDBDatasetByUsernameTest.php
index 52df602..4430db8 100644
--- a/tests/UsersDBDatasetByUsernameTest.php
+++ b/tests/UsersDBDatasetByUsernameTest.php
@@ -5,6 +5,7 @@
use ByJG\AnyDataset\Db\Factory;
use ByJG\Authenticate\Definition\UserDefinition;
use ByJG\Authenticate\Definition\UserPropertiesDefinition;
+use ByJG\Authenticate\MapperFunctions\ClosureMapper;
use ByJG\Authenticate\Model\UserModel;
use ByJG\Authenticate\UsersDBDataset;
use ByJG\Util\Uri;
@@ -16,6 +17,7 @@ class UsersDBDatasetByUsernameTest extends UsersAnyDatasetByUsernameTest
protected $db;
+ #[\Override]
public function __setUp($loginField)
{
$this->prefix = "";
@@ -59,11 +61,13 @@ public function __setUp($loginField)
$this->object->addUser('User 3', 'user3', 'user3@gmail.com', 'pwd3');
}
+ #[\Override]
public function setUp(): void
{
$this->__setUp(UserDefinition::LOGIN_IS_USERNAME);
}
+ #[\Override]
public function tearDown(): void
{
$uri = new Uri(self::CONNECTION_STRING);
@@ -73,13 +77,17 @@ public function tearDown(): void
$this->propertyDefinition = null;
}
+ /**
+ * @return void
+ */
+ #[\Override]
public function testAddUser()
{
$this->object->addUser('John Doe', 'john', 'johndoe@gmail.com', 'mypassword');
$login = $this->__chooseValue('john', 'johndoe@gmail.com');
- $user = $this->object->getByLoginField($login);
+ $user = $this->object->get($login, $this->object->getUserDefinition()->loginField());
$this->assertEquals('4', $user->getUserid());
$this->assertEquals('John Doe', $user->getName());
$this->assertEquals('john', $user->getUsername());
@@ -92,46 +100,53 @@ public function testAddUser()
$user->setAdmin('y');
$this->object->save($user);
- $user2 = $this->object->getByLoginField($login);
+ $user2 = $this->object->get($login, $this->object->getUserDefinition()->loginField());
$this->assertEquals('y', $user2->getAdmin());
}
+ /**
+ * @return void
+ */
+ #[\Override]
public function testCreateAuthToken()
{
$login = $this->__chooseValue('user2', 'user2@gmail.com');
$this->expectedToken('tokenValue', $login, 2);
}
+ /**
+ * @return void
+ */
public function testWithUpdateValue()
{
// For Update Definitions
- $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_NAME, function ($value, $instance) {
+ $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_NAME, new ClosureMapper(function ($value, $instance) {
return '[' . $value . ']';
- });
- $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_USERNAME, function ($value, $instance) {
+ }));
+ $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_USERNAME, new ClosureMapper(function ($value, $instance) {
return ']' . $value . '[';
- });
- $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_EMAIL, function ($value, $instance) {
+ }));
+ $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_EMAIL, new ClosureMapper(function ($value, $instance) {
return '-' . $value . '-';
- });
- $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_PASSWORD, function ($value, $instance) {
+ }));
+ $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_PASSWORD, new ClosureMapper(function ($value, $instance) {
return "@" . $value . "@";
- });
+ }));
$this->userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED);
// For Select Definitions
- $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_NAME, function ($value, $instance) {
+ $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_NAME, new ClosureMapper(function ($value, $instance) {
return '(' . $value . ')';
- });
- $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_USERNAME, function ($value, $instance) {
+ }));
+ $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_USERNAME, new ClosureMapper(function ($value, $instance) {
return ')' . $value . '(';
- });
- $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_EMAIL, function ($value, $instance) {
+ }));
+ $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_EMAIL, new ClosureMapper(function ($value, $instance) {
return '#' . $value . '#';
- });
- $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_PASSWORD, function ($value, $instance) {
+ }));
+ $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_PASSWORD, new ClosureMapper(function ($value, $instance) {
return '%'. $value . '%';
- });
+ }));
// Test it!
$newObject = new UsersDBDataset(
@@ -142,9 +157,9 @@ public function testWithUpdateValue()
$newObject->addUser('User 4', 'user4', 'user4@gmail.com', 'pwd4');
- $login = $this->__chooseValue(']user4[', '-user4@gmail.com-');
+ $login = $this->__chooseValue('user4', 'user4@gmail.com');
- $user = $newObject->getByLoginField($login);
+ $user = $newObject->get($login, $newObject->getUserDefinition()->loginField());
$this->assertEquals('4', $user->getUserid());
$this->assertEquals('([User 4])', $user->getName());
$this->assertEquals(')]user4[(', $user->getUsername());
@@ -153,12 +168,16 @@ public function testWithUpdateValue()
$this->assertEquals('2017-12-04 00:00:00', $user->getCreated());
}
+ /**
+ * @return void
+ */
+ #[\Override]
public function testSaveAndSave()
{
- $user = $this->object->getById("1");
+ $user = $this->object->get("1");
$this->object->save($user);
- $user2 = $this->object->getById("1");
+ $user2 = $this->object->get("1");
$this->assertEquals($user, $user2);
}
diff --git a/tests/UsersDBDatasetDefinitionTest.php b/tests/UsersDBDatasetDefinitionTest.php
index a568842..c7c0472 100644
--- a/tests/UsersDBDatasetDefinitionTest.php
+++ b/tests/UsersDBDatasetDefinitionTest.php
@@ -7,34 +7,17 @@
use ByJG\Authenticate\Definition\UserDefinition;
use ByJG\Authenticate\Definition\UserPropertiesDefinition;
use ByJG\Authenticate\Exception\UserExistsException;
+use ByJG\Authenticate\MapperFunctions\ClosureMapper;
use ByJG\Authenticate\Model\UserModel;
use ByJG\Authenticate\UsersDBDataset;
use ByJG\MicroOrm\Exception\OrmBeforeInvalidException;
use ByJG\MicroOrm\Exception\OrmInvalidFieldsException;
use ByJG\MicroOrm\Exception\OrmModelInvalidException;
use Exception;
+use Override;
use ReflectionException;
-
-class MyUserModel extends UserModel
-{
- protected $otherfield;
-
- public function __construct($name = "", $email = "", $username = "", $password = "", $admin = "no", $field = "")
- {
- parent::__construct($name, $email, $username, $password, $admin);
- $this->setOtherfield($field);
- }
-
- public function getOtherfield()
- {
- return $this->otherfield;
- }
-
- public function setOtherfield($otherfield)
- {
- $this->otherfield = $otherfield;
- }
-}
+use Tests\Fixture\MyUserModel;
+use Tests\Fixture\TestUniqueIdGenerator;
class UsersDBDatasetDefinitionTest extends UsersDBDatasetByUsernameTest
{
@@ -48,6 +31,7 @@ class UsersDBDatasetDefinitionTest extends UsersDBDatasetByUsernameTest
* @throws UserExistsException
* @throws ReflectionException
*/
+ #[Override]
public function __setUp($loginField)
{
$this->prefix = "";
@@ -113,6 +97,7 @@ public function __setUp($loginField)
* @throws ReflectionException
* @throws UserExistsException
*/
+ #[Override]
public function setUp(): void
{
$this->__setUp(UserDefinition::LOGIN_IS_USERNAME);
@@ -122,14 +107,17 @@ public function setUp(): void
* @throws UserExistsException
* @throws DatabaseException
* @throws \ByJG\Serializer\Exception\InvalidArgumentException
+ *
+ * @return void
*/
+ #[Override]
public function testAddUser()
{
$this->object->save(new MyUserModel('John Doe', 'johndoe@gmail.com', 'john', 'mypassword', 'no', 'other john'));
$login = $this->__chooseValue('john', 'johndoe@gmail.com');
- $user = $this->object->getByLoginField($login);
+ $user = $this->object->get($login, $this->object->getUserDefinition()->loginField());
$this->assertEquals('4', $user->getUserid());
$this->assertEquals('John Doe', $user->getName());
$this->assertEquals('john', $user->getUsername());
@@ -144,49 +132,52 @@ public function testAddUser()
$user->setAdmin('y');
$this->object->save($user);
- $user2 = $this->object->getByLoginField($login);
+ $user2 = $this->object->get($login, $this->object->getUserDefinition()->loginField());
$this->assertEquals('y', $user2->getAdmin());
}
/**
* @throws Exception
+ *
+ * @return void
*/
+ #[Override]
public function testWithUpdateValue()
{
// For Update Definitions
- $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_NAME, function ($value, $instance) {
+ $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_NAME, new ClosureMapper(function ($value, $instance) {
return '[' . $value . ']';
- });
- $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_USERNAME, function ($value, $instance) {
+ }));
+ $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_USERNAME, new ClosureMapper(function ($value, $instance) {
return ']' . $value . '[';
- });
- $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_EMAIL, function ($value, $instance) {
+ }));
+ $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_EMAIL, new ClosureMapper(function ($value, $instance) {
return '-' . $value . '-';
- });
- $this->userDefinition->defineClosureForUpdate(UserDefinition::FIELD_PASSWORD, function ($value, $instance) {
+ }));
+ $this->userDefinition->defineMapperForUpdate(UserDefinition::FIELD_PASSWORD, new ClosureMapper(function ($value, $instance) {
return "@" . $value . "@";
- });
+ }));
$this->userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED);
- $this->userDefinition->defineClosureForUpdate('otherfield', function ($value, $instance) {
+ $this->userDefinition->defineMapperForUpdate('otherfield', new ClosureMapper(function ($value, $instance) {
return "*" . $value . "*";
- });
+ }));
// For Select Definitions
- $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_NAME, function ($value, $instance) {
+ $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_NAME, new ClosureMapper(function ($value, $instance) {
return '(' . $value . ')';
- });
- $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_USERNAME, function ($value, $instance) {
+ }));
+ $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_USERNAME, new ClosureMapper(function ($value, $instance) {
return ')' . $value . '(';
- });
- $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_EMAIL, function ($value, $instance) {
+ }));
+ $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_EMAIL, new ClosureMapper(function ($value, $instance) {
return '#' . $value . '#';
- });
- $this->userDefinition->defineClosureForSelect(UserDefinition::FIELD_PASSWORD, function ($value, $instance) {
+ }));
+ $this->userDefinition->defineMapperForSelect(UserDefinition::FIELD_PASSWORD, new ClosureMapper(function ($value, $instance) {
return '%' . $value . '%';
- });
- $this->userDefinition->defineClosureForSelect('otherfield', function ($value, $instance) {
+ }));
+ $this->userDefinition->defineMapperForSelect('otherfield', new ClosureMapper(function ($value, $instance) {
return ']' . $value . '[';
- });
+ }));
// Test it!
$newObject = new UsersDBDataset(
@@ -199,9 +190,9 @@ public function testWithUpdateValue()
new MyUserModel('User 4', 'user4@gmail.com', 'user4', 'pwd4', 'no', 'other john')
);
- $login = $this->__chooseValue(']user4[', '-user4@gmail.com-');
+ $login = $this->__chooseValue('user4', 'user4@gmail.com');
- $user = $newObject->getByLoginField($login);
+ $user = $newObject->get($login, $newObject->getUserDefinition()->loginField());
$this->assertEquals('4', $user->getUserid());
$this->assertEquals('([User 4])', $user->getName());
$this->assertEquals(')]user4[(', $user->getUsername());
@@ -211,4 +202,85 @@ public function testWithUpdateValue()
$this->assertEquals(']*other john*[', $user->getOtherfield());
$this->assertEquals('2017-12-04 00:00:00', $user->getCreated());
}
+
+ /**
+ * @throws Exception
+ */
+ public function testDefineGenerateKeyWithInterface(): void
+ {
+ // Create a separate table with varchar userid for testing custom generators
+ $this->db->execute('create table users_custom (
+ userid varchar(50) primary key,
+ name varchar(45),
+ email varchar(200),
+ username varchar(20),
+ password varchar(40),
+ created datetime default (datetime(\'2017-12-04\')),
+ admin char(1));'
+ );
+
+ // Create a new user definition with custom generator
+ $userDefinition = new UserDefinition('users_custom', UserModel::class, UserDefinition::LOGIN_IS_USERNAME);
+ $generator = new TestUniqueIdGenerator('CUSTOM-');
+ $userDefinition->defineGenerateKey($generator);
+
+ // Create dataset with custom definition
+ $dataset = new UsersDBDataset($this->db, $userDefinition, $this->propertyDefinition);
+
+ // Add a user - the custom generator should be used
+ $user = $dataset->addUser('Test User', 'testuser', 'test@example.com', 'password123');
+
+ // Verify the user ID was generated with the custom prefix
+ $this->assertStringStartsWith('CUSTOM-', (string)$user->getUserid());
+ $this->assertEquals('Test User', $user->getName());
+ $this->assertEquals('testuser', $user->getUsername());
+
+ // Cleanup
+ $this->db->execute('drop table users_custom');
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function testDefineGenerateKeyWithString(): void
+ {
+ // Create a separate table with varchar userid for testing custom generators
+ $this->db->execute('create table users_custom2 (
+ userid varchar(50) primary key,
+ name varchar(45),
+ email varchar(200),
+ username varchar(20),
+ password varchar(40),
+ created datetime default (datetime(\'2017-12-04\')),
+ admin char(1));'
+ );
+
+ // Create a new user definition with generator class string
+ $userDefinition = new UserDefinition('users_custom2', UserModel::class, UserDefinition::LOGIN_IS_USERNAME);
+ $userDefinition->defineGenerateKey(TestUniqueIdGenerator::class);
+
+ // Create dataset with custom definition
+ $dataset = new UsersDBDataset($this->db, $userDefinition, $this->propertyDefinition);
+
+ // Add a user - the custom generator should be instantiated and used
+ $user = $dataset->addUser('Test User 2', 'testuser2', 'test2@example.com', 'password123');
+
+ // Verify the user ID was generated with the default TEST- prefix
+ $this->assertStringStartsWith('TEST-', (string)$user->getUserid());
+ $this->assertEquals('Test User 2', $user->getName());
+
+ // Cleanup
+ $this->db->execute('drop table users_custom2');
+ }
+
+ public function testDefineGenerateKeyClosureThrowsException(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('defineGenerateKeyClosure is deprecated. Use defineGenerateKey with MapperFunctionsInterface instead.');
+
+ $userDefinition = new UserDefinition();
+ $userDefinition->defineGenerateKeyClosure(function ($executor, $instance) {
+ return 'test-id';
+ });
+ }
}