Schedule task on the first workday of the month - php

I've currently got a task that collects usage statistics from my website and is set to automatically email them to the client.
The problem is that the 1st of the month may be a non-work day which I guess is not a disaster but looks a bit unprofessional.
This is how I've scheduled it currently:
$schedule
->command("report", [ "--email" => "example#example.com" ]) //My command which accepts the email as a parameter
->monthly();
I was thinking of doing the following:
$schedule
->command("report", [ "--email" => "example#example.com" ]) //My command which accepts the email as a parameter
->monthlyOn(1)
->when(function () {
if (in_array(Carbon::now()->dayOfWeek,[Carbon::SATURDAY,Carbon::SUNDAY])) {
return false;
}
return true;
});
$schedule
->command("report", [ "--email" => "example#example.com" ]) //My command which accepts the email as a parameter
->monthlyOn(2)
->when(function () {
if (Carbon::now()->dayOfWeek == Carbon::MONDAY) {
return true; //1st was in the weekend
}
return false;
});
$schedule
->command("report", [ "--email" => "example#example.com" ]) //My command which accepts the email as a parameter
->monthlyOn(3)
->when(function () {
if (Carbon::now()->dayOfWeek == Carbon::MONDAY) {
return true; //1st and 2nd was in the weekend
}
return false;
});
However this looks like a very strange thing to do for something as simple as that.
So my questions:
If a when condition fails, does the task get attempted again until it succeeds? (Assuming this is a no but not sure)
Is there a simpler way to run a task on the first work-day of the month?

I'll post this as a community wiki answer for others to utilise if they ever need to in the future:
You can put the condition into the actual crontab command:
[ "$(date '+%a')" = "Mon" ] && echo "It's Monday"
Now, if this
condition is true on one of the first seven days in a month, you have
its first Monday. Note that in the crontab, the percent-syntax needs
to be escaped though:
0 12 1-7 * * [ "$(date '+\%a')" = "Mon" ] && echo "It's Monday"
the above is referenced from: https://superuser.com/questions/428807/run-a-cron-job-on-the-first-monday-of-every-month
This way it's set to a cronjob which will only run once per month on the Monday. I believe this will be your most effective method to achieve what you're trying to do.

I know the accepted answer is correct but, there are always different ways I prefer this:
->monthlyOn(Carbon::parse('first monday of this month')->format('d'), '5:00')

Related

Use schedules in Laravel settings

I have profiles that run on a schedule, in the settings of each profile there is a period of everyFiveMinutes type, Then in the kernel I use this
$tasks = ProfileExchange::where('active', true)->whereNotNull('frequency')->get();
foreach ($tasks as $task) {
$frequency = $task->frequency;
$schedule->command(RunExchange::class, ['--id' => $task->id])
->runInBackground()
->$frequency();
}
but if in setup I want to use this at('13:00') then I get an error, help me fix it
Your code won't work because a value of at('13:00') in $task->frequency will become this in your code:
$schedule->command(RunExchange::class, ['--id' => $task->id])
->runInBackground()
->at('13:00')();
It's a bad idea to run code from your database like this anyway, it opens you up to potential code injection.
If I were you, I'd change $task->frequency to be either "every 5 minutes", "every 10 minutes" and other similar options, then if-else or switch-case for them in your code:
if($task->frequency == "every 5 minutes") {
$schedule->command(RunExchange::class, ['--id' => $task->id])
->runInBackground()
->everyFiveMinutes();
} else if
...
Then you can do a regex check for whether $task->frequency is a time, and use at to run it at that time:
...
else if(preg_match("\d?\d:\d\d", $task->frequency)) {
$schedule->command(RunExchange::class, ['--id' => $task->id])
->runInBackground()
->at($task->frequency);
}
You'll need to change how you store the time in your database too, it will have to be 13:00 instead of at('13:00')

sending daily summary email

