How to add auth user id or session to logging in Laravel 5.7? - laravel

My Goal
I'm willing to add the current authenticated user id to log lines (I'm using syslog driver) as a custom log formatting.
As part of my research, I've seen there are nice packages and tutorials to log user activity to database. But at least for now my intention is to move forward without extra packages and logging to syslog as I'm currently doing.
A typical log call in my app would look like:
logger()->info("Listing all items");
I could also do something like:
$uid = Auth::id();
logger()->info("Listing all items for {$uid}");
It would be similar to what a documentation example suggests:
I'm currently doing this for certain lines, but since I'd like to have it in all log calls it would become like repeating myself every time I log something.
Desired Output
Current:
Dec 10 23:54:05 trinsic Laravel[13606]: local.INFO: Hello world []
Desired:
Dec 10 23:54:05 trinsic Laravel[13606]: local.INFO [AUTH_USER_ID=2452]: Hello world []
My Approach
I have tried to tap the logger with success on changing the format, as suggested on docs.
But my current problem is that at the point the CustomizeFormatter class is excecuted, the Auth id seems not to be resolved just yet (not sure about this, but dd() returns null, so that's my guess):
<?php
namespace App\Logging;
use Illuminate\Support\Facades\Auth;
use Monolog\Formatter\LineFormatter;
class CustomizeFormatter
{
/**
* Customize the given logger instance.
*
* #param \Illuminate\Log\Logger $logger
* #return void
*/
public function __invoke($logger)
{
$authUser = Auth::id(); // null
foreach ($logger->getHandlers() as $handler) {
$formatter = new LineFormatter("%channel%.%level_name% [AUTH={$authUser}]: %message% %extra%");
$handler->setFormatter($formatter);
}
}
}
Setup
Laravel 5.7
Log Driver: syslog
// config/logging.php
'syslog' => [
'driver' => 'syslog',
'tap' => [ App\Logging\CustomizeFormatter::class ],
'level' => 'debug',
],
My Question(s)
Is there any way to resolve the auth user at this point, or any other approach to achieve this?

Try passing the request object as a constructor arg to your custom formatter class, then access the user from there:
<?php
namespace App\Logging;
use Illuminate\Support\Facades\Auth;
use Monolog\Formatter\LineFormatter;
class CustomizeFormatter
{
public $request;
public function __construct(Request $request)
{
$this->request = $request;
}
/**
* Customize the given logger instance.
*
* #param \Illuminate\Log\Logger $logger
* #return void
*/
public function __invoke($logger)
{
$authUser = $this->request->user();
foreach ($logger->getHandlers() as $handler) {
$formatter = new LineFormatter("%channel%.%level_name% [AUTH={$authUser->id}]: %message% %extra%");
$handler->setFormatter($formatter);
}
}
}

Based on the answer of DigitalDrifter and part of this post from Emir Karşıyakalı I managed to get a fair enough solution.
I could not grab the User ID since (as per my understanding at this point) it can't be resolved just yet. But I felt satisfied with getting a more or less accurate client id and session id, so I can trace a user interaction thread on logs:
<?php
namespace App\Loggers;
use Monolog\Formatter\LineFormatter;
class LocalLogger
{
private $request;
public function __construct(\Illuminate\Http\Request $request)
{
$this->request = $request;
}
public function __invoke($logger)
{
foreach ($logger->getHandlers() as $handler) {
$handler->setFormatter($this->getLogFormatter());
}
}
protected function getLogFormatter()
{
$uniqueClientId = $this->getUniqueClientId();
$format = str_replace(
'[%datetime%] ',
sprintf('[%%datetime%%] %s ', $uniqueClientId),
LineFormatter::SIMPLE_FORMAT
);
return new LineFormatter($format, null, true, true);
}
protected function getUniqueClientId()
{
$clientId = md5($this->request->server('HTTP_USER_AGENT').'/'.$this->request->ip());
$sessionId = \Session::getId();
return "[{$clientId}:{$sessionId}]";
}
}
Config:
// config/logger.php
'syslog' => [
'driver' => 'syslog',
'level' => 'debug',
'tap' => [App\Loggers\LocalLogger::class],
],
Result
Now I can get something like:
Dec 11 13:54:13 trinsic Fimedi[13390]: [2018-12-11 16:54:13] c6c6cb03fafd4b31493478e76b490650 local.INFO: Hello world
or
Dec 11 13:55:44 trinsic Fimedi[13390]: [2018-12-11 16:55:44] [c6c6cb03fafd4b31493478e76b490650:xUs2WxIb3TvKcpCpFNPFyvBChE88Nk0YbwZI3KrY] local.INFO: Hello world
Depending on how I calculate the user/session ids.

Related

Laravel request remove fields before validation

I am trying to remove some fields before they are validated.
Trying to attempt this inside prepareForValidation()
use Illuminate\Foundation\Http\FormRequest;
class VideoRequest extends ApiRequest
{
// ..code..
protected function prepareForValidation()
{
$this->merge([
'public' => $this->toBoolean($this->public),
'notify' => $this->toBoolean($this->notify),
]);
$video_id = $this->route('video_id');
if($this->isMethod('put') && Video::salesStarted($video_id)){
Log::info('removing sales');
// attempt 1
$this->except(['sales_start', 'tickets', 'price']);
// attempt 2
$this->request->remove('sales_start');
// attempt 3
$this->offsetUnset('sales_start');
}
if($this->isMethod('put') && Video::streamStarted($video_id)){
Log::info('removing streams');
// attempt 1
$this->except(['stream_start', 'stream_count', 'archive']);
// attempt 2
$this->request->remove('sales_start');
// attempt 3
$this->offsetUnset('sales_start');
}
$thumb = $this->uploadThumbnail($video_id);
if($thumb !== null){
$this->merge($thumb);
}
}
// ..code..
}
I made sure the code was entering inside the if statement, however the fields are not being removed.
Running $this->request->remove() and $this->except() have no effect.
If I add safe() it throws Call to a member function safe() on null.
I also tried to use unset() but nothing seems to work.
The rules for the dates are like so:
'sales_start' => 'sometimes|required|date|after:now|before:stream_start',
'stream_start' => 'sometimes|required|date|after:now',
but the $request->validated() returns the errors although it shouldn't be validating the deleted fields.
"sales_start": [
"The sales start must be a date after now."
],
"stream_start": [
"The stream start must be a date after now."
]
Why are the fields not being deleted?
Edit
As requested I added some code.
This is what ApiRequest looks like:
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
abstract class ApiRequest extends FormRequest
{
protected function failedValidation(Validator $validator): void
{
$response['data'] = [];
$response['api_status'] = 'ng';
$response['status_message'] = 'failed validation';
$response['errors'] = $validator->errors()->toArray();
throw new HttpResponseException(
response()->json( $response, 422 )
);
}
protected function toBoolean($booleable)
{
return filter_var($booleable, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
}
}
And the request is called from the controller like so:
public function update(VideoRequest $request, $video_id)
{
... some code ...
$validated = $request->validated();
... some code ...
}
so $this refers to the VideoRequest that extends FormRequest.
Can't find anything about deleting. But acording to Laravel docs you pick what keys you need from a request as follows:
$request->only(['username', 'password']);
// plug everything you need into the array.
$request->except(['username', 'password']);
//plug everything you don't need into the array
The latter is probably most useful to you.
Example:
Say I have the following keys: ['username', 'password', 'randomVal'];
$request->only(['username', 'password']);
// Output:
['username', 'password']
$request->except(['username', 'password']);
// Output:
['randomVal']
To remove (unset) a key from a Request before it goes to the Controller you can use offsetUnset()
inside your request:
protected function prepareForValidation()
{
$this->offsetUnset('sales_start');//same goes for the other key to remove...
}
This is a bit of an ugly answer.
Instead of modifying the request before the validation, I tried adding exclude when getting rules().
So something along these lines:
public function rules() {
$ex = $this->isMethod('put') && Video::salesStarted($video_id) ? 'exclude|' : '';
return [
'sales_start' => $ex.'sometimes|required|other_stuff',
];
}
Note that the validation 'exclude' only works if added first.
So this won't work:
'sometimes|required|other_stuff|exclude' //exclude is called last
I am still unable to find out why remove(), exclude(), offsetUnset() were not working, so this is not the right answer, but I hope it helps if someone is having the same issue.
Edit
Setting this as correct answer as I was unable to find an alternative solution/fix.

Why using additive paramerer in Resource collection raised error?

In laravel 9 app I want to add additive paramerer into Resource and looking at this
Laravel 5.6 - Pass additional parameters to API Resource?
branch I remade in app/Http/Resources/CurrencyResource.php :
<?php
namespace App\Http\Resources;
use App\Library\Services\DateFunctionalityServiceInterface;
use Illuminate\Http\Resources\Json\JsonResource;
use App;
use Illuminate\Support\Facades\File;
use Spatie\Image\Image;
use App\Http\Resources\MediaImageResource;
class CurrencyResource extends JsonResource
{
protected $show_default_image = false;
public function showDefaultImage($value){
$this->show_default_image = $value;
return $this;
}
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
*
* #return array
*/
public function toArray($request)
{
$dateFunctionality = App::make(DateFunctionalityServiceInterface::class);
$currencyImage = [];
$currencyMedia = $this->getFirstMedia(config('app.media_app_name'));
if ( ! empty($currencyMedia) and File::exists($currencyMedia->getPath())) {
$currencyImage['url'] = $currencyMedia->getUrl();
$imageInstance = Image::load($currencyMedia->getUrl());
$currencyImage['width'] = $imageInstance->getWidth();
$currencyImage['height'] = $imageInstance->getHeight();
$currencyImage['size'] = $currencyMedia->size;
$currencyImage['file_title'] = $currencyMedia->file_name;
}
else {
\Log::info( varDump($this->show_default_image, ' -1 $this->show_default_image::') );
$currencyImage['url'] = $this->show_default_image ? '/images/default-currency.jpg' : '';
}
// $currencyMedia = $currency->getFirstMedia(config('app.media_app_name'));
return [
'id' => $this->id,
'name' => $this->name,
...
and with code in control :
'currency' => (new CurrencyResource($currency))->showDefaultImage(false),
its work ok, but I got an error :
Method Illuminate\Support\Collection::showDefaultImage does not exist.
when I applyed this method for collection:
return (CurrencyResource::collection($currencies))->showDefaultImage(true);
But in link above there is a similar way :
UserResource::collection($users)->foo('bar');
What is wrong in my code and how that can be fixed ?
Thanks!
I wonder if there is a reason you can't use this approach: https://stackoverflow.com/a/51689732/8485567
Then you can simply use the request parameters to modify your response.
If you really want to get that example working in that way, it seems you are not following the example correctly.
You need to override the static collection method inside your CurrencyResource class:
public static function collection($resource){
return new CurrencyResourceCollection($resource);
}
You also need to create the CurrencyResourceCollection class and define the showDefaultImage method and $show_default_image property on that class as in the example you referred to.
Then you should be able to do:
CurrencyResource::collection($currencies)->showDefaultImage(true);
The reason the way you are doing it doesn't work is because you haven't defined the static collection method on your resource hence it's defaulting to the normal behavior of returning a default collection object as you can see in your error message.

Operation without entity

I've been looking for a solution for a while but none of the one I find really allows me to do what I want. I would just like to create routes that don't necessarily require an entity or id to be used. Can you help me the documentation is not clear to do this.
Thank you beforehand.
As you can read in the General Design Considerations, just make an ordinary PHP class (POPO). Give it an ApiResource annontation like this:
* #ApiResource(
* collectionOperations={
* "post"
* },
* itemOperations={}
* )
Make sure the folder your class is in is in the paths list in api/config/packages/api_platform.yaml. There usually is the following configuration:
api_platform:
mapping:
paths: ['%kernel.project_dir%/src/Entity']
You should add your path if your class is not in the Entity folder.
Api Platform will expect json to be posted and try to unserialize it into an instance of your class. Make a custom DataPersister to process the instance, for example if your class is App\ApiCommand\Doit:
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\ApiCommand\Doit;
use App\ApiResult\DoitResult;
final class DoitDataPersister implements ContextAwareDataPersisterInterface
{
public function supports($data, array $context = []): bool
{
return $data instanceof Doit;
}
public function persist($data, array $context = [])
{
// code to process $data
$result = new DoitResult();
$result->description = 'Hello world';
return $result;
}
public function remove($data, array $context = [])
{
// will not be called if you have no delete operation
}
}
If you need Doctrine, add:
public function __construct(ManagerRegistry $managerRegistry)
{
$this->managerRegistry = $managerRegistry;
}
See Injecting Extensions for how to use it.
Notice that the result returned by ::persist is not an instance of Doit. If you return a Doit api platform will try to serialize that as the result of your operation. But we have marked Doit as an ApiResource so (?) api platform looks for an item operation that can retrieve it, resulting in an error "No item route associated with the type App\ApiCommand\Doit". To avoid this you can return any object that Symfonies serializer can serialize that is not an ApiResource. In the example an instance of DoitResult. Alternatively you can return an instance of Symfony\Component\HttpFoundation\Response but then you have to take care of the serialization yourself.
The post operation should already work, but the swagger docs are made from metadata. To tell api platform that it should expect a DoitResult to be returned, change the #ApiResource annotation:
* collectionOperations={
* "post"={
* "output"=DoitResult::class
* }
* },
This will the add a new type for DoitResult to the swagger docs, but the descriptions are still wrong. You can correct them using a SwaggerDecorator. Here is one for a 201 post response:
namespace App\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class SwaggerDecorator implements NormalizerInterface
{
private $decorated;
public function __construct(NormalizerInterface $decorated)
{
$this->decorated = $decorated;
}
public function normalize($object, string $format = null, array $context = [])
{
$summary = 'short explanation about DoitResult';
$docs = $this->decorated->normalize($object, $format, $context);
$docs['paths']['/doit']['post']['responses']['201']['description'] = 'Additional explanation about DoitResult';
$responseContent = $docs['paths']['/doit']['post']['responses']['201']['content'];
$this->setByRef($docs, $responseContent['application/ld+json']['schema']['properties']['hydra:member']['items']['$ref'],
'description', $summary);
$this->setByRef($docs, $responseContent['application/json']['schema']['items']['$ref'],
'description', $summary);
return $docs;
}
public function supportsNormalization($data, string $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
private function setByRef(&$docs, $ref, $key, $value)
{
$pieces = explode('/', substr($ref, 2));
$sub =& $docs;
foreach ($pieces as $piece) {
$sub =& $sub[$piece];
}
$sub[$key] = $value;
}
}
To configure the service add the following to api/config/services.yaml:
'App\Swagger\SwaggerDecorator':
decorates: 'api_platform.swagger.normalizer.api_gateway'
arguments: [ '#App\Swagger\SwaggerDecorator.inner' ]
autoconfigure: false
If your post operation is not actually creating something you may not like the 201 response. You can change that by specifying the response code in the #ApiResource annotation, for example:
* collectionOperations={
* "post"={
* "output"=DoitResult::class,
* "status"=200
* }
* },
You may want to adapt the SwaggerDecorator accordingly.
Creating a "get" collection operation is similar, but you need to make a DataProvider instead of a DataPersister. The chapter9-api branch of my tutorial contains an example of a SwaggerDecorator for a collection response.
Thanks you for answer. I had some information but not everything. I will try the weekend.

Why is my validation rule not working in my Laravel Controller?

I am trying to write a test that checks that a date is formatted correctly. Looking at the docs it seems pretty straight-forward.
Here is my test. I am submitting an invalid date format to my controller:
MyTest.php
/** #test */
public function a_date_must_be_formatted_correctly()
{
$foo = factory(Foo::class)->raw([
'date' => date('d/m/Y'),
]);
$response = $this->postJson('api/v1/foo', $foo)
->assertStatus(422)
->assertJsonValidationErrors('date');
}
Here is my controller method:
public function store(Request $request)
{
$attributes = $request->validate([
'date' => 'required|date_format:Y-m-d',
]);
...
}
I get a passing test each time.
I have also tried wrapping the format in quotes: 'date' => 'required|date_format:"Y-m-d"',
I'm expecting to get a 422 back saying my date is invalid. What am I doing wrong?
Well, I ended up writing a custom rule. I don't think this is the correct solution though given that Laravel has the handy built-in stuff. I feel like I should be leveraging that instead. In my case I am working with Blackout date(s).
Hers is what I ended up with. If there is a way to make the built-in methods work, please let me know so I can update the answer.
MyTest.php
/** #test */
public function a_blackout_date_must_be_formatted_correctly()
{
$blackout = factory(Blackout::class)->raw([
'date' => date('d/m/Y'),
]);
$response = $this->postJson('api/v1/blackouts', $blackout)
->assertStatus(422)
->assertJsonValidationErrors('date');
}
MyController.php
public function store(Request $request)
{
$attributes = $request->validate([
'date' => ['required', new DateFormatRule],
...
]);
$blackout = new Blackout($attributes);
...
}
DateFormatRule.php
public function passes($attribute, $value)
{
// The format we're looking for.
$format = 'Y-m-d';
// Create a new DateTime object.
$date = DateTime::createFromFormat($format, $value);
// Verify that the date provided is formatted correctly.
return $date && $date->format($format) === $value;
}
/**
* Get the validation error message.
*
* #return string
*/
public function message()
{
return 'Invalid date format.';
}
In my test I am getting a 422 response as expected. I can verify things are working by changing the name of the expected error from date to date123 and I'll get an error -- saying It was expecting date not date123.
Hope this helps someone!

Laravel: Is possible send notification with delay, but changing smtp settings dynamically?

I'm developing a Multi Tenant (multiple database) with Laravel v5.7 and I'm successful in sending queue emails.
In some specific situations, I'd like to send on-demand notifications with 'delay', similar to the guide On-Demand Notifications, but informing the SMTP settings that should be used before sending.
I've developed a class that changes the values of config().
app/Tenant/SmtpConfig.php
class SmtpConfig
{
public static function setConnection(SmtpConta $conta = null)
{
// get connection default settings
$config = config()->get("mail");
// populate connection default settings
foreach ($config as $key => $value) {
if ( $key == 'host' ) { $config[$key] = $conta->mail_host ?? $config[$key]; }
if ( $key == 'from' ) { $config[$key] = [
'address' => ( $conta->mail_host === 'smtp.mailtrap.io' ) ? $config[$key]['address'] : $conta->mail_username,
'name' => $conta->conta ?? $config[$key]['name']
]; }
if ( $key == 'username' ) { $config[$key] = $conta->mail_username ?? $config[$key]; }
if ( $key == 'password' ) { $config[$key] = !empty($conta->mail_password) ? $conta->mail_password : $config[$key]; }
}
$config['encryption'] = ( $conta->mail_host === 'smtp.mailtrap.io' ) ? null : 'ssl';
// set connection default settings
config()->set("mail", $config);
}
}
... and I call this SmtpConfig class in notification:
/**
* Create a new notification instance.
*
* #param $conta
* #param $subject
* #return void
*/
public function __construct(SmtpConta $conta = null, $subject = null)
{
$this->conta = $conta;
$this->subject = $subject;
$when = \Carbon\Carbon::now()->addSecond(100);
$this->delay($when);
app(\App\Tenant\SmtpConfig::class)::setConnection($this->conta);
}
I can send the 'delayed' notification successfully, but apparently it always uses the default values of the .env file.
Now I'm not sure if where I'm calling the class makes any sense or even how can I tell the notification what SMTP configuration it should use.
I'm currently facing a similar challenge, on a Laravel 5.2 codebase using the Notification backport library.
This is an example of my solution, similar to Kit Loong's suggestion. We just extend the Illuminate\Notifications\Channels\MailChannel class and override the send() method.
You'll need to be able to determine the SMTP config from the recipient(s), or notification objects, so you'll need to edit my example as necessary.
Also this assumes your app is using the default Swift_Mailer so YMMV...
<?php
declare (strict_types = 1);
namespace App\Notifications\Channels;
use Illuminate\Notifications\Channels\MailChannel;
use Illuminate\Notifications\Notification;
class DynamicSmtpMailChannel extends MailChannel
{
/**
* Send the given notification.
*
* #param mixed $notifiable
* #param \Illuminate\Notifications\Notification $notification
* #return void
*/
public function send($notifiable, Notification $notification)
{
//define this method on your model (note $notifiable could be an array or collection of notifiables!)
$customSmtp = $notifiable->getSmtpConfig();
if ($customSmtp) {
$previousSwiftMailer = $this->mailer->getSwiftMailer();
$swiftTransport = new \Swift_SmtpTransport(
$customSmtp->smtp_server,
$customSmtp->smtp_port,
$customSmtp->smtp_encryption
);
$swiftTransport->setUsername($customSmtp->smtp_user);
$swiftTransport->setPassword($customSmtp->smtp_password);
$this->mailer->setSwiftMailer(new \Swift_Mailer($swiftTransport));
}
$result = parent::send($notifiable, $notification);
if (isset($previousSwiftMailer)) {
//restore the previous mailer
$this->mailer->setSwiftMailer($previousSwiftMailer);
}
return $result;
}
}
It may also be beneficial to keep an ephemeral store of custom swift mailers so you can re-use them in the same invokation/request (think about long-running workers) - like a collection class where a hash of the smtp config is used as the item key.
Best of luck with it.
Edit:
I should probably mention you may need to bind this in the service container. Something like this should suffice:
// in a service provider
public function register()
{
$this->app->bind(
\Illuminate\Notifications\Channels\MailChannel::class
\App\Notifications\Channels\DynamicSmtpMailChannel::class
);
}
Or alternatively, register it as a seperate notification channel.
I think you can also refer to this implementation.
https://stackoverflow.com/a/46135925/6011908
You could execute by passing custom smtp configs.
$transport = new Swift_SmtpTransport(
$customSmtp->host,
$customSmtp->port,
$customSmtp->encryption
);

Categories

Resources