Compare commits
1 Commits
main
...
feature/la
Author | SHA1 | Date | |
---|---|---|---|
1bbe96c246 |
136
app/Components/Help/Models/HelpContent.php
Normal file
136
app/Components/Help/Models/HelpContent.php
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Components\Help\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use App\Traits\SoftDeleteResolution;
|
||||||
|
|
||||||
|
class HelpContent extends Model
|
||||||
|
{
|
||||||
|
use SoftDeletes, SoftDeleteResolution;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The table associated with the model.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $table = 'config.help_content';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'context_key',
|
||||||
|
'title',
|
||||||
|
'content',
|
||||||
|
'version',
|
||||||
|
'is_active'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $casts = [
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'version' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define which fields should be considered unique for conflict detection
|
||||||
|
* This is used by the SoftDeleteResolution trait to detect conflicts
|
||||||
|
*/
|
||||||
|
protected function uniqueFields(): array
|
||||||
|
{
|
||||||
|
return ['context_key', 'version'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boot the model
|
||||||
|
*/
|
||||||
|
protected static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
// Before saving, check for conflicting soft-deleted records
|
||||||
|
static::saving(function ($model) {
|
||||||
|
try {
|
||||||
|
$duplicate = $model->findSoftDeletedDuplicate();
|
||||||
|
|
||||||
|
if ($duplicate) {
|
||||||
|
// If we find a soft-deleted duplicate, restore it with the new attributes
|
||||||
|
$model->restoreSoftDeletedRecord($duplicate, $model->getAttributes());
|
||||||
|
return false; // Prevent original save
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\Log::error('Error checking for soft-deleted help content', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'model' => get_class($model),
|
||||||
|
'attributes' => $model->getAttributes()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the latest active version of help content for a given context key
|
||||||
|
*
|
||||||
|
* @param string $contextKey
|
||||||
|
* @return HelpContent|null
|
||||||
|
*/
|
||||||
|
public static function getLatestForContext(string $contextKey): ?HelpContent
|
||||||
|
{
|
||||||
|
return static::where('context_key', $contextKey)
|
||||||
|
->where('is_active', true)
|
||||||
|
->orderBy('version', 'desc')
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new version of help content
|
||||||
|
*
|
||||||
|
* @param string $contextKey
|
||||||
|
* @param string $title
|
||||||
|
* @param string $content
|
||||||
|
* @return HelpContent
|
||||||
|
*/
|
||||||
|
public static function createNewVersion(string $contextKey, string $title, string $content): HelpContent
|
||||||
|
{
|
||||||
|
// Find the latest version
|
||||||
|
$latestVersion = static::where('context_key', $contextKey)
|
||||||
|
->orderBy('version', 'desc')
|
||||||
|
->value('version') ?? 0;
|
||||||
|
|
||||||
|
// Create new version
|
||||||
|
return static::create([
|
||||||
|
'context_key' => $contextKey,
|
||||||
|
'title' => $title,
|
||||||
|
'content' => $content,
|
||||||
|
'version' => $latestVersion + 1,
|
||||||
|
'is_active' => true
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set this help content as the active version for its context
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function setAsActive(): bool
|
||||||
|
{
|
||||||
|
// Deactivate all other versions for this context
|
||||||
|
static::where('context_key', $this->context_key)
|
||||||
|
->where('id', '!=', $this->id)
|
||||||
|
->update(['is_active' => false]);
|
||||||
|
|
||||||
|
// Set this version as active
|
||||||
|
$this->is_active = true;
|
||||||
|
return $this->save();
|
||||||
|
}
|
||||||
|
}
|
58
app/Components/Help/Providers/HelpServiceProvider.php
Normal file
58
app/Components/Help/Providers/HelpServiceProvider.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Components\Help\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use App\Components\Help\View\Components\HelpIcon;
|
||||||
|
use App\Components\Help\View\Components\HelpModal;
|
||||||
|
|
||||||
|
class HelpServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
// Register routes
|
||||||
|
Route::middleware('web')
|
||||||
|
->group(function () {
|
||||||
|
$this->loadRoutesFrom(__DIR__.'/../routes/web.php');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register views
|
||||||
|
$this->loadViewsFrom(__DIR__.'/../resources/views', 'help');
|
||||||
|
|
||||||
|
// Register Blade components
|
||||||
|
Blade::component('help-icon', HelpIcon::class);
|
||||||
|
Blade::component('help-modal', HelpModal::class);
|
||||||
|
|
||||||
|
// Register migrations
|
||||||
|
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
|
||||||
|
|
||||||
|
// Publish assets
|
||||||
|
$this->publishes([
|
||||||
|
__DIR__.'/../resources/js' => public_path('js/components/help'),
|
||||||
|
__DIR__.'/../resources/css' => public_path('css/components/help'),
|
||||||
|
], 'help-assets');
|
||||||
|
|
||||||
|
// Publish views for customization
|
||||||
|
$this->publishes([
|
||||||
|
__DIR__.'/../resources/views' => resource_path('views/vendor/help'),
|
||||||
|
], 'help-views');
|
||||||
|
|
||||||
|
// Register the main help script to be included in the layout
|
||||||
|
view()->composer('*', function ($view) {
|
||||||
|
$view->with('helpScriptPath', asset('js/components/help/help-system.js'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Components\SoftDelete\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Traits\SoftDeleteConflictController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SoftDeleteController
|
||||||
|
*
|
||||||
|
* This controller handles soft delete conflict resolution endpoints.
|
||||||
|
*/
|
||||||
|
class SoftDeleteController extends Controller
|
||||||
|
{
|
||||||
|
use SoftDeleteConflictController;
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Components\SoftDelete\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class SoftDeleteServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register services.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap services.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function boot()
|
||||||
|
{
|
||||||
|
// Load routes
|
||||||
|
$this->loadRoutesFrom(__DIR__ . '/../routes/web.php');
|
||||||
|
|
||||||
|
// Publish assets
|
||||||
|
if ($this->app->runningInConsole()) {
|
||||||
|
$this->publishes([
|
||||||
|
__DIR__ . '/../resources/js' => public_path('js/soft-delete'),
|
||||||
|
], 'soft-delete-assets');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register blade components
|
||||||
|
$this->loadViewsFrom(__DIR__ . '/../resources/views', 'soft-delete');
|
||||||
|
|
||||||
|
// Register the Blade component
|
||||||
|
\Illuminate\Support\Facades\Blade::component('soft-delete::components.conflict-modal', 'soft-delete-conflict-modal');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* Soft Delete Conflict Handler
|
||||||
|
*
|
||||||
|
* This module handles the detection and resolution of soft delete conflicts.
|
||||||
|
* Save this file to public/js/soft-delete-conflict-handler.js
|
||||||
|
*/
|
||||||
|
const SoftDeleteConflictHandler = {
|
||||||
|
/**
|
||||||
|
* Initialize the handler for a specific form.
|
||||||
|
*
|
||||||
|
* @param {string} formSelector - The CSS selector for the form
|
||||||
|
* @param {string} modelClass - The fully qualified class name of the model
|
||||||
|
* @param {object} options - Additional options
|
||||||
|
*/
|
||||||
|
init: function(formSelector, modelClass, options = {}) {
|
||||||
|
const form = document.querySelector(formSelector);
|
||||||
|
|
||||||
|
if (!form) {
|
||||||
|
console.error(`Form not found: ${formSelector}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the model class on the form for later use
|
||||||
|
form.dataset.modelClass = modelClass;
|
||||||
|
|
||||||
|
// Set success and cancel redirect URLs if provided
|
||||||
|
if (options.successRedirect) {
|
||||||
|
form.dataset.successRedirect = options.successRedirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.cancelRedirect) {
|
||||||
|
form.dataset.cancelRedirect = options.cancelRedirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intercept form submission
|
||||||
|
form.addEventListener('submit', (event) => {
|
||||||
|
// Don't prevent submission if we've already checked for conflicts
|
||||||
|
if (form.dataset.conflictChecked === 'true') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent the default form submission
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Check for conflicts
|
||||||
|
this.checkForConflicts(form, modelClass, (hasConflict, conflictData) => {
|
||||||
|
if (hasConflict) {
|
||||||
|
// Show the conflict resolution modal
|
||||||
|
this.showConflictModal(conflictData, form);
|
||||||
|
} else {
|
||||||
|
// No conflict, continue with form submission
|
||||||
|
form.dataset.conflictChecked = 'true';
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for soft delete conflicts before form submission.
|
||||||
|
*
|
||||||
|
* @param {HTMLFormElement} form - The form element
|
||||||
|
* @param {string} modelClass - The fully qualified class name of the model
|
||||||
|
* @param {Function} callback - Callback function to handle the result
|
||||||
|
*/
|
||||||
|
checkForConflicts: function(form, modelClass, callback) {
|
||||||
|
// Create a FormData object from the form
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
// Add the model class
|
||||||
|
formData.append('model_class', modelClass);
|
||||||
|
|
||||||
|
// Send the request to check for conflicts
|
||||||
|
fetch('/admin/soft-delete/check-conflict', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.conflict) {
|
||||||
|
// Conflict found
|
||||||
|
callback(true, data.duplicate);
|
||||||
|
} else {
|
||||||
|
// No conflict
|
||||||
|
callback(false, null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error checking for conflicts:', error);
|
||||||
|
// On error, allow the form to submit to let server-side validation handle it
|
||||||
|
callback(false, null);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the conflict resolution modal.
|
||||||
|
*
|
||||||
|
* @param {object} conflictData - Data about the conflicting record
|
||||||
|
* @param {HTMLFormElement} form - The form element
|
||||||
|
*/
|
||||||
|
showConflictModal: function(conflictData, form) {
|
||||||
|
// Get form data as an object
|
||||||
|
const formDataObj = {};
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
for (const [key, value] of formData.entries()) {
|
||||||
|
formDataObj[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch a custom event to open the modal
|
||||||
|
window.dispatchEvent(new CustomEvent('open-soft-delete-modal', {
|
||||||
|
detail: {
|
||||||
|
conflict: conflictData,
|
||||||
|
formData: formDataObj,
|
||||||
|
formElement: form,
|
||||||
|
modelClass: form.dataset.modelClass
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Expose to window for global access
|
||||||
|
window.SoftDeleteConflictHandler = SoftDeleteConflictHandler;
|
@ -0,0 +1,213 @@
|
|||||||
|
{{--
|
||||||
|
Save this file to resources/views/components/soft-delete-conflict-modal.blade.php
|
||||||
|
or to the appropriate component location in your structure
|
||||||
|
--}}
|
||||||
|
|
||||||
|
@props(['title' => 'Record Already Exists'])
|
||||||
|
|
||||||
|
<div
|
||||||
|
x-data="{ show: false, conflict: null, formData: null, formElement: null, modelClass: null }"
|
||||||
|
x-show="show"
|
||||||
|
x-cloak
|
||||||
|
class="fixed inset-0 z-50 overflow-y-auto"
|
||||||
|
id="softDeleteConflictModal"
|
||||||
|
aria-labelledby="modal-title"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
x-on:open-soft-delete-modal.window="
|
||||||
|
show = true;
|
||||||
|
conflict = $event.detail.conflict;
|
||||||
|
formData = $event.detail.formData;
|
||||||
|
formElement = $event.detail.formElement;
|
||||||
|
modelClass = $event.detail.modelClass;
|
||||||
|
"
|
||||||
|
x-on:close-soft-delete-modal.window="show = false"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center min-h-screen p-4 text-center sm:p-0">
|
||||||
|
<div
|
||||||
|
x-show="show"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="ease-in duration-200"
|
||||||
|
x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75"
|
||||||
|
aria-hidden="true"
|
||||||
|
@click="show = false"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
x-show="show"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
x-transition:leave="ease-in duration-200"
|
||||||
|
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
class="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6"
|
||||||
|
>
|
||||||
|
<div class="sm:flex sm:items-start">
|
||||||
|
<div class="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-yellow-100 rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
||||||
|
<svg class="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
|
<h3 class="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||||
|
{{ $title }}
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2">
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
A record with the same unique identifier has been deleted previously. This might indicate a duplicate entry.
|
||||||
|
</p>
|
||||||
|
<div class="mt-4 p-3 bg-gray-50 rounded-lg">
|
||||||
|
<h4 class="text-sm font-medium text-gray-900">Deleted record details:</h4>
|
||||||
|
<div x-html="formatConflictDetails(conflict)" class="mt-2 text-sm text-gray-600"></div>
|
||||||
|
<div class="mt-1 text-xs text-gray-500">
|
||||||
|
Deleted at: <span x-text="conflict?.deleted_at || 'Unknown'"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-sm text-gray-600">
|
||||||
|
You have the following options:
|
||||||
|
</p>
|
||||||
|
<ul class="mt-2 ml-5 list-disc text-sm text-gray-600">
|
||||||
|
<li>Restore the deleted record with your new values</li>
|
||||||
|
<li>Modify your current form input to avoid the conflict</li>
|
||||||
|
<li>Cancel the operation</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex justify-center w-full px-4 py-2 text-base font-medium text-white bg-green-600 border border-transparent rounded-md shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 sm:ml-3 sm:w-auto sm:text-sm"
|
||||||
|
@click="resolveConflict()"
|
||||||
|
>
|
||||||
|
Restore Deleted Record
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex justify-center w-full px-4 py-2 mt-3 text-base font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:w-auto sm:text-sm"
|
||||||
|
@click="show = false"
|
||||||
|
>
|
||||||
|
Modify Current Form
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex justify-center w-full px-4 py-2 mt-3 mr-3 text-base font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:w-auto sm:text-sm"
|
||||||
|
@click="cancelOperation()"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.data('softDeleteConflictModal', () => ({
|
||||||
|
show: false,
|
||||||
|
conflict: null,
|
||||||
|
formData: null,
|
||||||
|
formElement: null,
|
||||||
|
modelClass: null,
|
||||||
|
|
||||||
|
formatConflictDetails(conflict) {
|
||||||
|
if (!conflict || !conflict.attributes) {
|
||||||
|
return 'No details available';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a formatted HTML string with the conflict details
|
||||||
|
let html = '<dl class="grid grid-cols-2 gap-x-4 gap-y-2">';
|
||||||
|
|
||||||
|
// Skip internal fields like id, created_at, updated_at, deleted_at
|
||||||
|
const skipFields = ['id', 'created_at', 'updated_at', 'deleted_at'];
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(conflict.attributes)) {
|
||||||
|
if (!skipFields.includes(key) && value !== null) {
|
||||||
|
html += `
|
||||||
|
<dt class="text-xs font-medium text-gray-500">${key}:</dt>
|
||||||
|
<dd class="text-xs text-gray-900">${value}</dd>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</dl>';
|
||||||
|
return html;
|
||||||
|
},
|
||||||
|
|
||||||
|
resolveConflict() {
|
||||||
|
if (!this.conflict || !this.formData || !this.modelClass) {
|
||||||
|
console.error('Missing required data for conflict resolution');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a form data object with the current form values
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// Add all current form values
|
||||||
|
for (const [key, value] of Object.entries(this.formData)) {
|
||||||
|
formData.append(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the model class
|
||||||
|
formData.append('model_class', this.modelClass);
|
||||||
|
|
||||||
|
// Send the request to restore the soft-deleted record
|
||||||
|
fetch(`/admin/soft-delete/resolve-conflict/${this.conflict.id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Show success message
|
||||||
|
this.showNotification('Success', data.message, 'success');
|
||||||
|
|
||||||
|
// Close the modal
|
||||||
|
this.show = false;
|
||||||
|
|
||||||
|
// Redirect to the appropriate page (usually the index page)
|
||||||
|
const form = this.formElement;
|
||||||
|
const redirectUrl = form.dataset.successRedirect || form.dataset.cancelRedirect || '/';
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
} else {
|
||||||
|
// Show error message
|
||||||
|
this.showNotification('Error', data.message || 'Failed to restore record', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error resolving conflict:', error);
|
||||||
|
this.showNotification('Error', 'An unexpected error occurred', 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelOperation() {
|
||||||
|
this.show = false;
|
||||||
|
|
||||||
|
// If a cancel URL is specified on the form, redirect to it
|
||||||
|
const form = this.formElement;
|
||||||
|
if (form && form.dataset.cancelRedirect) {
|
||||||
|
window.location.href = form.dataset.cancelRedirect;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showNotification(title, message, type) {
|
||||||
|
// This can be customized to use your preferred notification system
|
||||||
|
// For now, we'll use a simple alert
|
||||||
|
alert(`${title}: ${message}`);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
[x-cloak] { display: none !important; }
|
||||||
|
</style>
|
22
app/Components/SoftDelete/routes/web.php
Normal file
22
app/Components/SoftDelete/routes/web.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Components\SoftDelete\Http\Controllers\SoftDeleteController;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Soft Delete Conflict Resolution Routes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These routes provide endpoints for handling soft delete conflicts in the UI.
|
||||||
|
| They are prefixed with 'admin/soft-delete' and require authentication.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
Route::prefix('admin/soft-delete')->name('admin.soft-delete.')->middleware(['web', 'auth'])->group(function () {
|
||||||
|
Route::post('check-conflict', [SoftDeleteController::class, 'checkSoftDeleteConflict'])
|
||||||
|
->name('check-conflict');
|
||||||
|
|
||||||
|
Route::post('resolve-conflict/{id}', [SoftDeleteController::class, 'resolveSoftDeleteConflict'])
|
||||||
|
->name('resolve-conflict');
|
||||||
|
});
|
171
app/Traits/SoftDeleteConflictController.php
Normal file
171
app/Traits/SoftDeleteConflictController.php
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SoftDeleteConflictController Trait
|
||||||
|
*
|
||||||
|
* This trait provides controller methods for handling soft delete conflicts in the UI.
|
||||||
|
*/
|
||||||
|
trait SoftDeleteConflictController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Check if a record would conflict with a soft-deleted record.
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @param string $modelClass The fully qualified class name of the model
|
||||||
|
* @return JsonResponse
|
||||||
|
*/
|
||||||
|
public function checkSoftDeleteConflict(Request $request, string $modelClass): JsonResponse
|
||||||
|
{
|
||||||
|
if (!class_exists($modelClass)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Invalid model class provided',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a new instance of the model
|
||||||
|
/** @var Model $model */
|
||||||
|
$model = new $modelClass();
|
||||||
|
|
||||||
|
// Ensure the model uses SoftDeleteResolution trait
|
||||||
|
if (!method_exists($model, 'findSoftDeletedDuplicate')) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Model does not support soft delete conflict resolution',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include fields that are present in request
|
||||||
|
$attributes = $request->only($model->getFillable());
|
||||||
|
|
||||||
|
// If editing an existing record, pass the ID to exclude it from the check
|
||||||
|
if ($request->has('id')) {
|
||||||
|
$attributes['id'] = $request->input('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for soft-deleted duplicate
|
||||||
|
$duplicate = $model->findSoftDeletedDuplicate($attributes);
|
||||||
|
|
||||||
|
if ($duplicate) {
|
||||||
|
// Return details about the conflicting record
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'conflict' => true,
|
||||||
|
'message' => 'A deleted record with the same unique identifier exists.',
|
||||||
|
'duplicate' => [
|
||||||
|
'id' => $duplicate->getKey(),
|
||||||
|
'attributes' => $duplicate->toArray(),
|
||||||
|
'deleted_at' => $duplicate->deleted_at->format('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No conflict found
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'conflict' => false,
|
||||||
|
'message' => 'No conflict found',
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error checking for soft delete conflict', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'model' => $modelClass,
|
||||||
|
'data' => $request->all(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'An error occurred while checking for conflicts',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a soft delete conflict by restoring the deleted record.
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @param string $modelClass The fully qualified class name of the model
|
||||||
|
* @param mixed $id The ID of the soft-deleted record to restore
|
||||||
|
* @return JsonResponse
|
||||||
|
*/
|
||||||
|
public function resolveSoftDeleteConflict(Request $request, string $modelClass, $id): JsonResponse
|
||||||
|
{
|
||||||
|
if (!class_exists($modelClass)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Invalid model class provided',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Attempt to find the soft-deleted record
|
||||||
|
/** @var Model $model */
|
||||||
|
$modelInstance = new $modelClass();
|
||||||
|
|
||||||
|
// Ensure the model uses SoftDeleteResolution trait
|
||||||
|
if (!method_exists($modelInstance, 'restoreSoftDeletedRecord')) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Model does not support soft delete conflict resolution',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$deletedRecord = $modelClass::withTrashed()->find($id);
|
||||||
|
|
||||||
|
if (!$deletedRecord) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Deleted record not found',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$deletedRecord->trashed()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Record is not deleted',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include fields that are present in request and fillable
|
||||||
|
$attributes = $request->only($modelInstance->getFillable());
|
||||||
|
|
||||||
|
// Restore the record with updated attributes
|
||||||
|
$restored = $modelInstance->restoreSoftDeletedRecord($deletedRecord, $attributes);
|
||||||
|
|
||||||
|
if (!$restored) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to restore record',
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Record restored successfully',
|
||||||
|
'record' => $restored->fresh()->toArray(),
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error restoring soft-deleted record', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'model' => $modelClass,
|
||||||
|
'id' => $id,
|
||||||
|
'data' => $request->all(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'An error occurred while restoring the record',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
244
app/Traits/SoftDeleteResolution.php
Normal file
244
app/Traits/SoftDeleteResolution.php
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Illuminate\Database\QueryException;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SoftDeleteResolution Trait
|
||||||
|
*
|
||||||
|
* This trait provides functionality for handling conflicts with soft-deleted records
|
||||||
|
* that violate unique constraints when creating or updating models.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* 1. Add this trait to any model that uses SoftDeletes
|
||||||
|
* 2. Define uniqueFields() method in your model to specify which fields should be considered unique
|
||||||
|
* 3. Use findSoftDeletedDuplicate() to detect conflicts before saving
|
||||||
|
* 4. Use restoreSoftDeletedRecord() to restore a soft-deleted record with updated attributes
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* ```
|
||||||
|
* class User extends Model
|
||||||
|
* {
|
||||||
|
* use SoftDeletes, SoftDeleteResolution;
|
||||||
|
*
|
||||||
|
* // Define which fields should be considered for uniqueness checks
|
||||||
|
* protected function uniqueFields(): array
|
||||||
|
* {
|
||||||
|
* return ['email'];
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // In your controller:
|
||||||
|
* $newUser = new User(['name' => 'John', 'email' => 'john@example.com']);
|
||||||
|
*
|
||||||
|
* // Check for soft-deleted duplicate
|
||||||
|
* $duplicate = $newUser->findSoftDeletedDuplicate();
|
||||||
|
*
|
||||||
|
* if ($duplicate) {
|
||||||
|
* // Restore with updated attributes
|
||||||
|
* $restored = $newUser->restoreSoftDeletedRecord($duplicate, ['name' => 'John']);
|
||||||
|
* return "Restored previously deleted user with updated details.";
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // Otherwise, proceed with normal save
|
||||||
|
* $newUser->save();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
trait SoftDeleteResolution
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Check if the current model has the SoftDeletes trait
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function usesSoftDeletes(): bool
|
||||||
|
{
|
||||||
|
return in_array(SoftDeletes::class, class_uses_recursive($this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get fields that should be considered unique for this model
|
||||||
|
* This method should be implemented in the model using this trait
|
||||||
|
*
|
||||||
|
* @return array An array of field names that should be unique
|
||||||
|
*/
|
||||||
|
protected function uniqueFields(): array
|
||||||
|
{
|
||||||
|
// Default implementation returns empty array
|
||||||
|
// Models should override this method to specify their unique fields
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there's a soft-deleted record that would conflict with this one
|
||||||
|
*
|
||||||
|
* @param array $attributes Optional custom attributes to check instead of the model's current attributes
|
||||||
|
* @return Model|null The conflicting soft-deleted model, or null if none exists
|
||||||
|
*/
|
||||||
|
public function findSoftDeletedDuplicate(array $attributes = []): ?Model
|
||||||
|
{
|
||||||
|
// Ensure the model uses soft deletes
|
||||||
|
if (!$this->usesSoftDeletes()) {
|
||||||
|
Log::warning('SoftDeleteResolution trait used on model without SoftDeletes', [
|
||||||
|
'model' => get_class($this)
|
||||||
|
]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get unique fields for this model
|
||||||
|
$uniqueFields = $this->uniqueFields();
|
||||||
|
|
||||||
|
if (empty($uniqueFields)) {
|
||||||
|
Log::warning('No unique fields defined for model using SoftDeleteResolution', [
|
||||||
|
'model' => get_class($this)
|
||||||
|
]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use provided attributes or current model attributes
|
||||||
|
$checkAttributes = empty($attributes) ? $this->getAttributes() : $attributes;
|
||||||
|
|
||||||
|
// Build query to find soft-deleted duplicates
|
||||||
|
$query = static::onlyTrashed();
|
||||||
|
|
||||||
|
foreach ($uniqueFields as $field) {
|
||||||
|
// Only include fields that exist in checkAttributes and have a value
|
||||||
|
if (array_key_exists($field, $checkAttributes) && !is_null($checkAttributes[$field])) {
|
||||||
|
$query->where($field, $checkAttributes[$field]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the first matching soft-deleted record
|
||||||
|
return $query->first();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error checking for soft-deleted duplicates', [
|
||||||
|
'model' => get_class($this),
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'attributes' => $attributes ?: $this->getAttributes()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore a soft-deleted record and optionally update its attributes
|
||||||
|
*
|
||||||
|
* @param Model $model The soft-deleted model to restore
|
||||||
|
* @param array $attributes New attributes to apply during restoration
|
||||||
|
* @return Model|null The restored model, or null if restoration failed
|
||||||
|
*/
|
||||||
|
public function restoreSoftDeletedRecord(Model $model, array $attributes = []): ?Model
|
||||||
|
{
|
||||||
|
// Ensure the model is actually soft-deleted
|
||||||
|
if (!method_exists($model, 'restore') || is_null($model->deleted_at)) {
|
||||||
|
Log::warning('Attempted to restore a model that is not soft-deleted', [
|
||||||
|
'model' => get_class($model),
|
||||||
|
'id' => $model->getKey()
|
||||||
|
]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Begin transaction to ensure atomicity
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
// Apply any new attributes
|
||||||
|
if (!empty($attributes)) {
|
||||||
|
$model->fill($attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the model (removes deleted_at timestamp)
|
||||||
|
$model->restore();
|
||||||
|
|
||||||
|
// Save any updated attributes
|
||||||
|
if (!empty($attributes)) {
|
||||||
|
$model->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit transaction
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
Log::info('Successfully restored soft-deleted record', [
|
||||||
|
'model' => get_class($model),
|
||||||
|
'id' => $model->getKey(),
|
||||||
|
'attributes_updated' => !empty($attributes)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $model;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Rollback transaction on error
|
||||||
|
DB::rollBack();
|
||||||
|
|
||||||
|
Log::error('Failed to restore soft-deleted record', [
|
||||||
|
'model' => get_class($model),
|
||||||
|
'id' => $model->getKey(),
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'attributes' => $attributes
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect if a QueryException is due to a unique constraint violation on a soft-deleted record
|
||||||
|
*
|
||||||
|
* @param QueryException $exception The exception to check
|
||||||
|
* @return bool True if the exception is caused by a conflict with a soft-deleted record
|
||||||
|
*/
|
||||||
|
public function isSoftDeletedUniqueConstraintViolation(QueryException $exception): bool
|
||||||
|
{
|
||||||
|
// Check if the model uses soft deletes
|
||||||
|
if (!$this->usesSoftDeletes()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the exception is due to a unique constraint violation
|
||||||
|
// PostgreSQL error code 23505 is for unique_violation
|
||||||
|
$isPgsqlUniqueViolation = $exception->getCode() === '23505';
|
||||||
|
|
||||||
|
// SQLite and MySQL use different error codes
|
||||||
|
$isMysqlUniqueViolation = strpos($exception->getMessage(), 'Duplicate entry') !== false;
|
||||||
|
$isSqliteUniqueViolation = strpos($exception->getMessage(), 'UNIQUE constraint failed') !== false;
|
||||||
|
|
||||||
|
// If it's not a unique constraint violation, return false
|
||||||
|
if (!($isPgsqlUniqueViolation || $isMysqlUniqueViolation || $isSqliteUniqueViolation)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, we have a unique constraint violation
|
||||||
|
// Now we need to check if the conflicting record is soft-deleted
|
||||||
|
|
||||||
|
$uniqueFields = $this->uniqueFields();
|
||||||
|
if (empty($uniqueFields)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's a soft-deleted record with the same unique values
|
||||||
|
return $this->findSoftDeletedDuplicate() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to resolve a unique constraint violation by restoring a soft-deleted record
|
||||||
|
*
|
||||||
|
* @param array $attributes Attributes for the new or updated record
|
||||||
|
* @return Model|null The restored model, or null if no resolution was possible
|
||||||
|
*/
|
||||||
|
public function resolveSoftDeletedConflict(array $attributes = []): ?Model
|
||||||
|
{
|
||||||
|
$duplicate = $this->findSoftDeletedDuplicate($attributes);
|
||||||
|
|
||||||
|
if ($duplicate) {
|
||||||
|
return $this->restoreSoftDeletedRecord($duplicate, $attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Helpers\Timer;
|
||||||
|
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
protected $connection = 'pgsql';
|
||||||
|
protected $schema = 'config';
|
||||||
|
protected $basename = 'help_content';
|
||||||
|
|
||||||
|
protected ConsoleOutput $msg;
|
||||||
|
protected int $msgKtr = 0;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->msg = new ConsoleOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function upInstructions(): void
|
||||||
|
{
|
||||||
|
$this->writeMsg("Creating table: {$this->schema}.tbl_{$this->basename}");
|
||||||
|
$tblDef = "CREATE TABLE IF NOT EXISTS {$this->schema}.tbl_{$this->basename}
|
||||||
|
(
|
||||||
|
id BIGSERIAL,
|
||||||
|
context_key VARCHAR(255) NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
version INTEGER DEFAULT 1,
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
created_at timestamp(0) without time zone,
|
||||||
|
updated_at timestamp(0) without time zone,
|
||||||
|
deleted_at timestamp(0) with time zone,
|
||||||
|
CONSTRAINT pk_{$this->basename}_id PRIMARY KEY (id)
|
||||||
|
)
|
||||||
|
";
|
||||||
|
$idxs = ([
|
||||||
|
"CREATE UNIQUE INDEX unq_{$this->basename}_context_key_version ON {$this->schema}.tbl_{$this->basename} (context_key, version)",
|
||||||
|
"CREATE INDEX idx_{$this->basename}_context_key ON {$this->schema}.tbl_{$this->basename} (context_key)",
|
||||||
|
"CREATE INDEX idx_{$this->basename}_created_at ON {$this->schema}.tbl_{$this->basename} (created_at)",
|
||||||
|
"CREATE INDEX idx_{$this->basename}_updated_at ON {$this->schema}.tbl_{$this->basename} (updated_at)",
|
||||||
|
"CREATE INDEX idx_{$this->basename}_deleted_at ON {$this->schema}.tbl_{$this->basename} (deleted_at)"
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::connection($this->connection)->statement($tblDef);
|
||||||
|
|
||||||
|
foreach($idxs AS $idx){
|
||||||
|
DB::connection($this->connection)->statement($idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->writeMsg("Creating view: {$this->schema}.{$this->basename}");
|
||||||
|
$viewDef = "SELECT * FROM {$this->schema}.tbl_{$this->basename} WHERE deleted_at IS NULL";
|
||||||
|
$viewQry = "CREATE OR REPLACE VIEW {$this->schema}.{$this->basename} AS {$viewDef}";
|
||||||
|
DB::connection($this->connection)->statement($viewQry);
|
||||||
|
|
||||||
|
$this->writeMsg("Creating view: {$this->schema}.vdel_{$this->basename}");
|
||||||
|
$viewDef = "SELECT * FROM {$this->schema}.tbl_{$this->basename} WHERE deleted_at IS NOT NULL";
|
||||||
|
$viewQry = "CREATE OR REPLACE VIEW {$this->schema}.vdel_{$this->basename} AS {$viewDef}";
|
||||||
|
DB::connection($this->connection)->statement($viewQry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function downInstructions(): void
|
||||||
|
{
|
||||||
|
$this->writeMsg("Dropping view: {$this->schema}.vdel_{$this->basename}");
|
||||||
|
$dropViewQry = "DROP VIEW IF EXISTS {$this->schema}.vdel_{$this->basename}";
|
||||||
|
DB::connection($this->connection)->statement($dropViewQry);
|
||||||
|
|
||||||
|
$this->writeMsg("Dropping view: {$this->schema}.{$this->basename}");
|
||||||
|
$dropViewQry = "DROP VIEW IF EXISTS {$this->schema}.{$this->basename}";
|
||||||
|
DB::connection($this->connection)->statement($dropViewQry);
|
||||||
|
|
||||||
|
$this->writeMsg("Dropping table: {$this->schema}.tbl_{$this->basename}");
|
||||||
|
$dropTblQry = "DROP TABLE IF EXISTS {$this->schema}.tbl_{$this->basename}";
|
||||||
|
DB::connection($this->connection)->statement($dropTblQry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$upTimer = new Timer();
|
||||||
|
$this->upInstructions();
|
||||||
|
$this->writeMsg("This task took {$upTimer->getElapsedTime()} minutes");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$downTimer = new Timer();
|
||||||
|
$this->downInstructions();
|
||||||
|
$this->writeMsg("This task took {$downTimer->getElapsedTime()} minutes");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output messages to the console when the
|
||||||
|
public function writeMsg($msg): void
|
||||||
|
{
|
||||||
|
if(!$this->msgKtr){
|
||||||
|
$this->msg->writeln('');
|
||||||
|
}
|
||||||
|
$this->msgKtr++;
|
||||||
|
$this->msg->writeln("$this->msgKtr) $msg");
|
||||||
|
}
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user