Yii2: Web service Request and Response Logger - php

I am trying to create a Request and Response Logger for a web service created in Yii 2.0.5 for debugging purpose.
Basically my motive is to track all the request, request data, response data and for this purpose I am using Yii Events. So far I have coded something like this:
UserController
use yii\rest\ActiveController;
use yii\base\Event;
use yii\web\Response;
Event::on(ActiveController::className(), ActiveController::EVENT_AFTER_ACTION, ['app\models\LogHandler', 'saveRequest'], ['request' => Yii::$app->request->getRawBody(), 'response' => Yii::$app->response->content]);
class UserController extends ActiveController
{
public $modelClass = 'app\models\User';
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['contentNegotiator']['formats']['text/html'] = Response::FORMAT_XML;
return $behaviors;
}
// POST demo
public function actionDemo()
{
$data = array('status' => 200, 'message' => 'Success');
return $data;
}
}
In the above code if you have noticed then I have used the Class level Event handler (doc). Here I am trying to capture the Controllers EVENT_AFTER_ACTION event and passing the Request & Response object to my LogHandler's static method. However in my handler I am able to get the request's raw body but I am not able to get the response data which the actionDemo() is returning.
LogHandler
namespace app\models;
use yii\db\ActiveRecord;
class LogHandler extends ActiveRecord
{
public static function tableName()
{
return 'request_log';
}
public static function saveRequest($event)
{
// self::load($event);
// self::save();
var_dump($event);
}
}
How do I get the response data as well...

According to the Yii guide here, the best way to create a logger component is to override the class yii\log\Target. Then to send the logs, you need to override the abstract method export() of this class
Since this class is accessed after the life cycle of the request, you will have the request and response data Yii::$app->request and Yii::$app->response and you can access them to create whatever message you want. This is an example of the LogHandler class (this can be edited to include whatever details you want about the request and response)
namespace app\models;
use yii\db\ActiveRecord;
class LogHandler extends ActiveRecord {
$requestPath;
$responseBody;
public static function tableName() {
return 'request_log';
}
}
And an example of a logger class is as follows:
namespace app\components\Logs;
use yii\helpers\VarDumper;
use yii\log\Target;
use Yii;
class LoggerComponent extends Target {
public function export() {
$logMessage = new LogHandler();
$logMessage->requestPath = VarDumper::export(Yii::$app->request->absoluteUrl);
$logMessage->responseBody = VarDumper::export(Yii::$app->response->data);
$logMessage->save();
}
}

Related

Automatically load modules or libraries in Laravel controller or model

In my Laravel project, many Controllers and Models have the same headers.
For example, they all include
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use DB;
So, each time I ever create a new Controller, I have to insert the same header part. (like above)
Is there any way to autoload the above libraries in all controllers and models?
You could create a base class which accepts the dependencies in the constructor.
You could also create a Container class which reduces the amount of direct dependencies you have in a controller :
class Container
{
public function __construct(/* Your dependencies */) {
/* Set dependencies */
}
/* Dependency getters */
}
Controller:
class MyController
{
public function __construct(Container $container)
{
$this->container = $container;
}
public function index() {
/* Access dependencies on container */
}
}
But to be honest if you depend on request in a model class you have done something wrong. Typically in MVC the request info is passed from within the controller to the model, so the model does not know about the Request object, the values from Request are passed through as primitive values or value objects.
Update to explain my answer
You can reduce the amount of dependencies in your model classes (which is the best option) by simply following these rules, as explained here: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
So for example instead of writing:
//Note this is not a laravel specific example
class MyModel
{
private $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function getInfo()
{
return /* find info on $request->get('id'); */
}
}
You can write:
class MyController
{
public function __construct(MyModel $myModel)
{
$this->myModel = $myModel;
}
public function index()
{
$info = $this->myModel->getInfo($this->getRequest()->get('id'));
}
}
Where Request dependency is now removed from the Model:
//Note this is not a laravel specific example
class MyModel
{
public function getInfo(int $id)
{
return /* find info on $id; */
}
}
This was the simplest explanation, it will get more complex, so the best idea is to read and understand the article.

Laravel: Is there a way to reuse a method that is using a Request variable as parameter?

