Laravel Tasks - Storing when last ran - php

I've made CRON Job using Laravel's task scheduling. But what I need is to store somewhere when that task was last ran,
Does anyone have any methods of how they store that and also, If Laravel outputs anything that can tell you when it was last ran?
Thanks

Not possible directly, however it is possible if you cache a date-time string on each run (either at the beginning or end of your script).
Cache::rememberForever('name_of_artisan_task', function () {
return now()->toDateTimeString();
});
The example shows using the Cache facade's ::rememberForever method to create a key/value of the last time the task was ran. As the name suggests, this is saved forever.
You can easily retrieve this date and time using the cache() helper:
cache('name_of_artisan_task');
The con with this method is that if your cache is cleared, you will not longer have this stored.

Using a cache is not a safe way to do this, as #thisiskelvin hinted, clearing the cache will remove the data (which should happen on each deployment) but he didn't provide an alternative
So here is one if you need this date reliably (if you use it to know the interval to run an export for instance)
In which case I recommend creating a model php artisan make:model ScheduleRuns -m
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* #property string $task
*/
class ScheduleRuns extends Model
{
public const UPDATED_AT = false;
public $timestamps = true;
protected $attributes = [
'task' => '',
];
protected $fillable = [
'task',
'created_at',
];
}
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('schedule_runs', function (Blueprint $table) {
$table->id();
$table->string('task');
$table->timestamp('created_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::dropIfExists('schedule_runs');
}
};
Then use schedule hooks to create it (or do it within the task if you want to avoid possible seconds differences)
$schedule->command('export:users')
->weekly()->onSuccess(fn () => ScheduleRuns::create(['task' => 'export:users']))
And to retrieve the latest run
ScheduleRuns::query()->where('task', 'export:users')->latest();

Just write log each time the task was run, or you can push it into database.
<?php
namespace App\Console\Commands\Tasks;
use Illuminate\Console\Command;
class ScheduledTask extends Command
{
public function handle()
{
//
// ...handle you task
//
$file = 'logs/jobs/' . __CLASS__ . '.log';
$message = 'Executed at: ' . date('Y-m-d H:i:s', time());
file_put_contents(storage_path($file), $message, FILE_APPEND);
}
}

Related

Laravel Query Builder hard coded data is not inserted with post method

I am trying to insert hard coded data with QueryBuilder insertGetId() method.
my code is-
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class StudentController extends Controller
{
public function addStudent()
{
$foreign_key = DB::table('students')->insertGetId([
'id' => 'stu-000002',
'name' => 'Ahsan',
'email' => 'ahsan#example.net',
]);
echo $foreign_key;
}
}
My migration file is-
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('students', function (Blueprint $table) {
$table->string('id', 30)->primary();
$table->string('name', 100);
$table->string('email', 100)->unique();
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::dropIfExists('students');
}
};
My route is -
Route::post('/add-student', [StudentController::class, 'addStudent']);
But result is -
Symfony \ Component \ HttpKernel \ Exception \ MethodNotAllowedHttpException
The GET method is not supported for this route. Supported methods: POST.
But when I try to insert data with get method like-
Route::get('/add-student', [StudentController::class, 'addStudent']);
Data has been inserted . But primary key has been retrived 0 as primary key is custom string.
How can I solve this problem. Thank you.
run this command in terminal:
php artisan route:cache
So what this command does is, it registers new routes or gives an error if there is something wrong with your route file.
There are two problems with what you're doing:
Problem 1:
The first is the MethodNotAllowedException. I guess you're trying to use a GET request on a POST URL. This won't work, because Laravel blocks the 'wrong' method.
Use POST when you have data to submit (or if you really want to stick to the 'use post when saving'-creed use a form). Use GET when you want to access an URL.
Problem 2
According to the API (This one) insertGetId returns an integer which is the ID. Since your ID's are strings, you can't use that method.
A solution to that problem would be to change the code like this:
public function addStudent()
{
$student_id = 'stu-000002'
$insert = DB::table('students')->insert([
'id' => $student_id,
'name' => 'Ahsan',
'email' => 'ahsan#example.net',
]);
if ( ! $insert ) {
return false;
}
echo $student_id;
}
The insert method returns a boolean. Leveraging that you can check whether or not the entry was inserted. If it is, your ID should be good.

Changing the type of last_activity column when using database session storage in Laravel 5

I've just started using the database session driver in Laravel 5.5 along with PostgreSQL, and I've ran into a slight inconvenience.
I would like to keep the last_activity column as timestamp with timezone, but Laravel wants to write an integer into it, and it sometimes also attempts to delete from it based on an integer value.
I tried doing the following in my Session model:
public function setLastActivityAttribute($ts){
$this->attributes['last_activity'] = date('c', $ts);
}
which works well for saving, but when Laravel tries to garbage collect the sessions it uses an integer value which causes a PDOException:
SQLSTATE[22008]: Datetime field overflow: 7 ERROR: date/time field value out of range: "1506794381"
HINT: Perhaps you need a different "datestyle" setting. (SQL: delete from "sessions" where "last_activity" <= 1506794381)
Is there any way I could specify a format or intercept the process to keep the timestamptz column type?
I suggest creating a custom session driver by overwriting the gc and expired methods of the DatabaseSessionHandler.php driver to work with timestamp with timezone instead of integers.
The gc currently passes time() - $lifetime but you can change this to date('c', time() - $lifetime). And in expired you can call strtotime on $session->last_activity to convert to the same unit as getTimestamp().
<?php
namespace App\Extensions;
use Illuminate\Session\DatabaseSessionHandler;
class MyPostgresHandler extends DatabaseSessionHandler
{
public function gc($lifetime) {
$sessions = $this->getQuery()->where('last_activity', '<=', date('c', time() - $lifetime))->get();
foreach ($sessions as $session) {
$this->destroy($session->id);
}
}
protected function expired($session)
{
return isset($session->last_activity) &&
strtotime($session->last_activity) < Carbon::now()->subMinutes($this->minutes)->getTimestamp();
}
}
Then you can register your new driver by extending ServiceProvider
<?php
namespace App\Providers;
use App\Extensions\MyPostgresHandler;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\ServiceProvider;
class SessionServiceProvider extends ServiceProvider
{
/**
* Perform post-registration booting of services.
*
* #return void
*/
public function boot()
{
Session::extend('my-db-driver', function ($app) {
return new MyPostgresHandler;
});
}
/**
* Register bindings in the container.
*
* #return void
*/
public function register()
{
//
}
}
Now you can use my-db-driver in config/session.php
For more info see https://laravel.com/docs/5.5/session#implementing-the-driver
Update from OP
Adding on top of FuzzyTree's answer, there's some more code that needs to be added than what the docs say, so I wanted to post the rest of the process. Because we're extending the built-in class it has some ties to other parts of the framework, but thankfully there are ways to tap into them. The final SessionServiceProvider looks like this:
namespace App\Providers;
use App\Extensions\MyPostgresHandler;
use Illuminate\Database\Connectors\PostgresConnector;
use Illuminate\Database\PostgresConnection;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Connection;
class SessionServiceProvider extends ServiceProvider {
public function boot(){
Session::extend('postgres', function ($app){
return new MyPostgresHandler;
});
Connection::resolverFor('postgres', function ($connection, $database, $prefix, $config) {
return new PostgresConnection($connection, $database, $prefix, $config);
});
$this->app->bind('db.connector.postgres', function(){
return new PostgresConnector;
});
}
}
I've named my driver postgres and the driver's class is identical to what's in the original answer. My IDE was showing an error on the return new MyPostgresHandler; line due to missing parameters, but it seems like the code works regardless.
These resolvers are necessary for the rest of Laravel to accept our new driver because the built-in classes only map the pgsql name to the appropriate classes, and since we've made a driver with a different name it will deem it invalid otherwise.
Finally, the service provider has to be registered in config/app.php:
'providers' => [
// ...
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\SessionServiceProvider::class, // <--
],

Acces to data using relationships Laravel

i want to access to data in the function of the controller using relationships on Laravel.
I will explain first my code:
I have 3 tables, projects, client and client_project.
At this moment, client_project don't have any relationship, i just add it manually.
Now i want to use relationships on laravel, but it's a bit confusing (for me at least).
I think it's not too much important the code of projects and clients table, just have id like primary key, and some fields more.
My migration of client_project looks like here:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateClientProjectTable extends Migration {
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('client_project', function(Blueprint $table)
{
$table->increments('id');
$table->integer('client_id');
$table->integer('project_id');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::drop('client_project');
}
}
Client_Project model looks like here:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Client_Project extends Model
{
protected $fillable = ['client_id','project_id'];
public $table = 'client_project';
public function clients()
{
return $this->hasMany('App\Models\Project');
}
public function projects()
{
return $this->hasOne('App\Models\Client');
}
}
One client can have more than one project, but one project is only created by one client. I think relationships are declared good.
(At first, i think with relationships i don't need to make the client_project table), but i think that's a wrong idea. I want to make it with this table too.
So, now, the problem it's when i try to call on the function controller, i think i can access to data using por example:
App\Models\Project::find(1), like doc of laravel says.
The function is this:
$client = new Client();
$client->name = $request->input("nameClient");
$client->slug = $request->input("slugClient");
$client->priority = $request->input("priorityClient");
$client->save();
$client_project = new Client_Project();
$client_project->client_id = App\Models\Client::max('id');
$client_project->project_id = App\Models\Projects::max('id');
$client_project->save();
The part of the client, is working. I just take the value of some inputs and i create a new one.
The problem is with $client_project. I want to make it dynamic. I create the client and the project, and my code get the last one(the bigger id), and the last one(the bigger id too) of projects.
How can i access them using relationships?
Maybe need edit migration of client_project and put some key in project_id or client_id?
If need more information, please ask it.
Any help will be appreciated!
Here is your ans. You are going good way you created pivot table for client and project so you can attached as many projects to any client. Here is relationship with model.
Client Model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Client extends Model
{
public function projects() {
return $this->belongsToMany(Project::class,'client_project');
}
}
Project model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Projects extends Model
{
public function client() {
return $this->belongsToMany(Client::class,'client_project');
}
}
?>
For Save project id use following way in controller method
$client = new Client();
$client->name = $request->input("nameClient");
$client->slug = $request->input("slugClient");
$client->priority = $request->input("priorityClient");
$client->save();
$project = new Project();
//include fields as per your table
$project->save();
$client->projects()->attach($project->id);
.

Laravel Saving event in case of bulk-insert

Iam bulk-inserting large database in chunks with this command:
DB::table($table)->insert($chunk);
But want I want is before actual insert operation, I want to be able to modify $chunk array for each table to add/remove certain attributes going into database. In order to do so, I setup saving event in my model:
public static function boot()
{
parent::boot();
static::saving(function ($model) {
Log::info('saving');
return true;
});
}
However, it seems events don't work for Model::insert operations.
Can anybody tell how can I achieve this ?
PS: I can't use save() (though saving event would work with it) method as it would only allow me to save one record at a time whereas I need to do bulk insert of each chunk.
Thanks
In this case you have to create your own Event/Listner.
Something like this :
php artisan make:event SomeEventName
//App\Events\SomeEventName
class SomeEventName extends Event
{
use SerializesModels;
public $chunk;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct(&$chunk)
{
$this->chunk = &$chunk;
}
}
Then you make your listener (you can create a separate file according to laravel docs) or just put it on boot Model method
\Event::listen('App\Events\SomeEventName', function($event) {
$event->chunk = ['hello new world']; // this will replace the old chunk
});
and then use it like this :
$chunk = ['hello old world'];
event(new App\Events\SomeEventName($chunk));
dd($chunk);
DB::table($table)->insert($chunk);

Laravel : Migrations & Seeding for production data

My application needs a pre registered data set to work. So i need to insert them in the database when i set up the application.
Laravel propose two mechanisms :
Database migrations : "They allow a team to modify the database schema and stay up to date on the current schema state."
Database seeding : "Laravel also includes a simple way to seed your database with test data using seed classes."
When I read this description, none of these solutions seems to be adapted.
A similar question has been asked on stackoverflow and answered. The answer proposes to use the a database seeder to populate the database by detecting the current environment :
<?php
class DatabaseSeeder extends Seeder {
public function run()
{
Eloquent::unguard();
if (App::environment() === 'production')
{
$this->call('ProductionSeeder');
}
else
{
$this->call('StagingSeeder');
}
}
}
Of course, this solution works. But i am not sure that it is the right way to do this, because by inserting data using seeders you are losing all the advantages provided by the migration mechanism (database upgrate, rollback...)
I want to know what is the best practice in this case.
Laravel development is about freedom. So, if you need to seed your production database and think DatabaseSeeder is the best place to do so, why not?
Okay, seeder is mainly to be used with test data, but you'll see some folks using it as you are.
I see this important kind of seed as part of my migration, since this is something that cannot be out of my database tables and artisan migrate is ran everytime I deploy a new version of my application, so I just do
php artisan migrate:make seed_models_table
And create my seedind stuff in it:
public function up()
{
$models = array(
array('name' => '...'),
);
DB::table('models')->insert($models);
}
I've often found myself wondering what the right answer to this is. Personally, I'd steer clear of using seeding to populate required rows in the database as you'll have to put a load of conditional logic in to ensure that you don't attempt to populate something that's already there. (Deleting and recreating the data is very inadvisable as you could end up with key mismatches and if you're using cascading deletes you may accidentally wipe a load of your database by mistake! ;-)
I put the 'seeding' of rows into the migration script as the chances are, the data will need to be there as part of the rollout process.
It's worth noting that you should use the DB class instead of Eloquent models to populate this data as your class structure could change over time which will then prevent you from re-creating the database from scratch (without rewriting history and changing you migration files, which I'm sure is a bad thing.)
I'd tend to go with something like this:
public function up()
{
DB::beginTransaction();
Schema::create(
'town',
function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->timestamps();
}
);
DB::table('town')
->insert(
array(
array('London'),
array('Paris'),
array('New York')
)
);
Schema::create(
'location',
function (Blueprint $table) {
$table->increments('id');
$table->integer('town_id')->unsigned()->index();
$table->float('lat');
$table->float('long');
$table->timestamps();
$table->foreign('town_id')->references('id')->on('town')->onDelete('cascade');
}
);
DB::commit();
}
This then allows me to 'seed' the town table easily when I first create it, and won't interfere with any additions made to it at run-time.
This is what I use in production.
Since I run migration on each deployment
artisan migrate
I create a seeder (just to keep seeding data out of migration for easy access later) and then run that seeder along with the migration
class YourTable extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
//migrate your table // Example
Schema::create('test_table', function(Blueprint $table)
{
$table->increments('id');
$table->timestamps();
$table->softDeletes();
});
//seed this table
$seeder = new YourTableSeeder();
$seeder->run();
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::drop('test_table');
}
}
I do not add this seed call to seeds/DatabaseSeeder.php to avoid running it twice on a new installation.
The Artisan Command Solution
Create a new artisan command
php artisan make:command UpsertConfigurationTables
Paste this into the newly generated file: UpsertConfigurationTables.php
<?php
namespace App\Console\Commands;
use Exception;
use Illuminate\Console\Command;
class UpsertConfigurationTables extends Command
{
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'upsert:configuration';
/**
* The console command description.
*
* #var string
*/
protected $description = 'Upserts the configuration tables.';
/**
* The models we want to upsert configuration data for
*
* #var array
*/
private $_models = [
'App\ExampleModel'
];
/**
* Create a new command instance.
*
* #return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* #return mixed
*/
public function handle()
{
foreach ($this->_models as $model) {
// check that class exists
if (!class_exists($model)) {
throw new Exception('Configuration seed failed. Model does not exist.');
}
// check that seed data exists
if (!defined($model . '::CONFIGURATION_DATA')) {
throw new Exception('Configuration seed failed. Data does not exist.');
}
/**
* seed each record
*/
foreach ($model::CONFIGURATION_DATA as $row) {
$record = $this->_getRecord($model, $row['id']);
foreach ($row as $key => $value) {
$this->_upsertRecord($record, $row);
}
}
}
}
/**
* _fetchRecord - fetches a record if it exists, otherwise instantiates a new model
*
* #param string $model - the model
* #param integer $id - the model ID
*
* #return object - model instantiation
*/
private function _getRecord ($model, $id)
{
if ($this->_isSoftDeletable($model)) {
$record = $model::withTrashed()->find($id);
} else {
$record = $model::find($id);
}
return $record ? $record : new $model;
}
/**
* _upsertRecord - upsert a database record
*
* #param object $record - the record
* #param array $row - the row of update data
*
* #return object
*/
private function _upsertRecord ($record, $row)
{
foreach ($row as $key => $value) {
if ($key === 'deleted_at' && $this->_isSoftDeletable($record)) {
if ($record->trashed() && !$value) {
$record->restore();
} else if (!$record->trashed() && $value) {
$record->delete();
}
} else {
$record->$key = $value;
}
}
return $record->save();
}
/**
* _isSoftDeletable - Determines if a model is soft-deletable
*
* #param string $model - the model in question
*
* #return boolean
*/
private function _isSoftDeletable ($model)
{
$uses = array_merge(class_uses($model), class_uses(get_parent_class($model)));
return in_array('Illuminate\Database\Eloquent\SoftDeletes', $uses);
}
}
Populate $_models with the Eloquent models you want to seed.
Define the seed rows in the model: const CONFIGURATION_DATA
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class ExampleModel extends Model
{
use SoftDeletes;
const CONFIG_VALUE_ONE = 1;
const CONFIG_VALUE_TWO = 2;
const CONFIGURATION_DATA = [
[
'id' => self::CONFIG_VALUE_ONE,
'col1' => 'val1',
'col2' => 'val2',
'deleted_at' => false
],
[
'id' => self::CONFIG_VALUE_TWO,
'col1' => 'val1',
'col2' => 'val2',
'deleted_at' => true
],
];
}
Add the command to your Laravel Forge deployment script (or any other CI deployment script): php artisan upsert:configuration
Other noteworthy things:
Upsert Functionality: If you ever want to alter any of the seeded rows, simply update them in your model and it was update your database values next time you deploy. It will never create duplicate rows.
Soft-Deletable Models: Note that you define deletions by setting deleted_at to true or false. The Artisan command will handle calling the correct method to delete or recover your record.
Problems with other mentioned solutions:
Seeder: Running seeders in production is an abuse of the seeders. My concern would be that an engineer in the future would alter the seeders thinking that it's harmless since the documentation states that they are designed to seed test data.
Migrations: Seeding data in a migration is strange and an abuse of the purpose of the migration. It also doesn't let you update these values once your migration has been run.

Categories