I am writing up a cron job for daily email notification.
Here are the scenario lets say
User A gets 10 leads a day.
User B gets 5 leads a day.
User C gets 2 leads a day.
i want to send one email to User A having 10 leads information in it.
then one email to User B having 5 leads information in it.
then one email to User C having 2 leads information in it.
so I want to try to create a summary email having lead information in it for a particular user.
foreach ($today_leads as $today_lead) {
$data[] = [
'user_id' => $today_lead->user_id,
'user_fullname' => $today_lead->user_fullname,
'user_email' => $today_lead->user_email,
'lead_firstname' => $today_lead->first_name,
'lead_lastname' => $today_lead->last_name,
'lead_email' => $today_lead->email,
'lead_phone' => $today_lead->phone,
'lead_source' => $today_lead->source,
];
Mail::to(data['user_email'])->send(new DailyLeadSummary($data));
}
if I write my in foreach loop then I end up sending 10 emails to User A, 5 emails to User B, and so on.
Any other approach to handle this?
You can first group your results based on user_email and then send email in second loop.
$grouped_data = [];
foreach ($today_leads as $lead) {
$grouped_data[$lead->user_email][] = [
'user_id' => $lead->user_id,
'user_fullname' => $lead->user_fullname,
'user_email' => $lead->user_email, // you can remove it if not required in email body anywhere
'lead_firstname' => $lead->first_name,
'lead_lastname' => $lead->last_name,
'lead_email' => $lead->email,
'lead_phone' => $lead->phone,
'lead_source' => $lead->source,
];
}
foreach($grouped_data AS $user_email => $data)
{
Mail::to($user_email)->send(new DailyLeadSummary($data));
}
Looking at your implementation, you're on the right path, but you need to make some adjustments.
first of all, you need to query the user table to get users with leads on the particular day. do that like this
// This will return only users who got leads today
$usersWithLeads = User::whereHas('leads', function($query) {
// This will match all leads that was created today
$query->where('created_at', >=, now()->toStartOfDay());
})->get();
// then you can loop over $usersWithLeads
foreach($usersWithLeads as $usersWithLead) {
// You can then retrieve the leads gotten today via the leads relationship
$leads = $usersWithLead->leads()->where('created_at', now()->startOfDay());
// Then send your mail
Mail::to($usersWithLead)->send(new DailyLeadSummary($leads));
}
You can then render the leads on your mailable view however you like. But this makes sure you send only one email per user.
Also, You can modify the query to retrieve $usersWithLeads to also retrieve the daily leads like this:
$usersWithLeads = User::with(['leads' => function ($query) {
$query->where('created_at', now()->toStartOfDay());
}])->whereHas('leads', function($query) {
// This will match all leads that was created today
$query->where('created_at', >=, now()->toStartOfDay());
})->get();
Then your loop will look like:
foreach($usersWithLeads as $usersWithLead) {
// Then send your mail
Mail::to($usersWithLead)->send(new DailyLeadSummary($usersWithLead->leads));
}
Note: the now() method used in the query is a Carbon helper method for Carbon::now() which gives you the current time, chaining the ->startOfDay() method on now() retrieves the datetime at 0:00 (when the day started)
This Answer also assumes you have a table to hold leads and have declared a relationship in your User model like:
public function leads() {
return $this->hasMany(App\Leads::class, 'user_id', 'id');
}
Let me know if you need more help with this.

I want to change value of data if countdown timer has passed in Laravel 6

