How do Nested Transactions work in Laravel? - php

I've a master function A that is called before function B and after function C. Both save one model and have their own begin transaction and master transaction in function A, how is data stored?
I tried to throw an Exception on function C, but function B stores variable $modelB anywhere
public function B(){
DB::beginTransaction();
try{
$modelB->save();
DB::commit();
}catch(\Exception $e){
DB::rollback();
}
}
public function C(){
DB::beginTransaction();
try{
$modelC->save();
DB::commit();
}catch(\Exception $e){
DB::rollback();
}
}
public function A(){
DB::beginTransaction();
try{
$this->B();
$this->C();
DB::commit();
} catch(\Exception $e){
DB::rollback();
}
}

In Laravel 6 you can use:
DB::connection(DB::getDefaultConnection())->transactionLevel()
to get the curret active transaction number. Personally I prefer to use single transaction like:
if(DB::connection(DB::getDefaultConnection())->transactionLevel()==0){
DB::beginTransaction();
}
try{
//DO SOME STUFF
if(DB::connection(DB::getDefaultConnection())->transactionLevel()==0)
{
DB::commit();
} // else, leave commit to parent transaction
}catch(\Throwable $e)
{
DB::rollback();
throw $e;
}

Based on the response of #cirkopel, I've created a simple class that creates a single transaction, then only commits if the method that is invoking the commit method is the first caller to the transaction.
Each time that we call DB::beginTransaction() the transaction level is incremented, that's the point of only keeping 1 transaction as had mentioned by #cirkopel.
Taking care of this, another problem that we need to approach is when some error happens and we need to call DB::rollback() it must be called before any DB::commit().
To summarize, we need to only start the transaction in the root invoker and commit the transaction in this same, considering that we could have many services related between them with their own transaction management.
The class:
<?php
namespace App\Models;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class DBUtils
{
/**
* This method starts a transaction only if the level of the transaction is 0.
* So it is useful when you have nested transactions.
* #return int The number of transaction-level that invokes the method.
*/
public static function beginSingleTransaction(): int {
$current_level = static::getCurrentTransactionLevel();
if($current_level ==0 ){
Log::debug('-------------- Beginning Transaction at LEVEL = ' . $current_level .' -------------- ');
DB::beginTransaction();
}
return $current_level;
}
/**
* This method commits the current transaction up to reach the root invoker.
* So it is useful when you have nested transactions.
*
* #param int $at_level indicate the method that invokes the current transaction, pass the value of {#beginSingleTransaction}
*
* #return void
*/
public static function commitSingleTransaction(int $at_level) {
if($at_level == 0) {
Log::debug('-------------- Commit Transaction at LEVEL = ' . $at_level. ' -------------- ');
DB::commit();
}
}
private static function getCurrentTransactionLevel(): int {
return DB::connection(DB::getDefaultConnection())->transactionLevel();
}
}
Now we can have multiples services that could be combined.
DeleteUseCase.php
<?php
namespace App\Features\UseCases;
use App\Models\DBUtils;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* DeleteUseCase.
* ...
*/
class DeleteUseCase
{
/**
* #throws \Exception
*/
public function delete(int $id): bool {
$level = DBUtils::beginSingleTransaction();
try {
// your process of deletion (repository, eloquent, etc)
DBUtils::commitSingleTransaction($level);
return true;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
}
We can call DeleteUseCase inside another class creating nested transactions:
OtherUseCase.php
<?php
namespace App\Features\UseCases;
use App\Features\UseCases\DeleteUseCase.php;
use App\Models\DBUtils;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* OtherUseCase.
* ...
*/
class OtherUseCase
{
private DeleteUseCase $deleteUseCase;
public function __construct(
DeleteUseCase $deleteUseCase)
{
$this->deleteUseCase = $deleteUseCase;
}
/**
* #throws \Exception
*/
public function other(int $id): bool {
// the actual level is 0, after this the level will be 1
$level = DBUtils::beginSingleTransaction();
try {
// your OTHER process
// .....
// call to the inner service, here no transaction will be created
// and also any commit will be performed, because the delete method
// is not the root invoker.
$this->deleteUseCase->delete($id);
// here the transaction will be performed.
DBUtils::commitSingleTransaction($level);
return true;
} catch (\Exception $e) {
// at any point, as only one transaction was created the rollback is performed without problem.
DB::rollBack();
throw $e;
}
}
}
You can invoke the services together or separately and a single transaction would be created.
I hope that it could be useful for someone.
Regards.

Related

Laravel arbitary set exit status code to custom command

I am implementing the following custom command for running a background processes:
namespace App\Console\Commands;
use App\Model\MyModel;
use Exception;
use Illuminate\Console\Command;
class MyCommand extends Command
{
/**
* #var string
*/
protected $description = "Doing StuffyStuff";
/**
* #var string
*/
protected $signature = "mycommand:dostuff";
public function __construct()
{
parent::__construct();
}
public function handle(MyModel $model): void
{
//#todo Implement Upload
try {
$model->findOrFail(12);
//Set Exit Status Code 0
} catch (Exception $e) {
//Set status code 1
}
}
}
So as you can see I want to specify the statuc code depending if an exception has been thrown or not. If success I want to use exit status code 0 and if fail I want to use exit status code 1 as Unix Spec specifies.
So do you have any Idea how to do that?
You can return the code wherever you want
public function handle(MyModel $model): int
{
//#todo Implement Upload
try {
$model->findOrFail(12);
return 0;
} catch (Exception $e) {
$this->error($e->getMessage());
return 1;
}
}
You can see an example & explain here

Possibility to return child method from parent class method?

As I am pretty new to Laravel I am currently using the queue and have the following issue with a job.
Because the API I am calling can throttle, I need to have that logic for every method I call, so I created a parent (base) class. (not sure if this is the right way, please correct me if wrong here as well)
So I have the JobClass that extends the BaseJobClass, which should handle creation of the API client.
BaseJob
class BaseJob implements ShouldQueue
{
protected function performActionOrThrottle($method, $parameters, Customer $customer, Marketplace $marketplace) {
$client = $this->createClient($customer, $marketplace);
if (!method_exists($client, $method)) {
return $this->fail(new \Exception("Method {$method} does not exist in " . get_class($client)));
}
try {
$result = $client->{$method}($parameters);
} catch (\Exception $exception)
{
echo $exception->getMessage().PHP_EOL;
return $this->release(static::THROTTLE_LIMIT);
}
return $result;
}
}
Job
class Job extends BaseJob
{
CONST THROTTLE_LIMIT = 60;
CONST RETRY_DELAY = 10;
private $customer;
private $requestId;
/**
* Create a new job instance.
*
* #return void
*/
public function __construct(Customer $customer, $requestId = null)
{
$this->customer = $customer;
$this->requestId = $requestId;
echo "Updating Inventory for {$customer->name}\n";
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
$marketplace = Marketplace::findOrFail(5);
if (!$report = $this->performActionOrThrottle('GetReport', $this->requestId, $this->customer, $marketplace)) {
echo "Report not available, trying again in " . self::RETRY_DELAY;
return $this->release(self::RETRY_DELAY);
}
...
handle Data
...
return;
}
}
In order do end the job, I need to return the handle() method. How can I return the handle method from the parent method without having to check for the return value and implementing to return it? If I would to so, there is no point for having that parent that should contain all the logic I need for several jobs.

Is it possible to get last failed_jobs record id after Laravel job failed

I want to create GUI for failed_jobs and associate them with other tables record. So user could know which jobs property value failed during job handle and could be retried.
Laravel Job has a function failed(\Exception $exception) but it is called after the exception but before the record is saved in the failed_jobs table.
Also Laravel has Queue:failing(FailedJob $job) event but there I have only serialized job but not failed_jobs.
Did anyone run into similar problem? Is there any relation with processed job and failed one?
After much fussing, I accomplished this by embedding the related model within the Exception that is stored to the database. You could easily do similar but store only the id within the exception and use it to look up the model later. Up to you...
Wherever the exception that fails the job occurs:
try {
doSomethingThatFails();
} catch (\Exception $e) {
throw new MyException(OtherModel::find($id), 'Some string error message.');
}
app/Exceptions/MyException.php:
<?php
namespace App\Exceptions;
use App\Models\OtherModel;
use Exception;
class MyException extends Exception
{
/**
* #var OtherModel
*/
private $otherModel = null;
/**
* Construct
*/
public function __construct(OtherModel $otherModel, string $message)
{
$this->otherModel = $otherModel;
parent::__construct(json_encode((object)[
'message' => $message,
'other_model' => $otherModel,
]));
}
/**
* Get OtherModel
*/
public function getOtherModel(): ?object
{
return $this->otherModel;
}
}
That will store a string containing an object to the exception column on the failed_jobs table. Then you just need to decode that later on...
app/Models/FailedJob (or just app/FailedJob):
/**
* Return the human-readable message
*/
protected function getMessageAttribute(): string
{
if (!$payload = $this->getPayload()) {
return 'Unexpected error.';
}
$data = $this->decodeMyExceptionData();
return $data->message ?? '';
}
/**
* Return the related record
*/
protected function getRelatedRecordAttribute(): ?object
{
if (!$payload = $this->getPayload()) {
return null;
}
$data = $this->decodeMyExceptionData();
return $data->other_model ?? null;
}
/**
* Return the payload
*/
private function getPayload(): ?object
{
$payload = json_decode($this->payload);
return $payload ?? null;
}
/**
* Return the data encoded in a WashClubTransactionProcessingException
*/
private function decodeMyExceptionData(): ?object
{
$data = json_decode(preg_replace('/^App\\\Exceptions\\\WashClubTransactionProcessingException: ([^\n]*) in.*/s', '$1', $this->exception));
return $data ?? null;
}
anywhere:
$failedJob = FailedJob::find(1);
dd([
$failedJob->message,
$failedJob->related_record,
]);

How to write unit tests throwing PDOException

I have a class which handles a variety of database exceptions such as deadlocks and serialized transaction failures. I'm trying to unit test it and ran into a roadblock. An example of the code I want to test:
public function run(callable $callable)
{
$this->beginTransaction();
try {
$callable();
$this->commit();
} catch (\PDOException $e) {
if ($e->getCode() == '40P01' || $e->getCode() == '55P03') {
// Deadlock (40P01) or lock not available (55P03).
...
}
...
}
}
The documentation for PDOException says developers should not throw it themselves, and today I found out why when trying to write a unit test:
$obj->run(function() {
...
throw new \PDOException('Deadlock', '40P01');
});
A non well formed numeric value encountered. PDOException breaks the contract with Exception because exception codes must be int and PDOException creates string codes to match SQL standards. They should have made a separate SQLCode property but instead reused the built-in code. Therefore it's impossible to throw a PDOException with a real SQL error code within a unit test.
Next I tried to mock PDOException:
class PDOExceptionMock extends \PDOException
{
protected $code = '';
public function setCode($code)
{
$this->code = $code;
}
// This won't work because getCode is final
public function getCode()
{
return $this->code;
}
}
This won't compile because 'Cannot override final method Exception->getCode()'.
Since I can't (and don't want to) recreate every type of deadlock and transaction error using a real database, how can one write unit tests which need to throw PDOExceptions?
You don't need to overwrite getCode. It is already there, so you need just to set the code. E.g.
class A {
public function run(callable $callable)
{
try {
$callable();
} catch (\PDOException $e) {
if ($e->getCode() == '40P01' || $e->getCode() == '55P03') {
return 'expected';
}
return 'unexpected';
}
return 'no caught';
}
}
class StubException extends \PDOException {
public function __construct() {
parent::__construct();
$this->code = '40P01';
}
}
$a = new A;
echo $a->run(function(){throw new StubException;});
echoes 'expected';

Unable to catch a global exception

Monolog's StreamHandler throws a \UnexpectedValueException if there's a problem with the specified log file.
In my code I'm trying to catch \UnexpectedValueException but I've been not able to.
My code is:
<?php
namespace myNamespace;
use Monolog\Logger as Monolog;
use Monolog\Handler\StreamHandler;
class Logger {
private static $instance;
private static function init()
{
if (!self::$instance)
{
$logger = new Monolog(MY_LOG_CHANNEL);
try
{
$logger->pushHandler(new StreamHandler(MY_LOG_PATH . "/" .
MY_LOG_NAME, Monolog::NOTICE));
}
catch (\UnexpectedValueException $e)
{
writeln("Error starting logger" . $e->getMessage());
die;
}
// and so on
It doesn't work, I get this:
Fatal error: Uncaught exception 'UnexpectedValueException' with message 'The stream or
file [somefile.log] could not be opened: failed to open stream: No such file or
directory' in [path to src/Monolog/Handler/StreamHandler.php ]
As I understand it they have escaped to the global namespace so I should be able to pick it up there. Why can't I? I've tried all combinations of namespace \myNamesace\UnexpectedValueException even Monolog\UnexpectedValueException global or local, to no avail.
Clearly I'm missing something, what is it please?
Edit:
In another class I'm doing an if (file_exists($fileName)) /** do file_get_contents() etc */ else Logger::error($filename . "does not exist")
The error is being triggered within Logger::error() when I call self::init()
The error is caused because I have (purposely) munged the log file path, if it's a valid log file path then the code runs fine. Clearly I want to catch that error, hence the try/catch.
The next line in the trace is the line in the code above: $logger->pushHandler(new StreamHandler(MY_LOG_PATH . "/" . MY_LOG_NAME, Monolog::NOTICE));
Interestingly, the only other place I'm catching an exception (this is just scaffolding code at the moment, no business logic yet) is within the /** do file_get_contents() etc */ bit if I purposely mis-spell the filename var, file_get_contents barfs and a standard
catch (Exception $e) works as I'd expect around the file_get_contents()
This is a pretty old question which probably got resolved in the meanwhile. However, for the sake of future visitors, I would like to point out that the exception is thrown when a log entry will be written to the given file, not at construction time.
The way you have the try { } catch () {} setup, the exception is expected at the creation of StreamHandler. However, the real exception happens whenever you try to send to the log via call like $logger->error("Error message") so you should be catching exceptions there.
On another note, I think throwing exceptions from a logging library is one of the silliest things that one could do. Logging should be idempotent and not affect the state of the running application.
In an old silex application, I also ran into this issue. I want to log to a logstash instance, yet it should not break if logstash isn't available.
Luckily, my application used the logger only typed against the Psr\Log\LoggerInterface, so I could write a decorator preventing the exceptions to break the app at one place, instead of adding a try and catch call on every call within the codebase.
It looks like this:
<?php
namespace Dreamlines\DirectBookingForm\ServiceProvider;
use Psr\Log\LoggerInterface;
/**
* DontThrowLoggerDecorator
*
* Monolog will break on info, error, etc. calls
* This decorator wrap the LoggerInterface and ensures that failure to log won't break the app
**/
class DontThrowLoggerDecorator implements LoggerInterface
{
/**
* #var LoggerInterface
*/
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* #inheritDoc
*/
public function emergency($message, array $context = array())
{
try {
$this->logger->emergency($message, $context);
} catch (\Exception $e) {
}
}
/**
* #inheritDoc
*/
public function alert($message, array $context = array())
{
try {
$this->logger->alert($message, $context);
} catch (\Exception $e) {
}
}
/**
* #inheritDoc
*/
public function critical($message, array $context = array())
{
try {
$this->logger->critical($message, $context);
} catch (\Exception $e) {
}
}
/**
* #inheritDoc
*/
public function error($message, array $context = array())
{
try {
$this->logger->error($message, $context);
} catch (\Exception $e) {
}
}
/**
* #inheritDoc
*/
public function warning($message, array $context = array())
{
try {
$this->logger->warning($message, $context);
} catch (\Exception $e) {
}
}
/**
* #inheritDoc
*/
public function notice($message, array $context = array())
{
try {
$this->logger->notice($message, $context);
} catch (\Exception $e) {
}
}
/**
* #inheritDoc
*/
public function info($message, array $context = array())
{
try {
$this->logger->info($message, $context);
} catch (\Exception $e) {
}
}
/**
* #inheritDoc
*/
public function debug($message, array $context = array())
{
try {
$this->logger->debug($message, $context);
} catch (\Exception $e) {
}
}
/**
* #inheritDoc
*/
public function log($level, $message, array $context = array())
{
try {
$this->logger->log($level, $message, $context);
} catch (\Exception $e) {
}
}
}
and I just wrap my logger instance around this once, in my silex application I am doing it like this:
$app['NOT_BREAKING_LOGGER'] = $app->share(
function () use ($app) {
return new DontThrowLoggerDecorator($app['monolog']);
}
);
and I am injecting the NOT_BREAKING_LOGGER in each of my services instead of the monolog one.

Categories