I write test for Laravel app with codeception and modules Laravel5, REST.
One of api test:
public function testEmailRegistration(ApiTester $I) {
...
// Not correct data
$I->sendPOST($route, [
'first_name' => (string)$this->faker->randomNumber(),
'password' => $this->faker->password(1, 7),
'email' => 'not_valid_email',
]);
$I->seeResponseCodeIs(HttpCode::UNPROCESSABLE_ENTITY);
// Correct data
\Illuminate\Support\Facades\Queue::fake();
$I->sendPOST($route, [
'first_name' => $firstName,
'password' => $password,
'email' => $email,
]);
\Illuminate\Support\Facades\Queue::assertPushed(\App\Jobs\SendEmail::class);
...
}
I send requests on incorrect and correct data and make some assertions. In addition I check, that job is present in queue.
After execute test I give error:
[Error] Call to undefined method Illuminate\Queue\SyncQueue::assertPushed()
After Queue:fake facade \Illuminate\Support\Facades\Queue must resolves to QueueFake, but in fact is still QueueManager, thus assertPushed function is undefined.
Execution of $I->sendPOST() reset call Queue::fake. It happened in laravel 5 module \Codeception\Lib\Connector\Laravel5, method doRequest.
protected function doRequest($request)
{
if (!$this->firstRequest) {
$this->initialize($request);
}
$this->firstRequest = false;
$this->applyBindings();
$this->applyContextualBindings();
$this->applyInstances();
$this->applyApplicationHandlers();
$request = Request::createFromBase($request);
$response = $this->kernel->handle($request);
$this->app->make('Illuminate\Contracts\Http\Kernel')->terminate($request, $response);
return $response;
}
Each call of doRequest except the first init app again and some configurations as Queue::fake are cleared.
One of decision is one request per test. Is there another variant to work Queue::fake when in test make more then one request?
I am not sure why the Laravel module does this but I found a work-around that allows you to use fakes:
public function someTest(ApiTester $I): void
{
// what the SomeFacade::fake method call does is basically create
// a fake object and swaps it for the original implementation in
// the app container, so the we're recreating that behavior here
// only this will be persisted even after the request is issued:
$notification_fake = new NotificationFake();
// `haveInstance` is a method from Laravel Codeception Module
// which sets an object in the app container for you:
$I->haveInstance(ChannelManager::class, $notification_fake);
// making the request
$I->sendPUT('some url', $some_payload);
// assertions
$I->canSeeResponseCodeIs(Response::HTTP_OK);
$notification_fake->assertSentToTimes($expected_user, MyNotification::class, 1);
}
note that this test method is only for illustrative purposes and it misses so of the details, hence the undefined variables and such.
Also note that I user the notification fake which get registered at Illuminate\Notifications\ChannelManager, unlike most fakes that you can register under their aliased name e.g. queue. So you have to check what is being instantiated and how to swap it on your own. You can either find this in respective service providers for each service. Most of the time it's lowercase name of the facade.
Related
Using Google API v3 I try to move a file from one folder to another. I am using a wrapper class in Laravel, the file and parent IDs are correct. Developing from the documentation, I have tried code as:
public function moveFileFromTo($fileID, $toParentID) {
$fromFile = $this->service->files->get($fileID, ['fields' => '*']);
$fromParentID = $fromFile->getParents();
$blankFile = new Google_Service_Drive_DriveFile();
$this->service->files->update($fileID, $blankFile, [
"removeParents" => $fromParentID,
"addParents" => $toParentID
]);
}
However, this seems to move the file but strips out all the meta data.
I have also tried
public function moveFileFromTo($fileID, $toParentID) {
$fromFile = $this->service->files->get($fileID, ['fields' => '*']);
$fromParentID = $fromFile->getParents();
$fromFile->setParents($fromParentID);
$this->service->files->update($fileID, $fromFile);
}
However, this gives the error:
Google\Service\Exception
{ "error": { "errors": [ { "domain": "global", "reason":
"fieldNotWritable", "message": "The resource body includes fields
which are not directly writable." } ], "code": 403, "message": "The
resource body includes fields which are not directly writable." } }
I wish to simply move the file and retain all its metadata. From the documentation, it seems either a new empty file is required in update (really weird) or I must somehow strip out the fields of the object used in the second argument ($fromFile) it does not like to be written to (even though I am simply updating the files parents - which is writable).
See also https://issuetracker.google.com/issues/199921300
Problems with Answers so far:
but grateful for responses
$fromFile = $this->service->files->get($fileID, ['fields' => 'parents, id']);
returns all ~75 attributes a lot of which are not writeable.
Instead of the expected 2 as per PHPStorm debug (note the break is at the statement immediately following the GET request so irrelevant at this point
using
unset($fromFile->shared);
still leaves other writable attributes
and indeed the file is not actually shared
UPDATE TO MY CODING
public function moveFileFromTo($fileID, $toParentID) {
$fromFile = $this->service->files->get($fileID, ["fields" => "id,parents"]);
$fromFile = $this->getParsedWritableFile($fromFile);
$fromFile->setParents($toParentID);
$this->service->files->update($fileID, $fromFile, ['addParents' => $toParentID]);
}
getParsedWritableFile is trying to just set writable attributes on a new Google Drive file object:
public function getParsedWritableFile($gdrivefile) {
$gdrivefile = new \Google_Service_Drive_DriveFile();//comment or delete, just here to check auto complete function names
$parsedFile = new \Google_Service_Drive_DriveFile();
//$parsedFile=$gdrivefile;
// currently only allow these according to https://developers.google.com/drive/api/v3/reference/files#resource-representations
$parsedFile->setName($gdrivefile->getName());//name
$parsedFile->setMimeType($gdrivefile->getMimeType());//mimeType
$parsedFile->setDescription($gdrivefile->getDescription());//description
$parsedFile->setStarred($gdrivefile->getStarred());//starred
$parsedFile->setTrashed($gdrivefile->getTrashed());//trashed
$parsedFile->setParents($gdrivefile->getParents());//parents
$parsedFile->setProperties($gdrivefile->getProperties());//properties [object]
$parsedFile->setAppProperties($gdrivefile->getAppProperties());//appProperties [object]
$parsedFile->setCreatedTime($gdrivefile->getCreatedTime());//createdTime
$parsedFile->setModifiedTime($gdrivefile->getModifiedTime());//modifiedTime
$parsedFile->setWritersCanShare($gdrivefile->getWritersCanShare());//writersCanShare
$parsedFile->setViewedByMeTime($gdrivefile->getViewedByMeTime());//viewedByMeTime
$parsedFile->setFolderColorRgb($gdrivefile->getFolderColorRgb());//folderColorRgb
$parsedFile->setOriginalFilename($gdrivefile->getOriginalFilename());//originalFilename
$parsedFile->setCopyRequiresWriterPermission($gdrivefile->getCopyRequiresWriterPermission());//copyRequiresWriterPermission
/*complex permissions*/
/*
contentHints.thumbnail.image
contentHints.thumbnail.mimeType
contentHints.indexableText
*/
$contenthints=$gdrivefile->getContentHints();//could be null meaning further functions eg getThumbnail cause exception
if($contenthints){
$parsedFile->setContentHints($contenthints->getThumbnail()->getImage());
$parsedFile->setContentHints($contenthints->getThumbnail()->getMimeType());
$parsedFile->setContentHints($contenthints->getIndexableText());
}
/*no function to get indiviual attributes*/
/*
contentRestrictions[].readOnly
ccontentRestrictions[].reason
*/
$parsedFile->setContentRestrictions($gdrivefile->getContentRestrictions());
//</ end>
return $parsedFile;
}
This is proving a bit successful but this is original meta
the above code does move it, with seemingly proper meta data, created time and EXIF data is now intact
The problem is that you are using file.update which uses HTTP PATCH methodology. By using a PATCH its going to try to update all of the properties in the file object that you send.
You did a file.get and you included fileds *
$fromFile = $this->service->files->get($fileID, ['fields' => '*']);
By including 'fields' => '*' you told the API to return to you all of the properties that the file has. A file resource has a lot of properties and they are not all writeable.
By sending the file.update method all of the fields you are telling it you want to update everything including some of the properties that you are not allowed to update. To which the api responds with fieldNotWritable
The soultion would be to do the following
$fromFile = $this->service->files->get($fileID, ['fields' => 'parents, id']);
Which will cause the method to only return the two parameters that you actual need. The id and the parents parameter. (TBH you may just need the parents part, I would need to test that.)
{
"id": "1x8-vD-XiA5Spf3qp8x2wltablGF22Lpwup8VtxNY",
"parents": ["0B1bbSFgVLpoXcVDRFRF8tTkU"
]
}
You should then be allowed to update the parent and move your file.
I don't know how to do in laravel but the problem might be
I have also faced the same problem some time and after searching internet for over months found nothing and one day Referring to documentation, seen that shared
isn't a writable field.
That's it, if you are sharing the File and trying to move the file it wouldn't move. Try to un-share the File and then try to move the File it would be done.
I ended up creating a custom function to only include writeable attributes as specified in the Google Documentation:
public function getParsedWritableFile($gdrivefile) {
$gdrivefile = new \Google_Service_Drive_DriveFile();//comment or delete, just here to check auto complete function names
$parsedFile = new \Google_Service_Drive_DriveFile();
// currently only allow these according to https://developers.google.com/drive/api/v3/reference/files#resource-representations
$parsedFile->setName($gdrivefile->getName());//name
$parsedFile->setMimeType($gdrivefile->getMimeType());//mimeType
$parsedFile->setDescription($gdrivefile->getDescription());//description
$parsedFile->setStarred($gdrivefile->getStarred());//starred
$parsedFile->setTrashed($gdrivefile->getTrashed());//trashed
$parsedFile->setParents($gdrivefile->getParents());//parents
$parsedFile->setProperties($gdrivefile->getProperties());//properties [object]
$parsedFile->setAppProperties($gdrivefile->getAppProperties());//appProperties [object]
$parsedFile->setCreatedTime($gdrivefile->getCreatedTime());//createdTime
$parsedFile->setModifiedTime($gdrivefile->getModifiedTime());//modifiedTime
$parsedFile->setWritersCanShare($gdrivefile->getWritersCanShare());//writersCanShare
$parsedFile->setViewedByMeTime($gdrivefile->getViewedByMeTime());//viewedByMeTime
$parsedFile->setFolderColorRgb($gdrivefile->getFolderColorRgb());//folderColorRgb
$parsedFile->setOriginalFilename($gdrivefile->getOriginalFilename());//originalFilename
$parsedFile->setCopyRequiresWriterPermission($gdrivefile->getCopyRequiresWriterPermission());//copyRequiresWriterPermission
/*complex permissions*/
/*
contentHints.thumbnail.image
contentHints.thumbnail.mimeType
contentHints.indexableText
*/
$contenthints=$gdrivefile->getContentHints();//could be null meaning further functions eg getThumbnail cause exception
if($contenthints){
$parsedFile->setContentHints($contenthints->getThumbnail()->getImage());
$parsedFile->setContentHints($contenthints->getThumbnail()->getMimeType());
$parsedFile->setContentHints($contenthints->getIndexableText());
}
/*no function to get indiviual attributes*/
/*
contentRestrictions[].readOnly
ccontentRestrictions[].reason
*/
$parsedFile->setContentRestrictions($gdrivefile->getContentRestrictions());
return $parsedFile;
}
and called with
public function moveFileFromTo($fileID, $toParentID) {
$fromFile = $this->service->files->get($fileID, ["fields" => "id,parents"]);
$fromFile = $this->getParsedWritableFile($fromFile);
$fromFile->setParents($toParentID);
$this->service->files->update($fileID, $fromFile, ['addParents' => $toParentID]);
}
We are building an api endpoint where precision is required. We want to enforce strict validation on the parameters that are POST/PUT to the server.
If the api user sends a key=value pair that is not supported (eg. we allow the parameters [first_name, last_name] and the user includes an unsupported parameter [country]), we want the validation to fail.
Have tried building a custom validator called allowed_attributes (used as allowed_attributes:attr1,attr2,...), but for it to be usable in a $validationRules array, it has to be applied to the parent of a list of nested/child attributes (...because otherwise our custom validator did not have access to the attributes being validated).
Validator::extend('allowed_attributes', 'App\Validators\AllowedAttributesValidator#validate');
This created issues with other validators, where we then had to anticipate this parent/child structure and code around it, including additional post-validation clean-up of error keys and error message strings.
tl;dr: very dirty, not a clean implementation.
$validationRules = [
'parent' => 'allowed_attributes:first_name,last_name',
'parent.first_name' => 'required|string|max:40',
'parent.last_name' => 'required|string|max:40'
];
$isValid = Validator::make(['parent' => $request], $validationRules);
var_dump("Validation results: " . ($isValid ? "passed" : "failed"));
Any ideas/suggestions on how this can be accomplished more cleanly in laravel, without requiring the use of parent/child relationship to get access to the list of all $request attributes (within the custom validator)?
I preferred to post a new answer as the approach is different from the previous one and a bit more cleaner. So I would rather keep the two approaches separated and not mixed together in the same answer.
Better problem handling
After digging deeper into the Validation's namespace's source code since my last answer I figured out that the easiest way would have been to extend the Validator class to remplement the passes() function to also check what you needed.
This implementation has the benefit to also correcly handle specific error messages for single array/object fields without any effor and should be fully compatible with the usual error messages translations.
Create a custom validator class
You should first create a Validator class within your app folder (I placed it under app/Validation/Validator.php) and implement the passes method like this:
<?php
namespace App\Validation;
use Illuminate\Support\Arr;
use Illuminate\Validation\Validator as BaseValidator;
class Validator extends BaseValidator
{
/**
* Determine if the data passes the validation rules.
*
* #return bool
*/
public function passes()
{
// Perform the usual rules validation, but at this step ignore the
// return value as we still have to validate the allowance of the fields
// The error messages count will be recalculated later and returned.
parent::passes();
// Compute the difference between the request data as a dot notation
// array and the attributes which have a rule in the current validator instance
$extraAttributes = array_diff_key(
Arr::dot($this->data),
$this->rules
);
// We'll spin through each key that hasn't been stripped in the
// previous filtering. Most likely the fields will be top level
// forbidden values or array/object values, as they get mapped with
// indexes other than asterisks (the key will differ from the rule
// and won't match at earlier stage).
// We have to do a deeper check if a rule with that array/object
// structure has been specified.
foreach ($extraAttributes as $attribute => $value) {
if (empty($this->getExplicitKeys($attribute))) {
$this->addFailure($attribute, 'forbidden_attribute', ['value' => $value]);
}
}
return $this->messages->isEmpty();
}
}
This would essentially extend the default Validator class to add additional checks on the passes method. The check compute the array difference by keys between the input attributes converted to dot notation (to support array/object validation) and the attributes which have at least one rule assigned.
Replace the default Validator in the container
Then the last step you miss is to bind the new Validator class in the boot method of a service provider. To do so you can just override the resolver of the Illuminate\Validation\Factory class binded into the IoC container as 'validator':
// Do not forget the class import at the top of the file!
use App\Validation\Validator;
// ...
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
$this->app->make('validator')
->resolver(function ($translator, $data, $rules, $messages, $attributes) {
return new Validator($translator, $data, $rules, $messages, $attributes);
});
}
// ...
Pratical use in a controller
You don't have to do anything specific to use this feature. Just call the validate method as usual:
$this->validate(request(), [
'first_name' => 'required|string|max:40',
'last_name' => 'required|string|max:40'
]);
Customize Error messages
To customize the error message you just have to add a translation key in your lang file with a key equal to forbidden_attribute (you can customize the error key name in the custom Validator class on the addFailure method call).
Example: resources/lang/en/validation.php
<?php
return [
// ...
'forbidden_attribute' => 'The :attribute key is not allowed in the request body.',
// ...
];
Note: this implementation has been tested in Laravel 5.3 only.
It should work for simple key/value pairs with this custom validator:
Validator::extendImplicit('allowed_attributes', function ($attribute, $value, $parameters, $validator) {
// If the attribute to validate request top level
if (strpos($attribute, '.') === false) {
return in_array($attribute, $parameters);
}
// If the attribute under validation is an array
if (is_array($value)) {
return empty(array_diff_key($value, array_flip($parameters)));
}
// If the attribute under validation is an object
foreach ($parameters as $parameter) {
if (substr_compare($attribute, $parameter, -strlen($parameter)) === 0) {
return true;
}
}
return false;
});
The validator logic is pretty simple:
If $attribute doesn't contains a ., we're dealing with a top level parameter, and we just have to check if it is present in the allowed_attributes list that we pass to the rule.
If $attribute's value is an array, we diff the input keys with the allowed_attributes list, and check if any attribute key has left. If so, our request had an extra key we didn't expect, so we return false.
Otherwise $attribute's value is an object we have to check if each parameter we're expecting (again, the allowed_attributes list) is the last segment of the current attribute (as laravel gives us the full dot notated attribute in $attribute).
The key here is to apply it to validation rules should like this (note the first validation rule):
$validationRules = [
'parent.*' => 'allowed_attributes:first_name,last_name',
'parent.first_name' => 'required|string|max:40',
'parent.last_name' => 'required|string|max:40'
];
The parent.* rule will apply the custom validator to each key of the 'parent' object.
To answer your question
Just don't wrap your request in an object, but use the same concept as above and apply the allowed_attributes rule with a *:
$validationRules = [
'*' => 'allowed_attributes:first_name,last_name',
'first_name' => 'required|string|max:40',
'last_name' => 'required|string|max:40'
];
This will apply the rule to all the present top level input request fields.
NOTE: Keep in mind that laravel validation is influenced by order of the rules as they are putted in rules array.
For example, moving the parent.* rule on bottom will trigger that rule on parent.first_name and parent.last_name; as opposed, keeping it as the first rule will not trigger the validation for the first_name and last_name.
This means that you could eventually remove the attributes that has further validation logic from the allowed_attributes rule's parameter list.
For example, if you would like to require only the first_name and last_name and prohibit any other field in the parent object, you might use these rules:
$validationRules = [
// This will be triggered for all the request fields except first_name and last_name
'parent.*' => 'allowed_attributes',
'parent.first_name' => 'required|string|max:40',
'parent.last_name' => 'required|string|max:40'
];
But, the following WON'T work as expected:
$validationRules = [
'parent.first_name' => 'required|string|max:40',
'parent.last_name' => 'required|string|max:40',
// This, instead would be triggered on all fields, also on first_name and last_name
// If you put this rule as last, you MUST specify the allowed fields.
'parent.*' => 'allowed_attributes',
];
Array Minor Issues
As far as I know, per Laravel's validation logic, if you were up to validate an array of objects, this custom validator would work, but the error message you would get would be generic on the array item, not on the key of that array item that wasn't allowed.
For example, you allow a products field in your request, each with an id:
$validationRules = [
'products.*' => 'allowed_attributes:id',
];
If you validate a request like this:
{
"products": [{
"id": 3
}, {
"id": 17,
"price": 3.49
}]
}
You will get an error on product 2, but you won't be able to tell which field is causing the problem!
Im using this laravel package "https://github.com/kanazaca/easypay" to create a MB reference using the Easypay API.
I have this method to create the reference:
public function generateReference()
{
$amount = Session::get('total');
$payment_info = [
't_value' => $amount,
'o_obs' => '',
't_key' => 1
];
$easypay = new EasyPay($payment_info);
$reference = $easypay->createReference();
Session::put('entity', $reference['ep_entity']);
Session::put('reference', $reference['ep_reference']);
Session::put('value', $reference['ep_value']);
}
And it works fine with this code I get some reference codes which can be payed using MB or credit-card.
Then, when a payment is made, easypay will call a "Notification URL".
that we should configure on easypay's backoffice under "URL Configurations".
Because when the easypay service receives the payment they will call the URL that we provided. So I defined a url in the easypay´s backoffice and created a route in the project:
Route::get('/easypay/notification-callback', [
'uses' => 'PaymentController#receiveNotifications',
'as' =>'mb.notifications'
]);
In the api backoffice there is a button that simulates a payment, after this button is clicked nothing happens and if I manually access "http://....ngrok.io/easypay/notification-callback" it appears an empty array:
[]
But in the documentation (https://docs.easypay.pt/workflow/payment-notification) says that when Easypay calls this endpoint, it comes with a few parameters: "ep_cin", "ep_user" and "ep_doc" that will be necessary in the process. So it should not appear an empty array.
Do you know what can be the issue? Im a beginner uing API´s so Im not understnading what the issue can be.
PaymentController receiveNotifications() method:
public function receiveNotifications(Request $request)
{
dd($request->all());
//$easypay = new EasyPay($payment_info);
//$xml = $easypay->processPaymentInfo();
//return \Response::make($xml, '200')->header('Content-Type', 'text/xml'); //must return in text/xml for easypay
}
receiveNotifications() method with log:
public function receiveNotifications(Request $request)
{
//dd($request->all());
Log::info('Showing info: ' .var_export($request->all(),true));
$payment_info = [
'ep_cin' => $request->ep_cin,
'ep_user' => $request->ep_user,
'ep_doc' => $request->ep_doc
];
Log::info('Showing info: ' .var_export($payment_info,true));
//dd($payment_info);
$easypay = new EasyPay($payment_info);
$xml = $easypay->processPaymentInfo();
return \Response::make($xml, '200')->header('Content-Type', 'text/xml'); //must return in text/xml for easypay
}
The session is saved in the session file of the user that visits your website that initiates the payment.
The receiveNotifications would call data from the session file that belongs to the payment gateway server if you were doing anything there. The data isn't matched because the two don't know about each other.
Also, you might not have a Session::save() somewhere in your request handling which writes the session data to file.
Store the reference in a database. Create a model for storing this data, so you can then query that model for the correct reference ID to verify/do stuff.
When the request comes back from the payment gateway use the variables ep_cin, ep_user and ep_doc to get the data from the model.
When you manually request that data you are requesting it with a GET request, which doesn't send the above data along.
The request made by the payment provider will get the result of the DD but that's logged nowhere, so nothing happens.
Log your data for requests triggered by remote api's to see what happens.
I'm introducing to Phalcon, a php framework, by following the tutorial: https://docs.phalconphp.com/en/latest/reference/tutorial-rest.html
I'm facing with this problem: I have this error in the code below
class Robots must be declared abstract or implement methods
'getConnectionService(), setForceExists() etc..'
<?php
use Phalcon\Mvc\Model;
use Phalcon\Mvc\Model\Message;
use Phalcon\Mvc\Model\Validator\Uniqueness;
use Phalcon\Mvc\Model\Validator\InclusionIn;
class Robots extends Model{
public function validation()
{
// Type must be: droid, mechanical or virtual
$this->validate(
new InclusionIn(
array(
"field" => "type",
"domain" => array(
"droid",
"mechanical",
"virtual"
)
)
)
);
// Robot name must be unique
$this->validate(
new Uniqueness(
array(
"field" => "name",
"message" => "The robot name must be unique"
)
)
);
// Year cannot be less than zero
if ($this->year < 0) {
$this->appendMessage(new Message("The year cannot be less than zero"));
}
// Check if any messages have been produced
if ($this->validationHasFailed() == true) {
return false;
}
}
}
?>
And even if I try to execute an HTTP request i get:
Cannot instantiate abstract class Robots
Any ideas?
You probably didn't set your database service properly. Make sure that:
The service name is db (I've experienced problems changing the default services names)
You have set the db service in your main $di (some might instantiate another DI container and isolate the database service there)
Always use $di->setShared(...) for registering global services
Please provide more information and will be glad to help.
I have a Laravel 4 app where I need to send certain emails to users. I have no idea why but I keep getting this error on my test server (it does not happen on my local vagrant box)
Argument 1 passed to Illuminate\Mail\Mailer::getQueuedCallable() must
be of the type array, null given, called in
blablabla/vendor/laravel/framework/src/Illuminate/Mail/Mailer.php
on line 238 and defined
I tried resetting everything: database, clean clone from git. None of these worked. I also cleared my browser cache and cookies out of desperation. I am using Sync queue. So it's not even an actual queue.
Mail::send works just fine. Mail::queue throws the error above.
I literally started to pull my hair out so any help will be greatly appreciated.
Here is my code:
This is my BookingMailer class that extends Mailer class.
// partner bir arabanın rezervasyonuna onay verdiğinde müşteriye gönderilecek ödeme linki
public function sendPaymentLinkToCustomer($customer, $partner, $booking) {
$view = 'emails.bookings.request_confirmed';
$data = [
'user' => $customer->full_name // string,
'partner' => $partner->display_name // string,
'reference' => $booking->reference // integer,
'booking_id' => $booking->id // integer
];
return $this->sendTo($customer, 'Booking Request Confirmed', $view, $data, true, true);
}
And this is my Mailer class that actually queues the mail.
public function sendTo($user, $subject, $view, $data = array(), $admin_copy=false, $send_as_pm=false)
{
Mail::queue($view, $data, function ($message) use ($user, $view, $data, $subject, $admin_copy) {
$message = $message->to($user->email)->replyTo(Config::get('mail.from.address'), Config::get('mail.from.name'))->subject($subject);
return $message;
});
}
Here is the solution to this problem (at least in my case)
I was including the user first and last name to the mail. I was only showing the first letter of the last name, though. And I was using substr for a UTF-8 string, and that's why sometimes it returned problematic characters that get inside the data array and ultimately cause the queue to break.
When i used mb_substr instead of substr, my problem disappeared.
Phew, I thought I would never figure this one out on my own.