Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions resources/lang/en/schedule.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
'create' => 'Create new schedule',
'edit' => 'Edit schedule',
'show' => 'Show run history',
'back_to_application' => 'Back to application'
'back_to_application' => 'Back to application',
'extract' => 'Extract Schedules from Kernel'
],
'fields' => [
'command' => 'Command',
Expand Down Expand Up @@ -33,7 +34,8 @@
'updated_at' => 'Updated At',
'never' => 'Never',
'groups' => 'Groups',
'environments' => 'Environments'
'environments' => 'Environments',
'settings' => 'Settings'
],
'messages' => [
'no-records-found' => 'No records found.',
Expand All @@ -48,7 +50,13 @@
'help-type' => 'Multiple :type can be specified separated by commas',
'attention-type-function' => "ATTENTION: parameters of the type 'function' are executed before the execution of the scheduling and its return is passed as parameter. Use with care, it can break your job",
'delete_cronjob' => 'Delete cronjob',
'delete_cronjob_confirm' => 'Do you really want to delete the cronjob ":cronjob"?'
'delete_cronjob_confirm' => 'Do you really want to delete the cronjob ":cronjob"?',
'import-success' => 'Schedules extracted successfully',
'import-error' => 'Error extracting schedules',
'no-schedules-found' => 'No schedules found in Kernel.',
'all-environments' => 'All',
'no-overlap' => 'No Overlap',
'one-server' => 'One Server'
],
'status' => [
'active' => 'Active',
Expand All @@ -65,7 +73,11 @@
'delete' => 'Delete',
'history' => 'History',
'cancel' => 'Cancel',
'restore' => 'Restore'
'restore' => 'Restore',
'extract_kernel' => 'Extract from Kernel',
'select_all' => 'Select All',
'select_none' => 'Select None',
'import_selected' => 'Import Selected'
],
'validation' => [
'cron' => 'The field must be filled in the cron expression format.',
Expand Down
120 changes: 120 additions & 0 deletions resources/views/extract.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
@extends('schedule::layout.master')

@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>{{ trans('schedule::schedule.titles.extract') }}</span>
<a href="{{ action('\RobersonFaria\DatabaseSchedule\Http\Controllers\ScheduleController@index') }}"
class="btn btn-secondary btn-sm">
{{ trans('schedule::schedule.buttons.back') }}
</a>
</div>

<div class="card-body">
@include('schedule::messages')

@if(count($extractedSchedules) > 0)
<form method="POST" action="{{ route(config('database-schedule.route.name', 'database-schedule') . '.import') }}">
@csrf

<div class="mb-3">
<button type="button" class="btn btn-sm btn-outline-primary" onclick="selectAll()">
{{ trans('schedule::schedule.buttons.select_all') }}
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectNone()">
{{ trans('schedule::schedule.buttons.select_none') }}
</button>
<button type="submit" class="btn btn-success btn-sm ml-2">
{{ trans('schedule::schedule.buttons.import_selected') }}
</button>
</div>

<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th style="width: 50px;">
<input type="checkbox" id="selectAllCheckbox" onchange="toggleAll()">
</th>
<th>{{ trans('schedule::schedule.fields.command') }}</th>
<th>{{ trans('schedule::schedule.fields.expression') }}</th>
<th>{{ trans('schedule::schedule.fields.environments') }}</th>
<th>{{ trans('schedule::schedule.fields.settings') }}</th>
</tr>
</thead>
<tbody>
@foreach($extractedSchedules as $index => $schedule)
<tr>
<td>
<input type="checkbox"
name="schedules[]"
value="{{ json_encode($schedule) }}"
class="schedule-checkbox">
</td>
<td>
<strong>{{ $schedule['command'] }}</strong>
@if(!empty($schedule['params']))
<br>
@foreach($schedule['params'] as $key => $param)
<small class="text-muted">{{ $key }}: {{ $param }}</small>
@endforeach
@endif
</td>
<td><code>{{ $schedule['expression'] }}</code></td>
<td>
@if($schedule['environments'])
<span class="badge badge-warning">{{ $schedule['environments'] }}</span>
@else
<span class="text-muted">{{ trans('schedule::schedule.messages.all-environments') }}</span>
@endif
</td>
<td>
@if($schedule['without_overlapping'])
<span class="badge badge-secondary">{{ trans('schedule::schedule.messages.no-overlap') }}</span>
@endif
@if($schedule['on_one_server'])
<span class="badge badge-info">{{ trans('schedule::schedule.messages.one-server') }}</span>
@endif
@if($schedule['status'])
<span class="badge badge-success">{{ trans('schedule::schedule.status.active') }}</span>
@else
<span class="badge badge-danger">{{ trans('schedule::schedule.status.inactive') }}</span>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</form>
@else
<div class="alert alert-info">
{{ trans('schedule::schedule.messages.no-schedules-found') }}
</div>
@endif
</div>
</div>
</div>
</div>
</div>

<script>
function selectAll() {
document.querySelectorAll('.schedule-checkbox').forEach(cb => cb.checked = true);
document.getElementById('selectAllCheckbox').checked = true;
}

function selectNone() {
document.querySelectorAll('.schedule-checkbox').forEach(cb => cb.checked = false);
document.getElementById('selectAllCheckbox').checked = false;
}

function toggleAll() {
const selectAll = document.getElementById('selectAllCheckbox').checked;
document.querySelectorAll('.schedule-checkbox').forEach(cb => cb.checked = selectAll);
}
</script>
@endsection
4 changes: 4 additions & 0 deletions resources/views/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ class="bi {{ ($schedule->status ? 'bi-pause' : 'bi-play') }}"></i>
</div>
</div>
<div class="card-footer text-right">
<a href="{{ route(config('database-schedule.route.name', 'database-schedule') . '.extract') }}"
class="btn btn-primary">
<i class="bi bi-download"></i> {{ trans('schedule::schedule.buttons.extract_kernel') }}
</a>
<a href="{{ action('\RobersonFaria\DatabaseSchedule\Http\Controllers\ScheduleController@create') }}"
class="btn btn-primary">
{{ trans('schedule::schedule.buttons.create') }}
Expand Down
4 changes: 4 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
->name(config('database-schedule.route.name', 'database-schedule') . '.filter-reset');
Route::get('/create', 'ScheduleController@create')
->name(config('database-schedule.route.name', 'database-schedule') . '.create');
Route::get('/extract', 'ScheduleController@extractFromKernel')
->name(config('database-schedule.route.name', 'database-schedule') . '.extract');
Route::post('/import', 'ScheduleController@importFromKernel')
->name(config('database-schedule.route.name', 'database-schedule') . '.import');
Route::put('/{schedule}', 'ScheduleController@update')
->name(config('database-schedule.route.name', 'database-schedule') . '.update');
Route::get('/{schedule}', 'ScheduleController@show')
Expand Down
45 changes: 45 additions & 0 deletions src/Http/Controllers/ScheduleController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use RobersonFaria\DatabaseSchedule\Http\Requests\ScheduleRequest;
use RobersonFaria\DatabaseSchedule\Http\Services\CommandService;
use RobersonFaria\DatabaseSchedule\Http\Services\ImportService;
use RobersonFaria\DatabaseSchedule\Http\Services\KernelExtractorService;
use RobersonFaria\DatabaseSchedule\Models\Schedule;
use RobersonFaria\DatabaseSchedule\View\Helpers;

Expand Down Expand Up @@ -63,6 +65,9 @@ public function index()
return view('schedule::index')->with(compact('schedules', 'orderBy', 'direction'));
}

