Skip to content

Commit

Permalink
Merge pull request #31 from cycle/transact
Browse files Browse the repository at this point in the history
Add ActiveRecord::transact() method
  • Loading branch information
roxblnfk authored Jul 24, 2024
2 parents 4d1183b + b72ccfa commit 111abaf
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 90 deletions.
Binary file removed docs/.gitbook/assets/screenshot (1).png
Binary file not shown.
2 changes: 1 addition & 1 deletion docs/general/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ This project uses a Makefile to streamline common development tasks. The Makefil
make help
```

<figure><img src="../.gitbook/assets/screenshot (1).png" alt=""><figcaption><p>visual view of make help command</p></figcaption></figure>
<figure><img src="../.gitbook/assets/screenshot.png" alt=""><figcaption><p>visual view of make help command</p></figcaption></figure>

#### → Key Commands

Expand Down
20 changes: 20 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ parameters:
count: 1
path: src/ActiveRecord.php

-
message: "#^Method Cycle\\\\ORM\\\\EntityManagerInterface\\:\\:run\\(\\) invoked with 1 parameter, 0 required\\.$#"
count: 2
path: src/ActiveRecord.php

-
message: "#^Method Cycle\\\\ORM\\\\EntityManagerInterface\\:\\:run\\(\\) invoked with 2 parameters, 0 required\\.$#"
count: 1
path: src/Internal/TransactionFacade.php

-
message: "#^Template type TEntity is declared as covariant, but occurs in contravariant position in parameter select of method Cycle\\\\ActiveRecord\\\\Repository\\\\ActiveRepository\\:\\:with\\(\\)\\.$#"
count: 1
Expand All @@ -25,6 +35,16 @@ parameters:
count: 1
path: tests/src/Functional/ActiveRecordTest.php

-
message: "#^Cannot call method isSuccess\\(\\) on null\\.$#"
count: 1
path: tests/src/Functional/ActiveRecordTest.php

-
message: "#^Result of method Cycle\\\\ActiveRecord\\\\ActiveRecord\\:\\:saveOrFail\\(\\) \\(void\\) is used\\.$#"
count: 1
path: tests/src/Functional/ActiveRecordTest.php

-
message: "#^Call to an undefined method Cycle\\\\Database\\\\Driver\\\\DriverInterface\\:\\:setLogger\\(\\)\\.$#"
count: 1
Expand Down
38 changes: 31 additions & 7 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="5.24.0@462c80e31c34e58cc4f750c656be3927e80e550e">
<files psalm-version="5.25.0@01a8eb06b9e9cc6cfb6a320bf9fb14331919d505">
<file src="src/ActiveRecord.php">
<PossiblyUnusedMethod>
<code><![CDATA[deleteOrFail]]></code>
</PossiblyUnusedMethod>
<PossiblyUnusedReturnValue>
<code><![CDATA[EntityManagerInterface]]></code>
<code><![CDATA[EntityManagerInterface]]></code>
</PossiblyUnusedReturnValue>
<TooManyArguments>
<code><![CDATA[run]]></code>
<code><![CDATA[run]]></code>
</TooManyArguments>
</file>
<file src="src/Bridge/Laravel/Providers/ActiveRecordProvider.php">
<UnusedClass>
Expand All @@ -24,26 +24,50 @@
<code><![CDATA[setOrm]]></code>
</PossiblyUnusedMethod>
</file>
<file src="src/Internal/TransactionFacade.php">
<TooManyArguments>
<code><![CDATA[run]]></code>
</TooManyArguments>
</file>
<file src="src/Repository/ActiveRepository.php">
<PossiblyUnusedMethod>
<code><![CDATA[findAll]]></code>
<code><![CDATA[forUpdate]]></code>
</PossiblyUnusedMethod>
</file>
<file src="tests/src/Functional/ActiveRecordTest.php">
<AssignmentToVoid>
<code><![CDATA[$entityManager]]></code>
</AssignmentToVoid>
<NullReference>
<code><![CDATA[isSuccess]]></code>
</NullReference>
<PossiblyUnusedMethod>
<code><![CDATA[it_creates_entity_instance_using_make]]></code>
<code><![CDATA[it_deletes_entity]]></code>
<code><![CDATA[it_deletes_multiple_entities_using_remove_method]]></code>
<code><![CDATA[it_deletes_multiple_entities_in_single_transaction]]></code>
<code><![CDATA[it_finds_all_entities]]></code>
<code><![CDATA[it_finds_entity_by_primary_key]]></code>
<code><![CDATA[it_finds_one_entity]]></code>
<code><![CDATA[it_gets_default_repository_of_entity]]></code>
<code><![CDATA[it_persists_multiple_entities]]></code>
<code><![CDATA[it_persists_multiple_entities_in_single_transaction]]></code>
<code><![CDATA[it_runs_transaction_in_current_transaction_mode_without_opened_transaction]]></code>
<code><![CDATA[it_runs_transaction_in_transaction]]></code>
<code><![CDATA[it_runs_transaction_without_actions]]></code>
<code><![CDATA[it_saves_entity]]></code>
<code><![CDATA[it_triggers_exception_when_tries_to_save_entity_using_save_or_fail]]></code>
<code><![CDATA[it_uses_query_to_select_entity]]></code>
</PossiblyUnusedMethod>
<UndefinedVariable>
<code><![CDATA[$userOne]]></code>
<code><![CDATA[$userOne]]></code>
<code><![CDATA[$userOne]]></code>
<code><![CDATA[$userTwo]]></code>
<code><![CDATA[$userTwo]]></code>
</UndefinedVariable>
<UnusedVariable>
<code><![CDATA[$entityManager]]></code>
</UnusedVariable>
</file>
<file src="tests/src/Functional/Bridge/Spiral/Bootloader/ActiveRecordBootloaderTest.php">
<PossiblyUnusedMethod>
Expand Down
108 changes: 54 additions & 54 deletions src/ActiveRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@

namespace Cycle\ActiveRecord;

use Cycle\ActiveRecord\Exception\Transaction\TransactionException;
use Cycle\ActiveRecord\Internal\TransactionFacade;
use Cycle\ActiveRecord\Query\ActiveQuery;
use Cycle\ORM\EntityManager;
use Cycle\ORM\EntityManagerInterface;
use Cycle\ORM\ORMInterface;
use Cycle\ORM\RepositoryInterface;
use Cycle\ORM\Transaction\StateInterface;

/**
* A base class for entities that are managed by the ORM.
Expand Down Expand Up @@ -69,6 +68,26 @@ final public static function findAll(array $scope = []): iterable
return static::query()->where($scope)->fetchAll();
}

/**
* Execute a callback within a single transaction.
*
* All the ActiveRecord write operations within the callback will be registered
* using the Entity Manager without being executed until the end of the callback.
*
* @template TResult
* @param callable(): TResult $callback
* @return TResult
*
* @throws TransactionException
* @throws \Throwable
*/
public static function transact(
callable $callback,
TransactionMode $mode = TransactionMode::OpenNew,
): mixed {
return TransactionFacade::transact($callback, $mode);
}

/**
* Get an ActiveQuery instance for the entity.
*
Expand All @@ -86,81 +105,62 @@ public static function getRepository(): RepositoryInterface

/**
* Persist the entity.
*
* @throws \Throwable
*/
final public function save(bool $cascade = true): StateInterface
final public function save(bool $cascade = true): bool
{
/** @var EntityManager $entityManager */
$entityManager = Facade::getEntityManager();
$entityManager->persist($this, $cascade);

return $entityManager->run(throwException: false);
$transacting = TransactionFacade::getEntityManager();
if ($transacting === null) {
return Facade::getEntityManager()
->persist($this, $cascade)
->run(false)
->isSuccess();
}

$transacting->persist($this, $cascade);
return true;
}

/**
* Persist the entity and throw an exception if an error occurs.
* The exception will be thrown if the action is happening not in a {@see self::transcat()} scope.
*
* @throws \Throwable
*/
final public function saveOrFail(bool $cascade = true): StateInterface
final public function saveOrFail(bool $cascade = true): void
{
/** @var EntityManager $entityManager */
$entityManager = Facade::getEntityManager();
$entityManager->persist($this, $cascade);

return $entityManager->run();
}

/**
* Prepare the entity for persistence.
*
* @note This function is experimental and may be removed in the future.
*/
final public function persist(bool $cascade = true): EntityManagerInterface
{
return Facade::getEntityManager()->persist($this, $cascade);
TransactionFacade::getEntityManager()?->persist($this, $cascade) ?? Facade::getEntityManager()
->persist($this, $cascade)
->run();
}

/**
* Delete the entity.
*
* @throws \Throwable
*/
final public function delete(bool $cascade = true): StateInterface
final public function delete(bool $cascade = true): bool
{
/** @var EntityManager $entityManager */
$entityManager = Facade::getEntityManager();
$entityManager->delete($this, $cascade);

return $entityManager->run(throwException: false);
$transacting = TransactionFacade::getEntityManager();
if ($transacting === null) {
return Facade::getEntityManager()
->delete($this, $cascade)
->run(false)
->isSuccess();
}

$transacting->delete($this, $cascade);
return true;
}

/**
* Delete the entity and throw an exception if an error occurs.
* The exception will be thrown if the action is happening not in a {@see self::transcat()} scope.
*
* @throws \Throwable
*/
final public function deleteOrFail(bool $cascade = true): StateInterface
{
/** @var EntityManager $entityManager */
$entityManager = Facade::getEntityManager();
$entityManager->delete($this, $cascade);

return $entityManager->run();
}

/**
* Prepare the entity for deletion.
*
* @note This function is experimental and may be removed in the future.
*/
final public function remove(bool $cascade = true): EntityManagerInterface
final public function deleteOrFail(bool $cascade = true): void
{
/** @var EntityManager $entityManager */
$entityManager = Facade::getEntityManager();

return $entityManager->delete($this, $cascade);
TransactionFacade::getEntityManager()?->delete($this, $cascade) ?? Facade::getEntityManager()
->delete($this, $cascade)
->run();
}

private static function getOrm(): ORMInterface
Expand Down
9 changes: 9 additions & 0 deletions src/Exception/Transaction/TransactionException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Cycle\ActiveRecord\Exception\Transaction;

use Cycle\ActiveRecord\Exception\ActiveRecordException;

class TransactionException extends \RuntimeException implements ActiveRecordException {}
53 changes: 53 additions & 0 deletions src/Internal/TransactionFacade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Cycle\ActiveRecord\Internal;

use Cycle\ActiveRecord\Exception\Transaction\TransactionException;
use Cycle\ActiveRecord\Facade;
use Cycle\ActiveRecord\TransactionMode;
use Cycle\ORM\EntityManagerInterface;
use Cycle\ORM\Transaction\Runner;

final class TransactionFacade
{
private static ?EntityManagerInterface $em = null;

public static function getEntityManager(): ?EntityManagerInterface
{
return self::$em;
}

/**
* @template TResult
* @param callable(): TResult $callback
* @return TResult
*
* @throws TransactionException
* @throws \Throwable
*/
public static function transact(
callable $callback,
TransactionMode $mode = TransactionMode::OpenNew,
): mixed {
$runner = match ($mode) {
TransactionMode::Ignore => Runner::outerTransaction(strict: false),
TransactionMode::Current => Runner::outerTransaction(strict: true),
TransactionMode::OpenNew => Runner::innerTransaction(),
};

self::$em === null or throw new TransactionException(
'A transaction is already in progress.',
);
self::$em = Facade::getEntityManager();

try {
$result = $callback();
self::$em->run(true, $runner);
return $result;
} finally {
self::$em = null;
}
}
}
29 changes: 29 additions & 0 deletions src/TransactionMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Cycle\ActiveRecord;

enum TransactionMode
{
/**
* Do nothing with transactions.
*
* @see \Cycle\ORM\Transaction\Runner::innerTransaction() with non-strict mode.
*/
case Ignore;

/**
* The currently opened transaction will be used. If no transaction is opened, a new one will be created.
*
* @see \Cycle\ORM\Transaction\Runner::innerTransaction() with strict mode.
*/
case Current;

/**
* A new transaction will be open for each used driver connection and will close they on finish.
*
* @see \Cycle\ORM\Transaction\Runner::innerTransaction()
*/
case OpenNew;
}
5 changes: 2 additions & 3 deletions tests/app/Repository/RepositoryWithActiveQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,20 @@
use Cycle\App\Query\UserQuery;
use Cycle\Database\Injection\Fragment;
use Cycle\ORM\ORMInterface;
use Cycle\ORM\Select;

/**
* @method UserQuery select()
* @extends ActiveRepository<User>
*/
final class RepositoryWithActiveQuery extends ActiveRepository
{
#[\Override]
public function __construct()
{
parent::__construct(User::class);
}

#[\Override]
public function initSelect(ORMInterface $orm, string $role): Select
public function initSelect(ORMInterface $orm, string $role): UserQuery
{
return new UserQuery();
}
Expand Down
Loading

0 comments on commit 111abaf

Please sign in to comment.