I want to reuse my method store that is in generar\productoController
public function store(Request $request){}
and I want to reuse it in this class adquisicion\ComprasController, I know that I have to import the class to use the method i want, but the problem is the $request variable, should I create a new object of it with $request = new Request(), adding the data I want with this and sending it as parameter?
Thx for the help I'm really new with laravel
you can try it like this $this->store(request(),$otherData)
use the helper to get the current object of request
You can pass Request data to other method
productoController(Request $request){
// anything here
return redirect('your route name')->with('data', $request->all());
}
Here are two ways that can make methods reusable in laravel application:
Make a helper method
Create a Helpers folder in app folder, and create all static methods inside a helper.php
Helper.php
namespace App\Helpers;
class Helper {
public static function store() {
$request = request();
// ....
}
}
YourController.php
namespace App\Repositories;
use App\Helpers\Helper;
use Illuminate\Http\Request;
class YourController extends Controller
{
public function store(Request $request) {
// call the store method as
Helper::store();
}
}
The downside here is you will mix up all the non-related helper methods here and may difficult to organize.
Repository
You can use a Repository Pattern to architect your application, for example, if you store a foo object to your datastore, then you can first create Repositories folder in app folder, and create FooRepository.php in Repositories folder:
FooRepository.php
namespace App\Repositories;
class FooRepository {
public function store() {
$request = request();
// ...
}
}
YourController.php
namespace App\Http\Controllers;
use App\Repositories\FooRepository;
use Illuminate\Http\Request;
class YourController extends Controller
{
private $fooRepository = null;
public function __construct(FooRepository $fooRepository) {
parent::__construct();
$this->fooRepository = $fooRepository;
}
public function store(Request $request) {
// call the method as
$this->fooRepository->store();
}
}

How to override my default response method in laravel

I have a method named response in my controller. But it shows the following error
Fatal error: Cannot redeclare response()
HomeController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class HomeController extends Controller
{
function response($params, $salt) {
if (!is_array($params))
throw new Exception('response params is empty');
if (empty($salt))
throw new Exception('Salt is empty');
if (empty($params['status']))
throw new Exception('Status is empty');
$response = new Response($salt);
$result = $response->get_response($_POST);
unset($response);
return $result;
}
}
The response method is already defined in laravel base controller and can't be overridden. Its provided by the framework as a convenience to create a new response object.
If you want to change the base response functionnality, just extend the Response class
If you want something else, just use another name.
You can extend the response class...
use Illuminate\Support\Facades\Response;
class myResponse extends Response{
public function __construct()
{
// do something cool...
}
}
Or maybe...
use Illuminate\Support\Facades\Response as BaseResponse;
class Response extends BaseResponse{
public function __construct()
{
// do something cool...
}
}
Then you need to replace Laravels facade with your own in config/app.php.
'Response' => 'Path\Facades\Response',

How to call a 'parent' trait method in PHP?

