Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Create TransactionalTrait #320

Merged
merged 1 commit into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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;
}
Loading