Scheduled Command to update records - laravel

Morning all,
I am trying to create a command that I can schedule to check if a certification date has expired and if it has, update the boolean from 0 to 1. I have never used commands before and I have read the OctoberCMS documentation but I found it confusing.
If anyone could help me, that would be perfect.
Here is what I have so far.
<?php
namespace Bitpixlimited\Concert\Console;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use BitpixLimited\ConCert\Models\Certification;
use Carbon\Carbon;
/**
* CheckExpiredCertifications Command
*/
class CheckExpiredCertifications extends Command
{
/**
* #var string name is the console command name
*/
protected $name = 'concert:checkexpiredcertifications';
/**
* #var string description is the console command description
*/
protected $description = 'No description provided yet...';
/**
* handle executes the console command
*/
public function handle()
{
$certifications = Certification::all();
$date = now();
$expiredValue = '1';
foreach ($certifications as $certification) {
if ($certification->expiry_date < $date) {
$certification->status = $expiredValue;
}
$certification->save();
}
}
/**
* getArguments get the console command arguments
*/
protected function getArguments()
{
return [];
}
/**
* getOptions get the console command options
*/
protected function getOptions()
{
return [];
}
}

Take a look at this code:
public function handle()
{
$certifications = Certification::query()
->where('expiry_date', '<', now())
->update(['status' => '1'])
}
It does what you are trying to achieve, it's a simplified version of your code and it is more performant.
We don't actually get the records, we update them directly
We update all records that have a expiry_date before now()
All these records now have the status equals to 1
Since we don't store the records in memory and we don't need to "build" the Collection, it's far better performance wise.
The drawback is that you lose model events (if you declared any) and mutators, but I assume that's not the case here.
If you need to access all models mutators, methods, events (now or in the future), then use the following code:
public function handle()
{
$certifications = Certification::query()
->where('expiry_date', '<', now())
->each(function(Certification $certification){
$certification->status = '1';
$certification->save();
});
}
The main difference is that we actually retrieve the records and build all the Certification instances. It gives you more capabilities but the performances will take a hit.
There are more optimized ways to do this, especially if you have a large number of rows, but this is another topic.
You should run this command in your scheduler at the frequency you wish, for instance every minute:
protected function schedule(Schedule $schedule)
{
$schedule->command('concert:checkexpiredcertifications')->everyMinute();
}
Every minute, we will update all records that have expiry_date in the past and set their status to '1'.
Of course, you must have a working scheduler to do this, but that's a little bit off topic here (docs here: https://laravel.com/docs/8.x/scheduling#running-the-scheduler).

Related

Laravel - formatting all dates simultaneously