I'm making a Queueing system and I want the called queue number to go back to last queue in case the user doesn't show for the transaction for a given period of time. Please help me how to do this.
This is my function for calling a queue:
public function callqueue(Request $request)
{
$request->validate([
'called' => ['max:255'],
'counter' => ['max:255']
]);
$dept = Auth::user()->department;
$call = Queue::where([
['department',$dept],
['called', 'no']
])
->whereDate('created_at', Carbon::today())
->orderBy('id', 'asc')
->first();
if ( ! is_null($call)) {
$call->update([
'called' => $request->called,
'counter' => $request->counter
]);
$call->save();
event(new NewQueue($call));
return redirect('admin')->withStatus(__('Queue has been called.'));
} else {
return redirect('admin')->withStatus(__('No available queue for calling.'));
}
}
I have created similar queue system, do comment if you need any modification
I want the called queue number to go back to last queue in case the
user doesn't show for the transaction for a given period of time.
On queue table put 2 timestamps last_updated_at and next_update_at
Pick the first in queue ordering by nexy_update_at
$call=Queue::...
->where('next_update_at', '>', Carbon::now()->timestamp) # I recommend to use timestamp not datetime
->orderBy('next_update_at', 'asc')
->first();
if ($call) {
// do the task
}
Now whenever person shows up in website or anything you wish, just pick his queue task and update the next_update_at
Queue::where('user_id', ...)
->...
->update(['next_update_at', now()->timestamp]);

Laravel Collection Transform/Map Method Inconsistent Behaviour

In my HTML frontend, I have a jQuery DataTable displaying all records fetched via AJAX from the database - a rather pretty straight forward thing. I use the Laravel Collection's ->transform(function($o){ . . . }) to iterate through the collection and return it in an array-esque manner. Just think of the following piece of code in a controller:
$cAllRecords = DatabaseRecord::all();
if(!empty($aData['sFilterIds']))
{
$cAllRecords = $cAllRecords->whereIn('creator', explode(',', $aData['sFilterIds']));
}
return response()->json(['data' => $cAllRecords->transform(function ($oDatabaseRecord) {
/** #var $oDatabaseRecord DatabaseRecord */
$sActionsHtml = 'edit';
$sUrl = route('some.route', ['iDatabaseRecordId' => $oDatabaseRecord->getAttribute('od')]);
return [
$oDatabaseRecord->getAttribute('id'),
$oDatabaseRecord->getAttribute('updated_at')->toDateTimeString(),
$oDatabaseRecord->getAttribute('created_at')->toDateTimeString(),
$sActionsHtml
];
})]);
I'm actually just filtering for records created by certain user IDs (the whereIn() call in line 4. However, the response sent back to the client looks different for different users filtered leading the jQuery table to show 'no records available', as it had received an malformed answer from the server. For one user, the response looks like this:
{
"data":[
[
1,
"2019-05-29 16:44:53",
"2019-05-29 16:44:53",
"<a href=\"#\">edit<\/a>"
]
]
}
This is a correctly formed server response and will show up in the table regularly. Great! Now something that drives me insane - the same code for another user (ID 1, while the first request was for user ID 2) returns this:
{
"data":{
"1":[
3,
"2019-05-29 17:08:49",
"2019-05-29 17:08:49",
"<a href=\"#\">edit<\/a>"
]
}
}
which, pretty obviously, is malformed and is not correctly parsed by the datatable. OK, now combing them two filters and filtering for user ID 1 and 2 will, again, return the response correctly formatted:
{
"data":[
[
1,
"2019-05-29 16:44:53",
"2019-05-29 16:44:53",
"<a href=\"#\">edit<\/a>"
],
[
3,
"2019-05-29 17:08:49",
"2019-05-29 17:08:49",
"<a href=\"#\">edit<\/a>"
]
]
}
I tried a number of things, none of which had worked since it's merely guessing why it could work with one user and not with another. (Things like reversing the order of IDs to be filtered, etc., but I found out that the filtering is not the problem. It MUST be the transform, which behaves inconsistent.)
Any ideas on why this happens and how to tackle it? I mean, it's not the only way to achieve what I'm after, I was using ->each() and array_push for all the time before but wanted to get rid of it for the sake of making use of Laravel's helpers (or possibilites) - the manual iteration and array pushing process worked out seamlessly before, and even other parts of the app work well with the Collection transform over array iteration and pushing. Why doesn't it here?
Update: The ->map() collection method behaves exactly same. Map, as opposed by transform, does not alter the collection itself. However, this should not be a relevant part within this application any way. I really can't understand what's going wrong. Is this possibly Laravel's fault?
Please note that transform method returns a Illuminate\Support\Collection.
It's better that you call all() after the transform to get an array result.
Like this:
...
return response()->json(['data' => $cAllRecords->transform(function ($oDatabaseRecord) {
/** #var $oDatabaseRecord DatabaseRecord */
$sActionsHtml = 'edit';
$sUrl = route('some.route', ['iDatabaseRecordId' => $oDatabaseRecord->getAttribute('od')]);
return [
$oDatabaseRecord->getAttribute('id'),
$oDatabaseRecord->getAttribute('updated_at')->toDateTimeString(),
$oDatabaseRecord->getAttribute('created_at')->toDateTimeString(),
$sActionsHtml
];
})->all()]);
#Cvetan Mihaylov's answer made me look at all the available collection methods (https://laravel.com/docs/5.8/collections#available-methods) and I found ->values() to return the values reindexed. And - that did the trick! :-)
return response()->json(['data' => $cAllRecords->transform(function ($oDatabaseRecord) {
/** #var $oDatabaseRecord DatabaseRecord */
$sActionsHtml = 'edit';
$sUrl = route('some.route', ['iDatabaseRecordId' => $oDatabaseRecord->getAttribute('od')]);
return [
$oDatabaseRecord->getAttribute('id'),
$oDatabaseRecord->getAttribute('updated_at')->toDateTimeString(),
$oDatabaseRecord->getAttribute('created_at')->toDateTimeString(),
$sActionsHtml
];
})->values()]);

