Skip to content

Commit

Permalink
add ActiveRecordModelBehavior
Browse files Browse the repository at this point in the history
  • Loading branch information
klimov-paul committed Jun 15, 2023
1 parent 7fbf128 commit 8fc21af
Show file tree
Hide file tree
Showing 3 changed files with 426 additions and 0 deletions.
198 changes: 198 additions & 0 deletions src/ActiveRecordModelBehavior.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<?php

namespace yii1tech\web\user;

use CBehavior;
use CEvent;

/**
* ActiveRecordModelBehavior allows operating AcriveRecord model at the WebUser component level.
*
* Application configuration example:
*
* ```php
* return [
* 'components' => [
* 'user' => [
* 'class' => yii1tech\web\user\WebUser::class,
* 'behaviors' => [
* 'modelBehavior' => [
* 'class' => yii1tech\web\user\ActiveRecordModelBehavior::class,
* 'modelClass' => app\models\User::class,
* ],
* ],
* ],
* // ...
* ],
* // ...
* ];
* ```
*
* Model access example:
*
* ```php
* $model = Yii::app()->user->getModel();
* ```
*
* @property \yii1tech\web\user\WebUser $owner
* @property \CActiveRecord|null $model
*
* @author Paul Klimov <[email protected]>
* @since 1.0
*/
class ActiveRecordModelBehavior extends CBehavior
{
/**
* @var string|\CActiveRecord name of the {@see \CActiveRecord} model class, which stores users.
*/
public $modelClass;

/**
* @var array|string|\CDbCriteria user model search query additional condition or criteria.
* For example:
*
* ```php
* [
* 'scopes' => [
* 'activeOnly',
* ],
* ]
* ```
*/
public $modelFindCriteria = '';

/**
* @var bool whether to automatically synchronize owner WebUser component with model.
* @see syncModel()
*/
public $autoSyncModel = true;

/**
* @var array<string, string> map defining which model attribute should be saved as WebUser state on model synchronization.
* For example:
*
* ```php
* [
* 'username' => '__name',
* 'email' => 'email',
* ]
* ```
*/
public $attributeToStateMap = [];

/**
* @var array<int, \CActiveRecord> stores related model instance.
*/
private $_model = [];

/**
* Returns model matching currently authenticated user.
*
* @return \CActiveRecord|null user model, `null` - if not found.
*/
public function getModel()
{
$userId = $this->owner->getId();

if (empty($userId)) {
return null;
}

if (!array_key_exists($userId, $this->_model)) {
$this->_model = [
$userId => $this->findModel($userId),
];
}

return $this->_model[$userId];
}

/**
* Sets the user model, changing authenticated user' ID at related WebUser component.
*
* > Note: this method can be used for user identity switching, however it is not equal
* to {@see \CWebUser::login()} or {@see \CWebUser::changeIdentity()}.
*
* @param \CActiveRecord|null $model user model.
* @return \yii1tech\web\user\WebUser|static owner WebUser component.
*/
public function setModel($model)
{
if (empty($model)) {
$this->_model = [];

$this->owner->setId(null);

return $this->owner;
}

$userId = $model->getPrimaryKey();

$this->_model = [
$userId => $model,
];

$this->owner->setId($userId);

return $this->syncModel();
}

/**
* Finds user model by the given ID.
*
* @param mixed $userId user's ID.
* @return \CActiveRecord|null user model, `null` - if not found.
*/
protected function findModel($userId)
{
return $this->modelClass::model($this->modelClass)
->findByPk($userId, $this->modelFindCriteria);
}

/**
* Synchronizes owner WebUser component with the model.
* If related model does not exist performs logout.
* If related model does exist - synchronizes WebUser states with it.
*
* @return \yii1tech\web\user\WebUser|static owner WebUser component.
*/
public function syncModel()
{
if ($this->owner->getIsGuest()) {
return $this->owner;
}

$model = $this->getModel();
if (empty($model)) {
$this->owner->logout(false);
} else {
foreach ($this->attributeToStateMap as $attribute => $state) {
$this->owner->setState($state, $model->{$attribute});
}
}

return $this->owner;
}

/**
* {@inheritdoc}
*/
public function events()
{
return [
'onAfterRestore' => 'afterRestore',
];
}

/**
* Responds to {@see \yii1tech\web\user\WebUser::$onAfterRestore} event.
*
* @param \CEvent $event event parameter.
*/
public function afterRestore(CEvent $event): void
{
if ($this->autoSyncModel) {
$this->syncModel();
}
}
}
195 changes: 195 additions & 0 deletions tests/ActiveRecordModelBehaviorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<?php

