Symfony notifier attach custom metadata to envelope - php

I'm using the Symfony notifier and messenger components to asynchronously send SMS messages (and in the future push and email notifications).
Everything works just fine, however once a message is sent, I'd like to log information about it.
I can catch a successful message by subscribing to WorkerMessageHandledEvent which provides me the Message object, along with the containing Envelope and all its Stamp objects inside. From all the available information, I'll be logging this in my database using an entity named MessageLog.
class MessengerSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents() {
return [
WorkerMessageHandledEvent::class => ['onHandled']
];
}
public function onHandled(WorkerMessageHandledEvent $event) {
$log = new MessageLog();
$log->setSentAt(new DateTime());
if($event->getEnvelope()->getMessage() instanceof SmsMessage) {
$log->setSubject($event->getEnvelope()->getMessage()->getSubject());
$log->setRecipient($event->getEnvelope()->getMessage()->getPhone());
}
// Do more tracking
}
}
What I'd like to do, is track the object that "invoked" the message. For example, if I have a news feed, and posting a post sends out a notification, I'd like to attribute each logged message to that post (to display audience reach/delivery stats per post - and from an admin POV auditing and reporting).
I've tried to go about adding a Stamp, or other means of trying to attach custom metadata to the message, but it seems to be abstracted when using the symfony/notifier bundle.
The below is what I'm using to send notifications (more or less WIP):
class PostService {
protected NotifierInterface $notifier;
public function ___construct(NotifierInterface $notifier) {
$this->notifier = $notifier;
}
public function sendNotifications(Post $post) {
$notification = new PostNotification($post);
$recipients = [];
foreach($post->getNewsFeed()->getSubscribers() as $user) {
$recipients[] = new Recipient($user->getEmail(), $user->getMobilePhone());
}
$this->notifier->send($notification, ...$recipients);
}
}
class PostNotification extends Notification implements SmsNotificationInterface {
protected Post $post;
public function __construct(Post $post) {
parent::__construct();
$this->post = $post;
}
public function getChannels(RecipientInterface $recipient): array {
return ['sms'];
}
public function asSmsMessage(SmsRecipientInterface $recipient, string $transport = null): ?SmsMessage {
if($transport === 'sms') {
return new SmsMessage($recipient->getPhone(), $this->getPostContentAsSms());
}
return null;
}
private function getPostContentAsSms() {
return $post->getTitle()."\n\n".$post->getContent();
}
}
By the time this is all done, this is all I have in the WorkerMessageHandledEvent
^ Symfony\Component\Messenger\Event\WorkerMessageHandledEvent^ {#5590
-envelope: Symfony\Component\Messenger\Envelope^ {#8022
-stamps: array:7 [
"Symfony\Component\Messenger\Stamp\BusNameStamp" => array:1 [
0 => Symfony\Component\Messenger\Stamp\BusNameStamp^ {#10417
-busName: "messenger.bus.default"
}
]
"Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceivedStamp" => array:1 [
0 => Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceivedStamp^ {#10419
-id: "2031"
}
]
"Symfony\Component\Messenger\Stamp\TransportMessageIdStamp" => array:1 [
0 => Symfony\Component\Messenger\Stamp\TransportMessageIdStamp^ {#10339
-id: "2031"
}
]
"Symfony\Component\Messenger\Stamp\ReceivedStamp" => array:1 [
0 => Symfony\Component\Messenger\Stamp\ReceivedStamp^ {#5628
-transportName: "async"
}
]
"Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp" => array:1 [
0 => Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp^ {#7306}
]
"Symfony\Component\Messenger\Stamp\AckStamp" => array:1 [
0 => Symfony\Component\Messenger\Stamp\AckStamp^ {#7159
-ack: Closure(Envelope $envelope, Throwable $e = null)^ {#6205
class: "Symfony\Component\Messenger\Worker"
this: Symfony\Component\Messenger\Worker {#5108 …}
use: {
$transportName: "async"
$acked: & false
}
}
}
]
"Symfony\Component\Messenger\Stamp\HandledStamp" => array:1 [
0 => Symfony\Component\Messenger\Stamp\HandledStamp^ {#11445
-result: Symfony\Component\Notifier\Message\SentMessage^ {#2288
-original: Symfony\Component\Notifier\Message\NullMessage^ {#6625
-decoratedMessage: Symfony\Component\Notifier\Message\SmsMessage^ {#10348
-transport: null
-subject: ".................................................."
-phone: "0412345678"
}
}
-transport: "null"
-messageId: null
}
-handlerName: "Symfony\Component\Notifier\Messenger\MessageHandler::__invoke"
}
]
]
-message: Symfony\Component\Notifier\Message\SmsMessage^ {#10348}
}
-receiverName: "async"
}
The doco shows me ways to add my own stamps to the envelope, which I'm guessing I can use to attach metadata such as my Post object, but this means I need to use the MessageBusInterface to send notifications. I don't want to do because I would like to route messages through the NotifierInterface to gain all the benefits of channel policies, texter transports, etc.
tl;dr: how do I get some metadata through to a WorkerMessageHandledEvent if I send a message using the NotifierInterface

I've found a way to make it work!
Essentially what happens is that we have two components here, the Symfony notifier and the Symfony messenger. When used together, they create a powerful way to send messages to any number of endpoints.
Firstly what I did was create an interface called NotificationStampsInterface and a trait called NotificationStamps that satisfies the interface (by storing a protected array using the interface methods to read/write to it).
class NotificationStampsInterface {
public function getStamps(): array;
public function addStamp(StampInterface $stamp);
public function removeStamp(StampInterface $stamp);
}
This interface can then be added onto your custom notification object, in this instance PostNotification, alongside with the NotificationStamps trait to satisfy the interface methods.
The trick here is that when sending a notification via the notifier, it ultimately calls on the messenger component to send the message. The bit that handles this is Symfony\Component\Notifier\Channel\SmsChannel. Essentially, if a MessageBus is available, it will push messages through that rather than going straight though the notifier.
We can extend the SmsChannel class to add our own logic inside notify() method.
class SmsNotify extends \Symfony\Component\Notifer\Channel\SmsChannel {
public function notify(Notification $notification, RecipientInterface $recipient, string $transportName = null): void {
$message = null;
if ($notification instanceof SmsNotificationInterface) {
$message = $notification->asSmsMessage($recipient, $transportName);
}
if (null === $message) {
$message = SmsMessage::fromNotification($notification, $recipient);
}
if (null !== $transportName) {
$message->transport($transportName);
}
if (null === $this->bus) {
$this->transport->send($message);
} else {
// New logic
if($notification instanceof NotificationStampsInterface) {
$envelope = Envelope::wrap($message, $notification->getStamps());
$this->bus->dispatch($envelope);
} else {
$this->bus->dispatch($message);
}
// Old logic
// $this->bus->dispatch($message);
}
}
}
Lastly we need to override the service by adding the following in services.yaml
notifier.channel.sms:
class: App\Notifier\Channel\SmsChannel
arguments: ['#texter.transports', '#messenger.default_bus']
tags:
- { name: notifier.channel, channel: sms }
And that's it! We now have a way to append stamps to our Notification object that will carry all the way through to the WorkerMessageHandledEvent.
An example use would be (for my situation at least)
class RelatedEntityStamp implements StampInterface {
private string $className;
private int $classId;
public function __construct(object $entity) {
$this->className = get_class($entity);
$this->classId = $entity->getId();
}
/**
* #return string
*/
public function getClassName(): string {
return $this->className;
}
/**
* #return int
*/
public function getClassId(): int {
return $this->classId;
}
}
class PostService {
protected NotifierInterface $notifier;
public function ___construct(NotifierInterface $notifier) {
$this->notifier = $notifier;
}
public function sendNotifications(Post $post) {
$notification = new PostNotification($post);
$stamp = new RelatedEntityStamp($post); // Solution
$notification->addStamp($stamp); // Solution
$recipients = [];
foreach($post->getNewsFeed()->getSubscribers() as $user) {
$recipients[] = new Recipient($user->getEmail(), $user->getMobilePhone());
}
$this->notifier->send($notification, ...$recipients);
}
}
Once the message is sent, dumping the result shows that we do indeed have our stamp registered at the point where our event fires.
^ Symfony\Component\Messenger\Event\WorkerMessageHandledEvent^ {#1078
-envelope: Symfony\Component\Messenger\Envelope^ {#1103
-stamps: array:8 [
"App\Notification\Stamp\RelatedEntityStamp" => array:1 [
0 => App\Notification\Stamp\RelatedEntityStamp^ {#1062
-className: "App\Entity\Post"
-classId: 207
}
]
"Symfony\Component\Messenger\Stamp\BusNameStamp" => array:1 [
0 => Symfony\Component\Messenger\Stamp\BusNameStamp^ {#1063
-busName: "messenger.bus.default"
}
]
"Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceivedStamp" => array:1 [
0 => Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceivedStamp^ {#1066
-id: "2590"
}
]
"Symfony\Component\Messenger\Stamp\TransportMessageIdStamp" => array:1 [
0 => Symfony\Component\Messenger\Stamp\TransportMessageIdStamp^ {#1067
-id: "2590"
}
]
"Symfony\Component\Messenger\Stamp\ReceivedStamp" => array:1 [
0 => Symfony\Component\Messenger\Stamp\ReceivedStamp^ {#1075
-transportName: "async"
}
]
"Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp" => array:1 [
0 => Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp^ {#1076}
]
"Symfony\Component\Messenger\Stamp\AckStamp" => array:1 [
0 => Symfony\Component\Messenger\Stamp\AckStamp^ {#1077
-ack: Closure(Envelope $envelope, Throwable $e = null)^ {#1074
class: "Symfony\Component\Messenger\Worker"
this: Symfony\Component\Messenger\Worker {#632 …}
use: {
$transportName: "async"
$acked: & false
}
}
}
]
"Symfony\Component\Messenger\Stamp\HandledStamp" => array:1 [
0 => Symfony\Component\Messenger\Stamp\HandledStamp^ {#1101
-result: Symfony\Component\Notifier\Message\SentMessage^ {#1095
-original: Symfony\Component\Notifier\Message\NullMessage^ {#1091
-decoratedMessage: Symfony\Component\Notifier\Message\SmsMessage^ {#1060
-transport: null
-subject: ".................................................."
-phone: "0412345678"
}
}
-transport: "null"
-messageId: null
}
-handlerName: "Symfony\Component\Notifier\Messenger\MessageHandler::__invoke"
}
]
]
-message: Symfony\Component\Notifier\Message\SmsMessage^ {#1060}
}
-receiverName: "async"
}

Related

Laravel Cache helper "driver" not implemented

I have a test that is calling a Controller, which literally handles a call to some logic service I mocked.
The test
public function test_it_can_list_all_folders()
{
$mockedLogicResponse = [
"id" => 1111,
"parent_id" => 2222,
"name" => "Some Name",
"children" => [
[
"id" => 3333,
"parent_id" => 5555,
"name" => "Ad-hoc"
],
[
"id" => 4444,
"parent_id" => 6666,
"name" => "Another thing"
]
]
];
Cache::shouldReceive('has')
->once()
->with('campaign_folders')
->andReturn(false);
$this->instance(
Logic::class,
Mockery::mock(Logic::class, function (MockInterface $mock) use ($mockedLogicResponse) {
$mock->shouldReceive('fetchData')
->once()
->andReturn($mockedLogicResponse);
})
);
// httpGet is just a wrapper for a call('GET', ..), it's tested and working fine
$response = $this->httpGet($route);
$response->assertOk(); //This goes well
$this->assertEquals($mockedLogicResponse, $response->json()); //This goes well too
}
Controller:
class LogicController extends Controller {
protected $logic;
public function __construct(Logic $logic)
{
$this->logic = $logic;
}
public function index(Request $request)
{
$id = $request->get('folder_id');
return $this->logic->fetchData($id);
}
}
Logic:
class Logic {
public function fetchData(string $id): array
{
if (Cache::has('folders')) {
return Cache::get('folders');
}
//This is returning correctly the data
$foldersList = $this->getFolders(...);
foreach ($foldersList[$folder['id']] as $folder) {
$res = [....];
// We perform some irrelevant logic
$children = ['children' => $res[$folder['id']]];
$fetchedFolders[] = array_merge($folder, $children);
}
Cache::put('folders', $fetchedFolders, 3600);
return $fetchedFolders;
}
}
The problem(s) are a few, for starters I'm receiving this:
Mockery\Exception\BadMethodCallException : Received Mockery_2_Illuminate_Cache_CacheManager::driver(), but no expectations were specified
It's good to point out that I am literally copying an example from the documentation here Laravel docs, so I can't see I'm missing any step.
Also, as the Cache is being called from the mocked logic, but the method is calling them (I dumped the result of the "has" Cache method)
As I also want to test (In another test) that the Cache::get() is begin called when requested the data for a second time, how can I clean the Cache (I set it for an hour), in order to test something like so:
Cache::shouldReceive('has')->twice();
Cache::shouldReceive('get')->once();
Cache::shouldReceive('put')->once();
Is there any step I am missing? If so, which ones?
UPDATE: After googling a bit, I found this solution, which in part solves my testing issues, but I'm concerned why the official documentation is not working, in order to use it instead of a custom solution.
Kind regards

Handling rate limits when using an API in Laravel

In my Laravel application I am using the HubSpot API extensively to perform various actions. I have read in the documentation that you can make 150 requests per each 10 second period.
To monitor this HubSpot provide the following Headers when making any API call.
"X-HubSpot-RateLimit-Daily" => array:1 [▶]
"X-HubSpot-RateLimit-Daily-Remaining" => array:1 [▶]
"X-HubSpot-RateLimit-Interval-Milliseconds" => array:1 [▶]
"X-HubSpot-RateLimit-Max" => array:1 [▶]
"X-HubSpot-RateLimit-Remaining" => array:1 [▶]
"X-HubSpot-RateLimit-Secondly" => array:1 [▶]
"X-HubSpot-RateLimit-Secondly-Remaining" => array:1 [▶]
In my application I am making use of Laravel's Http Client, which is basically just a wrapper for Guzzle.
In order to adhere to the rate limits would I literally just have to wrap an if statement around every request?
Here's an example:
$endpoint = 'https://api.hubapi.com/crm/v3/owners/';
$response = Http::get($endpoint, [
'limit' => 100,
'hapikey' => config('hubspot.api_key'),
]);
In this case the $response would contain the Headers but would there be a way to effectively use them, as surely I would only know what the rates were once I'd made the API call?
I ask as I have to pull down 1,000 + deals and then update some records, but this would definately go over the API limit. For reference, here is the command I wrote.
<?php
namespace App\Console\Commands;
use App\Events\DealImportedFromHubspot;
use App\Hubspot\PipelineHubspot;
use App\Models\Deal;
use App\Models\DealStage;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
class ImportHubspotDeals extends Command
{
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'import:hubspot-deals
{--force : Whether we should force the command}
';
/**
* The console command description.
*
* #var string
*/
protected $description = 'Import Deal objects from the HubSpot API in bulk.';
/**
* An array to store imported Deals
*
* #var array
*/
private $importedDeals = [];
/**
* Create a new command instance.
*
* #return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* #return int
*/
public function handle()
{
$this->line('Importing Pipelines & Deal Stages from HubSpot API...');
PipelineHubspot::import();
$this->line('Importing Deals from HubSpot API...');
$this->getDealsFromHubspot();
$this->line('Found ' . count($this->importedDeals) . ' Deals to import');
if ($this->option('force')) {
$this->doImport();
} else {
if ($this->confirm('Do you want to import these deals? (yes|no)', false)) {
$this->doImport();
} else {
$this->line('Process aborted');
}
}
}
/**
* Grab Deals from Hubspot by calling the Deals API and looping through the paginated data
*
* #param int $limit: the number of deals per page
* #param string $next: the link to the next page of results
*/
private function getDealsFromHubspot(?int $limit = 100, string $next = null)
{
$endpoint = 'https://api.hubapi.com/crm/v3/objects/deals';
$properties = [
'limit' => $limit,
'properties' => implode(',', Deal::HUBSPOT_DEAL_PROPERTIES),
'hapikey' => config('hubspot.api_key'),
'associations' => 'engagements',
];
// If there's another page, append the after parameter.
if ($next) {
$properties['after'] = $next;
}
$response = Http::get($endpoint, $properties);
if ($response->successful()) {
$data = $response->json();
// If there are results, get them.
if (isset($data['results'])) {
foreach ($data['results'] as $hubspotDeal) {
$this->importedDeals[] = $hubspotDeal['properties'];
}
}
// If there's paginate we need to call the function on itself
if (isset($data['paging']['next']['link'])) {
$this->getDealsFromHubspot(null, $data['paging']['next']['after']);
}
}
$response->json();
}
/**
* Pull the Deal data in order to create a Deal model.
*
* #param array $data
*/
private function syncDeal(array $data)
{
$excludedDealStages = DealStage::excludeFromDealImport()->pluck('hubspot_id');
if ($excludedDealStages->contains($data['dealstage'])) {
return false;
}
$deal = Deal::updateOrCreate([
'hubspot_id' => $data['hs_object_id'],
], [
'name' => $data['dealname'],
'deal_stage_id' => $data['dealstage'],
'hubspot_owner_id' => $data['hubspot_owner_id'] ?? null,
]);
event(new DealImportedFromHubspot($deal));
return $deal;
}
/**
* Create and increment a nice progress bar as we import deals.
*/
private function doImport()
{
$bar = $this->output->createProgressBar(count($this->importedDeals));
$bar->start();
foreach ($this->importedDeals as $deal) {
$this->syncDeal($deal);
$bar->advance();
}
$bar->finish();
$this->newLine(2);
$this->line('Successfully imported ' . count($this->importedDeals) . ' Deals from HubSpot.');
}
}
Building on this event(new DealImportedFromHubspot($deal)); also makes an API call back to HubSpot to add the URL of the portal it had just been pulled into.
In this situation I'm thinking I either need to treat the deal importing as its own job, or add in some kind of rate limiter.
Would it be bad practise just to use sleep(10) to get around the rate limiting?
Sounds like a job for a Queue.
You can define your own rate limiter on the Queue, but the correct solution is probably to extend ShouldQueue and run $this->fail() when you get a response saying your request has been throttled.

Display Laravel Notification (MailMessage) with markdown after sent

I'm saving every email I send to an entity into the database by creating a function storeEmail and make an insert of MailMessage class into EmailMessage model. Everything works fine, and the main goal is to display the message exactly as it was, when the recipient received it and retrieve all the messages I sent as a User, to a page. To be much easier to retrieve a render of each specific Message in foreach loop, I think is better to fetch it from the Model.
This is my Notification class:
class SimpleEmail extends Notification
{
use Queueable;
private $link;
private $user;
/**
* Create a new notification instance.
*
* #return void
*/
public function __construct($link)
{
$this->link = $link;
$this->user = Auth::user();
}
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* #param mixed $notifiable
* #return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$mail = (new MailMessage)
->from($this->user->email, $this->user->name)
->subject('My Dummy Subject')
->greeting('To: '.$notifiable->email)
->action('Action Button', url($this->link))
->line('Thank you for reading my message')
->salutation('Friendly, '.$this->user->name);
$this->storeEmail($mail,$notifiable);
return $mail;
}
public function storeEmail($mail,$notifiable){
$email = new EmailMessage;
$email->sender_type = 'App\User';
$email->sender_id = $this->user->id;
$email->mail = $mail;
$email->save();
$notifiable->email_messages()->save($email);
}
}
Note:
I'm using Illuminate\Notifications\Messages\MailMessage
My class extends Illuminate\Notifications\Notification
I'm saving (new MailMessage) in the $email->mail = $mail;
I tried to dd($email->mail); and I get this:
^ array:20 [▼
"view" => null
"viewData" => []
"markdown" => "notifications::email"
"theme" => null
"from" => array:2 [▶]
"replyTo" => []
"cc" => []
"bcc" => []
"attachments" => []
"rawAttachments" => []
"priority" => null
"callbacks" => []
"level" => "info"
"subject" => "My Dummy Subject"
"greeting" => "To: Dohn John"
"salutation" => "Friendly, Nikolas Diakosavvas"
"introLines" => array:2 [▶]
"outroLines" => array:1 [▶]
"actionText" => "Action Button"
"actionUrl" => "http://my-example-url.com ▶"
How can I display the Mail Notification, as it was when I sent it ? What is the optimal solution for that ?
Thanks, in advance
EDITED
Managed to render MailMessage using this code works :
$email = EmailMessage::first();
return (new \App\Notifications\SimpleEmail('my-link', $email->recipient->assignto))->toMail($email->recipient);
But this is not exactly what I wanted, because every time I need to find:
Which Notification class used on every email so I can render it.
Variables for each Notification class.
In order to accomplish this:
1. You can create a accessor.
2. Use Markdown's render method.
3. Pass in render method the mail's markdown you saved in storeEmail.
You can see an example above :
use \Illuminate\Mail\Markdown;
public function getRenderAttribute(){
$markdown = new Markdown(view());
return $markdown->render($this->mail['markdown'], $this->mail);
}

How to get all possible error messages of a Laravel Controller method

So I have a Laravel Application, which has many Controllers to handle various aspects of the applications.
Now each controller has various methods. Most of the methods have validations rules defined such as:
$validationArray = [
'id'=>'required|integer',
'status'=>'required|string'
];
$validator = Validator::make($request->all(),$validationArray);
if ($validator->fails()){
return Response::json(['response'=>implode(', ',$validator->messages()->all())],422);
}
Now the following line:
return Response::json(['response'=>implode(', ',$validator->messages()->all())],422);
actually returns whatever is wrong with the validation rules.
My question is: Is there any way to get all possible error messages programmatically?
Of course, one way to do it is going around the rule by rule and make a list manually but there are hundreds of the methods scattered over various controllers.
So, if anyone could point me in the direction of taking all the error messages in some easier way, would be much appreciated.
Thank you in advance!
UPDATE
So to clear further I need a list of all possible errors, like for above code the list will be like:
['id is required', 'id must be an integer', 'status is required', 'status must be an string']
UPDATE 2
Please keep in mind that there are hundreds of methods and also I do not want to change the final response of the method but to have some sort of external script which can help me getting the error messages without interfering with the controllers much.
In order to do that you have to extend Validator class and write a method that will iterate all rules and explicitly add error messages as if they failed.
First, create a new file app\Http\Custom\Validator.php:
<?php
namespace App\Http\Custom;
use Illuminate\Contracts\Validation\Rule as RuleContract;
use Illuminate\Support\MessageBag;
use Illuminate\Validation\ValidationRuleParser;
use Illuminate\Validation\Validator as BaseValidator;
class Validator extends BaseValidator {
/** #var MessageBag */
protected $errorMessages;
/** #var array */
protected $hasExplicitFileErrorMessage;
protected $explicitFileRules = [
'File', 'Image', 'Mimes', 'Mimetypes', 'Dimensions',
];
function availableErrors()
{
$this->errorMessages = new MessageBag();
$this->hasExplicitFileErrorMessage = [];
foreach($this->rules as $attribute => $rules) {
$attribute = str_replace('\.', '->', $attribute);
foreach($rules as $rule) {
[$rule, $parameters] = ValidationRuleParser::parse($rule);
if($rule == '') {
continue;
}
if(($keys = $this->getExplicitKeys($attribute)) &&
$this->dependsOnOtherFields($rule)) {
$parameters = $this->replaceAsterisksInParameters($parameters, $keys);
}
// explicitly add "failed to upload" error
if($this->hasRule($attribute, $this->explicitFileRules) && !in_array($attribute, $this->hasExplicitFileErrorMessage)) {
$this->addFailureMessage($attribute, 'uploaded', []);
$this->hasExplicitFileErrorMessage[] = $attribute;
}
if($rule instanceof RuleContract) {
$messages = $rule->message() ? (array)$rule->message() : [get_class($rule)];
foreach($messages as $message) {
$this->addFailureMessage($attribute, get_class($rule), [], $message);
}
} else {
$this->addFailureMessage($attribute, $rule, $parameters);
}
}
}
return $this->errorMessages->all();
}
function addFailureMessage($attribute, $rule, $parameters = [], $rawMessage = null)
{
$this->errorMessages->add($attribute, $this->makeReplacements(
$rawMessage ?? $this->getMessage($attribute, $rule), $attribute, $rule, $parameters
));
}
// we have to override this method since file-type errors depends on data value rather than rule type
protected function getAttributeType($attribute)
{
if($this->hasRule($attribute, $this->explicitFileRules)) {
return 'file';
}
return parent::getAttributeType($attribute);
}
}
Next, let's register this class in Validation factory:
<?php
namespace App\Providers;
use App\Http\Custom\Validator; // <-- our custom validator
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider {
public function boot()
{
app('validator')->resolver(function ($translator, $data, $rules, $messages) {
return new Validator($translator, $data, $rules, $messages);
});
}
}
And... that's all. Let's test it:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class HomeController extends Controller {
function index(Request $request)
{
$rules = [
'id' => 'required|int|between:2,10',
'status' => 'required_with:nonexisting|string|email',
'avatar' => 'required|file|mimes:png|max:1000',
'company' => 'required_without:id|unique:companies,id'
];
$validator = Validator::make([], $rules);
dump($validator->availableErrors());
}
}
array:13 [▼
0 => "The id field is required."
1 => "The id must be an integer."
2 => "The id must be between 2 and 10."
3 => "The status field is required when nonexisting is present."
4 => "The status must be a string."
5 => "The status must be a valid email address."
6 => "The avatar failed to upload."
7 => "The avatar field is required."
8 => "The avatar must be a file."
9 => "The avatar must be a file of type: png."
10 => "The avatar may not be greater than 1000 kilobytes."
11 => "The company field is required when id is not present."
12 => "The company has already been taken."
]
It isn't pretty but here's my shot:
$validationArray = [
'id'=>'required|integer',
'status'=>'required|string'
];
$validator = Validator::make($request->all(), $validationArray);
if ($validator->fails()) {
$messages = [];
$invalid_fields = array_keys($validator->messages()->toArray());
$rules = $v->getRules();
foreach($invalid_fields as $invalid_field) {
foreach($rules[$invalid_field] as $rule) {
if(str_contains($rule, ':') {
// complex rules that have parameters (min, between, size, format)
// are more difficult to work with. I haven't figured out how to do them yet
// but you should get the idea.
continue;
} else {
$messages[] = str_replace(':attribute', $invalid_field, $validator->getTranslator()->get("validation.$rule"));
}
}
}
return Response::json(['response' => implode(', ', $messages)], 422);
}
Number 1: Like I mentioned in my comment under the question, what you're trying to achieve may be done in simpler way.
Number 2: Since you do not want to change your already written code where you got ->messages() then you could do the following. I will list the steps and provide an example code.
We need to override Laravel's validator, (Validation) Factory, and ValidationService provider classes.
In App\Services folder you can create two classes Validator and ValidationFactory
in App\Providers create a class ValidationServiceProvider
Go into config/app.php file and under providers replace Illuminate\Validation\ValidationServiceProvider::class with App\Providers\ValidationServiceProvider::class
Validator class looks like so:
namespace App\Services;
use Illuminate\Support\MessageBag;
use Illuminate\Validation\ValidationRuleParser;
use Illuminate\Contracts\Translation\Translator;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Illuminate\Contracts\Validation\Rule as RuleContract;
class Validator extends \Illuminate\Validation\Validator
{
/**
* #var MessageBag $all_messages
*/
protected $all_messages;
public function __construct(Translator $translator, array $data, array $rules, array $messages = [], array $customAttributes = [])
{
parent::__construct($translator, $data, $rules, $messages, $customAttributes);
$this->all_messages = new MessageBag;
$this->getAllFormattedMessages();
}
public function makeAllRulesMessages($attribute, $rule, $parameters)
{
$this->all_messages->add($attribute, $this->makeReplacements(
$this->getMessage($attribute, $rule), $attribute, $rule, $parameters
));
}
public function messages(bool $validated_rules_messages = false)
{
return $validated_rules_messages
? $this->validatedMessages()
: $this->all_messages;
}
/**
* This is here in case the true validated messages are needed
*
* #return MessageBag
*/
public function validatedMessages()
{
return parent::messages();
}
public function getAllFormattedMessages()
{
// We'll spin through each rule and add all messages to it.
foreach ($this->rules as $attribute => $rules) {
$attribute = str_replace('\.', '->', $attribute);
foreach ($rules as $rule) {
// First we will get the correct keys for the given attribute in case the field is nested in
// an array. Then we determine if the given rule accepts other field names as parameters.
// If so, we will replace any asterisks found in the parameters with the correct keys.
[$rule, $parameters] = ValidationRuleParser::parse($rule);
if (($keys = $this->getExplicitKeys($attribute)) &&
$this->dependsOnOtherFields($rule)) {
$parameters = $this->replaceAsterisksInParameters($parameters, $keys);
}
$value = $this->getValue($attribute);
if ($value instanceof UploadedFile && $this->hasRule($attribute, array_merge($this->fileRules, $this->implicitRules))
) {
$this->makeAllRulesMessages($attribute, 'uploaded', []);
} elseif ($rule instanceof RuleContract) {
$this->makeCustomRuleMessage($attribute, $rule);
} else {
$this->makeAllRulesMessages($attribute, $rule, $parameters);
}
}
}
}
/**
* #param $attribute
* #param \Illuminate\Contracts\Validation\Rule $rule $rule
*/
public function makeCustomRuleMessage($attribute, $rule)
{
$this->failedRules[$attribute][get_class($rule)] = [];
$messages = (array)$rule->message();
foreach ($messages as $message) {
$this->all_messages->add($attribute, $this->makeReplacements(
$message, $attribute, get_class($rule), []
));
}
}
}
This class does one thing in summary, get all the messages of the passed rules into $all_messages property of the class. It extends and allows the base validation class run, and simply overrides messages() method to make all the collected rules available for use.
ValidationFactory overrides Illuminate\Validation\Factory and it looks like so:
namespace App\Services;
use Illuminate\Validation\Factory;
class ValidationFactory extends Factory
{
/**
* Resolve a new Validator instance.
*
* #param array $data
* #param array $rules
* #param array $messages
* #param array $customAttributes
* #return \Illuminate\Validation\Validator
*/
protected function resolve(array $data, array $rules, array $messages, array $customAttributes)
{
if (is_null($this->resolver)) {
return new \App\Services\Validator($this->translator, $data, $rules, $messages, $customAttributes);
}
return call_user_func($this->resolver, $this->translator, $data, $rules, $messages, $customAttributes);
}
}
This class does only one thing, overrides resolve() method in this class by making use of the instance of our custom \App\Services\Validator class instead.
ValidationServiceProvider extends Illuminate\Validation\ValidationServiceProvider and overrides registerValidationFactory() method and it looks like so:
namespace App\Providers;
use App\Services\ValidationFactory;
use Illuminate\Validation\ValidationServiceProvider as BaseValidationServiceProvider;
class ValidationServiceProvider extends BaseValidationServiceProvider
{
protected function registerValidationFactory()
{
$this->app->singleton('validator', function ($app) {
$validator = new ValidationFactory($app['translator'], $app);
// The validation presence verifier is responsible for determining the existence of
// values in a given data collection which is typically a relational database or
// other persistent data stores. It is used to check for "uniqueness" as well.
if (isset($app['db'], $app['validation.presence'])) {
$validator->setPresenceVerifier($app['validation.presence']);
}
return $validator;
});
}
}
What the above class does is also to instruct the provide to make use of our App\Services\ValidationFactory whenever the app requires one.
And we are done. All validation messages will be shown even if one of our validation rules failed.
Caveats
In order to achieve this, we needed to make a lot of changes and overriding. Except really critical this may signal that something about the app's design looks wrong.
Laravel validation implementation may change in future release and therefore may become a problem maintaining these changes.
I cannot tell if there are other side effects that might happen for overriding Laravel's default validation implementation or if all the rules return the right messages.
Normally you only want to return failed validation messages to user rather than all the possible failures.
I think that functions failed() (get the failed validation rules) or errors() (get the message container for the validator) may help you. If it does not - go to https://laravel.com/api/5.8/Illuminate/Validation/Validator.html and I hope that you find needed function.
I think you are looking for a way to have custom error messages. if this is the case then the answer is like this:
$messages = [
'id.required' => 'id is required',
'id.integer' => 'id must be an integer',
'status.required' => 'status is required',
'status.string'=> 'status must be an string'
];
$validationArray = [
'id'=>'required|integer',
'status'=>'required|string'
];
$validator = Validator::make($request->all(),$validationArray, $messages);
more info you can find here.
I hope this is what you are looking for and my answer is helping you:)
Based on the Laravel Form Validation Procedure you can write the statement by following:
$validationArray = [
'id'=>'required|integer',
'status'=>'required|string'
];
$validator = Validator::make($request->all(),$validationArray);
if ($validator->fails()){
return Response::json(['response'=> validator->errors())],422);
}
Where errors() method return all the errors as associative array where the message will be associate with the field name accordingly and that's how you can get the errors.

Get all relationships from Eloquent model

Having one Eloquent model, is it possible to get all its relationships and their type at runtime?
I've tried taking a look at ReflectionClass, but I couldn't find anything useful for this scenario.
For example, if we have the classic Post model, is there a way to extract relationships like this?
- belongsTo: User
- belongsToMany: Tag
To accomplish this, you will have you know the names of the methods within the model - and they can vary a lot ;)
Thoughts:
if you got a pattern in the method, like relUser / relTag, you can filter them out
or loop over all public methods, see if a Relation object pops up (bad idea)
you can define a protected $relationMethods (note: Laravel already uses $relations) which holds an array with method.
After calling Post->User() you will receive a BelongsTo or 1 of the other objects from the Relation family, so you can do you listing for the type of relation.
[edit: after comments]
If the models are equipped with a protected $with = array(...); then you are able to look into the loaded relations with $Model->getRelations() after a record is loaded. This is not possible when no record is loaded, since the relations aren't touched yet.
getRelations() is in /vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
But currently it doesn't show up in the api at laravel.com/api - this is because we got newer version
Like Rob stated. It is a bad idea to loop through every public method and check out if a relation is returned.
Barryvdh uses a Regex based approach in his very popular Laravel-ide-helper:
https://github.com/barryvdh/laravel-ide-helper/blob/master/src/Console/ModelsCommand.php
You just have to filter the properties you receive after calling getPropertiesFromMethods like this (untested example):
class classSniffer{
private $properties = [];
//...
public function getPropertiesFromMethods($model){
//the copied code from the class above (ModelsCommand#getPropertiesFromMethods)
}
public function getRelationsFrom($model){
$this->getPropertiesFromMethods($model);
$relations = [];
foreach($this->properties as $name => $property){
$type = $property;
$isRelation = strstr($property[$type], 'Illuminate\Database\Eloquent\Relations');
if($isRelation){
$relations[$name] = $property;
}
}
return $relations;
}
}
Is there a cleaner way of doing that without touching the Models?
I think we have to wait for PHP7 (Return Type Reflections) or for a new Reflection Service from Taylor ^^
I've been working on the same thing lately, and I don't think it can effectively be done without Reflection. But this is a little resource-intensive, so I've applied some caching. One check that's needed is to verify the return type, and pre-php7, that can only be done by actually executing each method. So I've also applied some logic that reduces the number of likely candidates before running that check.
/**
* Identify all relationships for a given model
*
* #param object $model Model
* #param string $heritage A flag that indicates whether parent and/or child relationships should be included
* #return array
*/
public function getAllRelations(\Illuminate\Database\Eloquent\Model $model = null, $heritage = 'all')
{
$model = $model ?: $this;
$modelName = get_class($model);
$types = ['children' => 'Has', 'parents' => 'Belongs', 'all' => ''];
$heritage = in_array($heritage, array_keys($types)) ? $heritage : 'all';
if (\Illuminate\Support\Facades\Cache::has($modelName."_{$heritage}_relations")) {
return \Illuminate\Support\Facades\Cache::get($modelName."_{$heritage}_relations");
}
$reflectionClass = new \ReflectionClass($model);
$traits = $reflectionClass->getTraits(); // Use this to omit trait methods
$traitMethodNames = [];
foreach ($traits as $name => $trait) {
$traitMethods = $trait->getMethods();
foreach ($traitMethods as $traitMethod) {
$traitMethodNames[] = $traitMethod->getName();
}
}
// Checking the return value actually requires executing the method. So use this to avoid infinite recursion.
$currentMethod = collect(explode('::', __METHOD__))->last();
$filter = $types[$heritage];
$methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); // The method must be public
$methods = collect($methods)->filter(function ($method) use ($modelName, $traitMethodNames, $currentMethod) {
$methodName = $method->getName();
if (!in_array($methodName, $traitMethodNames) //The method must not originate in a trait
&& strpos($methodName, '__') !== 0 //It must not be a magic method
&& $method->class === $modelName //It must be in the self scope and not inherited
&& !$method->isStatic() //It must be in the this scope and not static
&& $methodName != $currentMethod //It must not be an override of this one
) {
$parameters = (new \ReflectionMethod($modelName, $methodName))->getParameters();
return collect($parameters)->filter(function ($parameter) {
return !$parameter->isOptional(); // The method must have no required parameters
})->isEmpty(); // If required parameters exist, this will be false and omit this method
}
return false;
})->mapWithKeys(function ($method) use ($model, $filter) {
$methodName = $method->getName();
$relation = $model->$methodName(); //Must return a Relation child. This is why we only want to do this once
if (is_subclass_of($relation, \Illuminate\Database\Eloquent\Relations\Relation::class)) {
$type = (new \ReflectionClass($relation))->getShortName(); //If relation is of the desired heritage
if (!$filter || strpos($type, $filter) === 0) {
return [$methodName => get_class($relation->getRelated())]; // ['relationName'=>'relatedModelClass']
}
}
return false; // Remove elements reflecting methods that do not have the desired return type
})->toArray();
\Illuminate\Support\Facades\Cache::forever($modelName."_{$heritage}_relations", $methods);
return $methods;
}
I have the same needs on my project. My solution is using get_class function to check type of relation. example:
$invoice = App\Models\Invoice::with('customer', 'products', 'invoiceProducts', 'invoiceProduct')->latest()->first();
foreach ($invoice->getRelations() as $relation => $items) {
$model = get_class($invoice->{$relation}());
$type = explode('\\', $model);
$type = $type[count($type) - 1];
$relations[] = ['name' => $relation, 'type' => $type];
}
dd($relations);
example result:
array:4 [▼
0 => array:2 [▼
"name" => "customer"
"type" => "BelongsTo"
]
1 => array:2 [▼
"name" => "products"
"type" => "BelongsToMany"
]
2 => array:2 [▼
"name" => "invoiceProducts"
"type" => "HasMany"
]
3 => array:2 [▼
"name" => "invoiceProduct"
"type" => "HasOne"
]
]
I need it for duplicate an model item including the relation
composer require adideas/laravel-get-relationship-eloquent-model
https://packagist.org/packages/adideas/laravel-get-relationship-eloquent-model
Laravel get relationship all eloquent models!
You don't need to know the names of the methods in the model to do this. Having one or many Eloquent models, thanks to this package, you can get all of its relationships and their type at runtime
I know its bit late, but I have been visiting this question multiple times so thought to share my observations to help those who visits this question in future.
Here is the method i used to extract the relationships from an eloquent model class.
/**
*
* Returns all the relationship methods defined
* in the provided model class with related
* model class and relation function name
*
* #param string $modelClass exampe: App\Models\Post
* #return array $relattions array containing information about relationships
*/
protected function getModelRelationshipMethods(string $modelClass)
{
//can define this at class level
$relationshipMethods = [
'hasMany',
'hasOne',
'belongsTo',
'belongsToMany',
];
$reflector = new ReflectionClass($modelClass);
$path = $reflector->getFileName();
//lines of the file
$lines = file($path);
$methods = $reflector->getMethods();
$relations = [];
foreach ($methods as $method) {
//if its a concrete class method
if ($method->class == $modelClass) {
$start = $method->getStartLine();
$end = $method->getEndLine();
//loop through lines of the method
for($i = $start-1; $i<=$end-1; $i++) {
// look for text between -> and ( assuming that its on one line
preg_match('~\->(.*?)\(~', $lines[$i], $matches);
// if there is a match
if (count($matches)) {
//loop to check if the found text is in relationshipMethods list
foreach ($matches as $match) {
// if so add it to the output array
if (in_array($match, $relationshipMethods)) {
$relations[] = [
//function name of the relation definition
'method_name' => $method->name,
//type of relation
'relation' => $match,
//related Class name
'related' => (preg_match('/'.$match.'\((.*?),/', $lines[$i], $related) == 1) ? $related[1] : null,
];
}
}
}
}
}
}
return $relations;
}
If you dd() or dump() the returned $relations for the App/Post model, The output will be something like this
^ array:3 [
0 => array:3 [
"method_name" => "user"
"relation" => "belongsTo"
"related" => "User::class"
]
1 => array:3 [
"method_name" => "tag"
"relation" => "belongsToMany"
"related" => "Tag::class"
]
2 => array:3 [
"method_name" => "comments"
"relation" => "hasMany"
"related" => "Comment::class"
]
]

Categories