Skip to content

Commit

Permalink
save WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
PhilippGrashoff committed Aug 13, 2023
1 parent 672ec53 commit 28547fc
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 74 deletions.
34 changes: 25 additions & 9 deletions src/CryptIdTrait.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<?php declare(strict_types=1$currentDateTime);
<?php declare(strict_types=1);

namespace atkdatamodeltraits;

use Atk4\Data\Exception;
use Atk4\Data\Model;

/**
* @extends Model<Model>
* create cryptic IDs like X3gkd9S-df29D3j in a format if your choice. To do so, implement generateCryptId() function
* in each Model using this trait.
*/
Expand All @@ -13,12 +15,13 @@ trait CryptIdTrait

use UniqueFieldTrait;

//@codeCoverageIgnore Seems some Bug in Code Coverage, this line is marked as not covered
/** @var string */
protected string $cryptIdFieldName = 'crypt_id';

protected $cryptIdFieldName = 'crypt_id';

//removed I, l, O, 0 as they can be easily mixed up by humans
protected $possibleChars = [
/** @var array<string>
* Chars I, l, O, 0 as they can be easily mixed up by humans
*/
protected array $possibleChars = [
'1',
'2',
'3',
Expand Down Expand Up @@ -81,6 +84,10 @@ trait CryptIdTrait

/**
* sets a cryptic Id to the fieldName passed. Only does something if the field is empty.
*
* @return void
* @throws Exception
* @throws \Atk4\Core\Exception
*/
public function setCryptId(): void
{
Expand All @@ -91,25 +98,34 @@ public function setCryptId(): void
$this->set($this->cryptIdFieldName, $this->generateCryptId());
}
} else {
$this->getField($this->cryptIdFieldName)->read_only = true;
$this->getField($this->cryptIdFieldName)->readOnly = true;
}
}

/**
* @return string
*/
public function getCryptId(): string
{
return $this->get($this->cryptIdFieldName);
}

/**
* Overwrite to your own needs in Models using this trait
* Extend this to your own needs in Models using this trait
*
* @return string
* @throws Exception
*/
protected function generateCryptId(): string
{
throw new Exception(__FUNCTION__ . ' must be extended in child model');
throw new Exception(__FUNCTION__ . ' must be extended in child Model');
}

