Skip to content

Commit

Permalink
Create TransactionalTrait
Browse files Browse the repository at this point in the history
  • Loading branch information
Tigrov committed May 20, 2024
1 parent 537eca7 commit 9803ee8
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 136 deletions.
7 changes: 7 additions & 0 deletions src/AbstractActiveRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,13 @@ public function hasOne(string $class, array $link): ActiveQueryInterface
return $this->createRelationQuery($class, $link, false);
}

public function insert(array $attributes = null): bool

Check warning on line 291 in src/AbstractActiveRecord.php

View check run for this annotation

Codecov / codecov/patch

src/AbstractActiveRecord.php#L291

Added line #L291 was not covered by tests
{
return $this->insertInternal($attributes);

Check warning on line 293 in src/AbstractActiveRecord.php

View check run for this annotation

Codecov / codecov/patch

src/AbstractActiveRecord.php#L293

Added line #L293 was not covered by tests
}

abstract protected function insertInternal(array $attributes = null): bool;

/**
* @psalm-param class-string<ActiveRecordInterface> $arClass
*/
Expand Down
139 changes: 3 additions & 136 deletions src/ActiveRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Yiisoft\ActiveRecord\Trait\ArrayIteratorTrait;
use Yiisoft\ActiveRecord\Trait\MagicPropertiesTrait;
use Yiisoft\ActiveRecord\Trait\MagicRelationsTrait;
use Yiisoft\ActiveRecord\Trait\TransactionalTrait;
use Yiisoft\Arrays\ArrayableInterface;
use Yiisoft\Db\Exception\Exception;
use Yiisoft\Db\Exception\InvalidArgumentException;
Expand Down Expand Up @@ -92,63 +93,20 @@
* @template-implements ArrayAccess<string, mixed>
* @template-implements IteratorAggregate<string, mixed>
*/
class ActiveRecord extends AbstractActiveRecord implements ArrayableInterface, ArrayAccess, IteratorAggregate
class ActiveRecord extends AbstractActiveRecord implements ArrayableInterface, ArrayAccess, IteratorAggregate, TransactionalInterface
{
use ArrayableTrait;
use ArrayAccessTrait;
use ArrayIteratorTrait;
use MagicPropertiesTrait;
use MagicRelationsTrait;

/**
* The insert operation. This is mainly used when overriding {@see transactions()} to specify which operations are
* transactional.
*/
public const OP_INSERT = 0x01;

/**
* The update operation. This is mainly used when overriding {@see transactions()} to specify which operations are
* transactional.
*/
public const OP_UPDATE = 0x02;

/**
* The delete operation. This is mainly used when overriding {@see transactions()} to specify which operations are
* transactional.
*/
public const OP_DELETE = 0x04;

/**
* All three operations: insert, update, delete.
*
* This is a shortcut of the expression: OP_INSERT | OP_UPDATE | OP_DELETE.
*/
public const OP_ALL = 0x07;
use TransactionalTrait;

public function attributes(): array
{
return $this->getTableSchema()->getColumnNames();
}

public function delete(): int
{
if (!$this->isTransactional(self::OP_DELETE)) {
return $this->deleteInternal();
}

$transaction = $this->db->beginTransaction();

try {
$result = $this->deleteInternal();
$transaction->commit();

return $result;
} catch (Throwable $e) {
$transaction->rollBack();
throw $e;
}
}

public function filterCondition(array $condition, array $aliases = []): array
{
$result = [];
Expand Down Expand Up @@ -195,42 +153,6 @@ public function getTableSchema(): TableSchemaInterface
return $tableSchema;
}

public function insert(array $attributes = null): bool
{
if (!$this->isTransactional(self::OP_INSERT)) {
return $this->insertInternal($attributes);
}

$transaction = $this->db->beginTransaction();

try {
$result = $this->insertInternal($attributes);
if ($result === false) {
$transaction->rollBack();
} else {
$transaction->commit();
}

return $result;
} catch (Throwable $e) {
$transaction->rollBack();
throw $e;
}
}

/**
* Returns a value indicating whether the specified operation is transactional.
*
* @param int $operation The operation to check. Possible values are {@see OP_INSERT}, {@see OP_UPDATE} and
* {@see OP_DELETE}.
*
* @return array|bool Whether the specified operation is transactional.
*/
public function isTransactional(int $operation): array|bool
{
return $this->transactions();
}

/**
* Loads default values from database table schema.
*
Expand Down Expand Up @@ -303,61 +225,6 @@ public function refresh(): bool
return $this->refreshInternal($query->onePopulate());
}

/**
* Declares which DB operations should be performed within a transaction in different scenarios.
*
* The supported DB operations are: {@see OP_INSERT}, {@see OP_UPDATE} and {@see OP_DELETE}, which correspond to the
* {@see insert()}, {@see update()} and {@see delete()} methods, respectively.
*
* By default, these methods are NOT enclosed in a DB transaction.
*
* In some scenarios, to ensure data consistency, you may want to enclose some or all of them in transactions. You
* can do so by overriding this method and returning the operations that need to be transactional. For example,
*
* ```php
* return [
* 'admin' => self::OP_INSERT,
* 'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
* // the above is equivalent to the following:
* // 'api' => self::OP_ALL,
*
* ];
* ```
*
* The above declaration specifies that in the "admin" scenario, the insert operation ({@see insert()}) should be
* done in a transaction; and in the "api" scenario, all the operations should be done in a transaction.
*
* @return array The declarations of transactional operations. The array keys are scenarios names, and the array
* values are the corresponding transaction operations.
*/
public function transactions(): array
{
return [];
}

public function update(array $attributeNames = null): int
{
if (!$this->isTransactional(self::OP_UPDATE)) {
return $this->updateInternal($attributeNames);
}

$transaction = $this->db->beginTransaction();

try {
$result = $this->updateInternal($attributeNames);
if ($result === 0) {
$transaction->rollBack();
} else {
$transaction->commit();
}

return $result;
} catch (Throwable $e) {
$transaction->rollBack();
throw $e;
}
}

/**
* Valid column names are table column names or column names prefixed with table name or table alias.
*
Expand Down
100 changes: 100 additions & 0 deletions src/Trait/TransactionalTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);

namespace Yiisoft\ActiveRecord\Trait;

use Throwable;
use Yiisoft\ActiveRecord\ActiveRecordInterface;
use Yiisoft\ActiveRecord\TransactionalInterface;

use function in_array;

/**
* Trait to implement transactional operations and {@see TransactionalInterface} for ActiveRecord.
*
* @see ActiveRecordInterface::insert()
* @see ActiveRecordInterface::update()
* @see ActiveRecordInterface::delete()
*/
trait TransactionalTrait
{
public function delete(): int
{
if (!$this->isTransactional(TransactionalInterface::OP_DELETE)) {
return $this->deleteInternal();

Check warning on line 25 in src/Trait/TransactionalTrait.php

View check run for this annotation

Codecov / codecov/patch

src/Trait/TransactionalTrait.php#L25

Added line #L25 was not covered by tests
}

$transaction = $this->db->beginTransaction();

try {
$result = $this->deleteInternal();
$transaction->commit();

return $result;
} catch (Throwable $e) {
$transaction->rollBack();
throw $e;
}
}

public function insert(array $attributes = null): bool
{
if (!$this->isTransactional(TransactionalInterface::OP_INSERT)) {
return $this->insertInternal($attributes);

Check warning on line 44 in src/Trait/TransactionalTrait.php

View check run for this annotation

Codecov / codecov/patch

src/Trait/TransactionalTrait.php#L44

Added line #L44 was not covered by tests
}

$transaction = $this->db->beginTransaction();

try {
$result = $this->insertInternal($attributes);
if ($result === false) {
$transaction->rollBack();

Check warning on line 52 in src/Trait/TransactionalTrait.php

View check run for this annotation

Codecov / codecov/patch

src/Trait/TransactionalTrait.php#L52

Added line #L52 was not covered by tests
} else {
$transaction->commit();
}

return $result;
} catch (Throwable $e) {
$transaction->rollBack();
throw $e;

Check warning on line 60 in src/Trait/TransactionalTrait.php

View check run for this annotation

Codecov / codecov/patch

src/Trait/TransactionalTrait.php#L58-L60

Added lines #L58 - L60 were not covered by tests
}
}

public function isTransactional(int $operation): bool
{
return in_array($operation, $this->transactions(), true);
}

public function transactions(): array
{
return [
TransactionalInterface::OP_INSERT,
TransactionalInterface::OP_UPDATE,
TransactionalInterface::OP_DELETE,
];
}

public function update(array $attributeNames = null): int
{
if (!$this->isTransactional(TransactionalInterface::OP_UPDATE)) {
return $this->updateInternal($attributeNames);

Check warning on line 81 in src/Trait/TransactionalTrait.php

View check run for this annotation

Codecov / codecov/patch

src/Trait/TransactionalTrait.php#L81

Added line #L81 was not covered by tests
}

$transaction = $this->db->beginTransaction();

try {
$result = $this->updateInternal($attributeNames);
if ($result === 0) {
$transaction->rollBack();
} else {
$transaction->commit();
}

return $result;
} catch (Throwable $e) {
$transaction->rollBack();
throw $e;
}
}
}
65 changes: 65 additions & 0 deletions src/TransactionalInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace Yiisoft\ActiveRecord;

interface TransactionalInterface
{
/**
* The insert operation. This is mainly used when overriding {@see transactions()} to specify which operations are
* transactional.
*/
public const OP_INSERT = 0x01;

/**
* The update operation. This is mainly used when overriding {@see transactions()} to specify which operations are
* transactional.
*/
public const OP_UPDATE = 0x02;

/**
* The delete operation. This is mainly used when overriding {@see transactions()} to specify which operations are
* transactional.
*/
public const OP_DELETE = 0x04;

/**
* Returns a value indicating whether the specified operation is transactional.
*
* @param int $operation The operation to check. Possible values are {@see OP_INSERT}, {@see OP_UPDATE} and
* {@see OP_DELETE}.
*
* @return bool Whether the specified operation is transactional.
*/
public function isTransactional(int $operation): bool;

/**
* Declares which DB operations should be performed within a transaction in different scenarios.
*
* The supported DB operations are: {@see OP_INSERT}, {@see OP_UPDATE} and {@see OP_DELETE}, which correspond to the
* {@see insert()}, {@see update()} and {@see delete()} methods, respectively.
*
* By default, these methods are NOT enclosed in a DB transaction.
*
* In some scenarios, to ensure data consistency, you may want to enclose some or all of them in transactions. You
* can do so by overriding this method and returning the operations that need to be transactional. For example,
*
* ```php
* return [
* 'admin' => self::OP_INSERT,
* 'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
* // the above is equivalent to the following:
* // 'api' => self::OP_ALL,
*
* ];
* ```
*
* The above declaration specifies that in the "admin" scenario, the insert operation ({@see insert()}) should be
* done in a transaction; and in the "api" scenario, all the operations should be done in a transaction.
*
* @return array The declarations of transactional operations. The array keys are scenarios names, and the array
* values are the corresponding transaction operations.
*/
public function transactions(): array;
}

0 comments on commit 9803ee8

Please sign in to comment.