T5k
T5k

Reputation: 51

Find out what controller and method executed helper function (from within the helper function itself)

Let's say we have a helper function, logDatabaseError($exception) that logs QueryExceptions to a special log.

helpers.php

function logDatabaseError ($exception) {
    $controller = ????;
    $function = ????;

    $log_string = "TIME: ".now().PHP_EOL;
    $log_string.= "User ID: ".Auth::user()->id.PHP_EOL;
    $log_string.= "Controller->Action:".$controller."->".$function.PHP_EOL;
    $log_string.= $exception.PHP_EOL;

    Storage::disk('logs')->append('database.log', $log_string);
}

This function is called from multiple controllers and multiple functions within those controllers.

Whenever something needs to be written to the database, in the catch part, we call this logDatabaseError function and pass to it the \Illuminate\Database\QueryException as $exception.

BestControllerEverController.php

class BestControllerEver extends Controller
{
    function writeStuffToDatabase (Request $request) {
        try {
            DB::does-its-thing
        } 
        catch(\Illuminate\Database\QueryException $exception) {
            logDatabaseError($exception)
        }
    }
}

Is it possible for the logDatabaseError function to get both Controller name and function name without passing them as function parameters?

In this particular example case, $controller and $function variables in logDatabaseError function would be set to BestControllerEver and writeStuffToDatabase, respectively.

I know this is logged in the stack trace, but their location in $exception object is not always the same and extracting it from there is not reliable, at least from my limited experience.

Upvotes: 2

Views: 276

Answers (2)

ManojKiran
ManojKiran

Reputation: 6351

You can use php debug_backtrace function to trace the error frames. Since spatie/backtrace is using debug_backtrace behind the scenes You can use the package

Install the package into application by running

composer require spatie/backtrace

Put that in your controller:

try {
            \Illuminate\Support\Facades\DB::table('myunavialbetable')->get();
        } 
        catch(\Illuminate\Database\QueryException $exception) {
            logDatabaseError($exception);
        }

Inside your helper file

function logDatabaseError ($exception) {

    $backtrace = Spatie\Backtrace\Backtrace::create();

    $controllerResponsible = collect($backtrace->frames())   
    ->filter(function(Spatie\Backtrace\Frame $frame){
        return ($frame->class);
    })
    ->filter(function(Spatie\Backtrace\Frame $frame){
        return is_subclass_of($frame->class, App\Http\Controllers\Controller::class);
    })
    ->first();   

    $log_string = "TIME: " . now() . PHP_EOL;
    $log_string .= "User ID: " . auth()->id() . PHP_EOL;
    if ($controllerResponsible){
        $log_string .= "Controller->Action:" . $controllerResponsible->class . "->" . $controllerResponsible->method . PHP_EOL;
    }
    $log_string .= $exception . PHP_EOL;

    \Illuminate\Support\Facades\Storage::disk('logs')->append('database.log', $log_string);

// if you want to use on-demand log feature you can uncomment this

//This feature is available from Laravel v8.66.0

    // Illuminate\Support\Facades\Log::build([
    //     'driver' => 'single',
    //     'path' => storage_path('logs/database.log'),
    // ])->info($log_string);
}

NOTE:CONTROLLER MUST EXTEND App\Http\Controllers\Controller

ADVANCE SOLUTION

Steps to be followed:

  1. Install spatie/backtrace
  2. Remove try/catch block from controller.
  3. Modify app/Exceptions/Handler.php to below content
class Handler extends ExceptionHandler
{
    public $controllerResponsible = null;

    /**
     * A list of the exception types that are not reported.
     *
     * @var array
     */
    protected $dontReport = [
        //
    ];

    /**
     * A list of the inputs that are never flashed for validation exceptions.
     *
     * @var array
     */
    protected $dontFlash = [
        'current_password',
        'password',
        'password_confirmation',
    ];

    /**
     * Register the exception handling callbacks for the application.
     *
     * @return void
     */
    public function register()
    {
        $this->reportable(function (Throwable $e) {

            $backtraceInstance = SpatieBacktrace::createForThrowable($e);

            $controllerResponsible = collect($backtraceInstance->frames())
                ->filter(function (SpatieBacktraceFrame $frame) {
                    return ($frame->class);
                })
                ->filter(function (SpatieBacktraceFrame $frame) {
                    return is_subclass_of($frame->class, \App\Http\Controllers\Controller::class);
                })
                ->first();

            $this->controllerResponsible = $controllerResponsible;
        });
    }

    /**
     * Get the default context variables for logging.
     *
     * @return array
     */
    protected function context()
    {
        $extraContext = [];

        if ($this->controllerResponsible instanceof SpatieBacktraceFrame) {
            $extraContext['controller'] = $this->controllerResponsible->class;
            $extraContext['method'] = $this->controllerResponsible->method;
            $extraContext['controller@method'] = $this->controllerResponsible->class . '@' . $this->controllerResponsible->method;
        }

        return array_merge(parent::context(), $extraContext);
    }
}```

So here is what happens.

By default you can add exta [content][3] by overriding context method inside `Handler.php`. And you dont need any other custom log. It will be logged by default logging.


  

Upvotes: 1

T5k
T5k

Reputation: 51

As per @waterloomatt's comment, the Route facade seems to provide a close enough solution:

using

$route_action = Route::currentRouteAction();

we can get output

App\Http\Controllers\BestControllerEver@writeStuffToDatabase

written into the log.

While this really only returns functions that are registered as routes, knowing where users encounter an error is enough; combined with the fact a full stack trace is also included in the $exception that gets passed to logDatabaseError.

P.S.: If you use this solution, remember to set

$route_action = (Route::currentRouteAction()) ? Route::currentRouteAction() : "Not registered as route!"

just in case.

Upvotes: 0

Related Questions