I'm currently working on a POC to showcase that it's going to be fairly painless to create an API with Laravel, the catch being that the database is already set in stone.
One problem I've run into is that they've used custom created at and updated at column names, e.g. for a car table, the created_at column would be car_time and the updated date would be cardata_time, and these are all saved as unix timestamps.
I know you can set the CREATED_AT and UPDATED_AT columns for each model. I want to go another step and return all dates in ISO 8601 format.
I've inserted a class between my models and Model called MasterModel and I want to do something like
protected function getCreatedAtAttribute($value)
{
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
to make all created at dates be in that format. The problem is that I the custom created at and updated columns mean that this never gets called.
Is there a way for me to identify the created at and updated at columns in such a way that I can use a single method to updated all created at dates at the same time?
UPDATE: I realize my original question was not clear enough - I need to identify all fields that are dates, not just created_at and updated_at, and have them formatted a certain way. They will always be unix timestamps. Not sure how I'd go about this.
Here an answer that will expand on #caddy dz answer who happen to be sitting with me.
 All the things that need to be known
Deactivation of auto management of timestamps
public $timestamps = false; // <-- deactivate the automatic handling
Change table attributes names
const CREATED_AT = 'creation_date'; // <--- change the names
const UPDATED_AT = 'last_update';
source doc:
https://laravel.com/docs/5.8/eloquent#eloquent-model-conventions
By default, Eloquent expects created_at and updated_at columns to
exist on your tables. If you do not wish to have these columns
automatically managed by Eloquent, set the $timestamps property on
your model to false:
 Creating the accessors
class User extends Model
{
/**
* Get the user's first name.
*
* #param string $value
* #return string
*/
public function getFirstNameAttribute($value)
{
// do whatever you want here (change and mutate the value)
return ucfirst($value);
}
}
First thing to know, is that the accessors are a global concept for
eloquent and can be writing for all attributes and not just
getCreatedAtAttribute or getUpdatedAtAttribute.
Second thing to know is that whatever the name of the column, that is
in camle case (firstName) or with _ (first_name) eloquent know to
match to it. The format of the accessor should be
get[NameOfATtribute]Attribute in pascal case (camle case but first
letter too in uppercase).
Three the method argument hold the value of the column in
question. Bellow a snippet that show how it's used
$user = App\User::find(1);
$firstName = $user->first_name; //|=> first_name => getFirstNameAttribute(columnVal)
The resolution is clear.
first_name (column name) => getFirstNameAttribute(columnValue)
All the snippets are from the doc: https://laravel.com/docs/5.8/eloquent-mutators#accessors-and-mutators
 Let's apply all of that
First we need to not use $table->timestamps() in the migration so we make the changment to the bellow.
Schema::create('cars', function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamp('cardata_time', 0)->nullable();
$table->timestamp('car_time', 0)->nullable();
});
Then we apply the modification on our model:
- we deactivate the auto handling of timestamps.
- Override the timestamps columns names.
- And create the accessors.
Here depend on what we want. If we want to only do the above here a snippet that show that:
// deactivate auto timestamps management
public $timestamps = false;
// change the columns names
const CREATED_AT = 'car_time';
const UPDATED_AT = 'cardata_time';
// creating the accessors (respect the naming)
protected function getCarTimeAttribute($value) //car_time => CarTime
{
// <-- do whatever you want here (example bellow)
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
protected function getCardataTimeAttribute($value) //cardata_time => CardataTime
{
// <-- do whatever you want here
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
 Doing it with renaming the attributes completely
If what you want is to use another accessing name. Then what my friend #caddy dz did is the way to go. Which happen to be sitting with me. And dared me to expand upon the answer. (hhhh)
You will need to know
$appends and $hidden
Part of the serialization API.
https://laravel.com/docs/master/eloquent-serialization#appending-values-to-json
https://laravel.com/docs/master/eloquent-serialization#hiding-attributes-from-json
$appends allow us to add attributes to the model. That don't exists on the table. We need also to create an accessors for them.
class User extends Model
{
/**
* The accessors to append to the model's array form.
*
* #var array
*/
protected $appends = ['is_admin'];
// ........
/**
* Get the administrator flag for the user.
*
* #return bool
*/
public function getIsAdminAttribute()
{
return $this->attributes['admin'] == 'yes';
}
}
and
$hidden allow us to remove and limit the attribute from the models. Like with the password field.
Doc examples:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = ['password'];
}
And from that what we need to do is to hide the attributes holding the time, that want to be changed to somehting else.
// remove the old attributes names
protected $hidden = ['car_time', 'cardata_time']; // renaming those
// append the new one \/ \/ <- to those
protected $appends = ['car_crated_at', 'cardata_created_at']; // names just for illustration
protected function getCarCreatedAtAttribute($value) // car_created_at => CarCreatedAt
{
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
protected function getCardataCreatedAtAttribute($value) // cardata_created_at => CardataCreatedAt
{
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
 Applying it for different models
The basic idea is to create a base model then extend it when you create your model.
Formatting all time attributes of the model without exception
If what you want is to apply the formatting for all the time attributes within the model.
Then override serializeDate() method. In practice write a trait, and then you can apply it. Otherwise a base model.
The answer bellow cover it well:
https://stackoverflow.com/a/41569026/7668448
And historically This thread is interesting :
https://github.com/laravel/framework/issues/21703
Serializing in Carbon level
In the documentation laravel 5.7 and up (what i checked [doc only]) :
https://laravel.com/docs/master/eloquent-serialization#date-serialization
We can change the formatting at the level of carbon serialization. But it happen that there was a bug in the past. Normally fixed but i didn't try it. Bug was in 5.7 and fixed in 5.7 if i'm not wrong. The git link above discuss it.
Snippet:
class AppServiceProvider extends ServiceProvider
{
/**
* Perform post-registration booting of services.
*
* #return void
*/
public function boot()
{
Carbon::serializeUsing(function ($carbon) {
return $carbon->format('U');
});
}
___THE_END ^ ^
Not sure what you're asking but if you have cardata_time and car_time in your table defined like this
Schema::create('cars', function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamp('cardata_time', 0)->nullable();
$table->timestamp('car_time', 0)->nullable();
});
And a MasterModel like so
/**
* Indicates if the model should be timestamped.
*
* #var bool
*/
public $timestamps = false;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
/**
* The accessors to append to the model's array form.
*
* #var array
*/
protected $appends = ['created_at', 'updated_at'];
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = ['car_time', 'cardata_time'];
protected function getCreatedAtAttribute($value)
{
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
protected function getUpdatedAtAttribute($value)
{
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
Results:
{
"id": 1,
"created_at": "2019-09-02T20:31:38Z",
"updated_at": "2019-09-02T20:31:38Z"
}
As in the documentation. The first approach. This requires the dates, to be defined in the $dates property. This will only be triggered if the Model is serialized.
public class YourModel extends Model
{
protected $dateFormat = "Y-m-d\TH:i:s\Z";
}
You can also define it in a provider in the boot method. Which will trigger when a Carbon date is serialized.
public function boot()
{
Carbon::serializeUsing(function ($carbon) {
return $carbon->format("Y-m-d\TH:i:s\Z");
});
}

Laravel 5.5 . Re-send data with the replacement of existing ones

there is a task:
1) the user will be retrieved and stored in the variable $user = User::find(1);
2) then the function displays experience;
3) In parallel with the operation of the function, the asynchronous method changes the experience by a random number every few seconds.
In the first function, the user experience is displayed once again. What will this conclusion be?
How can I implement the output on the page of deferred calculations?
Do I understand correctly that there should be the following sequence:
- on the page shows the experience;
- in parallel - every 3 seconds, the experience update is launched;
- in a minute (for example) the experience value is updated on the page?
There are 2 operations happening essentially independent of each other :
- Fetching and showing the information on frontend
- Updating the data in background
Each of this has some nitty gritty things like updates in X seconds etc which can be handled if we look at them as separate operations.
Frontend :
You can have simple route which gets the user using $user = User::find(1); as you mentioned in question and shows the information. That data will be what the respective user contains at the moment when the query is performed. It will have nothing to do with the background updates happening.
Then having requirements of fetching the updates, depending on which ever frontend javascript library you are using, you can have an Ajax call happening at an internal of X minutes depending on your refresh rate. This ajax call will get the updated information.
Handling background updates :
You can create an artisan command which has code to update the records.
Then you can schedule it to run every 3 minutes using laravel scheduling like :
$schedule->command('update:userexperience')->cron('3 * * * *');
And add * * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1 in your server's crontab`
All above details are in documentation
Tips while scheduling command :
When you are updating users, if you re running just one query to update all users then its great. But if you have a logic which assigns a new value for each user in an individual row update, use chunk to load limited records at a time. This will help you keep the memory utilisation under a limit.
Testing before scheduling :
Doing it every 3 seconds is a very very small interval. I would suggest first run a command manually and check how much time it takes. If the backgrund process takes 2 seconds to complete one time, 3 second interval is tooo small.
Also, if the number of records in users table are increasing quickly, you need to revisit sometime later to increase the cron interval. It is good to keep that in mind.
Update :
The smallest unit in a cron is 1 minute. So to schedule it for every 3 seconds you can do inside app/Console/Kernel.php's schedule function :
$secondInterval = 3;
$totalJobs = 60 / $secondInterval;
for ($i = 0; $i < $totalJobs; $i++) {
$schedule->command('update:userexperience', [ '--delay'=> $secondInterval ])->everyMinute();
}
And then inside your command you delay :
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class UpdateUserExperienceCommand extends Command
{
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'update:userexperience {--delay= : Number of seconds to delay command}';
/**
* The console command description.
*
* #var string
*/
protected $description = 'Import the CSVs';
/**
* Create a new command instance.
*
* #return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the command
*
* #return void
*/
public function handle()
{
sleep($this->option('delay'));
//update users
return;
}
}
Done.
Create a Job, which is designed for 30 requests within one and a half minutes. Every 3 seconds.
class UpdateUserExperienceJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $user;
/**
* Create a new job instance.
*
* #param User $user
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
for ($i = 3; $i <= 90; $i += 3) {
sleep(3);
$this->user->update(['experience' => rand()]);
}
}
}
Create a method in the controller. Depending on the parameter repeatRequest depends on the launch of Job.
public function getExperience(): array
{
$user = User::find(request()->get('user_id'));
request()->get('repeatRequest') === 'true' ?: UpdateUserExperienceJob::dispatch($user);
return ['experience' => $user->experience];
}
Front implemented through Vue.js. In the component, when loading, we first get the current experience of the user, and then every 5 seconds we run a repeated request with a positive value of the repeatedRequest property. Due to the calculated properties, the value will change dynamically.
<template>
<div class="container">
<p>experience is updated every 5 seconds</p>
<p v-html="computedExperience"></p>
</div>
</template>
<script>
export default {
data: function () {
return {
experience: 0,
user_id: parseInt(document.URL.replace(/\D+/g,""))
}
},
mounted() {
this.firstExperience();
setInterval(this.repeatedExperience.bind(this), 5000);
},
computed: {
computedExperience() {
return this.experience;
},
},
methods: {
firstExperience: function () {
axios.get(document.URL + '/api', {
params: {
user_id: this.user_id,
repeatRequest: false
}
}).then((response) => {
this.experience = response.data.experience;
});
},
repeatedExperience: function () {
axios.get(document.URL + '/api', {
params: {
user_id: this.user_id,
repeatRequest: true
}
}).then((response) => {
this.experience = response.data.experience;
});
}
},
}
</script>

Cache Eloquent query for response

In one of my applications I have a property that is needed throughout the app.
Multiple different parts of the application need access such as requests, local and global scopes but also commands.
I would like to "cache" this property for the duration of a request.
My current solution in my Game class looks like this:
/**
* Get current game set in the .env file.
* #return Game
*/
public static function current()
{
return Cache::remember('current_game', 1, function () {
static $game = null;
$id = config('app.current_game_id');
if ($game === null || $game->id !== $id) {
$game = Game::find($id);
}
return $game;
});
}
I can successfully call this using Game::current() but this solutions feels "hacky" and it will stay cached over the course of multiple requests.
I tried placing a property on the current request object but this won't be usable for the commands and seems inaccessible in the blade views and the objects (without passing the $request variable.
Another example of its usage is described below:
class Job extends Model
{
/**
* The "booting" method of the model.
*
* #return void
*/
protected static function boot()
{
parent::boot();
static::addGlobalScope('game_scope', function (Builder $builder) {
$builder->whereHas('post', function ($query) {
$query->where('game_id', Game::current()->id);
});
});
}
}
I do not believe I could easily access a request property in this boot method.
Another idea of mine would be to store the variable on a Game Facade but I failed to find any documentation on this practice.
Could you help me find a method of "caching" the Game::current() property accessible in most if not all of these cases without using a "hacky" method.
Use the global session helper like this:
// Retrieve a piece of data from the session...
$value = session('key');
// Store a piece of data in the session...
session(['key' => 'value']);
For configuration info and more options: https://laravel.com/docs/5.7/session

Laravel Model conditional formatting

I have a database and model called Vote_actions that looks like this:
id
group_id
user_id
action_type
anonymous (boolean)
User can ask to be anonymous (that would make the boolean value to be true).If that is the case, I want to change the group_id and user_id from the returned model to -1.
Is there a way in laravel that I can do it ?
I know this question is old. I was looking for a way to hide some fields on certain conditions, external conditions like Auth Roles, and internal conditions like Model attributes, and I found a very flexible way to hide them.
And since I saw the other OP's duplicated post Laravel Hidden Fields On Condition asking for hiding field instead, So I'm gonna share it with you.
I know a mutator can change the value of its field, but to Hide it, you need :
the $hidden array attribute
the constructor __Construct() (optional)
to override method newFromBuilder method of Laravel Model
Here are the processes in the Model app\Vote_actions.php:
Hidden. Let's say you normally want to hide the fields created_at and updated_at of Laravel, you use:
protected $hidden = ['created_at', 'updated_at'];
External Conditions. Now let's say if the Authenticated User is Staff you want to unhide them:
public function __Construct()
{
parent::__construct();
if(\Auth::check() && \Auth::user()->isStaff()) {
// remove all fields so Staff can access everything for example
$this->hidden = [];
} else {
// let's hide action_type for Guest for example
$this->hidden = array_merge($this->hidden, ['action_type'];
}
}
Internal Conditions Let's say now you want to hide anonymous field is its value is true:
/**
* Create a new model instance that is existing.
*
* #param array $attributes
* #param array $connection
* #return \Illuminate\Database\Eloquent\Model|static
*/
public function newFromBuilder($attributes = array(), $connection = null)
{
$instance = parent::newFromBuilder($attributes, $connection);
if((bool)$instance->anonymous === true) {
// hide it if array was already empty
// $instance->hidden = ['anonymous'];
// OR BETTER hide single field with makeHidden method
$instance->makeHidden('anonymous');
// the opposite is makeVisible method
}
return $instance;
}
You can't play with hidden attributes and method inside mutators, that's their weakness when we need to hide instead of changing values.
But in any case, understand that calling modification on high load of hundredths of rows can be costly in time.
You are leaning towards an edge case, with special conditions.
Make use of accessors:
class VoteActions extends \Eloquent {
public $casts = [
'anonymous' => 'boolean'
];
...
/**
* Accessors: Group ID
* #return int
*/
public function getGroupIdAttribute()
{
if((bool)$this->anonymous === true) {
return -1;
} else {
return $this->group_id;
}
}
/**
* Accessors: User ID
* #return int
*/
public function getUserIdAttribute()
{
if((bool)$this->anonymous === true) {
return -1;
} else {
return $this->user_id;
}
}
}
Official Documentation: https://laravel.com/docs/5.1/eloquent-mutators#accessors-and-mutators
However, i would recommend that you set the value in the database directly to -1 where necessary so as to preserve the integrity of your application.
Of course you can easily do that. Read about accessors (getters):
https://laravel.com/docs/5.1/eloquent-mutators
Example:
function getUserIdAttribute()
{
return $this->anonymous ? -1 : $this->user_id;
}
function getGroupIdAttribute()
{
return $this->anonymous ? -1 : $this->group_id;
}

Magento model delete function

I have Model class which extends Mage_Core_Model_Abstract. I have a resource which extends Mage_Core_Model_Mysql4_Abstract. Now I am deleting from controller with the following code:
public function deleteAction()
{
$params = $this->getRequest()->getParams();
$blogpost = $this->_createModel();
$blogpost->setId($params['id']);
$blogpost->delete();
echo 'Post with id ' . $params['id'] . ' is deleted';
}
The problem is there is no error shown when I supply non existing ids. What to do ? Do I have to load that id and check whether it exists or not? or should I use the try/catch. I used try and catch but did not give any results.
Of course it does not throw an exception, the DELETE statement gets executed without errors eventually, it only has an affected row count of 0.
Unfortunately the row count gets lost here (Mage_Core_Model_Resource_Db_Abstract):
/**
* Delete the object
*
* #param Varien_Object $object
* #return Mage_Core_Model_Resource_Db_Abstract
*/
public function delete(Mage_Core_Model_Abstract $object)
{
$this->_beforeDelete($object);
$this->_getWriteAdapter()->delete(
$this->getMainTable(),
$this->_getWriteAdapter()->quoteInto($this->getIdFieldName() . '=?', $object->getId())
);
$this->_afterDelete($object);
return $this;
}
because the return value of _getWriteAdapter()->delete() is not used anywhere
It looks like you will indeed have to load your model before deleting it, or at least check for its existence via the ressource collection.

Resources