This is my code
use Illuminate\Foundation\Auth\ResetsPasswords;
class PasswordController extends Controller {
use ResetsPasswords;
public function postReset(Request $request){
// do some stuff
// ...
return parent::postReset($request); // <-here is the problem
}
The method postReset is present in ResetsPasswords, but the code I've written is looking for this method within the Controller class.
Any ideas?
use Illuminate\Foundation\Auth\ResetsPasswords;
class PasswordController extends Controller {
use ResetsPasswords {
ResetsPassword::postReset as traitPostReset;
};
public function postReset(Request $request){
// do some stuff
// ...
return $this->traitPostReset($request);
}
}
The reason why parent::postReset($request) issues a fatal error is because its parent Controller class hasn't postReset method. The trait isn't considered as a parent in that case even if it has an implementation of postReset() method.
To work-around this, you can give it a different name:
class PasswordController extends Controller
{
use ResetsPasswords;
public function postResetPassword(Request $request) // <- giving it a different name
{
// do some stuff
// ...
return $this->postReset($request); // and calling ResetsPasswords::postReset
}

Issue with Facade and injected dependency in Laravel 4

I am having an issue getting a Facade to work properly with a dependency injected into the underlying class.
I have a class called 'Listing'. It has one dependency called 'AdvertRepository' which is an interface and a class called EloquentAdvert which implements the interface. The code for these three classes is here:
// PlaneSaleing\Providers\Listing.php
<?php namespace PlaneSaleing\Providers;
use PlaneSaleing\Repositories\Advert\AdvertRepository;
class Listing {
protected $advert;
public function __construct (AdvertRepository $advert_repository) {
$this->advert = $advert_repository;
}
public function test() {
$this->advert->test();
}
public function test2() {
echo "this has worked";
}
}
// PlaneSaleing\Repositories\Advert\AdvertRepository.php
<?php namespace PlaneSaleing\Repositories\Advert;
interface AdvertRepository {
public function test();
}
// PlaneSaleing\Repositories\Advert\EloquentAdvert.php;
<?php namespace PlaneSaleing\Repositories\Advert;
class EloquentAdvert implements AdvertRepository {
public function test() {
echo 'this has worked';
}
}
I have then created a service provider called ListingServiceProvider.php, which has the following code:
// PlaneSaleing/Providers/ListingServiceProvider.php
<?php namespace PlaneSaleing\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\App;
class ListingServiceProvider extends ServiceProvider {
public function register() {
App::bind('PlaneSaleing\Repositories\Advert\AdvertRepository', 'PlaneSaleing\Repositories\Advert\EloquentAdvert');
}
}
I also added this to the ServiceProviders array in app.php
Now, if I inject Listing as a dependency into a controller and call the test method (as shown below) Laravel correctly detects the dependency, instantiates EloquentAdvert via its binding and displays 'this has worked'.
// Controllers/TestController.php
use PlaneSaleing\Providers\Listing;
class TestController extends BaseController {
protected $listing;
public function __construct(Listing $listing) {
$this->listing = $listing;
}
public function test1() {
$this->listing->test();
}
}
Now, I then created a facade for Listing. I added a new facade as follows and added an alias in app.php:
// PlaneSaleing\Providers\ListingFacade.php
<?php namespace PlaneSaleing\Providers;
use Illuminate\Support\Facades\Facade;
class ListingFacade extends Facade {
protected static function getFacadeAccessor() {
return 'Listing';
}
}
I also added the following new lines to ListingServiceProvider.php:
<?php namespace PlaneSaleing\Providers;
use PlaneSaleing\Repositories\Advert\AdvertRepository;
use PlaneSaleing\Repositories\Advert\EloquentAdvert;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\App;
class ListingServiceProvider extends ServiceProvider {
public function register() {
App::bind('PlaneSaleing\Repositories\Advert\AdvertRepository', 'PlaneSaleing\Repositories\Advert\EloquentAdvert');
// New lines...
$this->app['Listing'] = $this->app->share(function() {
return new Listing(new AdvertRepository);
});
}
}
NOW...if I call Listing::test(), I get the following error: Cannot instantiate interface PlaneSaleing\Repositories\Advert\AdvertRepository.
If I call Listing::test2() , I get 'this has worked' so it seems the Facade is working correctly.
It seems that when accessing Listing via its Facade the binding between AdvertRepository and EloquentAdvert doesnt work. I have looked at my code in the ServiceProvider thinking it was the issue, but I cant figure it out.
Both the Facade and binding work when tested individually but not when both are used at the same time.
Any ideas???
OK, So I have figured it out...For those who run into a similar problem...
The offending statement was in ListingServiceProvider.php which read:
$this->app['Listing'] = $this->app->share(function() {
return new Listing(new AdvertRepository);
});
The error is the new AdvertRepository statement. The reason being is that, we are telling php to directly instantiate the interface 'AdvertRepository'. Instead, we need to tell Laravel to instantiate the appropriate implementation of the 'AdvertRepository' interface. To do that, we use App::make('AdvertRepository'). That way, Laravel uses the binding previously declared to instantiate the correct implementation.
If your constructor is not being inject with a class, you must tell Laravel what class will be used when it needs to instantiate a particular interface:
Put this in your filters or bindings file:
App::bind('PlaneSaleing\Repositories\Advert\AdvertRepository', function()
{
return new PlaneSaleing\Repositories\Advert\EloquentAdvert;
});

Categories