/**
* returns a random char from possibleChars. This function is usually called by generateCryptId
*
* @return string
* @throws \Exception
*/
protected function getRandomChar(): string
{
Expand Down
38 changes: 20 additions & 18 deletions src/UniqueFieldTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,47 @@
namespace atkdatamodeltraits;

use Atk4\Data\Exception;
use Atk4\Data\Model;

/**
* @extends Model<Model>
*
* Use the function provided by this trait to check if no other record in the same table has the same value.
* Example: Your model has a field which must be unique. Before inserting into Database you want to make sure
* no DB error will occur due to duplicate value for the field which must be unique.
* $this->onHook(
* Model::HOOK_BEFORE_SAVE,
* function($model, $isUpdate) {
* function(self $entity, bool $isUpdate) {
* if($isUpdate) {
* return;
* }
* while(!$model->isFieldUnique('some_field_which_must_be_unique) {
* $model->recalculateSomeFieldWhichMustBeUnique(); //some function which recalculates field value.
* while(!$entity->isFieldUnique('some_field_which_must_be_unique) {
* //some function which recalculates field value.
* }
* }
* );
*/
trait UniqueFieldTrait
{

public function isFieldUnique(string $fieldName, $allowEmpty = false): bool
/**
* @param string $fieldName
* @return bool
* @throws Exception
*/
public function isFieldUnique(string $fieldName,): bool
{
if (
empty($this->get($fieldName))
&& !$allowEmpty
) {
if (empty($this->get($fieldName))) {
throw new Exception(
'The value for a unique field may not be empty. Field name: ' . $fieldName . ' in ' . __FUNCTION__
);
}
$other = new static($this->persistence);
//only load field to save performance
$other->setOnlyFields([$this->id_field, $fieldName]);
$other->addCondition($this->id_field, '!=', $this->get($this->id_field));
try {
$other->loadBy($fieldName, $this->get($fieldName));
return false;
} catch (Exception $e) {
return true;
}
$checkModel = new static($this->getPersistence());
//only load ID field to save performance
$checkModel->setOnlyFields([$this->idField, $fieldName]);
$checkModel->addCondition($this->idField, '!=', $this->get($this->idField));
$checkModel->addCondition($fieldName, '=', $this->get($fieldName));

return $checkModel->tryLoadAny() === null;
}
}
26 changes: 13 additions & 13 deletions tests/CryptIdTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
use Atk4\Data\Model;
use Atk4\Data\Persistence;
use atkdatamodeltraits\CryptIdTrait;
use atkdatamodeltraits\TestCase;
use atkdatamodeltraits\tests\testclasses\ModelWithCryptIdTrait;
use atkextendedtestcase\TestCase;


class CryptIdTraitTest extends TestCase
{

protected $sqlitePersistenceModels = [ModelWithCryptIdTrait::class];
protected array $sqlitePersistenceModels = [ModelWithCryptIdTrait::class];

public function testExceptionOverwriteGenerate()
public function testExceptionOverwriteGenerate(): void
{
$modelClass = new class() extends Model {
use CryptIdTrait;
Expand All @@ -24,26 +24,26 @@ public function testExceptionOverwriteGenerate()

};
$model = new $modelClass(new Persistence\Array_());
self::expectException(Exception::class);
self::expectExceptionMessage('generateCryptId must be extended in child Model');
$this->callProtected($model, 'generateCryptId');
}

public function testsetCryptId()
public function testsetCryptId(): void
{
$model = (new ModelWithCryptIdTrait($this->getSqliteTestPersistence()))->createEntity();
$model->setCryptId();
$entity = (new ModelWithCryptIdTrait($this->getSqliteTestPersistence()))->createEntity();
$entity->setCryptId();
self::assertSame(
12,
strlen($model->get('crypt_id'))
strlen($entity->get('crypt_id'))
);
}


public function testFieldSetToReadOnlyIfCryptIdNotEmpty()
public function testFieldSetToReadOnlyIfCryptIdNotEmpty(): void
{
$model = (new ModelWithCryptIdTrait($this->getSqliteTestPersistence()))->createEntity();
$model->save();
$model->setCryptId();
self::assertTrue($model->getField('crypt_id')->read_only);
$entity = (new ModelWithCryptIdTrait($this->getSqliteTestPersistence()))->createEntity();
$entity->setCryptId();
$entity->save();
self::assertTrue($entity->getField('crypt_id')->readOnly);
}
}
49 changes: 21 additions & 28 deletions tests/UniqueFieldTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,42 @@

namespace atkdatamodeltraits\tests;

use atkdatamodeltraits\TestCase;
use Atk4\Data\Exception;
use Atk4\Data\Persistence;
use atkdatamodeltraits\tests\testclasses\ModelWithUniqueFieldTrait;
use atkextendedtestcase\TestCase;


class UniqueFieldTraitTest extends TestCase
{

protected $sqlitePersistenceModels = [ModelWithUniqueFieldTrait::class];
protected array $sqlitePersistenceModels = [ModelWithUniqueFieldTrait::class];

public function testExceptionOnEmptyValue()
public function testExceptionOnEmptyValue(): void
{
$model = $this->getTestModel();
self::expectException(Exception::class);
$model->isFieldUnique('unique_field');
$entity = $this->getTestEntity();
self::expectExceptionMessage(
'The value for a unique field may not be empty. Field name: unique_field in isFieldUnique'
);
$entity->isFieldUnique('unique_field');
}

public function testNoExceptionIfAllowEmptyIsTrue()
{
$model = $this->getTestModel();
$model->isFieldUnique('unique_field', true);
self::expectException(Exception::class);
$model->isFieldUnique('unique_field');
}

public function testReturnFalseIfOtherRecordWithSameUniqueFieldValueExists()
public function testReturnFalseIfOtherRecordWithSameUniqueFieldValueExists(): void
{
$persistence = $this->getSqliteTestPersistence();
$model = $this->getTestModel($persistence);
$model->set('unique_field', 'ABC');
$model->save();
self::assertTrue($model->isFieldUnique('unique_field'));

$model2 = $this->getTestModel($persistence);
$model2->save();
$model2->set('unique_field', 'DEF');
self::assertTrue($model2->isFieldUnique('unique_field'));
$model2->set('unique_field', 'ABC');
self::assertFalse($model2->isFieldUnique('unique_field'));
$entity = $this->getTestEntity($persistence);
$entity->set('unique_field', 'ABC');
$entity->save();
self::assertTrue($entity->isFieldUnique('unique_field'));

$entity2 = $this->getTestEntity($persistence);
$entity2->save();
$entity2->set('unique_field', 'DEF');
self::assertTrue($entity2->isFieldUnique('unique_field'));
$entity2->set('unique_field', 'ABC');
self::assertFalse($entity2->isFieldUnique('unique_field'));
}

protected function getTestModel(Persistence $persistence = null): ModelWithUniqueFieldTrait
protected function getTestEntity(Persistence $persistence = null): ModelWithUniqueFieldTrait
{
return (new ModelWithUniqueFieldTrait($persistence ?: $this->getSqliteTestPersistence()))->createEntity();
}
Expand Down
17 changes: 11 additions & 6 deletions tests/testclasses/ModelWithCryptIdTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,25 @@ class ModelWithCryptIdTrait extends Model

public $table = 'ModelWithCryptIdTrait';

public $addition = '';

protected $createSameCryptId = false;


protected function init(): void
{
parent::init();
$this->addField('crypt_id');
$this->onHook(
Model::HOOK_BEFORE_SAVE,
function (self $model, $isUpdate) {
function (self $model, bool $isUpdate) {
$model->setCryptId();
}
);
}

protected function generateCryptId(): string
{
$cryptId = '';
for ($i = 0; $i < 10; $i++) {
$cryptId .= $this->getRandomChar();
}

return $cryptId;
}
}

0 comments on commit 28547fc

Please sign in to comment.