Using PHP Faker in Laravel to generate "unique with" entry when seeding a database using a factory

So Similar to the unique with validation rule (See: https://github.com/felixkiss/uniquewith-validator), I want to know how to generate a entry, where one column is unique with another one. I want to seed my database as follows.
Example:
There are 12 steps in the "steps" table. Each step should have 5 categories associated with each one that are stored in the "step_categories" table. Each of those categories are assigned a unique order number 1 through 5 that is unique with each "step_id".
See this image here for an example of what the database should look like: https://imgur.com/a/XYA5yyn
I had to manually to make the entries in the database for the above image example. I don't want to have to generate this manually every time, say I make a mistake and have to rollback the migrations for example.
I am using a factory to generate this data. So the factory name is StepCategoriesFactory.php and clearly I'm calling the factory with the create() method from the DatabaseSeeder.php file.
I thought about doing this in a for loop, then i got as far as realizing when i called the 'step_id' => App\Model::all()->random()->id to grab a new id, that I wouldn't be able to ensure I wasn't grabbing the id that i just generated 5 entries for. I'm really new with Laravel, and I'm not sure where to even start on this. There's no real information on SO where faker can use the unique with another column. How would I Go about this?
NOTE: The step id is not always going to be 1-12. The step ID might be different depending on whether a step gets deleted and remade. So just assigning the step_id to equal 1-12 wont work.
UPDATE: Here's some code I just wrote, and I think I'm on the right track. Maybe. I've grabbed the step_id by it's number field as that will always be 1-12, and I've grabbed the IID out of the entry. But now I'm stuck on how to generate the order 1-5 without repeating itself. I still haven't run this yet as its incomplete and I know it'll throw an error without the correct order number.
UPDATE 2: I think I'm on the right track here. However I'm getting an undefined variable error. When I put the first line from within the anonymous function, it's resetting the order to "1" for every entry. How do i make the $autoIncrement variable available to the anonymous function? The Seeder has stayed the same between updates.
Image of the error: https://imgur.com/a/ywkd0Lb
Second image with the Die/Dump error in terminal: https://imgur.com/a/rOGRv32
Reference this article here: https://laracasts.com/discuss/channels/laravel/model-factory-increment-value-faker?page=1
UPDATE 3: I forgot the use ($autoIncrement) line of code for the anonymous function. Code below has been updated, but now I'm getting a different error saying that the order column has a null value and can't be inserted. clearly it should be '1'. Even after I call my $autoIncrement->next(); which should increment it to '1' it's still returning null according to the terminal. However, when I do a diedump on $autoIncrement->current() it's returning 1. Weird.
Update 3 error: https://imgur.com/a/STOmIjF
StepCategoriesFactory.php
use Faker\Generator as Faker;
$autoIncrement = autoIncrement();
$factory->define(App\StepCategory::class, function (Faker $faker) use ($autoIncrement) {
// Generate Created At and Updated at DATETIME
$DateTime = $faker->dateTime($max = 'now');
$autoIncrement->next();
$order = (int) $autoIncrement->current();
return [
// Generate Dummy Data
'order' => $order,
'name' => $faker->words(4, true),
'created_at' => $DateTime,
'updated_at' => $DateTime,
];
});
function autoIncrement()
{
for ($i = 0; $i < 5; $i++) {
yield $i;
}
}
Edit: Put a bounty on this question, as I think it would be helpful for the community to get a detailed answer. I'm looking for help to explain how to go about making sure I'm grabbing the same entry through each loop.
FINALLY SOLVED!
So I took in everyone's answers, and thought long and hard about using a for loop to create the order number. 1-5. The problem that I was running into at the end was that the $i variable was not resetting. So after the yield I had to check if the $i variable equalled 5 and then reset it back to zero.
Heres the code!
StepCategories.php
use Faker\Generator as Faker;
$autoIncrement = autoIncrement();
$factory->define(App\StepCategory::class, function (Faker $faker) use ($autoIncrement) {
// Generate Created At and Updated at DATETIME
$DateTime = $faker->dateTime($max = 'now');
// Get the next iteration of the autoIncrement Function
$autoIncrement->next();
// Assign the current $i value to a typecast variable.
$order = (int) $autoIncrement->current();
return [
// Generate Dummy Data
'order' => $order,
'name' => $faker->words(4, true),
'created_at' => $DateTime,
'updated_at' => $DateTime,
];
});
function autoIncrement()
{
// Start a loop
for ($i = 0; $i <= 5; $i++) {
// Yield the current value of $i
yield $i;
// If $i is equal to 5, that must mean the start of a new loop
if($i == 5) {
// Reset $i to 0 to start over.
$i = 0;
}
}
}
DatabaseSeeder.php
// Generate Dummy Categories
// Run the factory 12 times
foreach(range(1, 12) as $i) {
// Generate 5 entries each time
factory(App\StepCategory::class, 5)->create([
// Since all steps have a number 1-12 grab the step by the number column and get it's ID
'step_id' => App\Step::where('number', '=', $i)->first()->id,
]);
}
Thanks to all who helped!
Sorry if you don't understand my point so I'll try to explain it in code
use Illuminate\Database\Seeder;
$factory->define(App\StepCategory::class, function (Faker $faker) {
// Generate Created At and Updated at DATETIME
$DateTime = $faker->dateTime($max = 'now');
$step_id = function () {
return factory('App\Step')->create()->id;
};
return [
// Generate Dummy Data
'step_id' => $step_id,
'order' => uniqueOrder($step_id),
'name' => $faker->words(4, true),
'created_at' => $DateTime,
'updated_at' => $DateTime,
];
});
function uniqueOrder($step_id)
{
$unique = rand(1,5);
do {
$unique = rand(1,5);
}
while(StepCategory::where('step_id', $step_id)->andWhere( 'order', $unique)->exists())
return $unique;
}
for example if your Step model name is Steps :
$allSteps = Steps::all();
foreach($allSteps as $step){
for($i=1;$i<6;$i++){
//insert to table $step->id , $i for example
DB::table('yourTableName')->insert([
'name'=>'Step '.$step->id.'- Category '.$i ,
'order'=>$i ,
'step_id'=>$step->id
]);
}
}

Categories