namespace yii1tech\web\user\test;

use Yii;
use yii1tech\web\user\ActiveRecordModelBehavior;
use yii1tech\web\user\test\support\User;
use yii1tech\web\user\WebUser;

class ActiveRecordModelBehaviorTest extends TestCase
{
/**
* {@inheritdoc}
*/
protected function setUp(): void
{
parent::setUp();

$this->createTestDbSchema();
$this->seedTestDb();
}

/**
* @return string test table name
*/
protected function getTestTableName(): string
{
return 'user';
}

/**
* Creates test config table.
*/
protected function createTestDbSchema(): void
{
$dbConnection = Yii::app()->db;
$columns = [
'id' => 'pk',
'username' => 'string',
'email' => 'string',
'status' => 'int',
];
$dbConnection->createCommand()->createTable($this->getTestTableName(), $columns);
}

/**
* Inserts test data to the database.
*/
protected function seedTestDb(): void
{
$dbConnection = Yii::app()->db;

$dbConnection->getCommandBuilder()->createMultipleInsertCommand($this->getTestTableName(), [
[
'username' => 'active-user',
'email' => '[email protected]',
'status' => User::STATUS_ACTIVE,
],
[
'username' => 'inactive-user',
'email' => '[email protected]',
'status' => User::STATUS_INACTIVE,
],
])->execute();
}

/**
* @param array $behaviorConfig
* @return \yii1tech\web\user\WebUser|\yii1tech\web\user\ActiveRecordModelBehavior
*/
protected function createWebUser(array $behaviorConfig = []): WebUser
{
$user = Yii::createComponent([
'class' => WebUser::class,
'stateKeyPrefix' => '',
'behaviors' => [
'modelBehavior' => array_merge([
'class' => ActiveRecordModelBehavior::class,
'modelClass' => User::class,
], $behaviorConfig),
],
]);

$user->init();

return $user;
}

public function testGetModel(): void
{
$webUser = $this->createWebUser();

$this->assertNull($webUser->getModel());

$webUser->setId(1);

$model = $webUser->getModel();

$this->assertTrue($model instanceof User);

$webUser->setId(99);

$this->assertNull($webUser->getModel());
}

/**
* @depends testGetModel
*/
public function testSetModel(): void
{
$webUser = $this->createWebUser();

$model = User::model()->findByPk(1);

$webUser->setModel($model);

$this->assertSame($model, $webUser->getModel());
$this->assertEquals($model->id, $webUser->getId());
}

/**
* @depends testGetModel
*/
public function testSyncModel(): void
{
Yii::app()->session->open();

$_SESSION['__id'] = 1;

$webUser = $this->createWebUser([
'autoSyncModel' => false,
'attributeToStateMap' => [
'username' => '__name',
'email' => 'email',
],
]);

$webUser->syncModel();

$this->assertNotEmpty($webUser->getModel());

$this->assertEquals('active-user', $webUser->getName());
$this->assertEquals('[email protected]', $webUser->getState('email'));

$_SESSION['__id'] = 99;

$webUser->syncModel();

$this->assertNull($webUser->getModel());
$this->assertNull($webUser->getId());
}

/**
* @depends testSyncModel
*/
public function testAutoSyncModel(): void
{
Yii::app()->session->open();

$_SESSION['__id'] = 99;

$webUser = $this->createWebUser([
'autoSyncModel' => true,
]);

$this->assertNull($webUser->getModel());
$this->assertNull($webUser->getId());
}

/**
* @depends testGetModel
*/
public function testModelFindCriteria(): void
{
$webUser = $this->createWebUser([
'autoSyncModel' => false,
'modelFindCriteria' => [
'condition' => 'status = :status',
'params' => [
'status' => User::STATUS_ACTIVE,
],
],
]);

$webUser->setId(1);
$model = $webUser->getModel();

$this->assertNotEmpty($model);

$webUser->setId(2);
$model = $webUser->getModel();

$this->assertEmpty($model);
}
}
Loading

0 comments on commit 8fc21af

Please sign in to comment.