squirrelphp/entities

View on GitHub
src/Transaction.php

Summary

Maintainability
B
5 hrs
Test Coverage
A
100%
<?php

namespace Squirrel\Entities;

use Squirrel\Debug\Debug;
use Squirrel\Queries\DBException;
use Squirrel\Queries\DBInterface;
use Squirrel\Queries\Exception\DBInvalidOptionException;

/**
 * Run queries within a transaction
 */
class Transaction implements TransactionInterface
{
    public function __construct(
        private DBInterface $db,
    ) {
    }

    /**
     * Create transaction with given repositories, making sure they all use the same database connection
     *
     * @param array<RepositoryReadOnlyInterface|RepositoryBuilderReadOnlyInterface> $repositories
     * @return self
     *
     * @throws DBException Common minimal exception thrown if anything goes wrong
     */
    public static function withRepositories(array $repositories): self
    {
        // Connection to use for transaction
        $connection = null;

        // Go through all repositories
        foreach ($repositories as $repository) {
            // Builder repository found - get the base repository from it
            if ($repository instanceof RepositoryBuilderReadOnlyInterface) {
                try {
                    $builderRepositoryReflection = new \ReflectionClass($repository);
                    $builderRepositoryPropertyReflection = $builderRepositoryReflection->getProperty('repository');
                    $builderRepositoryPropertyReflection->setAccessible(true);
                    $repository = $builderRepositoryPropertyReflection->getValue($repository);
                } catch (\ReflectionException $e) {
                    throw Debug::createException(
                        DBInvalidOptionException::class,
                        'Base repository not found in builder repository via reflection. ' .
                        'Make sure you use officially supported classes',
                        ignoreClasses: [Transaction::class],
                    );
                }
            }

            // Base repository found - get the DBInterface from it
            if ($repository instanceof RepositoryReadOnlyInterface) {
                try {
                    $baseRepositoryReflection = new \ReflectionClass($repository);
                    $baseRepositoryPropertyReflection = $baseRepositoryReflection->getProperty('db');
                    $baseRepositoryPropertyReflection->setAccessible(true);
                    $foundConnection = $baseRepositoryPropertyReflection->getValue($repository);
                } catch (\ReflectionException $e) {
                    throw Debug::createException(
                        DBInvalidOptionException::class,
                        'Connection not found in base repository via reflection. ' .
                        'Make sure you use officially supported classes',
                        ignoreClasses: [Transaction::class],
                    );
                }

                // Make sure all repositories are using the same connection, otherwise a transaction is impossible
                if (isset($connection) && $connection !== $foundConnection) {
                    throw Debug::createException(
                        DBInvalidOptionException::class,
                        'Repositories have different database connections, transaction is not possible',
                        ignoreClasses: [TransactionInterface::class],
                    );
                }

                $connection = $foundConnection;
            } else { // No base repository - meaning this class is invalid
                throw Debug::createException(
                    DBInvalidOptionException::class,
                    'Invalid class specified to create transaction (not a repository)',
                    ignoreClasses: [TransactionInterface::class],
                );
            }
        }

        // No connection found, meaning no repositories were defined in arguments
        if (!isset($connection) || !($connection instanceof DBInterface)) {
            throw Debug::createException(
                DBInvalidOptionException::class,
                'No repositories for transaction defined',
                ignoreClasses: [TransactionInterface::class],
            );
        }

        return new self($connection);
    }

    public function run(callable $func, ...$arguments)
    {
        return $this->db->transaction($func, ...$arguments);
    }
}