Unable to catch a global exception - php

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.

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

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.

Unit testing guzzle http promises callbacks

I have the following code in a class:
private function makeRequest(DetailedPayload $detailedPayload, $request): void
{
$this->httpClient
->sendAsync($request)
->then(
function (ResponseInterface $response) use ($detailedPayload) {
$this->handleServerResponse($detailedPayload, $response);
},
function (RequestException $exception) use ($detailedPayload) {
$this->logRequestException($detailedPayload, $exception);
}
);
}
An now my handler functions looks like so:
private function handleServerResponse(DetailedPayload $detailedPayload, ResponseInterface $response): void
{
if (200 === $response->getStatusCode()) {
try {
$this->queueDataPublisher->publish($detailedPayload);
} catch (FailedToPublishQueueDataException $exception) {
$this->logPublisherException($detailedPayload, $response);
}
} else {
$this->logNonOkResponse($detailedPayload, $response);
}
}
Now I want to test my class which has the signature:
public function __construct(Client $httpClient, LoggerInterface $logger, QueueDataPublisher $queueDataPublisher)
I can mock all the logger and the publisher class and also can follow the instruction to mock the http request as mentioned on the guzzle documentation found here: http://docs.guzzlephp.org/en/stable/testing.html
My test looks as below:
/**
* #test
*
* #throws \Exception
*/
public function willMakeHttpRequestToServer()
{
$client = new Client(
[
'handler' => HandlerStack::create(
new MockHandler(
[
new Response(200)
]
)
)
]
);
$logger = $this->prophesize(LoggerInterface::class);
$queueDataPublisher = $this->prophesize(QueueDataPublisher::class);
$transportClient = new TransportClient($client, $logger->reveal(), $queueDataPublisher->reveal());
$detailedPayload = (new DetailedPayload())
->setStepId('test_step_id')
->setStageId('test_stage_id')
->setProtocolId('test_protocol_id')
->setPayload('test_payload');
$queueDataPublisher
->publish($detailedPayload)
->shouldBeCalled();
$transportClient->sendPayload($detailedPayload);
}
But I can never get this test to green. Has andbody tried something like this to test the async request.
Any idea on how I can approach to test this implementation.
The test requrns the response telling the expectation to call of a function on publisher failed as so:
No calls have been made that match:

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

Can you throw an array instead of a string as an exception in php?

I want to throw an array as an exception in php, instead of a string. Is it possible to do this if you define your own class that extends the Exception class?
For example throw new CustomException('string', $options = array('params'));
Sure. It will just be up to your error handling code to be aware of, and make use of the array property appropriately. You can define your custom exception class's constructor to take any parameters you want, and then just be sure to call the base class's constructor from within the constructor definition, eg:
class CustomException extends \Exception
{
private $_options;
public function __construct($message,
$code = 0,
Exception $previous = null,
$options = array('params'))
{
parent::__construct($message, $code, $previous);
$this->_options = $options;
}
public function GetOptions() { return $this->_options; }
}
Then, in your calling code...
try
{
// some code that throws new CustomException($msg, $code, $previousException, $optionsArray)
}
catch (CustomException $ex)
{
$options = $ex->GetOptions();
// do something with $options[]...
}
Have a look at the php docs for extending the exception class:
http://php.net/manual/en/language.exceptions.extending.php
I think I'm a bit too late with an answer, but I wanted to share my solution as well. There are probably more people looking for this :)
class JsonEncodedException extends \Exception
{
/**
* Json encodes the message and calls the parent constructor.
*
* #param null $message
* #param int $code
* #param Exception|null $previous
*/
public function __construct($message = null, $code = 0, Exception $previous = null)
{
parent::__construct(json_encode($message), $code, $previous);
}
/**
* Returns the json decoded message.
*
* #param bool $assoc
*
* #return mixed
*/
public function getDecodedMessage($assoc = false)
{
return json_decode($this->getMessage(), $assoc);
}
}
If you don't want to extend Exception, you can encode your array into a string:
try {
throw new Exception(serialize(['msg'=>"Booped up with %d.",'num'=>123]));
} catch (Exception $e) {
$data = unserialize($e->getMessage());
if (is_array($data))
printf($data['msg'],$data['num']);
else
print($e->getMessage());
}
You can also use json_encode/json_decode if you prefer.
Yes, you can. You will need to extend the Exception class and create a __construct() method to do what you want.

Categories