/**
* @return \Illuminate\Http\RedirectResponse
*/
public function filter()
{
session()->put(Schedule::SESSION_KEY_FILTERS, request()->input('filters'));
Expand Down Expand Up @@ -155,6 +160,11 @@ public function update(ScheduleRequest $request, Schedule $schedule)
}
}

/**
* @param Schedule $schedule
* @param bool $status
* @return \Illuminate\Http\RedirectResponse
*/
public function status(Schedule $schedule, bool $status)
{
try {
Expand Down Expand Up @@ -224,4 +234,39 @@ public function filterReset()

return redirect()->to(Helpers::indexRoute());
}

/**
* Extract schedules from Kernel
*
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Http\Response|\Illuminate\View\View
*/
public function extractFromKernel()
{
$extractorService = new KernelExtractorService();
$extractedSchedules = $extractorService->extract();

return view('schedule::extract')
->with(compact('extractedSchedules'));
}

/**
* Import selected schedules from kernel extraction
*
* @return \Illuminate\Http\RedirectResponse
*/
public function importFromKernel(ImportService $importService)
{
try {
$selectedSchedules = request()->input('schedules', []);

$importService->importSchedules($selectedSchedules);

return redirect()->to(Helpers::indexRoute())
->with('success', trans('schedule::schedule.messages.import-success'));
} catch (\Exception $e) {
report($e);
return back()
->with('error', trans('schedule::schedule.messages.import-error') . ' : ' . $e->getMessage());
}
}
}
23 changes: 23 additions & 0 deletions src/Http/Services/ImportService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace RobersonFaria\DatabaseSchedule\Http\Services;

