Laravel arbitary set exit status code to custom command - php

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

Related

How do Nested Transactions work in Laravel?

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.

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,
]);

Testing your code for multiple browsers using phpunit & php-webdrivers by facebook

I am working with php-webdrivers by facebook for selenium to implement integration testing for my site. I am using phphunit to run my tests and have wrapped my code inside the phpunitframework_testcase extended class:
class WebTest extends PHPUnit_Framework_TestCase
{
/**
* #var \RemoteWebDriver
*/
protected $driver;
protected $url='http://example.com';
protected $screenShotDirectoryPath = '/screenshots/';
/**
* #BeforeMethod
*/
protected function setUp()
{
try{
$capabilities = array( WebDriverCapabilityType::BROWSER_NAME=>WebDriverBrowserType::FIREFOX );
$this->driver = RemoteWebDriver::create('http://localhost:4444/wd/hub', $capabilities, 5000);
} catch (Exception $e){
echo $e->getMessage();
}
}
/**
* #AfterMethod
*/
public function tearDown()
{
$this->driver->close();
}
public function testImageUpload()
{
$errorSnaps='';
$myBrowserDriver = $this->driver;
//open the url
$myBrowserDriver->get($this->url);
try{
//get email field in login page
$emailField = $myBrowserDriver->findElement(WebDriverBy::id('email'));
//check if the field is displayed
if(!$emailField->isDisplayed()) {
try {
$errorSnaps = $this->takeScreenshot();
$this->errorLogs[] = "The email input Element is not present, a screen-shot of the error has been placed here --> " . $errorSnaps;
} catch (Exception $e){
$this->errorLogs[] = $e->getMessage();
}
}
} catch (NoSuchElementException $nse){
try {
$errorSnaps = $this->TakeScreenshot();
$this->errorLogs[] = "The email field on ".$this->driver->getCurrentURL()." not found , a screen-shot for the error has been placed here -->" . $errorSnaps;
}catch (Exception $e){
$this->errorLogs[]=$e->getMessage();
}
}
}
php-webdrivers documentation recommends this way to initialize the browser drivers
$capabilities=array(
\WebDriverCapabilityType::BROWSER_NAME=>WebDriverBrowserType::FIREFOX
);
$this->driver = RemoteWebDriver::create('http://localhost:4444/wd/hub', $capabilities, 5000);
but does not provide a mechanism to init multiple browser drivers to run my tests with only single test file means considering the code above I have to make different copies for all those browsers and with only one line code difference means if i want to run the test above for chrome then i have to change the line from
$capabilities=array(\WebDriverCapabilityType::BROWSER_NAME=>WebDriverBrowserType::FIREFOX);
to
$capabilities=array(\WebDriverCapabilityType::BROWSER_NAME=>WebDriverBrowserType::CHROME);
and save that code with rest all the same code as above in a different file and run my test suit. As you can see this is not an optimal way for implementing my tests and for the sake of code re-usability.
I came across 2 options:
Pass argument from the terminal sending the browser name with parameter like phpunit brName=chrome and getting it via $_SERVER['brName']. I would still have to type in each time I want to run tests for any other browser.
I came across the TestDecorator class below on phpunit site which looked like a more conventional way to achieve what I am doing but could not figure out how would I use it to run my tests.
Where should I put my code so that it detects and runs it? Every time I try to run the below sample code it says no tests were executed. If i have sample test function below how could I run it 4 times using the testdecorator as base class?
Sample Test:
public function sampleTest(){
$this->assertTrue(TRUE);
}
Test Decorator class:
require_once 'PHPUnit/Extensions/TestDecorator.php';
class PHPUnit_Extensions_RepeatedTest extends PHPUnit_Extensions_TestDecorator
{
private $timesRepeat = 1;
public function __construct(PHPUnit_Framework_Test $test, $timesRepeat = 1)
{
parent::__construct($test);
if (is_integer($timesRepeat) &&
$timesRepeat >= 0) {
$this->timesRepeat = $timesRepeat;
}
}
public function count()
{
return $this->timesRepeat * $this->test->count();
}
public function run(PHPUnit_Framework_TestResult $result = NULL)
{
if ($result === NULL) {
$result = $this->createResult();
}
for ($i = 0; $i < $this->timesRepeat && !$result->shouldStop(); $i++) {
$this->test->run($result);
}
return $result;
}
}
I was not going in the right direction to implement this scenario I should not use the testdecorator class but a more good and easy approach would be.
Setting an environment variable via cmd-line/terminal like
export BROWSER_REQUESTED=chrome for (unix) and set BROWSER_REQUESTED=chrome for (windows),or you can create a .sh file with the following code
export BROWSER_REQUESTED=chrome && phpunit and run it via cmd-line.
Create a BrowserFatory class which listens to the environment variable and initiates the drivers for the browser type requested.
create as much .sh files as much browsers you need to run the test for, i have added the code below to be more descriptive.
WebTest.php
class WebTest extends PHPUnit_Framework_TestCase
{
/**
* #var \RemoteWebDriver
*/
protected $driver;
protected $url='http://example.com';
protected $screenShotDirectoryPath = '/screenshots/';
/**
* #BeforeMethod
*/
protected function setUp()
{
try{
$this->driver = BrowserFactory::drivers();
} catch (Exception $e){
echo $e->getMessage();
}
}
}
BrowserFactory.php
class BrowserFactory
{
public static function drivers()
{
switch ($_SERVER['BROWSER_REQUESTED']) {
case 'chrome':
return self::createChrome();
break;
case "ie":
throw new Exception('Not implemented');
break;
case 'firefox':
default:
return self::createFirefox();
break;
}
}
public static function createChrome()
{
putenv("webdriver.chrome.driver=/path/to/chromedriver");
$service = ChromeDriverService::createDefaultService();
$service->start();
return ChromeDriver::start(DesiredCapabilities::chrome(), $service);
}
public static function createFirefox()
{
// these are just constants defined in bootstrap.php
$seleniumUrl = isset($_SERVER['JENKINS_URL']) ? TEST_ENV_SELENIUM_SERVER : LOCAL_ENV_SELENIUM_SERVER;
return RemoteWebDriver::create(
$seleniumUrl, DesiredCapabilities::firefox()
);
}
}
command-line.sh
export BROWSER_REQUESTED='chrome' && phpunit

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