class ImportService
{
public function importSchedules(array $selectedSchedules): void
{
$modelClass = config('database-schedule.model');

foreach ($selectedSchedules as $scheduleData) {
$data = json_decode($scheduleData, true, 512, JSON_THROW_ON_ERROR);

$modelClass::updateOrCreate(
[
'command' => $data['command'],
'expression' => $data['expression'],
],
$data
);
}
}
}
106 changes: 106 additions & 0 deletions src/Http/Services/KernelExtractorService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

namespace RobersonFaria\DatabaseSchedule\Http\Services;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\App;
use ReflectionClass;
use ReflectionMethod;

class KernelExtractorService
{

public function extract(): array
{
$kernel = App::make(Kernel::class);
$schedule = App::make(Schedule::class);

$method = new ReflectionMethod($kernel, 'schedule');
$method->setAccessible(true);
$method->invoke($kernel, $schedule);

$extractedSchedules = [];

foreach ($schedule->events() as $event) {
$extractedSchedules[] = $this->parseEvent($event);
}

return $extractedSchedules;
}

private function parseEvent($event): array
{
$command = $this->getCommand($event);

// Get cron expression
$expression = $event->getExpression();

// Get configurations
$withoutOverlapping = $this->getProperty($event, 'withoutOverlapping');
$onOneServer = $this->getProperty($event, 'onOneServer');
$environments = $this->getEnvironments($event);

return [
'command' => $command['command'],
'params' => $command['params'],
'expression' => $expression,
'without_overlapping' => $withoutOverlapping,
'on_one_server' => $onOneServer,
'environments' => $environments,
'status' => true,
];
}

private function getCommand($event): array
{
if (!empty($event->description)) {
return [
'command' => $event->description,
'params' => []
];
}

$fullCommand = $event->command ?? '';

$fullCommand = str_replace(["'/usr/local/bin/php'", "'artisan'"], '', $fullCommand);
$fullCommand = trim(preg_replace('/\s+/', ' ', $fullCommand));

$parts = explode(' ', $fullCommand);
$command = $parts[0] ?? '';
$params = array_slice($parts, 1);

return [
'command' => $command,
'params' => $this->formatParams($params),
];
}

private function formatParams(array $params): array
{
$formatted = [];
foreach ($params as $index => $param) {
$formatted["arg{$index}"] = ['value' => $param];
}
return $formatted;
}

private function getProperty($event, string $property)
{
try {
$reflection = new ReflectionClass($event);
$prop = $reflection->getProperty($property);
$prop->setAccessible(true);

return $prop->getValue($event);
} catch (\Exception $e) {
return false;
}
}

private function getEnvironments($event): ?string
{
$environments = $this->getProperty($event, 'environments');
return $environments ? implode(',', $environments) : null;
}
}
Loading